From f97a9bb486831fd65dd0a396419bd9d8cbe69695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Michel?= Date: Thu, 14 Nov 2024 18:55:07 +0100 Subject: [PATCH] Split down timestamp query example into multiple files --- sample/timestampQuery/PerfCounter.ts | 29 +++ .../timestampQuery/TimestampQueryManager.ts | 98 +++++++++++ sample/timestampQuery/main.ts | 166 ++++++------------ sample/timestampQuery/meta.ts | 4 +- .../sampleTextureMixColor.frag.wgsl | 10 -- sample/util.ts | 2 +- shaders/black.frag.wgsl | 4 + src/samples.ts | 2 +- 8 files changed, 188 insertions(+), 127 deletions(-) create mode 100644 sample/timestampQuery/PerfCounter.ts create mode 100644 sample/timestampQuery/TimestampQueryManager.ts delete mode 100644 sample/timestampQuery/sampleTextureMixColor.frag.wgsl create mode 100644 shaders/black.frag.wgsl diff --git a/sample/timestampQuery/PerfCounter.ts b/sample/timestampQuery/PerfCounter.ts new file mode 100644 index 00000000..ec2f05de --- /dev/null +++ b/sample/timestampQuery/PerfCounter.ts @@ -0,0 +1,29 @@ +// A minimalistic perf timer class that computes mean + stddev online +export default class PerfCounter { + sampleCount: number; + accumulated: number; + accumulatedSq: number; + + constructor() { + this.sampleCount = 0; + this.accumulated = 0; + this.accumulatedSq = 0; + } + + addSample(value: number) { + this.sampleCount += 1; + this.accumulated += value; + this.accumulatedSq += value * value; + } + + getAverage(): number { + return this.sampleCount === 0 ? 0 : this.accumulated / this.sampleCount; + } + + getStddev(): number { + if (this.sampleCount === 0) return 0; + const avg = this.getAverage(); + const variance = this.accumulatedSq / this.sampleCount - avg * avg; + return Math.sqrt(Math.max(0.0, variance)); + } +} diff --git a/sample/timestampQuery/TimestampQueryManager.ts b/sample/timestampQuery/TimestampQueryManager.ts new file mode 100644 index 00000000..6dd9529b --- /dev/null +++ b/sample/timestampQuery/TimestampQueryManager.ts @@ -0,0 +1,98 @@ +// Regroups all timestamp-related operations and resources. +export default class TimestampQueryManager { + // The device may not support timestamp queries, on which case this whole + // class does nothing. + timestampSupported: boolean + + // Number of timestamp counters + timestampCount: number + + // The query objects. This is meant to be used in a ComputePassDescriptor's + // or RenderPassDescriptor's 'timestampWrites' field. + timestampQuerySet: GPUQuerySet + + // A buffer where to store query results + timestampBuffer: GPUBuffer + + // A buffer to map this result back to CPU + timestampMapBuffer: GPUBuffer + + // State used to avoid firing concurrent readback of timestamp values + hasOngoingTimestampReadback: boolean + + // Device must have the "timestamp-query" feature + constructor(device: GPUDevice, timestampCount: number) { + this.timestampSupported = device.features.has("timestamp-query"); + if (!this.timestampSupported) return; + + this.timestampCount = timestampCount; + + // Create timestamp queries + this.timestampQuerySet = device.createQuerySet({ + type: "timestamp", + count: timestampCount, // begin and end + }); + + // Create a buffer where to store the result of GPU queries + const timestampByteSize = 8; // timestamps are uint64 + const timestampBufferSize = timestampCount * timestampByteSize; + this.timestampBuffer = device.createBuffer({ + size: timestampBufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE, + }); + + // Create a buffer to map the result back to the CPU + this.timestampMapBuffer = device.createBuffer({ + size: timestampBufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + this.hasOngoingTimestampReadback = false; + } + + // Resolve all timestamp queries and copy the result into the map buffer + resolveAll(commandEncoder: GPUCommandEncoder) { + if (!this.timestampSupported) return; + + // After the end of the measured render pass, we resolve queries into a + // dedicated buffer. + commandEncoder.resolveQuerySet( + this.timestampQuerySet, + 0 /* firstQuery */, + this.timestampCount /* queryCount */, + this.timestampBuffer, + 0, /* destinationOffset */ + ); + + if (!this.hasOngoingTimestampReadback) { + // Copy values to the mapped buffer + commandEncoder.copyBufferToBuffer( + this.timestampBuffer, 0, + this.timestampMapBuffer, 0, + this.timestampBuffer.size, + ); + } + } + + // Once resolved, we can read back the value of timestamps + readAsync(onTimestampReadBack: (timestamps: BigUint64Array) => void) { + if (!this.timestampSupported) return new Promise(() => {}); + if (this.hasOngoingTimestampReadback) return new Promise(() => {}); + + this.hasOngoingTimestampReadback = true; + + const buffer = this.timestampMapBuffer; + return new Promise(resolve => { + buffer.mapAsync(GPUMapMode.READ, 0, buffer.size) + .then(() => { + const rawData = buffer.getMappedRange(0, buffer.size); + const timestamps = new BigUint64Array(rawData); + + onTimestampReadBack(timestamps); + + buffer.unmap(); + this.hasOngoingTimestampReadback = false; + }) + }); + } +} diff --git a/sample/timestampQuery/main.ts b/sample/timestampQuery/main.ts index c7112bc4..a1bb4f46 100644 --- a/sample/timestampQuery/main.ts +++ b/sample/timestampQuery/main.ts @@ -1,4 +1,3 @@ -import { GUI } from 'dat.gui'; import { mat4, vec3 } from 'wgpu-matrix'; import { @@ -10,34 +9,32 @@ import { } from '../../meshes/cube'; import basicVertWGSL from '../../shaders/basic.vert.wgsl'; -import sampleTextureMixColorWGSL from '../../shaders/red.frag.wgsl'; -import { quitIfWebGPUNotAvailable, fail } from '../util'; +import fragmentWGSL from '../../shaders/black.frag.wgsl'; +import { quitIfWebGPUNotAvailable } from '../util'; + +import PerfCounter from './PerfCounter'; +import TimestampQueryManager from './TimestampQueryManager'; const canvas = document.querySelector('canvas') as HTMLCanvasElement; const adapter = await navigator.gpu?.requestAdapter(); -if (!adapter.features.has("timestamp-query")) { - fail('WebGPU timestamp queries are not supported on this system'); -} + +// The use of timestamps require a dedicated adapter feature: +// The adapter may or may not support timestamp queries. If not, we simply +// don't measure timestamps and deactivate the timer display. +const supportsTimestampQueries = adapter?.features.has("timestamp-query"); + const device = await adapter?.requestDevice({ // We request a device that has support for timestamp queries - requiredFeatures: [ "timestamp-query" ], + requiredFeatures: supportsTimestampQueries ? [ "timestamp-query" ] : [], }); quitIfWebGPUNotAvailable(adapter, device); -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'; - -const perfDisplay = document.createElement('pre'); -perfDisplayContainer.appendChild(perfDisplay); -if (canvas.parentNode) { - canvas.parentNode.appendChild(perfDisplayContainer); -} else { - console.error('canvas.parentNode is null'); -} +// GPU-side timer and the CPU-side counter where we accumulate statistics: +// NB: Look for 'timestampQueryManager' in this file to locate parts of this +// snippets that are related to timestamps. Most of the logic is in +// TimestampQueryManager.ts. +const timestampQueryManager = new TimestampQueryManager(device, 2); +const renderPassDurationCounter = new PerfCounter(); const context = canvas.getContext('webgpu') as GPUCanvasContext; @@ -51,24 +48,25 @@ context.configure({ format: presentationFormat, }); -// Create timestamp queries -const timestampQuerySet = device.createQuerySet({ - type: "timestamp", - count: 2, // begin and end -}); +// UI for perf counter +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'; -// Create a buffer where to store the result of GPU queries -const timestampBufferSize = 2 * 8; // timestamps are uint64 -const timestampBuffer = device.createBuffer({ - size: timestampBufferSize, - usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE, -}); +const perfDisplay = document.createElement('pre'); +perfDisplayContainer.appendChild(perfDisplay); +if (canvas.parentNode) { + canvas.parentNode.appendChild(perfDisplayContainer); +} else { + console.error('canvas.parentNode is null'); +} -// Create a buffer to map the result back to the CPU -const timestampMapBuffer = device.createBuffer({ - size: timestampBufferSize, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, -}); +if (!supportsTimestampQueries) { + perfDisplay.innerHTML = "Timestamp queries are not supported"; +} // Create a vertex buffer from the cube data. const verticesBuffer = device.createBuffer({ @@ -107,7 +105,7 @@ const pipeline = device.createRenderPipeline({ }, fragment: { module: device.createShaderModule({ - code: sampleTextureMixColorWGSL, + code: fragmentWGSL, }), targets: [ { @@ -162,7 +160,7 @@ const renderPassDescriptor: GPURenderPassDescriptor = { { view: undefined, // Assigned later - clearValue: [0.5, 0.5, 0.5, 1.0], + clearValue: [0.95, 0.95, 0.95, 1.0], loadOp: 'clear', storeOp: 'store', }, @@ -176,7 +174,7 @@ const renderPassDescriptor: GPURenderPassDescriptor = { }, // We instruct the render pass to write to the timestamp query before/after timestampWrites: { - querySet: timestampQuerySet, + querySet: timestampQueryManager.timestampQuerySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1, } @@ -202,41 +200,6 @@ function getTransformationMatrix() { return modelViewProjectionMatrix; } -// State used to avoid firing concurrent readback of timestamp values -let hasOngoingTimestampReadback = false; - -// A minimalistic perf timer class that computes mean + stddev online -class PerfCounter { - sampleCount: number; - accumulated: number; - accumulatedSq: number; - - constructor() { - this.sampleCount = 0; - this.accumulated = 0; - this.accumulatedSq = 0; - } - - addSample(value: number) { - this.sampleCount += 1; - this.accumulated += value; - this.accumulatedSq += value * value; - } - - getAverage(): number { - return this.sampleCount === 0 ? 0 : this.accumulated / this.sampleCount; - } - - getStddev(): number { - if (this.sampleCount === 0) return 0; - const avg = this.getAverage(); - const variance = this.accumulatedSq / this.sampleCount - avg * avg; - return Math.sqrt(Math.max(0.0, variance)); - } -} - -const renderPassDurationCounter = new PerfCounter(); - function frame() { const transformationMatrix = getTransformationMatrix(); device.queue.writeBuffer( @@ -258,49 +221,24 @@ function frame() { passEncoder.draw(cubeVertexCount); passEncoder.end(); - // After the end of the measured render pass, we resolve queries into a - // dedicated buffer. - commandEncoder.resolveQuerySet( - timestampQuerySet, - 0 /* firstQuery */, - 2 /* queryCount */, - timestampBuffer, - 0, /* destinationOffset */ - ); - - if (!hasOngoingTimestampReadback) { - // Copy values to the mapped buffer - commandEncoder.copyBufferToBuffer( - timestampBuffer, 0, - timestampMapBuffer, 0, - timestampBufferSize, - ); - } + // Resolve timestamp queries, so that their result is available in + // a GPU-sude buffer. + timestampQueryManager.resolveAll(commandEncoder); device.queue.submit([commandEncoder.finish()]); // Read timestamp value back from GPU buffers - if (!hasOngoingTimestampReadback) { - hasOngoingTimestampReadback = true; - timestampMapBuffer - .mapAsync(GPUMapMode.READ, 0, timestampBufferSize) - .then(() => { - const buffer = timestampMapBuffer.getMappedRange(0, timestampBufferSize); - const timestamps = new BigUint64Array(buffer); - - // Measure difference (in bigints) - const elapsedNs = timestamps[1] - timestamps[0]; - // Cast into regular int (ok because value is small after difference) - // and convert from nanoseconds to milliseconds: - const elapsedMs = Number(elapsedNs) * 1e-6; - renderPassDurationCounter.addSample(elapsedMs); - console.log("timestamps (ms): elapsed", elapsedMs, "avg", renderPassDurationCounter.getAverage()); - perfDisplay.innerHTML = `Render Pass duration: ${renderPassDurationCounter.getAverage().toFixed(3)} ms ± ${renderPassDurationCounter.getStddev().toFixed(3)} ms`; - - timestampMapBuffer.unmap(); - hasOngoingTimestampReadback = false; - }) - } + timestampQueryManager + .readAsync(timestamps => { + // Measure difference (in bigints) + const elapsedNs = timestamps[1] - timestamps[0]; + // Cast into regular int (ok because value is small after difference) + // and convert from nanoseconds to milliseconds: + const elapsedMs = Number(elapsedNs) * 1e-6; + renderPassDurationCounter.addSample(elapsedMs); + console.log("timestamps (ms): elapsed", elapsedMs, "avg", renderPassDurationCounter.getAverage()); + perfDisplay.innerHTML = `Render Pass duration: ${renderPassDurationCounter.getAverage().toFixed(3)} ms ± ${renderPassDurationCounter.getStddev().toFixed(3)} ms`; + }); requestAnimationFrame(frame); } diff --git a/sample/timestampQuery/meta.ts b/sample/timestampQuery/meta.ts index c6fdfbf3..9420c8ae 100644 --- a/sample/timestampQuery/meta.ts +++ b/sample/timestampQuery/meta.ts @@ -3,9 +3,11 @@ export default { description: 'This example shows how to use timestamp queries to measure render pass duration.', filename: __DIRNAME__, sources: [ + { path: 'TimestampQueryManager.ts' }, + { path: 'PerfCounter.ts' }, { path: 'main.ts' }, { path: '../../shaders/basic.vert.wgsl' }, - { path: '../../shaders/red.frag.wgsl' }, + { path: '../../shaders/black.frag.wgsl' }, { path: '../../meshes/cube.ts' }, ], }; diff --git a/sample/timestampQuery/sampleTextureMixColor.frag.wgsl b/sample/timestampQuery/sampleTextureMixColor.frag.wgsl deleted file mode 100644 index 8f0165c3..00000000 --- a/sample/timestampQuery/sampleTextureMixColor.frag.wgsl +++ /dev/null @@ -1,10 +0,0 @@ -@group(0) @binding(1) var mySampler: sampler; -@group(0) @binding(2) var myTexture: texture_2d; - -@fragment -fn main( - @location(0) fragUV: vec2f, - @location(1) fragPosition: vec4f -) -> @location(0) vec4f { - return textureSample(myTexture, mySampler, fragUV) * fragPosition; -} diff --git a/sample/util.ts b/sample/util.ts index 2cdf2c02..33031961 100644 --- a/sample/util.ts +++ b/sample/util.ts @@ -34,7 +34,7 @@ export function quitIfWebGPUNotAvailable( } /** Fail by showing a console error, and dialog box if possible. */ -export const fail = (() => { +const fail = (() => { type ErrorOutput = { show(msg: string): void }; function createErrorOutput() { diff --git a/shaders/black.frag.wgsl b/shaders/black.frag.wgsl new file mode 100644 index 00000000..cb832ded --- /dev/null +++ b/shaders/black.frag.wgsl @@ -0,0 +1,4 @@ +@fragment +fn main() -> @location(0) vec4f { + return vec4(0.0, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/samples.ts b/src/samples.ts index 1e7e13a8..626db011 100644 --- a/src/samples.ts +++ b/src/samples.ts @@ -95,6 +95,7 @@ export const pageCategories: PageCategory[] = [ occlusionQuery, samplerParameters, alphaToCoverage, + timestampQuery, }, }, @@ -172,7 +173,6 @@ export const pageCategories: PageCategory[] = [ samples: { animometer, workloadSimulator, - timestampQuery, }, }, ];