From cbe9f4c72786451d78c34fc30ab30bb4ba115b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Thu, 9 Nov 2023 15:20:29 +0100 Subject: [PATCH 1/6] Add timestamp query support to compute boids --- package-lock.json | 8 ++-- package.json | 2 +- src/sample/computeBoids/main.ts | 78 ++++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f829213..75588b63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "@types/stats.js": "^0.17.0", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "^5.41.0", - "@webgpu/types": "^0.1.21", + "@webgpu/types": "^0.1.38", "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", @@ -1098,9 +1098,9 @@ } }, "node_modules/@webgpu/types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", - "integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==", + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", "dev": true }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 6069dd6e..c9a76881 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/stats.js": "^0.17.0", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "^5.41.0", - "@webgpu/types": "^0.1.21", + "@webgpu/types": "^0.1.38", "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/src/sample/computeBoids/main.ts b/src/sample/computeBoids/main.ts index 53c27fe6..31b92816 100644 --- a/src/sample/computeBoids/main.ts +++ b/src/sample/computeBoids/main.ts @@ -6,7 +6,27 @@ import updateSpritesWGSL from './updateSprites.wgsl'; const init: SampleInit = async ({ canvas, pageState, gui }) => { const adapter = await navigator.gpu.requestAdapter(); assert(adapter, 'requestAdapter returned null'); - const device = await adapter.requestDevice(); + + const hasTimestampQuery = adapter.features.has('timestamp-query'); + const device = await adapter.requestDevice({ + requiredFeatures: hasTimestampQuery ? ['timestamp-query'] : [], + }); + + const perfDisplayContainer = document.createElement('div'); + perfDisplayContainer.style.color = 'white'; + perfDisplayContainer.style.background = 'black'; + perfDisplayContainer.style.position = 'absolute'; + perfDisplayContainer.style.top = '10px'; + perfDisplayContainer.style.left = '10px'; + perfDisplayContainer.style.textAlign = 'left'; + + const perfDisplay = document.createElement('pre'); + perfDisplayContainer.appendChild(perfDisplay); + if (canvas.parentNode) { + canvas.parentNode.appendChild(perfDisplayContainer); + } else { + console.error('canvas.parentNode is null'); + } if (!pageState.active) return; const context = canvas.getContext('webgpu') as GPUCanvasContext; @@ -97,6 +117,36 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { ], }; + const computePassDescriptor = {}; + + let querySet: GPUQuerySet | undefined = undefined; + let resolveBuffer: GPUBuffer | undefined = undefined; + let resultBuffer: GPUBuffer | undefined = undefined; + if (hasTimestampQuery) { + querySet = device.createQuerySet({ + type: 'timestamp', + count: 4, + }); + resolveBuffer = device.createBuffer({ + size: 4 * BigInt64Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + resultBuffer = device.createBuffer({ + size: 4 * BigInt64Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + renderPassDescriptor.timestampWrites = { + querySet, + beginningOfPassWriteIndex: 0, + endOfPassWriteIndex: 1, + }; + computePassDescriptor.timestampWrites = { + querySet, + beginningOfPassWriteIndex: 2, + endOfPassWriteIndex: 3, + }; + } + // prettier-ignore const vertexBufferData = new Float32Array([ -0.01, -0.02, 0.01, @@ -217,7 +267,9 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { const commandEncoder = device.createCommandEncoder(); { - const passEncoder = commandEncoder.beginComputePass(); + const passEncoder = commandEncoder.beginComputePass( + computePassDescriptor + ); passEncoder.setPipeline(computePipeline); passEncoder.setBindGroup(0, particleBindGroups[t % 2]); passEncoder.dispatchWorkgroups(Math.ceil(numParticles / 64)); @@ -231,8 +283,30 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { passEncoder.draw(3, numParticles, 0, 0); passEncoder.end(); } + + if (hasTimestampQuery && resultBuffer.mapState == 'unmapped') { + commandEncoder.resolveQuerySet(querySet, 0, 4, resolveBuffer, 0); + commandEncoder.copyBufferToBuffer( + resolveBuffer, + 0, + resultBuffer, + 0, + resultBuffer.size + ); + } + device.queue.submit([commandEncoder.finish()]); + if (hasTimestampQuery && resultBuffer.mapState == 'unmapped') { + resultBuffer.mapAsync(GPUMapMode.READ).then(() => { + const times = new BigInt64Array(resultBuffer.getMappedRange()); + const renderPassDuration = Number(times[1] - times[0]); + const computePassDuration = Number(times[3] - times[2]); + perfDisplay.textContent = ` render pass duration: ${renderPassDuration}ns\ncompute pass duration: ${computePassDuration}ns\n`; + resultBuffer.unmap(); + }); + } + ++t; requestAnimationFrame(frame); } From f7978db05e638dafcc9d8f1d3035eda55882ea0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Thu, 9 Nov 2023 16:38:37 +0100 Subject: [PATCH 2/6] Create buffers each frame --- src/sample/computeBoids/main.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/sample/computeBoids/main.ts b/src/sample/computeBoids/main.ts index 31b92816..f63ee6d1 100644 --- a/src/sample/computeBoids/main.ts +++ b/src/sample/computeBoids/main.ts @@ -120,21 +120,11 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { const computePassDescriptor = {}; let querySet: GPUQuerySet | undefined = undefined; - let resolveBuffer: GPUBuffer | undefined = undefined; - let resultBuffer: GPUBuffer | undefined = undefined; if (hasTimestampQuery) { querySet = device.createQuerySet({ type: 'timestamp', count: 4, }); - resolveBuffer = device.createBuffer({ - size: 4 * BigInt64Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, - }); - resultBuffer = device.createBuffer({ - size: 4 * BigInt64Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, - }); renderPassDescriptor.timestampWrites = { querySet, beginningOfPassWriteIndex: 0, @@ -284,7 +274,17 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { passEncoder.end(); } - if (hasTimestampQuery && resultBuffer.mapState == 'unmapped') { + let resolveBuffer: GPUBuffer | undefined = undefined; + let resultBuffer: GPUBuffer | undefined = undefined; + if (hasTimestampQuery) { + resolveBuffer = device.createBuffer({ + size: 4 * BigInt64Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + resultBuffer = device.createBuffer({ + size: 4 * BigInt64Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); commandEncoder.resolveQuerySet(querySet, 0, 4, resolveBuffer, 0); commandEncoder.copyBufferToBuffer( resolveBuffer, @@ -297,7 +297,7 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { device.queue.submit([commandEncoder.finish()]); - if (hasTimestampQuery && resultBuffer.mapState == 'unmapped') { + if (hasTimestampQuery) { resultBuffer.mapAsync(GPUMapMode.READ).then(() => { const times = new BigInt64Array(resultBuffer.getMappedRange()); const renderPassDuration = Number(times[1] - times[0]); From b1931e276257723b8fbf7ad34f825f3dd348f9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Thu, 9 Nov 2023 16:47:25 +0100 Subject: [PATCH 3/6] Fix lint --- src/sample/computeBoids/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sample/computeBoids/main.ts b/src/sample/computeBoids/main.ts index f63ee6d1..cb9e8fde 100644 --- a/src/sample/computeBoids/main.ts +++ b/src/sample/computeBoids/main.ts @@ -106,7 +106,7 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { }, }); - const renderPassDescriptor = { + const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: undefined as GPUTextureView, // Assigned later @@ -117,7 +117,7 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { ], }; - const computePassDescriptor = {}; + const computePassDescriptor: GPUComputePassDescriptor = {}; let querySet: GPUQuerySet | undefined = undefined; if (hasTimestampQuery) { From 5c261787ab8958015f9fce74c06c43b15ba2dd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Fri, 10 Nov 2023 10:46:35 +0100 Subject: [PATCH 4/6] Address kai's feedback --- src/sample/computeBoids/main.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/sample/computeBoids/main.ts b/src/sample/computeBoids/main.ts index cb9e8fde..2b3c446e 100644 --- a/src/sample/computeBoids/main.ts +++ b/src/sample/computeBoids/main.ts @@ -120,17 +120,23 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { const computePassDescriptor: GPUComputePassDescriptor = {}; let querySet: GPUQuerySet | undefined = undefined; + let resolveBuffer: GPUBuffer | undefined = undefined; + const freeBuffers = []; if (hasTimestampQuery) { querySet = device.createQuerySet({ type: 'timestamp', count: 4, }); - renderPassDescriptor.timestampWrites = { + resolveBuffer = device.createBuffer({ + size: 4 * BigInt64Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + computePassDescriptor.timestampWrites = { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1, }; - computePassDescriptor.timestampWrites = { + renderPassDescriptor.timestampWrites = { querySet, beginningOfPassWriteIndex: 2, endOfPassWriteIndex: 3, @@ -274,17 +280,14 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { passEncoder.end(); } - let resolveBuffer: GPUBuffer | undefined = undefined; let resultBuffer: GPUBuffer | undefined = undefined; if (hasTimestampQuery) { - resolveBuffer = device.createBuffer({ - size: 4 * BigInt64Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, - }); - resultBuffer = device.createBuffer({ - size: 4 * BigInt64Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, - }); + resultBuffer = + freeBuffers.pop() || + device.createBuffer({ + size: 4 * BigInt64Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); commandEncoder.resolveQuerySet(querySet, 0, 4, resolveBuffer, 0); commandEncoder.copyBufferToBuffer( resolveBuffer, @@ -300,10 +303,11 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { if (hasTimestampQuery) { resultBuffer.mapAsync(GPUMapMode.READ).then(() => { const times = new BigInt64Array(resultBuffer.getMappedRange()); - const renderPassDuration = Number(times[1] - times[0]); - const computePassDuration = Number(times[3] - times[2]); - perfDisplay.textContent = ` render pass duration: ${renderPassDuration}ns\ncompute pass duration: ${computePassDuration}ns\n`; + const computePassDuration = Number(times[1] - times[0]); + const renderPassDuration = Number(times[3] - times[2]); resultBuffer.unmap(); + freeBuffers.push(resultBuffer); + perfDisplay.textContent = `compute pass duration: ${computePassDuration}ns\n render pass duration: ${renderPassDuration}ns\nfree buffers capacity: ${freeBuffers.length} `; }); } From ca76dac69d606de699b1ae8675afe31456b3a032 Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Fri, 10 Nov 2023 15:26:15 -0800 Subject: [PATCH 5/6] 100-frame average in microseconds; add comments --- src/sample/computeBoids/main.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/sample/computeBoids/main.ts b/src/sample/computeBoids/main.ts index 2b3c446e..dc79046e 100644 --- a/src/sample/computeBoids/main.ts +++ b/src/sample/computeBoids/main.ts @@ -119,9 +119,15 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { const computePassDescriptor: GPUComputePassDescriptor = {}; + /** Storage for timestamp query results */ let querySet: GPUQuerySet | undefined = undefined; + /** Timestamps are resolved into this buffer */ let resolveBuffer: GPUBuffer | undefined = undefined; - const freeBuffers = []; + /** Pool of spare buffers for MAP_READing the timestamps back to CPU. A buffer + * is taken from the pool (if available) when a readback is needed, and placed + * back into the pool once the readback is done and it's unmapped. */ + const spareResultBuffers = []; + if (hasTimestampQuery) { querySet = device.createQuerySet({ type: 'timestamp', @@ -253,6 +259,8 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { } let t = 0; + let computePassDurationSum = 0; + let renderPassDurationSum = 0; function frame() { // Sample is no longer the active page. if (!pageState.active) return; @@ -283,7 +291,7 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { let resultBuffer: GPUBuffer | undefined = undefined; if (hasTimestampQuery) { resultBuffer = - freeBuffers.pop() || + spareResultBuffers.pop() || device.createBuffer({ size: 4 * BigInt64Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, @@ -303,11 +311,21 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { if (hasTimestampQuery) { resultBuffer.mapAsync(GPUMapMode.READ).then(() => { const times = new BigInt64Array(resultBuffer.getMappedRange()); - const computePassDuration = Number(times[1] - times[0]); - const renderPassDuration = Number(times[3] - times[2]); + computePassDurationSum += Number(times[1] - times[0]); + renderPassDurationSum += Number(times[3] - times[2]); resultBuffer.unmap(); - freeBuffers.push(resultBuffer); - perfDisplay.textContent = `compute pass duration: ${computePassDuration}ns\n render pass duration: ${renderPassDuration}ns\nfree buffers capacity: ${freeBuffers.length} `; + + // Periodically update the text for the timer stats + const kNumTimerSamples = 100; + if (t % kNumTimerSamples === 0) { + perfDisplay.textContent = `\ +avg compute pass duration: ${Math.round(computePassDurationSum / kNumTimerSamples / 1000)}µs +avg render pass duration: ${Math.round(renderPassDurationSum / kNumTimerSamples / 1000)}µs +spare readback buffers: ${spareResultBuffers.length}`; + computePassDurationSum = 0; + renderPassDurationSum = 0; + } + spareResultBuffers.push(resultBuffer); }); } From 3f1cb609a961689af25fb1e7803bd31f6bbada0c Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Fri, 10 Nov 2023 15:31:14 -0800 Subject: [PATCH 6/6] lint --- src/sample/computeBoids/main.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sample/computeBoids/main.ts b/src/sample/computeBoids/main.ts index dc79046e..b7671735 100644 --- a/src/sample/computeBoids/main.ts +++ b/src/sample/computeBoids/main.ts @@ -318,9 +318,15 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => { // Periodically update the text for the timer stats const kNumTimerSamples = 100; if (t % kNumTimerSamples === 0) { + const avgComputeMicroseconds = Math.round( + computePassDurationSum / kNumTimerSamples / 1000 + ); + const avgRenderMicroseconds = Math.round( + renderPassDurationSum / kNumTimerSamples / 1000 + ); perfDisplay.textContent = `\ -avg compute pass duration: ${Math.round(computePassDurationSum / kNumTimerSamples / 1000)}µs -avg render pass duration: ${Math.round(renderPassDurationSum / kNumTimerSamples / 1000)}µs +avg compute pass duration: ${avgComputeMicroseconds}µs +avg render pass duration: ${avgRenderMicroseconds}µs spare readback buffers: ${spareResultBuffers.length}`; computePassDurationSum = 0; renderPassDurationSum = 0;