Skip to content

Commit

Permalink
Cleanup timestamp-query example (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
greggman authored Nov 19, 2024
1 parent 8f9e6b6 commit 530c33a
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 78 deletions.
88 changes: 50 additions & 38 deletions sample/timestampQuery/TimestampQueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
}
13 changes: 13 additions & 0 deletions sample/timestampQuery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</style>
<script defer src="main.js" type="module"></script>
<script defer type="module" src="../../js/iframe-helper.js"></script>
</head>
<body>
<canvas></canvas>
<div id="info">
<pre></pre>
</div>
</body>
</html>
53 changes: 13 additions & 40 deletions sample/timestampQuery/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit 530c33a

Please sign in to comment.