Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup timestamp-query example #476

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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