"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GLTimer = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
class DisjointTimerExtension {
    constructor(system) {
        this._e = system.disjointTimerQuery;
        this._context = system.context;
    }
    get isSupported() { return this._e !== undefined; }
    didDisjointEventHappen() {
        return this._context.getParameter(this._e.GPU_DISJOINT_EXT);
    }
    createQuery() { return this._context.createQuery(); }
    deleteQuery(q) { this._context.deleteQuery(q); }
    beginQuery(q) { this._context.beginQuery(this._e.TIME_ELAPSED_EXT, q); }
    endQuery() { this._context.endQuery(this._e.TIME_ELAPSED_EXT); }
    isResultAvailable(q) {
        return this._context.getQueryParameter(q, this._context.QUERY_RESULT_AVAILABLE);
    }
    getResult(q) {
        return this._context.getQueryParameter(q, this._context.QUERY_RESULT);
    }
}
/** Record GPU hardware queries to profile independent of CPU.
 *
 * This is a wrapper around EXT_disjoint_timer_query. The extension should be available in the following browsers:
 *  * Chrome 67 and later
 *  * Chromium-based Edge
 *  * Firefox (with webgl.enable-privileged-extensions set to true in about:config)
 *
 * EXT_disjoint_timer_query only supports one active query per context without nesting. This wrapper keeps an internal stack to make
 * nesting work.
 *
 * The extension API makes timestamps look like a better solution than disjoint timers, but they are not actually supported.
 * See https://bugs.chromium.org/p/chromium/issues/detail?id=595172
 * @internal
 */
class GLTimer {
    constructor(system) {
        this._extension = new DisjointTimerExtension(system);
        this._queryStack = [];
        this._resultsCallback = undefined;
    }
    // This class is necessarily a singleton per context because of the underlying extension it wraps.
    // System is expected to call create in its constructor.
    static create(system) {
        return new GLTimer(system);
    }
    get isSupported() { return this._extension.isSupported; }
    set resultsCallback(callback) {
        if (this._queryStack.length !== 0)
            throw new core_common_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Do not set resultsCallback when a frame is already being drawn");
        this._resultsCallback = callback;
    }
    beginOperation(label) {
        if (!this._resultsCallback)
            return;
        this.pushQuery(label);
    }
    endOperation() {
        if (!this._resultsCallback)
            return;
        if (this._queryStack.length === 0)
            throw new core_common_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Mismatched calls to beginOperation/endOperation");
        this.popQuery();
    }
    beginFrame() {
        if (!this._resultsCallback)
            return;
        if (this._queryStack.length !== 0)
            throw new core_common_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Already recording timing for a frame");
        const query = this._extension.createQuery();
        this._extension.beginQuery(query);
        this._queryStack.push({ label: "Total", query, children: [] });
    }
    endFrame() {
        if (!this._resultsCallback)
            return;
        if (this._queryStack.length !== 1)
            throw new core_common_1.IModelError(core_bentley_1.BentleyStatus.ERROR, "Missing at least one endOperation call");
        this._extension.endQuery();
        const root = this._queryStack.pop();
        const userCallback = this._resultsCallback;
        const queryCallback = () => {
            if (this._extension.didDisjointEventHappen()) {
                // Have to throw away results for this frame after disjoint event occurs.
                this.cleanupAfterDisjointEvent(root);
                return;
            }
            // It takes one or more frames for results to become available.
            // Only checking time for root since it will always be the last query completed.
            // If there are any sibling queries then we will just check the last one.
            const finalQuery = (undefined === root.siblingQueries ? root.query : root.siblingQueries[root.siblingQueries.length - 1]);
            if (!this._extension.isResultAvailable(finalQuery)) {
                setTimeout(queryCallback, 0);
                return;
            }
            const processQueryEntry = (queryEntry) => {
                const time = this._extension.getResult(queryEntry.query);
                this._extension.deleteQuery(queryEntry.query);
                const result = { label: queryEntry.label, nanoseconds: time };
                if (undefined !== queryEntry.siblingQueries) {
                    for (const sib of queryEntry.siblingQueries) {
                        const sibTime = this._extension.getResult(sib);
                        this._extension.deleteQuery(sib);
                        result.nanoseconds += sibTime;
                    }
                    queryEntry.siblingQueries = undefined;
                }
                if (queryEntry.children === undefined)
                    return result;
                result.children = [];
                for (const child of queryEntry.children) {
                    const childResult = processQueryEntry(child);
                    result.children.push(childResult);
                    result.nanoseconds += childResult.nanoseconds;
                }
                return result;
            };
            userCallback(processQueryEntry(root));
        };
        setTimeout(queryCallback, 0);
    }
    cleanupAfterDisjointEvent(queryEntry) {
        this._extension.deleteQuery(queryEntry.query);
        if (undefined !== queryEntry.siblingQueries) {
            for (const sib of queryEntry.siblingQueries)
                this._extension.deleteQuery(sib);
            queryEntry.siblingQueries = undefined;
        }
        if (!queryEntry.children)
            return;
        for (const child of queryEntry.children)
            this.cleanupAfterDisjointEvent(child);
    }
    pushQuery(label) {
        this._extension.endQuery();
        const query = this._extension.createQuery();
        this._extension.beginQuery(query);
        const activeQuery = this._queryStack[this._queryStack.length - 1];
        const queryEntry = { label, query };
        this._queryStack.push(queryEntry);
        if (activeQuery.children === undefined)
            activeQuery.children = [queryEntry];
        else
            activeQuery.children.push(queryEntry);
    }
    popQuery() {
        this._extension.endQuery();
        this._queryStack.pop();
        const lastStackIndex = this._queryStack.length - 1;
        const activeQuery = this._queryStack[lastStackIndex];
        if (undefined === activeQuery.siblingQueries)
            activeQuery.siblingQueries = [];
        const newQuery = this._extension.createQuery();
        activeQuery.siblingQueries.push(newQuery);
        this._extension.beginQuery(newQuery);
    }
}
exports.GLTimer = GLTimer;
//# sourceMappingURL=GLTimer.js.map