From a17ffe1cd818cd47421dc917e3d572ef27a71409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Michel?= Date: Tue, 12 Nov 2024 15:48:44 +0100 Subject: [PATCH 1/5] Add timestamp query example --- sample/timestampQuery/index.html | 30 ++ sample/timestampQuery/main.ts | 307 ++++++++++++++++++ sample/timestampQuery/meta.ts | 11 + .../sampleTextureMixColor.frag.wgsl | 10 + sample/util.ts | 2 +- src/samples.ts | 2 + 6 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 sample/timestampQuery/index.html create mode 100644 sample/timestampQuery/main.ts create mode 100644 sample/timestampQuery/meta.ts create mode 100644 sample/timestampQuery/sampleTextureMixColor.frag.wgsl diff --git a/sample/timestampQuery/index.html b/sample/timestampQuery/index.html new file mode 100644 index 00000000..5094605f --- /dev/null +++ b/sample/timestampQuery/index.html @@ -0,0 +1,30 @@ + + + + + + webgpu-samples: timestampQuery + + + + + + + + diff --git a/sample/timestampQuery/main.ts b/sample/timestampQuery/main.ts new file mode 100644 index 00000000..c7112bc4 --- /dev/null +++ b/sample/timestampQuery/main.ts @@ -0,0 +1,307 @@ +import { GUI } from 'dat.gui'; +import { mat4, vec3 } from 'wgpu-matrix'; + +import { + cubeVertexArray, + cubeVertexSize, + cubeUVOffset, + cubePositionOffset, + cubeVertexCount, +} from '../../meshes/cube'; + +import basicVertWGSL from '../../shaders/basic.vert.wgsl'; +import sampleTextureMixColorWGSL from '../../shaders/red.frag.wgsl'; +import { quitIfWebGPUNotAvailable, fail } from '../util'; + +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'); +} +const device = await adapter?.requestDevice({ + // We request a device that has support for timestamp queries + requiredFeatures: [ "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'); +} + +const context = canvas.getContext('webgpu') as GPUCanvasContext; + +const devicePixelRatio = window.devicePixelRatio; +canvas.width = canvas.clientWidth * devicePixelRatio; +canvas.height = canvas.clientHeight * devicePixelRatio; +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, +}); + +// Create timestamp queries +const timestampQuerySet = device.createQuerySet({ + type: "timestamp", + count: 2, // begin and end +}); + +// 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, +}); + +// Create a buffer to map the result back to the CPU +const timestampMapBuffer = device.createBuffer({ + size: timestampBufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, +}); + +// Create a vertex buffer from the cube data. +const verticesBuffer = device.createBuffer({ + size: cubeVertexArray.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, +}); +new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray); +verticesBuffer.unmap(); + +const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: basicVertWGSL, + }), + buffers: [ + { + arrayStride: cubeVertexSize, + attributes: [ + { + // position + shaderLocation: 0, + offset: cubePositionOffset, + format: 'float32x4', + }, + { + // uv + shaderLocation: 1, + offset: cubeUVOffset, + format: 'float32x2', + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: sampleTextureMixColorWGSL, + }), + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-list', + + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + cullMode: 'back', + }, + + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, +}); + +const depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, +}); + +const uniformBufferSize = 4 * 16; // 4x4 matrix +const uniformBuffer = device.createBuffer({ + size: uniformBufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, +}); + +const uniformBindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: uniformBuffer, + }, + }, + ], +}); + +const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: undefined, // Assigned later + + clearValue: [0.5, 0.5, 0.5, 1.0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + // We instruct the render pass to write to the timestamp query before/after + timestampWrites: { + querySet: timestampQuerySet, + beginningOfPassWriteIndex: 0, + endOfPassWriteIndex: 1, + } +}; + +const aspect = canvas.width / canvas.height; +const projectionMatrix = mat4.perspective((2 * Math.PI) / 5, aspect, 1, 100.0); +const modelViewProjectionMatrix = mat4.create(); + +function getTransformationMatrix() { + const viewMatrix = mat4.identity(); + mat4.translate(viewMatrix, vec3.fromValues(0, 0, -4), viewMatrix); + const now = Date.now() / 1000; + mat4.rotate( + viewMatrix, + vec3.fromValues(Math.sin(now), Math.cos(now), 0), + 1, + viewMatrix + ); + + mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix); + + 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( + uniformBuffer, + 0, + transformationMatrix.buffer, + transformationMatrix.byteOffset, + transformationMatrix.byteLength + ); + renderPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + + const commandEncoder = device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, uniformBindGroup); + passEncoder.setVertexBuffer(0, verticesBuffer); + 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, + ); + } + + 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; + }) + } + + requestAnimationFrame(frame); +} +requestAnimationFrame(frame); diff --git a/sample/timestampQuery/meta.ts b/sample/timestampQuery/meta.ts new file mode 100644 index 00000000..c6fdfbf3 --- /dev/null +++ b/sample/timestampQuery/meta.ts @@ -0,0 +1,11 @@ +export default { + name: 'Timestamp Query', + description: 'This example shows how to use timestamp queries to measure render pass duration.', + filename: __DIRNAME__, + sources: [ + { path: 'main.ts' }, + { path: '../../shaders/basic.vert.wgsl' }, + { path: '../../shaders/red.frag.wgsl' }, + { path: '../../meshes/cube.ts' }, + ], +}; diff --git a/sample/timestampQuery/sampleTextureMixColor.frag.wgsl b/sample/timestampQuery/sampleTextureMixColor.frag.wgsl new file mode 100644 index 00000000..8f0165c3 --- /dev/null +++ b/sample/timestampQuery/sampleTextureMixColor.frag.wgsl @@ -0,0 +1,10 @@ +@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 33031961..2cdf2c02 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. */ -const fail = (() => { +export const fail = (() => { type ErrorOutput = { show(msg: string): void }; function createErrorOutput() { diff --git a/src/samples.ts b/src/samples.ts index 3c5d390c..1e7e13a8 100644 --- a/src/samples.ts +++ b/src/samples.ts @@ -34,6 +34,7 @@ import skinnedMesh from '../sample/skinnedMesh/meta'; import spookyball from '../sample/spookyball/meta'; import textRenderingMsdf from '../sample/textRenderingMsdf/meta'; import texturedCube from '../sample/texturedCube/meta'; +import timestampQuery from '../sample/timestampQuery/meta'; import transparentCanvas from '../sample/transparentCanvas/meta'; import twoCubes from '../sample/twoCubes/meta'; import videoUploading from '../sample/videoUploading/meta'; @@ -171,6 +172,7 @@ export const pageCategories: PageCategory[] = [ samples: { animometer, workloadSimulator, + timestampQuery, }, }, ]; 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 2/5] 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, }, }, ]; From 686c8014b2826bfe5358986e1be8984403db39e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Michel?= Date: Thu, 14 Nov 2024 18:58:01 +0100 Subject: [PATCH 3/5] Filter out negative samples --- sample/timestampQuery/main.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sample/timestampQuery/main.ts b/sample/timestampQuery/main.ts index a1bb4f46..0b2e6707 100644 --- a/sample/timestampQuery/main.ts +++ b/sample/timestampQuery/main.ts @@ -230,6 +230,9 @@ function frame() { // Read timestamp value back from GPU buffers timestampQueryManager .readAsync(timestamps => { + // This may happen (see spec https://gpuweb.github.io/gpuweb/#timestamp) + if (timestamps[1] < timestamps[0]) return; + // Measure difference (in bigints) const elapsedNs = timestamps[1] - timestamps[0]; // Cast into regular int (ok because value is small after difference) From 2e3489a88969f887eeea3ae10893e2558d8b8bff Mon Sep 17 00:00:00 2001 From: Elie Michel Date: Fri, 15 Nov 2024 00:05:18 +0100 Subject: [PATCH 4/5] Update sample/timestampQuery/TimestampQueryManager.ts Co-authored-by: Kai Ninomiya --- sample/timestampQuery/TimestampQueryManager.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/sample/timestampQuery/TimestampQueryManager.ts b/sample/timestampQuery/TimestampQueryManager.ts index 6dd9529b..0f7e7735 100644 --- a/sample/timestampQuery/TimestampQueryManager.ts +++ b/sample/timestampQuery/TimestampQueryManager.ts @@ -75,24 +75,22 @@ export default class TimestampQueryManager { } // 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(() => {}); + readAsync(onTimestampReadBack: (timestamps: BigUint64Array) => void): void { + if (!this.timestampSupported) return; + if (this.hasOngoingTimestampReadback) return; this.hasOngoingTimestampReadback = true; const buffer = this.timestampMapBuffer; - return new Promise(resolve => { - buffer.mapAsync(GPUMapMode.READ, 0, buffer.size) + void buffer.mapAsync(GPUMapMode.READ) .then(() => { - const rawData = buffer.getMappedRange(0, buffer.size); + const rawData = buffer.getMappedRange(); const timestamps = new BigUint64Array(rawData); onTimestampReadBack(timestamps); buffer.unmap(); this.hasOngoingTimestampReadback = false; - }) - }); + }); } } From 407a7bc56fb46361bfb73c16c782be19eb8ec06b Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Thu, 14 Nov 2024 15:41:43 -0800 Subject: [PATCH 5/5] npm run fix --- .../timestampQuery/TimestampQueryManager.ts | 41 +++++++++--------- sample/timestampQuery/main.ts | 42 +++++++++++-------- sample/timestampQuery/meta.ts | 3 +- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/sample/timestampQuery/TimestampQueryManager.ts b/sample/timestampQuery/TimestampQueryManager.ts index 0f7e7735..46696418 100644 --- a/sample/timestampQuery/TimestampQueryManager.ts +++ b/sample/timestampQuery/TimestampQueryManager.ts @@ -2,34 +2,34 @@ export default class TimestampQueryManager { // The device may not support timestamp queries, on which case this whole // class does nothing. - timestampSupported: boolean + timestampSupported: boolean; // Number of timestamp counters - timestampCount: number + timestampCount: number; // The query objects. This is meant to be used in a ComputePassDescriptor's // or RenderPassDescriptor's 'timestampWrites' field. - timestampQuerySet: GPUQuerySet + timestampQuerySet: GPUQuerySet; // A buffer where to store query results - timestampBuffer: GPUBuffer + timestampBuffer: GPUBuffer; // A buffer to map this result back to CPU - timestampMapBuffer: GPUBuffer + timestampMapBuffer: GPUBuffer; // State used to avoid firing concurrent readback of timestamp values - hasOngoingTimestampReadback: boolean + hasOngoingTimestampReadback: boolean; // Device must have the "timestamp-query" feature constructor(device: GPUDevice, timestampCount: number) { - this.timestampSupported = device.features.has("timestamp-query"); + this.timestampSupported = device.features.has('timestamp-query'); if (!this.timestampSupported) return; this.timestampCount = timestampCount; // Create timestamp queries this.timestampQuerySet = device.createQuerySet({ - type: "timestamp", + type: 'timestamp', count: timestampCount, // begin and end }); @@ -61,15 +61,17 @@ export default class TimestampQueryManager { 0 /* firstQuery */, this.timestampCount /* queryCount */, this.timestampBuffer, - 0, /* destinationOffset */ + 0 /* destinationOffset */ ); if (!this.hasOngoingTimestampReadback) { // Copy values to the mapped buffer commandEncoder.copyBufferToBuffer( - this.timestampBuffer, 0, - this.timestampMapBuffer, 0, - this.timestampBuffer.size, + this.timestampBuffer, + 0, + this.timestampMapBuffer, + 0, + this.timestampBuffer.size ); } } @@ -82,15 +84,14 @@ export default class TimestampQueryManager { this.hasOngoingTimestampReadback = true; const buffer = this.timestampMapBuffer; - void buffer.mapAsync(GPUMapMode.READ) - .then(() => { - const rawData = buffer.getMappedRange(); - const timestamps = new BigUint64Array(rawData); + void buffer.mapAsync(GPUMapMode.READ).then(() => { + const rawData = buffer.getMappedRange(); + const timestamps = new BigUint64Array(rawData); - onTimestampReadBack(timestamps); + onTimestampReadBack(timestamps); - buffer.unmap(); - this.hasOngoingTimestampReadback = false; - }); + buffer.unmap(); + this.hasOngoingTimestampReadback = false; + }); } } diff --git a/sample/timestampQuery/main.ts b/sample/timestampQuery/main.ts index 0b2e6707..66f11ad0 100644 --- a/sample/timestampQuery/main.ts +++ b/sample/timestampQuery/main.ts @@ -21,11 +21,11 @@ const adapter = await navigator.gpu?.requestAdapter(); // 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 supportsTimestampQueries = adapter?.features.has('timestamp-query'); const device = await adapter?.requestDevice({ // We request a device that has support for timestamp queries - requiredFeatures: supportsTimestampQueries ? [ "timestamp-query" ] : [], + requiredFeatures: supportsTimestampQueries ? ['timestamp-query'] : [], }); quitIfWebGPUNotAvailable(adapter, device); @@ -65,7 +65,7 @@ if (canvas.parentNode) { } if (!supportsTimestampQueries) { - perfDisplay.innerHTML = "Timestamp queries are not supported"; + perfDisplay.innerHTML = 'Timestamp queries are not supported'; } // Create a vertex buffer from the cube data. @@ -177,7 +177,7 @@ const renderPassDescriptor: GPURenderPassDescriptor = { querySet: timestampQueryManager.timestampQuerySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1, - } + }, }; const aspect = canvas.width / canvas.height; @@ -228,20 +228,26 @@ function frame() { device.queue.submit([commandEncoder.finish()]); // Read timestamp value back from GPU buffers - timestampQueryManager - .readAsync(timestamps => { - // This may happen (see spec https://gpuweb.github.io/gpuweb/#timestamp) - if (timestamps[1] < timestamps[0]) return; - - // 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`; - }); + timestampQueryManager.readAsync((timestamps) => { + // This may happen (see spec https://gpuweb.github.io/gpuweb/#timestamp) + if (timestamps[1] < timestamps[0]) return; + + // 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 9420c8ae..5fae2dc3 100644 --- a/sample/timestampQuery/meta.ts +++ b/sample/timestampQuery/meta.ts @@ -1,6 +1,7 @@ export default { name: 'Timestamp Query', - description: 'This example shows how to use timestamp queries to measure render pass duration.', + description: + 'This example shows how to use timestamp queries to measure render pass duration.', filename: __DIRNAME__, sources: [ { path: 'TimestampQueryManager.ts' },