Просмотр исходного кода

WebGPURenderer: Introduce `TimestampQueryPool` (#30359)

* WebGPURenderer: Introduce renderer.resolveAllTimestampsAsync(type)

* cleanup

* update some examples and cleanup

* white space

* test bird screenshot

* fix bird e2e

* revert bird screenshot

* feedbacks

* better to not use async as it affects the loop and slow CPU/frames

* dispose timestampPools, fix dispose of webglstate and fix webgl context never gets disposed

* fix CI

* Update Backend.js

* Update Backend.js

---------

Co-authored-by: Michael Herzog <michael.herzog@human-interactive.org>
Renaud Rohlinger 1 год назад
Родитель
Сommit
806d427f3c

+ 41 - 24
examples/webgpu_compute_birds.html

@@ -28,7 +28,8 @@
 					"three": "../build/three.webgpu.js",
 					"three/webgpu": "../build/three.webgpu.js",
 					"three/tsl": "../build/three.tsl.js",
-					"three/addons/": "./jsm/"
+					"three/addons/": "./jsm/",
+					"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js"
 				}
 			}
 		</script>
@@ -40,7 +41,7 @@
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
-			import Stats from 'three/addons/libs/stats.module.js';
+			import Stats from 'stats-gl';
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 			let container, stats;
@@ -168,7 +169,7 @@
 
 				//
 
-				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer = new THREE.WebGPURenderer( { antialias: true, forceWebGL: false } );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
@@ -310,30 +311,29 @@
 					// Destructure uniforms
 					const { alignment, separation, cohesion, deltaTime, rayOrigin, rayDirection } = effectController;
 
-					const zoneRadius = separation.add( alignment ).add( cohesion );
-					const separationThresh = separation.div( zoneRadius );
-					const alignmentThresh = ( separation.add( alignment ) ).div( zoneRadius );
-					const zoneRadiusSq = zoneRadius.mul( zoneRadius );
+					const zoneRadius = separation.add( alignment ).add( cohesion ).toConst();
+					const separationThresh = separation.div( zoneRadius ).toConst();
+					const alignmentThresh = ( separation.add( alignment ) ).div( zoneRadius ).toConst();
+					const zoneRadiusSq = zoneRadius.mul( zoneRadius ).toConst();
 
-					const position = positionStorage.element( instanceIndex );
-					const velocity = velocityStorage.element( instanceIndex );
-
-					// Add influence of pointer position to velocity.
-					const directionToRay = rayOrigin.sub( position );
-					const projectionLength = dot( directionToRay, rayDirection );
-
-					const closestPoint = rayOrigin.sub( rayDirection.mul( projectionLength ) );
+					// Cache current bird's position and velocity outside the loop
+					const birdIndex = instanceIndex.toConst( 'birdIndex' );
+					const position = positionStorage.element( birdIndex ).toVar();
+					const velocity = velocityStorage.element( birdIndex ).toVar();
 
-					const directionToClosestPoint = closestPoint.sub( position );
-					const distanceToClosestPoint = length( directionToClosestPoint );
-					const distanceToClosestPointSq = distanceToClosestPoint.mul( distanceToClosestPoint );
+					// Add influence of pointer position to velocity using cached position
+					const directionToRay = rayOrigin.sub( position ).toConst();
+					const projectionLength = dot( directionToRay, rayDirection ).toConst();
+					const closestPoint = rayOrigin.sub( rayDirection.mul( projectionLength ) ).toConst();
+					const directionToClosestPoint = closestPoint.sub( position ).toConst();
+					const distanceToClosestPoint = length( directionToClosestPoint ).toConst();
+					const distanceToClosestPointSq = distanceToClosestPoint.mul( distanceToClosestPoint ).toConst();
 
-					const rayRadius = float( 150.0 );
-					const rayRadiusSq = rayRadius.mul( rayRadius );
+					const rayRadius = float( 150.0 ).toConst();
+					const rayRadiusSq = rayRadius.mul( rayRadius ).toConst();
 
 					If( distanceToClosestPointSq.lessThan( rayRadiusSq ), () => {
 
-						// Scale bird velocity inversely with distance from prey radius center.
 						const velocityAdjust = ( distanceToClosestPointSq.div( rayRadiusSq ).sub( 1.0 ) ).mul( deltaTime ).mul( 100.0 );
 						velocity.addAssign( normalize( directionToClosestPoint ).mul( velocityAdjust ) );
 						limit.addAssign( 5.0 );
@@ -347,11 +347,18 @@
 
 					Loop( { start: uint( 0 ), end: uint( BIRDS ), type: 'uint', condition: '<' }, ( { i } ) => {
 
+						If( i.equal( birdIndex ), () => {
+
+							Continue();
+			
+						} );
+
+						// Cache bird's position and velocity
+
 						const birdPosition = positionStorage.element( i );
 						const dirToBird = birdPosition.sub( position );
 						const distToBird = length( dirToBird );
 
-						// Don't apply any changes to velocity if the distance to this bird is negligible.
 						If( distToBird.lessThan( 0.0001 ), () => {
 
 							Continue();
@@ -413,8 +420,10 @@
 
 					} );
 
-				} )().compute( BIRDS );
+					// Write back the final velocity to storage
+					velocityStorage.element( birdIndex ).assign( velocity );
 
+				} )().compute( BIRDS );
 				computePosition = Fn( () => {
 
 					const { deltaTime } = effectController;
@@ -430,7 +439,13 @@
 
 				scene.add( birdMesh );
 
-				stats = new Stats();
+				stats = new Stats( {
+					precision: 3,
+					horizontal: false,
+					trackGPU: true,
+					trackCPT: true
+				} );
+				stats.init( renderer );
 				container.appendChild( stats.dom );
 
 				container.style.touchAction = 'none';
@@ -468,6 +483,7 @@
 			function animate() {
 
 				render();
+				renderer.resolveTimestampsAsync();
 				stats.update();
 
 			}
@@ -489,6 +505,7 @@
 
 				renderer.compute( computeVelocity );
 				renderer.compute( computePosition );
+				renderer.resolveTimestampsAsync( 'compute' );
 				renderer.render( scene, camera );
 
 				// Move pointer away so we only affect birds when moving the mouse

+ 10 - 6
examples/webgpu_compute_particles_snow.html

@@ -14,11 +14,11 @@
 		<script type="importmap">
 			{
 				"imports": {
-					"three": "../build/three.webgpu.js",
-					"three/webgpu": "../build/three.webgpu.js",
+					"three": "../src/Three.WebGPU.js",
+					"three/webgpu": "../src/Three.WebGPU.js",
 					"three/tsl": "../build/three.tsl.js",
 					"three/addons/": "./jsm/",
-					"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@2.2.8/dist/main.js"
+					"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js"
 				}
 			}
 		</script>
@@ -273,7 +273,9 @@
 
 				stats = new Stats( {
 					precision: 3,
-					horizontal: false
+					horizontal: false,
+					trackGPU: true,
+					trackCPT: true
 				} );
 				stats.init( renderer );
 				document.body.appendChild( stats.dom );
@@ -343,11 +345,12 @@
 
 				scene.overrideMaterial = collisionPosMaterial;
 				renderer.setRenderTarget( collisionPosRT );
-				await renderer.renderAsync( scene, collisionCamera );
+				renderer.render( scene, collisionCamera );
 
 				// compute
 
-				await renderer.computeAsync( computeParticles );
+				renderer.compute( computeParticles );
+				renderer.resolveTimestampsAsync( 'compute' );
 
 				// result
 
@@ -356,6 +359,7 @@
 
 				await postProcessing.renderAsync();
 
+				renderer.resolveTimestampsAsync();
 				stats.update();
 
 			}

+ 29 - 2
examples/webgpu_compute_points.html

@@ -17,7 +17,8 @@
 					"three": "../build/three.webgpu.js",
 					"three/webgpu": "../build/three.webgpu.js",
 					"three/tsl": "../build/three.tsl.js",
-					"three/addons/": "./jsm/"
+					"three/addons/": "./jsm/",
+					"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js"
 				}
 			}
 		</script>
@@ -25,11 +26,15 @@
 		<script type="module">
 
 			import * as THREE from 'three';
+
+			import Stats from 'stats-gl';
+
+
 			import { Fn, uniform, instancedArray, float, vec2, vec3, color, instanceIndex } from 'three/tsl';
 
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
-			let camera, scene, renderer;
+			let camera, scene, renderer, stats;
 			let computeNode;
 
 			const pointerVector = new THREE.Vector2( - 10.0, - 10.0 ); // Out of bounds first
@@ -120,6 +125,19 @@
 				renderer.setAnimationLoop( animate );
 				document.body.appendChild( renderer.domElement );
 
+				stats = new Stats( {
+					precision: 4,
+					horizontal: false,
+					trackGPU: true,
+					trackCPT: true,
+					logsPerSecond: 10,
+					graphsPerSecond: 60,
+					samplesGraph: 30,
+				} );
+				stats.init( renderer );
+				document.body.appendChild( stats.dom );
+				stats.dom.style.position = 'absolute';
+
 				window.addEventListener( 'resize', onWindowResize );
 				window.addEventListener( 'mousemove', onMouseMove );
 
@@ -158,8 +176,17 @@
 			function animate() {
 
 				renderer.compute( computeNode );
+				renderer.resolveTimestampsAsync( 'compute' );
+
 				renderer.render( scene, camera );
 
+
+				renderer.resolveTimestampsAsync().then( () => {
+
+					stats.update();
+
+				} );
+
 			}
 
 		</script>

+ 15 - 6
examples/webgpu_performance.html

@@ -20,7 +20,9 @@
 				"imports": {
 					"three": "../build/three.webgpu.js",
 					"three/webgpu": "../build/three.webgpu.js",
-					"three/addons/": "./jsm/"
+					"three/addons/": "./jsm/",
+					"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js"
+
 				}
 			}
 		</script>
@@ -29,7 +31,7 @@
 
 			import * as THREE from 'three';
 
-			import Stats from 'three/addons/libs/stats.module.js';
+			import Stats from 'stats-gl';
 
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
@@ -71,7 +73,7 @@
 				scene = new THREE.Scene();
 
 
-				renderer = new THREE.WebGPURenderer( { antialias: true, forceWebGL: false } );
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
@@ -82,7 +84,13 @@
 
 				//
 
-				stats = new Stats();
+				stats = new Stats( {
+					precision: 3,
+					horizontal: false,
+					trackGPU: true,
+				} );
+				stats.init( renderer );
+
 				document.body.appendChild( stats.dom );
 
 				new RGBELoader()
@@ -149,9 +157,10 @@
 
 			//
 
-			function render() {
+			async function render() {
 
-				renderer.renderAsync( scene, camera );
+				await renderer.renderAsync( scene, camera );
+				renderer.resolveTimestampsAsync( 'render' );
 
 				stats.update();
 

+ 7 - 4
examples/webgpu_performance_renderbundle.html

@@ -29,10 +29,10 @@
 		{
 			"imports": {
 				"three": "../build/three.webgpu.js",
-					"three/webgpu": "../build/three.webgpu.js",
+				"three/webgpu": "../build/three.webgpu.js",
 				"three/tsl": "../build/three.tsl.js",
 				"three/addons/": "./jsm/",
-				"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@2.2.7/dist/main.js"
+				"stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js"
 			}
 		}
 	</script>
@@ -229,9 +229,10 @@
 
 			stats = new Stats( {
 				precision: 3,
-				horizontal: false
+				horizontal: false,
+				trackGPU: true,
 			} );
-			// stats.init( renderer );
+			stats.init( renderer );
 			document.body.appendChild( stats.dom );
 			stats.dom.style.position = 'absolute';
 
@@ -288,6 +289,8 @@
 				if ( renderTimeAverages.length > 60 ) renderTimeAverages.shift();
 		
 				const average = renderTimeAverages.reduce( ( a, b ) => a + b, 0 ) / renderTimeAverages.length;
+
+				renderer.resolveTimestampsAsync();
 				stats.update();
 
 				document.getElementById( 'backend' ).innerText = `Average Render Time ${api.renderBundle ? '(Bundle)' : ''}: ` + average.toFixed( 2 ) + 'ms';

+ 2 - 2
src/nodes/accessors/Arrays.js

@@ -9,7 +9,7 @@ import { getLengthFromType, getTypedArrayFromType } from '../core/NodeUtils.js';
  * TSL function for creating a storage buffer node with a configured `StorageBufferAttribute`.
  *
  * @function
- * @param {Number} count - The data count.
+ * @param {Number|TypedArray} count - The data count.
  * @param {String} [type='float'] - The data type.
  * @returns {StorageBufferNode}
  */
@@ -29,7 +29,7 @@ export const attributeArray = ( count, type = 'float' ) => {
  * TSL function for creating a storage buffer node with a configured `StorageInstancedBufferAttribute`.
  *
  * @function
- * @param {Number} count - The data count.
+ * @param {Number|TypedArray} count - The data count.
  * @param {String} [type='float'] - The data type.
  * @returns {StorageBufferNode}
  */

+ 37 - 5
src/renderers/common/Backend.js

@@ -3,7 +3,7 @@ let _color4 = null;
 
 import Color4 from './Color4.js';
 import { Vector2 } from '../../math/Vector2.js';
-import { createCanvasElement } from '../../utils.js';
+import { createCanvasElement, warnOnce } from '../../utils.js';
 import { REVISION } from '../../constants.js';
 
 /**
@@ -58,6 +58,16 @@ class Backend {
 		 */
 		this.domElement = null;
 
+		/**
+		 * A reference to the timestamp query pool.
+   		 *
+   		 * @type {{render: TimestampQueryPool?, compute: TimestampQueryPool?}}
+		 */
+		this.timestampQueryPool = {
+			'render': null,
+			'compute': null
+		};
+
 	}
 
 	/**
@@ -437,11 +447,33 @@ class Backend {
 	 *
 	 * @async
 	 * @abstract
-	 * @param {RenderContext} renderContext - The render context.
-	 * @param {String} type - The render context.
-	 * @return {Promise} A Promise that resolves when the time stamp has been computed.
+	 * @param {String} [type='render'] - The type of the time stamp.
+	 * @return {Promise<Number>} A Promise that resolves with the time stamp.
 	 */
-	async resolveTimestampAsync( /*renderContext, type*/ ) { }
+	async resolveTimestampsAsync( type = 'render' ) {
+
+		if ( ! this.trackTimestamp ) {
+
+			warnOnce( 'WebGPURenderer: Timestamp tracking is disabled.' );
+			return;
+
+		}
+
+		const queryPool = this.timestampQueryPool[ type ];
+		if ( ! queryPool ) {
+
+			warnOnce( `WebGPURenderer: No timestamp query pool for type '${type}' found.` );
+			return;
+
+		}
+
+		const duration = await queryPool.resolveQueriesAsync();
+
+		this.renderer.info[ type ].timestamp = duration;
+
+		return duration;
+
+	}
 
 	/**
 	 * Can be used to synchronize CPU operations with GPU tasks. So when this method is called,

+ 1 - 45
src/renderers/common/Info.js

@@ -52,9 +52,7 @@ class Info {
 		 * @property {Number} triangles - The number of rendered triangle primitives of the current frame.
 		 * @property {Number} points - The number of rendered point primitives of the current frame.
 		 * @property {Number} lines - The number of rendered line primitives of the current frame.
-		 * @property {Number} previousFrameCalls - The number of render calls of the previous frame.
 		 * @property {Number} timestamp - The timestamp of the frame when using `renderer.renderAsync()`.
-		 * @property {Number} timestampCalls - The number of render calls using `renderer.renderAsync()`.
 		 */
 		this.render = {
 			calls: 0,
@@ -64,8 +62,6 @@ class Info {
 			points: 0,
 			lines: 0,
 			timestamp: 0,
-			previousFrameCalls: 0,
-			timestampCalls: 0
 		};
 
 		/**
@@ -75,16 +71,12 @@ class Info {
 		 * @readonly
 		 * @property {Number} calls - The number of compute calls since the app has been started.
 		 * @property {Number} frameCalls - The number of compute calls of the current frame.
-		 * @property {Number} previousFrameCalls - The number of compute calls of the previous frame.
 		 * @property {Number} timestamp - The timestamp of the frame when using `renderer.computeAsync()`.
-		 * @property {Number} timestampCalls - The number of render calls using `renderer.computeAsync()`.
 		 */
 		this.compute = {
 			calls: 0,
 			frameCalls: 0,
-			timestamp: 0,
-			previousFrameCalls: 0,
-			timestampCalls: 0
+			timestamp: 0
 		};
 
 		/**
@@ -137,47 +129,11 @@ class Info {
 
 	}
 
-	/**
-	 * Used by async render methods to updated timestamp metrics.
-	 *
-	 * @param {('render'|'compute')} type - The type of render call.
-	 * @param {Number} time - The duration of the compute/render call in milliseconds.
-	 */
-	updateTimestamp( type, time ) {
-
-		if ( this[ type ].timestampCalls === 0 ) {
-
-			this[ type ].timestamp = 0;
-
-		}
-
-
-		this[ type ].timestamp += time;
-
-		this[ type ].timestampCalls ++;
-
-
-		if ( this[ type ].timestampCalls >= this[ type ].previousFrameCalls ) {
-
-			this[ type ].timestampCalls = 0;
-
-		}
-
-
-	}
-
 	/**
 	 * Resets frame related metrics.
 	 */
 	reset() {
 
-		const previousRenderFrameCalls = this.render.frameCalls;
-		this.render.previousFrameCalls = previousRenderFrameCalls;
-
-		const previousComputeFrameCalls = this.compute.frameCalls;
-		this.compute.previousFrameCalls = previousComputeFrameCalls;
-
-
 		this.render.drawCalls = 0;
 		this.render.frameCalls = 0;
 		this.compute.frameCalls = 0;

+ 15 - 5
src/renderers/common/Renderer.js

@@ -932,9 +932,7 @@ class Renderer {
 
 		if ( this._initialized === false ) await this.init();
 
-		const renderContext = this._renderScene( scene, camera );
-
-		await this.backend.resolveTimestampAsync( renderContext, 'render' );
+		this._renderScene( scene, camera );
 
 	}
 
@@ -2028,6 +2026,12 @@ class Renderer {
 		this._renderContexts.dispose();
 		this._textures.dispose();
 
+		Object.values( this.backend.timestampQueryPool ).forEach( queryPool => {
+
+			if ( queryPool !== null ) queryPool.dispose();
+
+		} );
+
 		this.setRenderTarget( null );
 		this.setAnimationLoop( null );
 
@@ -2215,8 +2219,6 @@ class Renderer {
 
 		this.compute( computeNodes );
 
-		await this.backend.resolveTimestampAsync( computeNodes, 'compute' );
-
 	}
 
 	/**
@@ -2234,6 +2236,14 @@ class Renderer {
 
 	}
 
+	async resolveTimestampsAsync( type = 'render' ) {
+
+		if ( this._initialized === false ) await this.init();
+
+		return this.backend.resolveTimestampsAsync( type );
+
+	}
+
 	/**
 	 * Checks if the given feature is supported by the selected backend. If the
 	 * renderer has not been initialized, this method always returns `false`.

+ 39 - 0
src/renderers/common/TimestampQueryPool.js

@@ -0,0 +1,39 @@
+class TimestampQueryPool {
+
+	constructor( maxQueries = 256 ) {
+
+		this.trackTimestamp = true;
+		this.maxQueries = maxQueries;
+		this.currentQueryIndex = 0; // how many queries allocated so far
+		this.queryOffsets = new Map(); // track offsets for different contexts
+		this.isDisposed = false;
+		this.lastValue = 0;
+		this.pendingResolve = false;
+
+	}
+
+	/**
+     * Allocate queries for a specific renderContext.
+	 *
+	 * @abstract
+     */
+	allocateQueriesForContext( /* renderContext */ ) {}
+
+	/**
+     * Resolve all timestamps and return data (or process them).
+	 *
+	 * @abstract
+	 * @returns {Promise<Number>|Number} The resolved timestamp value.
+     */
+	async resolveQueriesAsync() {}
+
+	/**
+	 * Dispose of the query pool.
+	 *
+	 * @abstract
+	 */
+	dispose() {}
+
+}
+
+export default TimestampQueryPool;

+ 15 - 69
src/renderers/webgl-fallback/WebGLBackend.js

@@ -13,6 +13,7 @@ import { WebGLBufferRenderer } from './WebGLBufferRenderer.js';
 
 import { warnOnce } from '../../utils.js';
 import { WebGLCoordinateSystem } from '../../constants.js';
+import WebGLTimestampQueryPool from './utils/WebGLTimestampQueryPool.js';
 
 /**
  * A backend implementation targeting WebGL 2.
@@ -354,29 +355,22 @@ class WebGLBackend extends Backend {
 
 		if ( ! this.disjoint || ! this.trackTimestamp ) return;
 
-		const renderContextData = this.get( renderContext );
+		const type = renderContext.isComputeNode ? 'compute' : 'render';
 
-		if ( this.queryRunning ) {
+		if ( ! this.timestampQueryPool[ type ] ) {
 
-		  if ( ! renderContextData.queryQueue ) renderContextData.queryQueue = [];
-		  renderContextData.queryQueue.push( renderContext );
-		  return;
+			// TODO: Variable maxQueries?
+			this.timestampQueryPool[ type ] = new WebGLTimestampQueryPool( this.gl, type, 2048 );
 
 		}
 
-		if ( renderContextData.activeQuery ) {
-
-		  this.gl.endQuery( this.disjoint.TIME_ELAPSED_EXT );
-		  renderContextData.activeQuery = null;
+		const timestampQueryPool = this.timestampQueryPool[ type ];
 
-		}
+		const baseOffset = timestampQueryPool.allocateQueriesForContext( renderContext );
 
-		renderContextData.activeQuery = this.gl.createQuery();
+		if ( baseOffset !== null ) {
 
-		if ( renderContextData.activeQuery !== null ) {
-
-		  this.gl.beginQuery( this.disjoint.TIME_ELAPSED_EXT, renderContextData.activeQuery );
-		  this.queryRunning = true;
+			timestampQueryPool.beginQuery( renderContext );
 
 		}
 
@@ -393,64 +387,13 @@ class WebGLBackend extends Backend {
 
 		if ( ! this.disjoint || ! this.trackTimestamp ) return;
 
-		const renderContextData = this.get( renderContext );
-
-		if ( renderContextData.activeQuery ) {
-
-		  this.gl.endQuery( this.disjoint.TIME_ELAPSED_EXT );
-
-		  if ( ! renderContextData.gpuQueries ) renderContextData.gpuQueries = [];
-		  renderContextData.gpuQueries.push( { query: renderContextData.activeQuery } );
-		  renderContextData.activeQuery = null;
-		  this.queryRunning = false;
-
-		  if ( renderContextData.queryQueue && renderContextData.queryQueue.length > 0 ) {
+		const type = renderContext.isComputeNode ? 'compute' : 'render';
+		const timestampQueryPool = this.timestampQueryPool[ type ];
 
-				const nextRenderContext = renderContextData.queryQueue.shift();
-				this.initTimestampQuery( nextRenderContext );
-
-			}
-
-		}
+		timestampQueryPool.endQuery( renderContext );
 
 	}
 
-	/**
-	 * Resolves the time stamp for the given render context and type.
-	 *
-	 * @async
-	 * @param {RenderContext} renderContext - The render context.
-	 * @param {String} type - The render context.
-	 * @return {Promise} A Promise that resolves when the time stamp has been computed.
-	 */
-	async resolveTimestampAsync( renderContext, type = 'render' ) {
-
-		if ( ! this.disjoint || ! this.trackTimestamp ) return;
-
-		const renderContextData = this.get( renderContext );
-
-		if ( ! renderContextData.gpuQueries ) renderContextData.gpuQueries = [];
-
-		for ( let i = 0; i < renderContextData.gpuQueries.length; i ++ ) {
-
-		  const queryInfo = renderContextData.gpuQueries[ i ];
-		  const available = this.gl.getQueryParameter( queryInfo.query, this.gl.QUERY_RESULT_AVAILABLE );
-		  const disjoint = this.gl.getParameter( this.disjoint.GPU_DISJOINT_EXT );
-
-		  if ( available && ! disjoint ) {
-
-				const elapsed = this.gl.getQueryParameter( queryInfo.query, this.gl.QUERY_RESULT );
-				const duration = Number( elapsed ) / 1000000; // Convert nanoseconds to milliseconds
-				this.gl.deleteQuery( queryInfo.query );
-				renderContextData.gpuQueries.splice( i, 1 ); // Remove the processed query
-				i --;
-				this.renderer.info.updateTimestamp( type, duration );
-
-			}
-
-		}
-
-	}
 
 	/**
 	 * Returns the backend's rendering context.
@@ -2358,6 +2301,9 @@ class WebGLBackend extends Backend {
 	 */
 	dispose() {
 
+		const extension = this.extensions.get( 'WEBGL_lose_context' );
+		if ( extension ) extension.loseContext();
+
 		this.renderer.domElement.removeEventListener( 'webglcontextlost', this._onContextLost );
 
 	}

+ 2 - 7
src/renderers/webgl-fallback/utils/WebGLState.js

@@ -8,7 +8,7 @@ import {
 } from '../../../constants.js';
 import { Vector4 } from '../../../math/Vector4.js';
 
-let initialized = false, equationToGL, factorToGL;
+let equationToGL, factorToGL;
 
 /**
  * A WebGL 2 backend utility module for managing the WebGL state.
@@ -81,13 +81,8 @@ class WebGLState {
 		this.currentBoundTextures = {};
 		this.currentBoundBufferBases = {};
 
-		if ( initialized === false ) {
 
-			this._init();
-
-			initialized = true;
-
-		}
+		this._init();
 
 	}
 

+ 357 - 0
src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js

@@ -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;

+ 12 - 94
src/renderers/webgpu/WebGPUBackend.js

@@ -14,6 +14,7 @@ import WebGPUPipelineUtils from './utils/WebGPUPipelineUtils.js';
 import WebGPUTextureUtils from './utils/WebGPUTextureUtils.js';
 
 import { WebGPUCoordinateSystem } from '../../constants.js';
+import WebGPUTimestampQueryPool from './utils/WebGPUTimestampQueryPool.js';
 
 /**
  * A backend implementation targeting WebGPU.
@@ -716,8 +717,6 @@ class WebGPUBackend extends Backend {
 
 		}
 
-		this.prepareTimestampBuffer( renderContext, renderContextData.encoder );
-
 		this.device.queue.submit( [ renderContextData.encoder.finish() ] );
 
 
@@ -1054,8 +1053,6 @@ class WebGPUBackend extends Backend {
 
 		groupData.passEncoderGPU.end();
 
-		this.prepareTimestampBuffer( computeGroup, groupData.cmdEncoderGPU );
-
 		this.device.queue.submit( [ groupData.cmdEncoderGPU.finish() ] );
 
 	}
@@ -1532,106 +1529,27 @@ class WebGPUBackend extends Backend {
 
 		if ( ! this.trackTimestamp ) return;
 
-		const renderContextData = this.get( renderContext );
-
-		// init query set if not exists
-
-		if ( ! renderContextData.timestampQuerySet ) {
-
-			const type = renderContext.isComputeNode ? 'compute' : 'render';
-
-			renderContextData.timestampQuerySet = this.device.createQuerySet( { type: 'timestamp', count: 2, label: `timestamp_${type}_${renderContext.id}` } );
+		const type = renderContext.isComputeNode ? 'compute' : 'render';
 
-		}
-
-		// augment descriptor
-
-		descriptor.timestampWrites = {
-			querySet: renderContextData.timestampQuerySet,
-			beginningOfPassWriteIndex: 0, // Write timestamp in index 0 when pass begins.
-			endOfPassWriteIndex: 1, // Write timestamp in index 1 when pass ends.
-		};
-
-	}
+		if ( ! this.timestampQueryPool[ type ] ) {
 
-	/**
-	 * Prepares the timestamp buffer.
-	 *
-	 * @param {RenderContext} renderContext - The render context.
-	 * @param {GPUCommandEncoder} encoder - The command encoder.
-	 */
-	prepareTimestampBuffer( renderContext, encoder ) {
-
-		if ( ! this.trackTimestamp ) return;
-
-		const renderContextData = this.get( renderContext );
-
-
-		const size = 2 * BigUint64Array.BYTES_PER_ELEMENT;
-
-		if ( renderContextData.currentTimestampQueryBuffers === undefined ) {
-
-			renderContextData.currentTimestampQueryBuffers = {
-				resolveBuffer: this.device.createBuffer( {
-					label: 'timestamp resolve buffer',
-					size: size,
-					usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
-				} ),
-				resultBuffer: this.device.createBuffer( {
-					label: 'timestamp result buffer',
-					size: size,
-					usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
-				} )
-			};
+			// TODO: Variable maxQueries?
+			this.timestampQueryPool[ type ] = new WebGPUTimestampQueryPool( this.device, type, 2048 );
 
 		}
 
-		const { resolveBuffer, resultBuffer } = renderContextData.currentTimestampQueryBuffers;
+		const timestampQueryPool = this.timestampQueryPool[ type ];
 
+		const baseOffset = timestampQueryPool.allocateQueriesForContext( renderContext );
 
-		encoder.resolveQuerySet( renderContextData.timestampQuerySet, 0, 2, resolveBuffer, 0 );
-
-		if ( resultBuffer.mapState === 'unmapped' ) {
-
-			encoder.copyBufferToBuffer( resolveBuffer, 0, resultBuffer, 0, size );
-
-		}
+		descriptor.timestampWrites = {
+			querySet: timestampQueryPool.querySet,
+			beginningOfPassWriteIndex: baseOffset,
+			endOfPassWriteIndex: baseOffset + 1,
+		  };
 
 	}
 
-	/**
-	 * Resolves the time stamp for the given render context and type.
-	 *
-	 * @async
-	 * @param {RenderContext} renderContext - The render context.
-	 * @param {String} type - The render context.
-	 * @return {Promise} A Promise that resolves when the time stamp has been computed.
-	 */
-	async resolveTimestampAsync( renderContext, type = 'render' ) {
-
-		if ( ! this.trackTimestamp ) return;
-
-		const renderContextData = this.get( renderContext );
-
-		if ( renderContextData.currentTimestampQueryBuffers === undefined ) return;
-
-		const { resultBuffer } = renderContextData.currentTimestampQueryBuffers;
-
-		if ( resultBuffer.mapState === 'unmapped' ) {
-
-			await resultBuffer.mapAsync( GPUMapMode.READ );
-
-			const times = new BigUint64Array( resultBuffer.getMappedRange() );
-			const duration = Number( times[ 1 ] - times[ 0 ] ) / 1000000;
-
-
-			this.renderer.info.updateTimestamp( type, duration );
-
-			resultBuffer.unmap();
-
-		}
-
-	}
 
 	// node builder
 

+ 274 - 0
src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js

@@ -0,0 +1,274 @@
+import { warnOnce } from '../../../utils.js';
+import TimestampQueryPool from '../../common/TimestampQueryPool.js';
+
+/**
+ * Manages a pool of WebGPU timestamp queries for performance measurement.
+ * Extends the base TimestampQueryPool to provide WebGPU-specific implementation.
+ * @extends TimestampQueryPool
+ */
+class WebGPUTimestampQueryPool extends TimestampQueryPool {
+
+	/**
+     * Creates a new WebGPU timestamp query pool.
+     * @param {GPUDevice} device - The WebGPU device to create queries on.
+     * @param {string} type - The type identifier for this query pool.
+     * @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
+     */
+	constructor( device, type, maxQueries = 2048 ) {
+
+		super( maxQueries );
+		this.device = device;
+		this.type = type;
+
+		this.querySet = this.device.createQuerySet( {
+			type: 'timestamp',
+			count: this.maxQueries,
+			label: `queryset_global_timestamp_${type}`
+		} );
+
+		const bufferSize = this.maxQueries * 8;
+		this.resolveBuffer = this.device.createBuffer( {
+			label: `buffer_timestamp_resolve_${type}`,
+			size: bufferSize,
+			usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
+		} );
+
+		this.resultBuffer = this.device.createBuffer( {
+			label: `buffer_timestamp_result_${type}`,
+			size: bufferSize,
+			usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
+		} );
+
+	}
+
+	/**
+     * 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 || this.isDisposed ) return null;
+
+		if ( this.currentQueryIndex + 2 > this.maxQueries ) {
+
+			warnOnce( 'WebGPUTimestampQueryPool: Maximum number of queries exceeded.' );
+			return null;
+
+		}
+
+		const baseOffset = this.currentQueryIndex;
+		this.currentQueryIndex += 2;
+
+		this.queryOffsets.set( renderContext.id, baseOffset );
+		return baseOffset;
+
+	}
+
+	/**
+     * Asynchronously resolves all pending queries and returns the total duration.
+     * If there's already a pending resolve operation, returns that promise instead.
+     * @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
+     */
+	async resolveQueriesAsync() {
+
+		if ( ! this.trackTimestamp || this.currentQueryIndex === 0 || this.isDisposed ) {
+
+			return this.lastValue;
+
+		}
+
+		if ( this.pendingResolve ) {
+
+			return this.pendingResolve;
+
+		}
+
+		this.pendingResolve = this._resolveQueries();
+
+		try {
+
+			const result = await this.pendingResolve;
+			return result;
+
+		} finally {
+
+			this.pendingResolve = null;
+
+		}
+
+	}
+
+	/**
+     * Internal method to resolve queries and calculate total duration.
+     * @private
+     * @returns {Promise<number>} The total duration in milliseconds.
+     */
+	async _resolveQueries() {
+
+		if ( this.isDisposed ) {
+
+			return this.lastValue;
+
+		}
+
+		try {
+
+			if ( this.resultBuffer.mapState !== 'unmapped' ) {
+
+				return this.lastValue;
+
+			}
+
+			const currentOffsets = new Map( this.queryOffsets );
+			const queryCount = this.currentQueryIndex;
+			const bytesUsed = queryCount * 8;
+
+			// Reset state before GPU work
+			this.currentQueryIndex = 0;
+			this.queryOffsets.clear();
+
+			const commandEncoder = this.device.createCommandEncoder();
+
+			commandEncoder.resolveQuerySet(
+				this.querySet,
+				0,
+				queryCount,
+				this.resolveBuffer,
+				0
+			);
+
+			commandEncoder.copyBufferToBuffer(
+				this.resolveBuffer,
+				0,
+				this.resultBuffer,
+				0,
+				bytesUsed
+			);
+
+			const commandBuffer = commandEncoder.finish();
+			this.device.queue.submit( [ commandBuffer ] );
+
+			if ( this.resultBuffer.mapState !== 'unmapped' ) {
+
+				return this.lastValue;
+
+			}
+
+			// Create and track the mapping operation
+			await this.resultBuffer.mapAsync( GPUMapMode.READ, 0, bytesUsed );
+
+			if ( this.isDisposed ) {
+
+				if ( this.resultBuffer.mapState === 'mapped' ) {
+
+					this.resultBuffer.unmap();
+
+				}
+
+				return this.lastValue;
+
+			}
+
+			const times = new BigUint64Array( this.resultBuffer.getMappedRange( 0, bytesUsed ) );
+			let totalDuration = 0;
+
+			for ( const [ , baseOffset ] of currentOffsets ) {
+
+				const startTime = times[ baseOffset ];
+				const endTime = times[ baseOffset + 1 ];
+				const duration = Number( endTime - startTime ) / 1e6;
+				totalDuration += duration;
+
+			}
+
+			this.resultBuffer.unmap();
+			this.lastValue = totalDuration;
+
+			return totalDuration;
+
+		} catch ( error ) {
+
+			console.error( 'Error resolving queries:', error );
+			if ( this.resultBuffer.mapState === 'mapped' ) {
+
+				this.resultBuffer.unmap();
+
+			}
+
+			return this.lastValue;
+
+		}
+
+	}
+
+	async dispose() {
+
+		if ( this.isDisposed ) {
+
+			return;
+
+		}
+
+		this.isDisposed = true;
+
+		// Wait for pending resolve operation
+		if ( this.pendingResolve ) {
+
+			try {
+
+				await this.pendingResolve;
+
+			} catch ( error ) {
+
+				console.error( 'Error waiting for pending resolve:', error );
+
+			}
+
+		}
+
+		// Ensure buffer is unmapped before destroying
+		if ( this.resultBuffer && this.resultBuffer.mapState === 'mapped' ) {
+
+			try {
+
+				this.resultBuffer.unmap();
+
+			} catch ( error ) {
+
+				console.error( 'Error unmapping buffer:', error );
+
+			}
+
+		}
+
+		// Destroy resources
+		if ( this.querySet ) {
+
+			this.querySet.destroy();
+			this.querySet = null;
+
+		}
+
+		if ( this.resolveBuffer ) {
+
+			this.resolveBuffer.destroy();
+			this.resolveBuffer = null;
+
+		}
+
+		if ( this.resultBuffer ) {
+
+			this.resultBuffer.destroy();
+			this.resultBuffer = null;
+
+		}
+
+		this.queryOffsets.clear();
+		this.pendingResolve = null;
+
+	}
+
+}
+
+export default WebGPUTimestampQueryPool;

粤ICP备19079148号