Skip to content

Commit

Permalink
Split down timestamp query example into multiple files
Browse files Browse the repository at this point in the history
  • Loading branch information
eliemichel committed Nov 14, 2024
1 parent a17ffe1 commit f97a9bb
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 127 deletions.
29 changes: 29 additions & 0 deletions sample/timestampQuery/PerfCounter.ts
Original file line number Diff line number Diff line change
@@ -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));
}
}
98 changes: 98 additions & 0 deletions sample/timestampQuery/TimestampQueryManager.ts
Original file line number Diff line number Diff line change
@@ -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;
})
});
}
}
166 changes: 52 additions & 114 deletions sample/timestampQuery/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GUI } from 'dat.gui';
import { mat4, vec3 } from 'wgpu-matrix';

import {
Expand All @@ -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;

Expand All @@ -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({
Expand Down Expand Up @@ -107,7 +105,7 @@ const pipeline = device.createRenderPipeline({
},
fragment: {
module: device.createShaderModule({
code: sampleTextureMixColorWGSL,
code: fragmentWGSL,
}),
targets: [
{
Expand Down Expand Up @@ -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',
},
Expand All @@ -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,
}
Expand All @@ -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(
Expand All @@ -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);
}
Expand Down
4 changes: 3 additions & 1 deletion sample/timestampQuery/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
};
10 changes: 0 additions & 10 deletions sample/timestampQuery/sampleTextureMixColor.frag.wgsl

This file was deleted.

2 changes: 1 addition & 1 deletion sample/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit f97a9bb

Please sign in to comment.