WebGLTimestampQueryPool.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import { warnOnce, warn } from '../../../utils.js';
  2. import TimestampQueryPool from '../../common/TimestampQueryPool.js';
  3. /**
  4. * Manages a pool of WebGL timestamp queries for performance measurement.
  5. * Handles creation, execution, and resolution of timer queries using WebGL extensions.
  6. *
  7. * @augments TimestampQueryPool
  8. */
  9. class WebGLTimestampQueryPool extends TimestampQueryPool {
  10. /**
  11. * Creates a new WebGL timestamp query pool.
  12. *
  13. * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - The WebGL context.
  14. * @param {string} type - The type identifier for this query pool.
  15. * @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
  16. */
  17. constructor( gl, type, maxQueries = 2048 ) {
  18. super( maxQueries );
  19. this.gl = gl;
  20. this.type = type;
  21. // Check for timer query extensions
  22. this.ext = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' ) ||
  23. gl.getExtension( 'EXT_disjoint_timer_query' );
  24. if ( ! this.ext ) {
  25. warn( 'EXT_disjoint_timer_query not supported; timestamps will be disabled.' );
  26. this.trackTimestamp = false;
  27. return;
  28. }
  29. // Create query objects
  30. this.queries = [];
  31. for ( let i = 0; i < this.maxQueries; i ++ ) {
  32. this.queries.push( gl.createQuery() );
  33. }
  34. this.activeQuery = null;
  35. this.queryStates = new Map(); // Track state of each query: 'inactive', 'started', 'ended'
  36. }
  37. /**
  38. * Allocates a pair of queries for a given render context.
  39. *
  40. * @param {string} uid - A unique identifier for the render context.
  41. * @returns {?number} The base offset for the allocated queries, or null if allocation failed.
  42. */
  43. allocateQueriesForContext( uid ) {
  44. if ( ! this.trackTimestamp ) return null;
  45. // Check if we have enough space for a new query pair
  46. if ( this.currentQueryIndex + 2 > this.maxQueries ) {
  47. 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() } ).` );
  48. return null;
  49. }
  50. const baseOffset = this.currentQueryIndex;
  51. this.currentQueryIndex += 2;
  52. // Initialize query states
  53. this.queryStates.set( baseOffset, 'inactive' );
  54. this.queryOffsets.set( uid, baseOffset );
  55. return baseOffset;
  56. }
  57. /**
  58. * Begins a timestamp query for the specified render context.
  59. *
  60. * @param {string} uid - A unique identifier for the render context.
  61. */
  62. beginQuery( uid ) {
  63. if ( ! this.trackTimestamp || this.isDisposed ) {
  64. return;
  65. }
  66. const baseOffset = this.queryOffsets.get( uid );
  67. if ( baseOffset == null ) {
  68. return;
  69. }
  70. // Don't start a new query if there's an active one
  71. if ( this.activeQuery !== null ) {
  72. return;
  73. }
  74. const query = this.queries[ baseOffset ];
  75. if ( ! query ) {
  76. return;
  77. }
  78. try {
  79. // Only begin if query is inactive
  80. if ( this.queryStates.get( baseOffset ) === 'inactive' ) {
  81. this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query );
  82. this.activeQuery = baseOffset;
  83. this.queryStates.set( baseOffset, 'started' );
  84. }
  85. } catch ( error ) {
  86. error( 'Error in beginQuery:', error );
  87. this.activeQuery = null;
  88. this.queryStates.set( baseOffset, 'inactive' );
  89. }
  90. }
  91. /**
  92. * Ends the active timestamp query for the specified render context.
  93. *
  94. * @param {string} uid - A unique identifier for the render context.
  95. */
  96. endQuery( uid ) {
  97. if ( ! this.trackTimestamp || this.isDisposed ) {
  98. return;
  99. }
  100. const baseOffset = this.queryOffsets.get( uid );
  101. if ( baseOffset == null ) {
  102. return;
  103. }
  104. // Only end if this is the active query
  105. if ( this.activeQuery !== baseOffset ) {
  106. return;
  107. }
  108. try {
  109. this.gl.endQuery( this.ext.TIME_ELAPSED_EXT );
  110. this.queryStates.set( baseOffset, 'ended' );
  111. this.activeQuery = null;
  112. } catch ( error ) {
  113. error( 'Error in endQuery:', error );
  114. // Reset state on error
  115. this.queryStates.set( baseOffset, 'inactive' );
  116. this.activeQuery = null;
  117. }
  118. }
  119. /**
  120. * Asynchronously resolves all completed queries and returns the total duration.
  121. *
  122. * @async
  123. * @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
  124. */
  125. async resolveQueriesAsync() {
  126. if ( ! this.trackTimestamp || this.pendingResolve ) {
  127. return this.lastValue;
  128. }
  129. this.pendingResolve = true;
  130. try {
  131. // Wait for all ended queries to complete
  132. const resolvePromises = new Map();
  133. for ( const [ uid, baseOffset ] of this.queryOffsets ) {
  134. const state = this.queryStates.get( baseOffset );
  135. if ( state === 'ended' ) {
  136. const query = this.queries[ baseOffset ];
  137. resolvePromises.set( uid, this.resolveQuery( query ) );
  138. }
  139. }
  140. if ( resolvePromises.size === 0 ) {
  141. return this.lastValue;
  142. }
  143. //
  144. const framesDuration = {};
  145. const frames = [];
  146. for ( const [ uid, promise ] of resolvePromises ) {
  147. const match = uid.match( /^(.*):f(\d+)$/ );
  148. const frame = parseInt( match[ 2 ] );
  149. if ( frames.includes( frame ) === false ) {
  150. frames.push( frame );
  151. }
  152. if ( framesDuration[ frame ] === undefined ) framesDuration[ frame ] = 0;
  153. const duration = await promise;
  154. this.timestamps.set( uid, duration );
  155. framesDuration[ frame ] += duration;
  156. }
  157. // Return the total duration of the last frame
  158. const totalDuration = framesDuration[ frames[ frames.length - 1 ] ];
  159. // Store the last valid result
  160. this.lastValue = totalDuration;
  161. this.frames = frames;
  162. // Reset states
  163. this.currentQueryIndex = 0;
  164. this.queryOffsets.clear();
  165. this.queryStates.clear();
  166. this.activeQuery = null;
  167. return totalDuration;
  168. } catch ( error ) {
  169. error( 'Error resolving queries:', error );
  170. return this.lastValue;
  171. } finally {
  172. this.pendingResolve = false;
  173. }
  174. }
  175. /**
  176. * Resolves a single query, checking for completion and disjoint operation.
  177. *
  178. * @async
  179. * @param {WebGLQuery} query - The query object to resolve.
  180. * @returns {Promise<number>} The elapsed time in milliseconds.
  181. */
  182. async resolveQuery( query ) {
  183. return new Promise( ( resolve ) => {
  184. if ( this.isDisposed ) {
  185. resolve( this.lastValue );
  186. return;
  187. }
  188. let timeoutId;
  189. let isResolved = false;
  190. const cleanup = () => {
  191. if ( timeoutId ) {
  192. clearTimeout( timeoutId );
  193. timeoutId = null;
  194. }
  195. };
  196. const finalizeResolution = ( value ) => {
  197. if ( ! isResolved ) {
  198. isResolved = true;
  199. cleanup();
  200. resolve( value );
  201. }
  202. };
  203. const checkQuery = () => {
  204. if ( this.isDisposed ) {
  205. finalizeResolution( this.lastValue );
  206. return;
  207. }
  208. try {
  209. // Check if the GPU timer was disjoint (i.e., timing was unreliable)
  210. const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
  211. if ( disjoint ) {
  212. finalizeResolution( this.lastValue );
  213. return;
  214. }
  215. const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
  216. if ( ! available ) {
  217. timeoutId = setTimeout( checkQuery, 1 );
  218. return;
  219. }
  220. const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
  221. resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
  222. } catch ( error ) {
  223. error( 'Error checking query:', error );
  224. resolve( this.lastValue );
  225. }
  226. };
  227. checkQuery();
  228. } );
  229. }
  230. /**
  231. * Releases all resources held by this query pool.
  232. * This includes deleting all query objects and clearing internal state.
  233. */
  234. dispose() {
  235. if ( this.isDisposed ) {
  236. return;
  237. }
  238. this.isDisposed = true;
  239. if ( ! this.trackTimestamp ) return;
  240. for ( const query of this.queries ) {
  241. this.gl.deleteQuery( query );
  242. }
  243. this.queries = [];
  244. this.queryStates.clear();
  245. this.queryOffsets.clear();
  246. this.lastValue = 0;
  247. this.activeQuery = null;
  248. }
  249. }
  250. export default WebGLTimestampQueryPool;
粤ICP备19079148号