diff --git a/sample/timestampQuery/TimestampQueryManager.ts b/sample/timestampQuery/TimestampQueryManager.ts index 46696418..4d0533d3 100644 --- a/sample/timestampQuery/TimestampQueryManager.ts +++ b/sample/timestampQuery/TimestampQueryManager.ts @@ -4,94 +4,106 @@ export default class TimestampQueryManager { // 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; + #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; + // Last queried elapsed time of the pass in nanoseconds. + passDurationMeasurementNs: number; // Device must have the "timestamp-query" feature - constructor(device: GPUDevice, timestampCount: number) { + constructor(device: GPUDevice) { this.timestampSupported = device.features.has('timestamp-query'); if (!this.timestampSupported) return; - this.timestampCount = timestampCount; + this.passDurationMeasurementNs = 0; // Create timestamp queries - this.timestampQuerySet = device.createQuerySet({ + this.#timestampQuerySet = device.createQuerySet({ type: 'timestamp', - count: timestampCount, // begin and end + count: 2, // 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, + this.#timestampBuffer = device.createBuffer({ + size: this.#timestampQuerySet.count * timestampByteSize, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE, }); // Create a buffer to map the result back to the CPU - this.timestampMapBuffer = device.createBuffer({ - size: timestampBufferSize, + this.#timestampMapBuffer = device.createBuffer({ + size: this.#timestampBuffer.size, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); + } - this.hasOngoingTimestampReadback = false; + // Add both a start and end timestamp. + addTimestampWrite( + passDescriptor: GPURenderPassDescriptor | GPUComputePassDescriptor + ) { + if (this.timestampSupported) { + // We instruct the render pass to write to the timestamp query before/after + passDescriptor.timestampWrites = { + querySet: this.#timestampQuerySet, + beginningOfPassWriteIndex: 0, + endOfPassWriteIndex: 1, + }; + } + return passDescriptor; } - // Resolve all timestamp queries and copy the result into the map buffer - resolveAll(commandEncoder: GPUCommandEncoder) { + // Resolve the timestamp queries and copy the result into the mappable buffer if possible. + resolve(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, + this.#timestampQuerySet, 0 /* firstQuery */, - this.timestampCount /* queryCount */, - this.timestampBuffer, + this.#timestampQuerySet.count /* queryCount */, + this.#timestampBuffer, 0 /* destinationOffset */ ); - if (!this.hasOngoingTimestampReadback) { - // Copy values to the mapped buffer + if (this.#timestampMapBuffer.mapState === 'unmapped') { + // Copy values to the mappable buffer commandEncoder.copyBufferToBuffer( - this.timestampBuffer, + this.#timestampBuffer, 0, - this.timestampMapBuffer, + this.#timestampMapBuffer, 0, - this.timestampBuffer.size + this.#timestampBuffer.size ); } } - // Once resolved, we can read back the value of timestamps - readAsync(onTimestampReadBack: (timestamps: BigUint64Array) => void): void { + // Read the values of timestamps. + tryInitiateTimestampDownload(): void { if (!this.timestampSupported) return; - if (this.hasOngoingTimestampReadback) return; + if (this.#timestampMapBuffer.mapState !== 'unmapped') return; - this.hasOngoingTimestampReadback = true; - - const buffer = this.timestampMapBuffer; + const buffer = this.#timestampMapBuffer; void buffer.mapAsync(GPUMapMode.READ).then(() => { const rawData = buffer.getMappedRange(); const timestamps = new BigUint64Array(rawData); - - onTimestampReadBack(timestamps); - + // Subtract the begin time from the end time. + // Cast into number. Number can be 9007199254740991 as max integer + // which is 109 days of nano seconds. + const elapsedNs = Number(timestamps[1] - timestamps[0]); + // It's possible elapsedNs is negative which means it's invalid + // (see spec https://gpuweb.github.io/gpuweb/#timestamp) + if (elapsedNs >= 0) { + this.passDurationMeasurementNs = elapsedNs; + } buffer.unmap(); - this.hasOngoingTimestampReadback = false; }); } } diff --git a/sample/timestampQuery/index.html b/sample/timestampQuery/index.html index 5094605f..a25ccfbb 100644 --- a/sample/timestampQuery/index.html +++ b/sample/timestampQuery/index.html @@ -20,11 +20,24 @@ max-width: 100%; display: block; } + #info { + color: white; + background-color: black; + position: absolute; + top: 10px; + left: 10px; + } + #info pre { + margin: 0.5em; + } +
+

+    
diff --git a/sample/timestampQuery/main.ts b/sample/timestampQuery/main.ts index 66f11ad0..374966c0 100644 --- a/sample/timestampQuery/main.ts +++ b/sample/timestampQuery/main.ts @@ -33,7 +33,7 @@ quitIfWebGPUNotAvailable(adapter, device); // 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 timestampQueryManager = new TimestampQueryManager(device); const renderPassDurationCounter = new PerfCounter(); const context = canvas.getContext('webgpu') as GPUCanvasContext; @@ -48,21 +48,7 @@ context.configure({ format: presentationFormat, }); -// 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'; - -const perfDisplay = document.createElement('pre'); -perfDisplayContainer.appendChild(perfDisplay); -if (canvas.parentNode) { - canvas.parentNode.appendChild(perfDisplayContainer); -} else { - console.error('canvas.parentNode is null'); -} +const perfDisplay = document.querySelector('#info pre'); if (!supportsTimestampQueries) { perfDisplay.innerHTML = 'Timestamp queries are not supported'; @@ -172,14 +158,10 @@ const renderPassDescriptor: GPURenderPassDescriptor = { depthLoadOp: 'clear', depthStoreOp: 'store', }, - // We instruct the render pass to write to the timestamp query before/after - timestampWrites: { - querySet: timestampQueryManager.timestampQuerySet, - beginningOfPassWriteIndex: 0, - endOfPassWriteIndex: 1, - }, }; +timestampQueryManager.addTimestampWrite(renderPassDescriptor); + const aspect = canvas.width / canvas.height; const projectionMatrix = mat4.perspective((2 * Math.PI) / 5, aspect, 1, 100.0); const modelViewProjectionMatrix = mat4.create(); @@ -222,32 +204,23 @@ function frame() { passEncoder.end(); // Resolve timestamp queries, so that their result is available in - // a GPU-sude buffer. - timestampQueryManager.resolveAll(commandEncoder); + // a GPU-side buffer. + timestampQueryManager.resolve(commandEncoder); 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: + if (timestampQueryManager.timestampSupported) { + // Show the last successfully downloaded elapsed time. + const elapsedNs = timestampQueryManager.passDurationMeasurementNs; + // 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.tryInitiateTimestampDownload(); requestAnimationFrame(frame); }