| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- import { warnOnce, warn } from '../../../utils.js';
- 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.
- *
- * @augments 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 ) {
- 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 {string} uid - A unique identifier for the render context.
- * @returns {?number} The base offset for the allocated queries, or null if allocation failed.
- */
- allocateQueriesForContext( uid ) {
- if ( ! this.trackTimestamp ) return null;
- // Check if we have enough space for a new query pair
- if ( this.currentQueryIndex + 2 > this.maxQueries ) {
- warnOnce( `WebGPUTimestampQueryPool [${ this.type }]: Maximum number of queries exceeded, when using trackTimestamp it is necessary to resolves the queries via renderer.resolveTimestampsAsync( THREE.TimestampQuery.${ this.type.toUpperCase() } ).` );
- return null;
- }
- const baseOffset = this.currentQueryIndex;
- this.currentQueryIndex += 2;
- // Initialize query states
- this.queryStates.set( baseOffset, 'inactive' );
- this.queryOffsets.set( uid, baseOffset );
- return baseOffset;
- }
- /**
- * Begins a timestamp query for the specified render context.
- *
- * @param {string} uid - A unique identifier for the render context.
- */
- beginQuery( uid ) {
- if ( ! this.trackTimestamp || this.isDisposed ) {
- return;
- }
- const baseOffset = this.queryOffsets.get( uid );
- 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 ) {
- error( 'Error in beginQuery:', error );
- this.activeQuery = null;
- this.queryStates.set( baseOffset, 'inactive' );
- }
- }
- /**
- * Ends the active timestamp query for the specified render context.
- *
- * @param {string} uid - A unique identifier for the render context.
- */
- endQuery( uid ) {
- if ( ! this.trackTimestamp || this.isDisposed ) {
- return;
- }
- const baseOffset = this.queryOffsets.get( uid );
- 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 ) {
- 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.
- *
- * @async
- * @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 = new Map();
- for ( const [ uid, baseOffset ] of this.queryOffsets ) {
- const state = this.queryStates.get( baseOffset );
- if ( state === 'ended' ) {
- const query = this.queries[ baseOffset ];
- resolvePromises.set( uid, this.resolveQuery( query ) );
- }
- }
- if ( resolvePromises.size === 0 ) {
- return this.lastValue;
- }
- //
- const framesDuration = {};
- const frames = [];
- for ( const [ uid, promise ] of resolvePromises ) {
- const match = uid.match( /^(.*):f(\d+)$/ );
- const frame = parseInt( match[ 2 ] );
- if ( frames.includes( frame ) === false ) {
- frames.push( frame );
- }
- if ( framesDuration[ frame ] === undefined ) framesDuration[ frame ] = 0;
- const duration = await promise;
- this.timestamps.set( uid, duration );
- framesDuration[ frame ] += duration;
- }
- // Return the total duration of the last frame
- const totalDuration = framesDuration[ frames[ frames.length - 1 ] ];
- // Store the last valid result
- this.lastValue = totalDuration;
- this.frames = frames;
- // Reset states
- this.currentQueryIndex = 0;
- this.queryOffsets.clear();
- this.queryStates.clear();
- this.activeQuery = null;
- return totalDuration;
- } catch ( error ) {
- error( 'Error resolving queries:', error );
- return this.lastValue;
- } finally {
- this.pendingResolve = false;
- }
- }
- /**
- * Resolves a single query, checking for completion and disjoint operation.
- *
- * @async
- * @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 ) {
- 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;
|