@@ -6,7 +6,27 @@ import updateSpritesWGSL from './updateSprites.wgsl';
6
6
const init : SampleInit = async ( { canvas, pageState, gui } ) => {
7
7
const adapter = await navigator . gpu . requestAdapter ( ) ;
8
8
assert ( adapter , 'requestAdapter returned null' ) ;
9
- const device = await adapter . requestDevice ( ) ;
9
+
10
+ const hasTimestampQuery = adapter . features . has ( 'timestamp-query' ) ;
11
+ const device = await adapter . requestDevice ( {
12
+ requiredFeatures : hasTimestampQuery ? [ 'timestamp-query' ] : [ ] ,
13
+ } ) ;
14
+
15
+ const perfDisplayContainer = document . createElement ( 'div' ) ;
16
+ perfDisplayContainer . style . color = 'white' ;
17
+ perfDisplayContainer . style . background = 'black' ;
18
+ perfDisplayContainer . style . position = 'absolute' ;
19
+ perfDisplayContainer . style . top = '10px' ;
20
+ perfDisplayContainer . style . left = '10px' ;
21
+ perfDisplayContainer . style . textAlign = 'left' ;
22
+
23
+ const perfDisplay = document . createElement ( 'pre' ) ;
24
+ perfDisplayContainer . appendChild ( perfDisplay ) ;
25
+ if ( canvas . parentNode ) {
26
+ canvas . parentNode . appendChild ( perfDisplayContainer ) ;
27
+ } else {
28
+ console . error ( 'canvas.parentNode is null' ) ;
29
+ }
10
30
11
31
if ( ! pageState . active ) return ;
12
32
const context = canvas . getContext ( 'webgpu' ) as GPUCanvasContext ;
@@ -86,7 +106,7 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
86
106
} ,
87
107
} ) ;
88
108
89
- const renderPassDescriptor = {
109
+ const renderPassDescriptor : GPURenderPassDescriptor = {
90
110
colorAttachments : [
91
111
{
92
112
view : undefined as GPUTextureView , // Assigned later
@@ -97,6 +117,38 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
97
117
] ,
98
118
} ;
99
119
120
+ const computePassDescriptor : GPUComputePassDescriptor = { } ;
121
+
122
+ /** Storage for timestamp query results */
123
+ let querySet : GPUQuerySet | undefined = undefined ;
124
+ /** Timestamps are resolved into this buffer */
125
+ let resolveBuffer : GPUBuffer | undefined = undefined ;
126
+ /** Pool of spare buffers for MAP_READing the timestamps back to CPU. A buffer
127
+ * is taken from the pool (if available) when a readback is needed, and placed
128
+ * back into the pool once the readback is done and it's unmapped. */
129
+ const spareResultBuffers = [ ] ;
130
+
131
+ if ( hasTimestampQuery ) {
132
+ querySet = device . createQuerySet ( {
133
+ type : 'timestamp' ,
134
+ count : 4 ,
135
+ } ) ;
136
+ resolveBuffer = device . createBuffer ( {
137
+ size : 4 * BigInt64Array . BYTES_PER_ELEMENT ,
138
+ usage : GPUBufferUsage . QUERY_RESOLVE | GPUBufferUsage . COPY_SRC ,
139
+ } ) ;
140
+ computePassDescriptor . timestampWrites = {
141
+ querySet,
142
+ beginningOfPassWriteIndex : 0 ,
143
+ endOfPassWriteIndex : 1 ,
144
+ } ;
145
+ renderPassDescriptor . timestampWrites = {
146
+ querySet,
147
+ beginningOfPassWriteIndex : 2 ,
148
+ endOfPassWriteIndex : 3 ,
149
+ } ;
150
+ }
151
+
100
152
// prettier-ignore
101
153
const vertexBufferData = new Float32Array ( [
102
154
- 0.01 , - 0.02 , 0.01 ,
@@ -207,6 +259,8 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
207
259
}
208
260
209
261
let t = 0 ;
262
+ let computePassDurationSum = 0 ;
263
+ let renderPassDurationSum = 0 ;
210
264
function frame ( ) {
211
265
// Sample is no longer the active page.
212
266
if ( ! pageState . active ) return ;
@@ -217,7 +271,9 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
217
271
218
272
const commandEncoder = device . createCommandEncoder ( ) ;
219
273
{
220
- const passEncoder = commandEncoder . beginComputePass ( ) ;
274
+ const passEncoder = commandEncoder . beginComputePass (
275
+ computePassDescriptor
276
+ ) ;
221
277
passEncoder . setPipeline ( computePipeline ) ;
222
278
passEncoder . setBindGroup ( 0 , particleBindGroups [ t % 2 ] ) ;
223
279
passEncoder . dispatchWorkgroups ( Math . ceil ( numParticles / 64 ) ) ;
@@ -231,8 +287,54 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
231
287
passEncoder . draw ( 3 , numParticles , 0 , 0 ) ;
232
288
passEncoder . end ( ) ;
233
289
}
290
+
291
+ let resultBuffer : GPUBuffer | undefined = undefined ;
292
+ if ( hasTimestampQuery ) {
293
+ resultBuffer =
294
+ spareResultBuffers . pop ( ) ||
295
+ device . createBuffer ( {
296
+ size : 4 * BigInt64Array . BYTES_PER_ELEMENT ,
297
+ usage : GPUBufferUsage . COPY_DST | GPUBufferUsage . MAP_READ ,
298
+ } ) ;
299
+ commandEncoder . resolveQuerySet ( querySet , 0 , 4 , resolveBuffer , 0 ) ;
300
+ commandEncoder . copyBufferToBuffer (
301
+ resolveBuffer ,
302
+ 0 ,
303
+ resultBuffer ,
304
+ 0 ,
305
+ resultBuffer . size
306
+ ) ;
307
+ }
308
+
234
309
device . queue . submit ( [ commandEncoder . finish ( ) ] ) ;
235
310
311
+ if ( hasTimestampQuery ) {
312
+ resultBuffer . mapAsync ( GPUMapMode . READ ) . then ( ( ) => {
313
+ const times = new BigInt64Array ( resultBuffer . getMappedRange ( ) ) ;
314
+ computePassDurationSum += Number ( times [ 1 ] - times [ 0 ] ) ;
315
+ renderPassDurationSum += Number ( times [ 3 ] - times [ 2 ] ) ;
316
+ resultBuffer . unmap ( ) ;
317
+
318
+ // Periodically update the text for the timer stats
319
+ const kNumTimerSamples = 100 ;
320
+ if ( t % kNumTimerSamples === 0 ) {
321
+ const avgComputeMicroseconds = Math . round (
322
+ computePassDurationSum / kNumTimerSamples / 1000
323
+ ) ;
324
+ const avgRenderMicroseconds = Math . round (
325
+ renderPassDurationSum / kNumTimerSamples / 1000
326
+ ) ;
327
+ perfDisplay . textContent = `\
328
+ avg compute pass duration: ${ avgComputeMicroseconds } µs
329
+ avg render pass duration: ${ avgRenderMicroseconds } µs
330
+ spare readback buffers: ${ spareResultBuffers . length } ` ;
331
+ computePassDurationSum = 0 ;
332
+ renderPassDurationSum = 0 ;
333
+ }
334
+ spareResultBuffers . push ( resultBuffer ) ;
335
+ } ) ;
336
+ }
337
+
236
338
++ t ;
237
339
requestAnimationFrame ( frame ) ;
238
340
}
0 commit comments