Skip to content

Commit f7f813c

Browse files
Add timestamp query support to compute boids (#323)
* Add timestamp query support to compute boids * Create buffers each frame * Fix lint * Address kai's feedback * 100-frame average in microseconds; add comments * lint --------- Co-authored-by: Kai Ninomiya <[email protected]>
1 parent d67ae2a commit f7f813c

File tree

3 files changed

+110
-8
lines changed

3 files changed

+110
-8
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"@types/stats.js": "^0.17.0",
4040
"@typescript-eslint/eslint-plugin": "^5.41.0",
4141
"@typescript-eslint/parser": "^5.41.0",
42-
"@webgpu/types": "^0.1.21",
42+
"@webgpu/types": "^0.1.38",
4343
"eslint": "^8.26.0",
4444
"eslint-config-prettier": "^8.5.0",
4545
"eslint-plugin-prettier": "^4.2.1",

src/sample/computeBoids/main.ts

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,27 @@ import updateSpritesWGSL from './updateSprites.wgsl';
66
const init: SampleInit = async ({ canvas, pageState, gui }) => {
77
const adapter = await navigator.gpu.requestAdapter();
88
assert(adapter, 'requestAdapter returned null');
9-
const device = await adapter.requestDevice();
9+
10+
const hasTimestampQuery = adapter.features.has('timestamp-query');
11+
const device = await adapter.requestDevice({
12+
requiredFeatures: hasTimestampQuery ? ['timestamp-query'] : [],
13+
});
14+
15+
const perfDisplayContainer = document.createElement('div');
16+
perfDisplayContainer.style.color = 'white';
17+
perfDisplayContainer.style.background = 'black';
18+
perfDisplayContainer.style.position = 'absolute';
19+
perfDisplayContainer.style.top = '10px';
20+
perfDisplayContainer.style.left = '10px';
21+
perfDisplayContainer.style.textAlign = 'left';
22+
23+
const perfDisplay = document.createElement('pre');
24+
perfDisplayContainer.appendChild(perfDisplay);
25+
if (canvas.parentNode) {
26+
canvas.parentNode.appendChild(perfDisplayContainer);
27+
} else {
28+
console.error('canvas.parentNode is null');
29+
}
1030

1131
if (!pageState.active) return;
1232
const context = canvas.getContext('webgpu') as GPUCanvasContext;
@@ -86,7 +106,7 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
86106
},
87107
});
88108

89-
const renderPassDescriptor = {
109+
const renderPassDescriptor: GPURenderPassDescriptor = {
90110
colorAttachments: [
91111
{
92112
view: undefined as GPUTextureView, // Assigned later
@@ -97,6 +117,38 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
97117
],
98118
};
99119

120+
const computePassDescriptor: GPUComputePassDescriptor = {};
121+
122+
/** Storage for timestamp query results */
123+
let querySet: GPUQuerySet | undefined = undefined;
124+
/** Timestamps are resolved into this buffer */
125+
let resolveBuffer: GPUBuffer | undefined = undefined;
126+
/** Pool of spare buffers for MAP_READing the timestamps back to CPU. A buffer
127+
* is taken from the pool (if available) when a readback is needed, and placed
128+
* back into the pool once the readback is done and it's unmapped. */
129+
const spareResultBuffers = [];
130+
131+
if (hasTimestampQuery) {
132+
querySet = device.createQuerySet({
133+
type: 'timestamp',
134+
count: 4,
135+
});
136+
resolveBuffer = device.createBuffer({
137+
size: 4 * BigInt64Array.BYTES_PER_ELEMENT,
138+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
139+
});
140+
computePassDescriptor.timestampWrites = {
141+
querySet,
142+
beginningOfPassWriteIndex: 0,
143+
endOfPassWriteIndex: 1,
144+
};
145+
renderPassDescriptor.timestampWrites = {
146+
querySet,
147+
beginningOfPassWriteIndex: 2,
148+
endOfPassWriteIndex: 3,
149+
};
150+
}
151+
100152
// prettier-ignore
101153
const vertexBufferData = new Float32Array([
102154
-0.01, -0.02, 0.01,
@@ -207,6 +259,8 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
207259
}
208260

209261
let t = 0;
262+
let computePassDurationSum = 0;
263+
let renderPassDurationSum = 0;
210264
function frame() {
211265
// Sample is no longer the active page.
212266
if (!pageState.active) return;
@@ -217,7 +271,9 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
217271

218272
const commandEncoder = device.createCommandEncoder();
219273
{
220-
const passEncoder = commandEncoder.beginComputePass();
274+
const passEncoder = commandEncoder.beginComputePass(
275+
computePassDescriptor
276+
);
221277
passEncoder.setPipeline(computePipeline);
222278
passEncoder.setBindGroup(0, particleBindGroups[t % 2]);
223279
passEncoder.dispatchWorkgroups(Math.ceil(numParticles / 64));
@@ -231,8 +287,54 @@ const init: SampleInit = async ({ canvas, pageState, gui }) => {
231287
passEncoder.draw(3, numParticles, 0, 0);
232288
passEncoder.end();
233289
}
290+
291+
let resultBuffer: GPUBuffer | undefined = undefined;
292+
if (hasTimestampQuery) {
293+
resultBuffer =
294+
spareResultBuffers.pop() ||
295+
device.createBuffer({
296+
size: 4 * BigInt64Array.BYTES_PER_ELEMENT,
297+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
298+
});
299+
commandEncoder.resolveQuerySet(querySet, 0, 4, resolveBuffer, 0);
300+
commandEncoder.copyBufferToBuffer(
301+
resolveBuffer,
302+
0,
303+
resultBuffer,
304+
0,
305+
resultBuffer.size
306+
);
307+
}
308+
234309
device.queue.submit([commandEncoder.finish()]);
235310

311+
if (hasTimestampQuery) {
312+
resultBuffer.mapAsync(GPUMapMode.READ).then(() => {
313+
const times = new BigInt64Array(resultBuffer.getMappedRange());
314+
computePassDurationSum += Number(times[1] - times[0]);
315+
renderPassDurationSum += Number(times[3] - times[2]);
316+
resultBuffer.unmap();
317+
318+
// Periodically update the text for the timer stats
319+
const kNumTimerSamples = 100;
320+
if (t % kNumTimerSamples === 0) {
321+
const avgComputeMicroseconds = Math.round(
322+
computePassDurationSum / kNumTimerSamples / 1000
323+
);
324+
const avgRenderMicroseconds = Math.round(
325+
renderPassDurationSum / kNumTimerSamples / 1000
326+
);
327+
perfDisplay.textContent = `\
328+
avg compute pass duration: ${avgComputeMicroseconds}µs
329+
avg render pass duration: ${avgRenderMicroseconds}µs
330+
spare readback buffers: ${spareResultBuffers.length}`;
331+
computePassDurationSum = 0;
332+
renderPassDurationSum = 0;
333+
}
334+
spareResultBuffers.push(resultBuffer);
335+
});
336+
}
337+
236338
++t;
237339
requestAnimationFrame(frame);
238340
}

0 commit comments

Comments
 (0)