|
|
@@ -0,0 +1,357 @@
|
|
|
+import TimestampQueryPool from '../../common/TimestampQueryPool.js';
|
|
|
+
|
|
|
+/**
|
|
|
+ * Manages a pool of WebGL timestamp queries for performance measurement.
|
|
|
+ * Handles creation, execution, and resolution of timer queries using WebGL extensions.
|
|
|
+ * @extends TimestampQueryPool
|
|
|
+ */
|
|
|
+class WebGLTimestampQueryPool extends TimestampQueryPool {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a new WebGL timestamp query pool.
|
|
|
+ * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - The WebGL context.
|
|
|
+ * @param {string} type - The type identifier for this query pool.
|
|
|
+ * @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
|
|
|
+ */
|
|
|
+ constructor( gl, type, maxQueries = 2048 ) {
|
|
|
+
|
|
|
+ super( maxQueries );
|
|
|
+
|
|
|
+ this.gl = gl;
|
|
|
+ this.type = type;
|
|
|
+
|
|
|
+ // Check for timer query extensions
|
|
|
+ this.ext = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' ) ||
|
|
|
+ gl.getExtension( 'EXT_disjoint_timer_query' );
|
|
|
+
|
|
|
+ if ( ! this.ext ) {
|
|
|
+
|
|
|
+ console.warn( 'EXT_disjoint_timer_query not supported; timestamps will be disabled.' );
|
|
|
+ this.trackTimestamp = false;
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create query objects
|
|
|
+ this.queries = [];
|
|
|
+ for ( let i = 0; i < this.maxQueries; i ++ ) {
|
|
|
+
|
|
|
+ this.queries.push( gl.createQuery() );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this.activeQuery = null;
|
|
|
+ this.queryStates = new Map(); // Track state of each query: 'inactive', 'started', 'ended'
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Allocates a pair of queries for a given render context.
|
|
|
+ * @param {Object} renderContext - The render context to allocate queries for.
|
|
|
+ * @returns {?number} The base offset for the allocated queries, or null if allocation failed.
|
|
|
+ */
|
|
|
+ allocateQueriesForContext( renderContext ) {
|
|
|
+
|
|
|
+ if ( ! this.trackTimestamp ) return null;
|
|
|
+
|
|
|
+ // Check if we have enough space for a new query pair
|
|
|
+ if ( this.currentQueryIndex + 2 > this.maxQueries ) {
|
|
|
+
|
|
|
+ return null;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const baseOffset = this.currentQueryIndex;
|
|
|
+ this.currentQueryIndex += 2;
|
|
|
+
|
|
|
+ // Initialize query states
|
|
|
+ this.queryStates.set( baseOffset, 'inactive' );
|
|
|
+ this.queryOffsets.set( renderContext.id, baseOffset );
|
|
|
+
|
|
|
+ return baseOffset;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Begins a timestamp query for the specified render context.
|
|
|
+ * @param {Object} renderContext - The render context to begin timing for.
|
|
|
+ */
|
|
|
+ beginQuery( renderContext ) {
|
|
|
+
|
|
|
+ if ( ! this.trackTimestamp || this.isDisposed ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const baseOffset = this.queryOffsets.get( renderContext.id );
|
|
|
+ if ( baseOffset == null ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Don't start a new query if there's an active one
|
|
|
+ if ( this.activeQuery !== null ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const query = this.queries[ baseOffset ];
|
|
|
+ if ( ! query ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ // Only begin if query is inactive
|
|
|
+ if ( this.queryStates.get( baseOffset ) === 'inactive' ) {
|
|
|
+
|
|
|
+ this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query );
|
|
|
+ this.activeQuery = baseOffset;
|
|
|
+ this.queryStates.set( baseOffset, 'started' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch ( error ) {
|
|
|
+
|
|
|
+ console.error( 'Error in beginQuery:', error );
|
|
|
+ this.activeQuery = null;
|
|
|
+ this.queryStates.set( baseOffset, 'inactive' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Ends the active timestamp query for the specified render context.
|
|
|
+ * @param {Object} renderContext - The render context to end timing for.
|
|
|
+ * @param {string} renderContext.id - Unique identifier for the render context.
|
|
|
+ */
|
|
|
+ endQuery( renderContext ) {
|
|
|
+
|
|
|
+ if ( ! this.trackTimestamp || this.isDisposed ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const baseOffset = this.queryOffsets.get( renderContext.id );
|
|
|
+ if ( baseOffset == null ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Only end if this is the active query
|
|
|
+ if ( this.activeQuery !== baseOffset ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ this.gl.endQuery( this.ext.TIME_ELAPSED_EXT );
|
|
|
+ this.queryStates.set( baseOffset, 'ended' );
|
|
|
+ this.activeQuery = null;
|
|
|
+
|
|
|
+ } catch ( error ) {
|
|
|
+
|
|
|
+ console.error( 'Error in endQuery:', error );
|
|
|
+ // Reset state on error
|
|
|
+ this.queryStates.set( baseOffset, 'inactive' );
|
|
|
+ this.activeQuery = null;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Asynchronously resolves all completed queries and returns the total duration.
|
|
|
+ * @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
|
|
|
+ */
|
|
|
+ async resolveQueriesAsync() {
|
|
|
+
|
|
|
+ if ( ! this.trackTimestamp || this.pendingResolve ) {
|
|
|
+
|
|
|
+ return this.lastValue;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this.pendingResolve = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ // Wait for all ended queries to complete
|
|
|
+ const resolvePromises = [];
|
|
|
+
|
|
|
+ for ( const [ baseOffset, state ] of this.queryStates ) {
|
|
|
+
|
|
|
+ if ( state === 'ended' ) {
|
|
|
+
|
|
|
+ const query = this.queries[ baseOffset ];
|
|
|
+ resolvePromises.push( this.resolveQuery( query ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( resolvePromises.length === 0 ) {
|
|
|
+
|
|
|
+ return this.lastValue;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const results = await Promise.all( resolvePromises );
|
|
|
+ const totalDuration = results.reduce( ( acc, val ) => acc + val, 0 );
|
|
|
+
|
|
|
+ // Store the last valid result
|
|
|
+ this.lastValue = totalDuration;
|
|
|
+
|
|
|
+ // Reset states
|
|
|
+ this.currentQueryIndex = 0;
|
|
|
+ this.queryOffsets.clear();
|
|
|
+ this.queryStates.clear();
|
|
|
+ this.activeQuery = null;
|
|
|
+
|
|
|
+ return totalDuration;
|
|
|
+
|
|
|
+ } catch ( error ) {
|
|
|
+
|
|
|
+ console.error( 'Error resolving queries:', error );
|
|
|
+ return this.lastValue;
|
|
|
+
|
|
|
+ } finally {
|
|
|
+
|
|
|
+ this.pendingResolve = false;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Resolves a single query, checking for completion and disjoint operation.
|
|
|
+ * @private
|
|
|
+ * @param {WebGLQuery} query - The query object to resolve.
|
|
|
+ * @returns {Promise<number>} The elapsed time in milliseconds.
|
|
|
+ */
|
|
|
+ async resolveQuery( query ) {
|
|
|
+
|
|
|
+ return new Promise( ( resolve ) => {
|
|
|
+
|
|
|
+ if ( this.isDisposed ) {
|
|
|
+
|
|
|
+ resolve( this.lastValue );
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ let timeoutId;
|
|
|
+ let isResolved = false;
|
|
|
+
|
|
|
+ const cleanup = () => {
|
|
|
+
|
|
|
+ if ( timeoutId ) {
|
|
|
+
|
|
|
+ clearTimeout( timeoutId );
|
|
|
+ timeoutId = null;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ const finalizeResolution = ( value ) => {
|
|
|
+
|
|
|
+ if ( ! isResolved ) {
|
|
|
+
|
|
|
+ isResolved = true;
|
|
|
+ cleanup();
|
|
|
+ resolve( value );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ const checkQuery = () => {
|
|
|
+
|
|
|
+ if ( this.isDisposed ) {
|
|
|
+
|
|
|
+ finalizeResolution( this.lastValue );
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ // Check if the GPU timer was disjoint (i.e., timing was unreliable)
|
|
|
+ const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
|
|
|
+ if ( disjoint ) {
|
|
|
+
|
|
|
+ finalizeResolution( this.lastValue );
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
|
|
|
+ if ( ! available ) {
|
|
|
+
|
|
|
+ timeoutId = setTimeout( checkQuery, 1 );
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
|
|
|
+ resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
|
|
|
+
|
|
|
+ } catch ( error ) {
|
|
|
+
|
|
|
+ console.error( 'Error checking query:', error );
|
|
|
+ resolve( this.lastValue );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ checkQuery();
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Releases all resources held by this query pool.
|
|
|
+ * This includes deleting all query objects and clearing internal state.
|
|
|
+ */
|
|
|
+ dispose() {
|
|
|
+
|
|
|
+ if ( this.isDisposed ) {
|
|
|
+
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isDisposed = true;
|
|
|
+
|
|
|
+ if ( ! this.trackTimestamp ) return;
|
|
|
+
|
|
|
+ for ( const query of this.queries ) {
|
|
|
+
|
|
|
+ this.gl.deleteQuery( query );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this.queries = [];
|
|
|
+ this.queryStates.clear();
|
|
|
+ this.queryOffsets.clear();
|
|
|
+ this.lastValue = 0;
|
|
|
+ this.activeQuery = null;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+export default WebGLTimestampQueryPool;
|