export class RendererTimingCollector {
    samples = [];
    gpuBackend;
    now;
    fallbackReason;
    constructor(options = {}) {
        this.gpuBackend = options.gpuBackend ?? createCpuFallbackGpuTimingBackend(options.fallbackReason);
        this.now = options.now ?? (() => performance.now());
        this.fallbackReason = options.fallbackReason ?? this.gpuBackend.unavailableReason ?? "GPU timing unavailable; using CPU timing fallback.";
    }
    measure(label, callback) {
        const timer = this.begin(label);
        try {
            return callback();
        }
        finally {
            timer.end();
        }
    }
    begin(label) {
        const trimmed = requireTimingLabel(label);
        const gpuToken = this.gpuBackend.begin(trimmed);
        const started = this.now();
        let ended = false;
        return {
            end: () => {
                if (ended) {
                    throw new Error(`Renderer timing sample already ended: ${trimmed}`);
                }
                ended = true;
                const cpuDurationMs = Math.max(0, this.now() - started);
                const gpuDurationMs = this.gpuBackend.end(gpuToken, cpuDurationMs);
                const sample = this.createSample(trimmed, cpuDurationMs, gpuDurationMs);
                this.samples.push(sample);
                return sample;
            }
        };
    }
    snapshot() {
        this.collectAvailableGpuResults();
        const cpuFallbackActive = this.samples.some((sample) => sample.source === "cpu-fallback");
        return {
            gpuTimingSupported: this.gpuBackend.supported,
            cpuFallbackActive,
            sampleCount: this.samples.length,
            ...(this.gpuBackend.supported && !cpuFallbackActive ? {} : { unavailableReason: this.fallbackReason }),
            samples: [...this.samples]
        };
    }
    createSample(label, cpuDurationMs, gpuDurationMs) {
        if (this.gpuBackend.supported && gpuDurationMs !== undefined && Number.isFinite(gpuDurationMs)) {
            return {
                label,
                durationMs: Number(gpuDurationMs.toFixed(3)),
                cpuDurationMs: Number(cpuDurationMs.toFixed(3)),
                gpuDurationMs: Number(gpuDurationMs.toFixed(3)),
                source: "gpu"
            };
        }
        return {
            label,
            durationMs: Number(cpuDurationMs.toFixed(3)),
            cpuDurationMs: Number(cpuDurationMs.toFixed(3)),
            source: "cpu-fallback",
            fallbackReason: this.fallbackReason
        };
    }
    collectAvailableGpuResults() {
        const results = this.gpuBackend.collectAvailable?.() ?? [];
        for (const result of results) {
            const sampleIndex = this.samples.findIndex((sample) => sample.label === result.label && sample.source === "cpu-fallback");
            if (sampleIndex < 0) {
                continue;
            }
            const existing = this.samples[sampleIndex];
            this.samples[sampleIndex] = {
                label: existing.label,
                durationMs: Number(result.durationMs.toFixed(3)),
                cpuDurationMs: existing.cpuDurationMs,
                gpuDurationMs: Number(result.durationMs.toFixed(3)),
                source: "gpu"
            };
        }
    }
}
export function createCpuFallbackGpuTimingBackend(unavailableReason = "GPU timing unavailable; using CPU timing fallback.") {
    return {
        supported: false,
        unavailableReason,
        begin(label) {
            return { label: requireTimingLabel(label) };
        },
        end(_token, _cpuDurationMs) {
            return undefined;
        }
    };
}
export function createImmediateGpuTimingBackend(durationMs) {
    if (!Number.isFinite(durationMs) || durationMs < 0) {
        throw new RangeError("Immediate GPU timing duration must be a finite non-negative number.");
    }
    return {
        supported: true,
        begin(label) {
            return { label: requireTimingLabel(label) };
        },
        end(_token, _cpuDurationMs) {
            return durationMs;
        }
    };
}
export function createWebGL2GpuTimingBackend(gl) {
    const extension = gl.getExtension("EXT_disjoint_timer_query_webgl2");
    if (!extension) {
        return createCpuFallbackGpuTimingBackend("EXT_disjoint_timer_query_webgl2 unavailable; using CPU timing fallback.");
    }
    return new WebGL2GpuTimingBackend(gl, extension);
}
class WebGL2GpuTimingBackend {
    gl;
    extension;
    supported = true;
    unavailableReason = "GPU timer query result pending or disjoint; using CPU timing fallback for this sample.";
    pending = [];
    constructor(gl, extension) {
        this.gl = gl;
        this.extension = extension;
    }
    begin(label) {
        const query = this.gl.createQuery();
        if (query) {
            this.gl.beginQuery(this.extension.TIME_ELAPSED_EXT, query);
        }
        return { label: requireTimingLabel(label), query };
    }
    end(token, _cpuDurationMs) {
        const query = token.query;
        if (!query) {
            return undefined;
        }
        this.gl.endQuery(this.extension.TIME_ELAPSED_EXT);
        this.pending.push(token);
        return undefined;
    }
    collectAvailable() {
        const results = [];
        for (let index = this.pending.length - 1; index >= 0; index -= 1) {
            const token = this.pending[index];
            const result = this.readQueryIfAvailable(token);
            if (!result.done) {
                continue;
            }
            this.pending.splice(index, 1);
            if (result.durationMs !== undefined) {
                results.push({ label: token.label, durationMs: result.durationMs });
            }
        }
        return results.reverse();
    }
    readQueryIfAvailable(token) {
        const query = token.query;
        if (!query) {
            return { done: true };
        }
        const available = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE);
        const disjoint = Boolean(this.gl.getParameter(this.extension.GPU_DISJOINT_EXT));
        if (!available) {
            return { done: false };
        }
        if (disjoint) {
            this.gl.deleteQuery(query);
            return { done: true };
        }
        const elapsedNanoseconds = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT);
        this.gl.deleteQuery(query);
        return { done: true, durationMs: elapsedNanoseconds / 1_000_000 };
    }
}
function requireTimingLabel(label) {
    const trimmed = label.trim();
    if (!trimmed) {
        throw new Error("Renderer timing sample label is required");
    }
    return trimmed;
}
//# sourceMappingURL=RendererTiming.js.map