Преглед на файлове

WebGPURenderer: Add timestamp queries support in WebGPU (#27597)

* add timestamp queries support

* add live example

* improve example

* honor async render and move to renderer await functions

* rename to trackTimestamp and cleanupg

* new async standard

* fix tabs

---------
Renaud Rohlinger преди 1 година
родител
ревизия
e43ed970ea

+ 8 - 2
examples/jsm/objects/QuadMesh.js

@@ -37,9 +37,9 @@ class QuadMesh {
 
 	}
 
-	render( renderer ) {
+	async renderAsync( renderer ) {
 
-		renderer.render( this._mesh, _camera );
+		await renderer.renderAsync( this._mesh, _camera );
 
 	}
 
@@ -55,6 +55,12 @@ class QuadMesh {
 
 	}
 
+	get render() {
+
+		return this.renderAsync;
+
+	}
+
 }
 
 export default QuadMesh;

+ 2 - 0
examples/jsm/renderers/common/Backend.js

@@ -90,6 +90,8 @@ class Backend {
 
 	// utils
 
+	resolveTimeStampAsync( renderContext, type ) { }
+
 	hasFeatureAsync( name ) { } // return Boolean
 
 	hasFeature( name ) { } // return Boolean

+ 25 - 1
examples/jsm/renderers/common/Info.js

@@ -16,7 +16,8 @@ class Info {
 		};
 
 		this.compute = {
-			calls: 0
+			calls: 0,
+			computeCalls: 0
 		};
 
 		this.memory = {
@@ -24,6 +25,11 @@ class Info {
 			textures: 0
 		};
 
+		this.timestamp = {
+			compute: 0,
+			render: 0
+		};
+
 	}
 
 	update( object, count, instanceCount ) {
@@ -54,6 +60,20 @@ class Info {
 
 	}
 
+	updateTimestamp( type, time ) {
+
+		this.timestamp[ type ] += time;
+
+	}
+
+	resetCompute() {
+
+		this.compute.computeCalls = 0;
+
+		this.timestamp.compute = 0;
+
+	}
+
 	reset() {
 
 		this.render.drawCalls = 0;
@@ -61,6 +81,8 @@ class Info {
 		this.render.points = 0;
 		this.render.lines = 0;
 
+		this.timestamp.render = 0;
+
 	}
 
 	dispose() {
@@ -72,6 +94,8 @@ class Info {
 		this.render.calls = 0;
 		this.compute.calls = 0;
 
+		this.timestamp.compute = 0;
+		this.timestamp.render = 0;
 		this.memory.geometries = 0;
 		this.memory.textures = 0;
 

+ 2 - 2
examples/jsm/renderers/common/PostProcessing.js

@@ -12,11 +12,11 @@ class PostProcessing {
 
 	}
 
-	render() {
+	async render() {
 
 		quadMesh.material.fragmentNode = this.outputNode;
 
-		quadMesh.render( this.renderer );
+		await quadMesh.render( this.renderer );
 
 	}
 

+ 22 - 2
examples/jsm/renderers/common/Renderer.js

@@ -182,7 +182,7 @@ class Renderer {
 
 	}
 
-	async render( scene, camera ) {
+	async renderAsync( scene, camera ) {
 
 		if ( this._initialized === false ) await this.init();
 
@@ -357,6 +357,9 @@ class Renderer {
 
 		sceneRef.onAfterRender( this, scene, camera, renderTarget );
 
+
+		await this.backend.resolveTimeStampAsync( renderContext, 'render' );
+
 	}
 
 	getMaxAnisotropy() {
@@ -698,7 +701,7 @@ class Renderer {
 
 	}
 
-	async compute( computeNodes ) {
+	async computeAsync( computeNodes ) {
 
 		if ( this._initialized === false ) await this.init();
 
@@ -710,10 +713,12 @@ class Renderer {
 
 		this.info.calls ++;
 		this.info.compute.calls ++;
+		this.info.compute.computeCalls ++;
 
 		nodeFrame.renderId = this.info.calls;
 
 		//
+		if ( this.info.autoReset === true ) this.info.resetCompute();
 
 		const backend = this.backend;
 		const pipelines = this._pipelines;
@@ -765,6 +770,8 @@ class Renderer {
 
 		backend.finishCompute( computeNodes );
 
+		await this.backend.resolveTimeStampAsync( computeNodes, 'compute' );
+
 		//
 
 		nodeFrame.renderId = previousRenderId;
@@ -1037,6 +1044,19 @@ class Renderer {
 
 	}
 
+
+	get compute() {
+
+		return this.computeAsync;
+
+	}
+
+	get render() {
+
+		return this.renderAsync;
+
+	}
+
 }
 
 export default Renderer;

+ 99 - 2
examples/jsm/renderers/webgpu/WebGPUBackend.js

@@ -43,6 +43,8 @@ class WebGPUBackend extends Backend {
 
 		this.parameters.requiredLimits = ( parameters.requiredLimits === undefined ) ? {} : parameters.requiredLimits;
 
+		this.trackTimestamp = ( parameters.trackTimestamp === true );
+
 		this.adapter = null;
 		this.device = null;
 		this.context = null;
@@ -323,6 +325,8 @@ class WebGPUBackend extends Backend {
 
 		}
 
+		this.initTimeStampQuery( renderContext, descriptor );
+
 		descriptor.occlusionQuerySet = occlusionQuerySet;
 
 		const depthStencilAttachment = descriptor.depthStencilAttachment;
@@ -490,8 +494,11 @@ class WebGPUBackend extends Backend {
 
 		}
 
+		this.prepareTimeStampBuffer( renderContext, renderContextData.encoder );
+
 		this.device.queue.submit( [ renderContextData.encoder.finish() ] );
 
+
 		//
 
 		if ( renderContext.textures !== null ) {
@@ -725,8 +732,14 @@ class WebGPUBackend extends Backend {
 
 		const groupGPU = this.get( computeGroup );
 
-		groupGPU.cmdEncoderGPU = this.device.createCommandEncoder( {} );
-		groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass();
+
+		const descriptor = {};
+
+		this.initTimeStampQuery( computeGroup, descriptor );
+
+		groupGPU.cmdEncoderGPU = this.device.createCommandEncoder();
+
+		groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass( descriptor );
 
 	}
 
@@ -753,6 +766,9 @@ class WebGPUBackend extends Backend {
 		const groupData = this.get( computeGroup );
 
 		groupData.passEncoderGPU.end();
+
+		this.prepareTimeStampBuffer( computeGroup, groupData.cmdEncoderGPU );
+
 		this.device.queue.submit( [ groupData.cmdEncoderGPU.finish() ] );
 
 	}
@@ -1014,6 +1030,87 @@ class WebGPUBackend extends Backend {
 
 	}
 
+
+	initTimeStampQuery( renderContext, descriptor ) {
+
+		if ( ! this.hasFeature( GPUFeatureName.TimestampQuery ) || ! this.trackTimestamp ) return;
+
+		const renderContextData = this.get( renderContext );
+
+		if ( ! renderContextData.timeStampQuerySet ) {
+
+			// Create a GPUQuerySet which holds 2 timestamp query results: one for the
+			// beginning and one for the end of compute pass execution.
+			const timeStampQuerySet = this.device.createQuerySet( { type: 'timestamp', count: 2 } );
+
+			const timestampWrites = {
+				querySet: timeStampQuerySet,
+				beginningOfPassWriteIndex: 0, // Write timestamp in index 0 when pass begins.
+				endOfPassWriteIndex: 1, // Write timestamp in index 1 when pass ends.
+			};
+			Object.assign( descriptor, {
+				timestampWrites,
+			} );
+			renderContextData.timeStampQuerySet = timeStampQuerySet;
+
+		}
+
+	}
+
+	// timestamp utils
+
+	prepareTimeStampBuffer( renderContext, encoder ) {
+
+		if ( ! this.hasFeature( GPUFeatureName.TimestampQuery ) || ! this.trackTimestamp ) return;
+
+		const renderContextData = this.get( renderContext );
+
+		const size = 2 * BigInt64Array.BYTES_PER_ELEMENT;
+		const resolveBuffer = this.device.createBuffer( {
+			size,
+			usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
+		} );
+
+		const resultBuffer = this.device.createBuffer( {
+			size,
+			usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+		} );
+
+		encoder.resolveQuerySet( renderContextData.timeStampQuerySet, 0, 2, resolveBuffer, 0 );
+		encoder.copyBufferToBuffer( resolveBuffer, 0, resultBuffer, 0, size );
+
+		renderContextData.currentTimeStampQueryBuffer = resultBuffer;
+
+	}
+
+	async resolveTimeStampAsync( renderContext, type = 'render' ) {
+
+		if ( ! this.hasFeature( GPUFeatureName.TimestampQuery ) || ! this.trackTimestamp ) return;
+
+		const renderContextData = this.get( renderContext );
+
+		// handle timestamp query results
+
+		const { currentTimeStampQueryBuffer } = renderContextData;
+
+		if ( currentTimeStampQueryBuffer ) {
+
+			renderContextData.currentTimeStampQueryBuffer = null;
+
+			await currentTimeStampQueryBuffer.mapAsync( GPUMapMode.READ );
+
+			const times = new BigUint64Array( currentTimeStampQueryBuffer.getMappedRange() );
+
+			const duration = Number( times[ 1 ] - times[ 0 ] ) / 1000000;
+			// console.log( `Compute ${type} duration: ${Number( times[ 1 ] - times[ 0 ] ) / 1000000}ms` );
+			this.renderer.info.updateTimestamp( type, duration );
+
+			currentTimeStampQueryBuffer.unmap();
+
+		}
+
+	}
+
 	// node builder
 
 	createNodeBuilder( object, renderer, scene = null ) {

+ 45 - 5
examples/webgpu_compute_particles.html

@@ -9,6 +9,19 @@
 
 		<div id="info">
 			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> WebGPU - Compute - 1M Particles
+			<div id="timestamps" style="
+				position: absolute;
+				top: 60px;
+				left: 0;
+				padding: 10px;
+				background: rgba( 0, 0, 0, 0.5 );
+				color: #fff;
+				font-family: monospace;
+				font-size: 12px;
+				line-height: 1.5;
+				pointer-events: none;
+				text-align: left;
+			"></div>
 		</div>
 
 		<script type="importmap">
@@ -49,6 +62,8 @@
 			let controls, stats;
 			let computeParticles;
 
+			const timestamps = document.getElementById( 'timestamps' );
+
 			init();
 
 			function init() {
@@ -166,7 +181,7 @@
 
 				//
 
-				renderer = new WebGPURenderer( { antialias: true } );
+				renderer = new WebGPURenderer( { antialias: true, trackTimestamp: true } );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
@@ -177,6 +192,8 @@
 
 				//
 
+				renderer.info.autoReset = false;
+
 				renderer.compute( computeInit );
 
 				// click event
@@ -227,7 +244,6 @@
 				// events
 
 				renderer.domElement.addEventListener( 'pointermove', onMove );
-
 				//
 
 				controls = new OrbitControls( camera, renderer.domElement );
@@ -262,11 +278,35 @@
 
 			}
 
-			function animate() {
+			async function animate() {
 
 				stats.update();
-				renderer.compute( computeParticles );
-				renderer.render( scene, camera );
+
+				await renderer.computeAsync( computeParticles );
+
+				await renderer.renderAsync( scene, camera );
+
+				// throttle the logging
+
+				if ( renderer.hasFeature( 'timestamp-query' ) ) {
+
+					if ( renderer.info.render.calls % 5 === 0 ) {
+
+						timestamps.innerHTML = `
+
+										Compute ${renderer.info.compute.computeCalls} pass in ${renderer.info.timestamp.compute}ms<br>
+										Draw ${renderer.info.render.drawCalls} pass in ${renderer.info.timestamp.render}ms`;
+
+					}
+
+				} else {
+
+					timestamps.innerHTML = 'Timestamp queries not supported';
+
+				}
+
+				renderer.info.resetCompute();
+				renderer.info.reset();
 
 			}