Skip to content

Commit 5c6b119

Browse files
authored
Add more timestamp tests (#4631)
* Test that resolving a timestamp twice produces the same result. * Test that slots that are never used resolve to zero.
1 parent 5234d65 commit 5c6b119

1 file changed

Lines changed: 169 additions & 53 deletions

File tree

src/webgpu/api/operation/command_buffer/queries/timestampQuery.spec.ts

Lines changed: 169 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,40 @@ import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../../../gpu_test.js';
1717

1818
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
1919

20+
function encodeTimestampQueries(
21+
encoder: GPUCommandEncoder,
22+
view: GPUTextureView,
23+
querySet: GPUQuerySet,
24+
stage: 'compute' | 'render',
25+
slot: number
26+
) {
27+
switch (stage) {
28+
case 'compute': {
29+
const pass = encoder.beginComputePass({
30+
timestampWrites: {
31+
querySet,
32+
beginningOfPassWriteIndex: slot,
33+
endOfPassWriteIndex: slot + 1,
34+
},
35+
});
36+
pass.end();
37+
break;
38+
}
39+
case 'render': {
40+
const pass = encoder.beginRenderPass({
41+
colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }],
42+
timestampWrites: {
43+
querySet,
44+
beginningOfPassWriteIndex: slot,
45+
endOfPassWriteIndex: slot + 1,
46+
},
47+
});
48+
pass.end();
49+
break;
50+
}
51+
}
52+
}
53+
2054
g.test('many_query_sets')
2155
.desc(
2256
`
@@ -61,31 +95,7 @@ and prevent pages from running.
6195
count: 2,
6296
});
6397

64-
switch (stage) {
65-
case 'compute': {
66-
const pass = encoder.beginComputePass({
67-
timestampWrites: {
68-
querySet,
69-
beginningOfPassWriteIndex: 0,
70-
endOfPassWriteIndex: 1,
71-
},
72-
});
73-
pass.end();
74-
break;
75-
}
76-
case 'render': {
77-
const pass = encoder.beginRenderPass({
78-
colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }],
79-
timestampWrites: {
80-
querySet,
81-
beginningOfPassWriteIndex: 0,
82-
endOfPassWriteIndex: 1,
83-
},
84-
});
85-
pass.end();
86-
break;
87-
}
88-
}
98+
encodeTimestampQueries(encoder, view, querySet, stage, 0);
8999
}
90100

91101
const shouldError = false; // just expect no error
@@ -119,34 +129,8 @@ function encoderQueryUsage(
119129
count: numSlots,
120130
});
121131

122-
switch (stage) {
123-
case 'compute': {
124-
for (let slot = 0; slot < numSlots; slot += 2) {
125-
const pass = encoder.beginComputePass({
126-
timestampWrites: {
127-
querySet,
128-
beginningOfPassWriteIndex: slot,
129-
endOfPassWriteIndex: slot + 1,
130-
},
131-
});
132-
pass.end();
133-
}
134-
break;
135-
}
136-
case 'render': {
137-
for (let slot = 0; slot < numSlots; slot += 2) {
138-
const pass = encoder.beginRenderPass({
139-
colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }],
140-
timestampWrites: {
141-
querySet,
142-
beginningOfPassWriteIndex: slot,
143-
endOfPassWriteIndex: slot + 1,
144-
},
145-
});
146-
pass.end();
147-
}
148-
break;
149-
}
132+
for (let slot = 0; slot < numSlots; slot += 2) {
133+
encodeTimestampQueries(encoder, view, querySet, stage, slot);
150134
}
151135

152136
return querySet;
@@ -217,3 +201,135 @@ to make sure the implementation doesn't mistakenly mark them as used.
217201
t.expectGPUBufferValuesEqual(buffer, expected);
218202
}
219203
});
204+
205+
g.test('multi_resolve')
206+
.desc(`Test resolving more than once does not change the results`)
207+
.params(u => u.combine('stage', ['compute', 'render'] as const))
208+
.fn(async t => {
209+
const { stage } = t.params;
210+
const kNumQueries = 64;
211+
212+
t.skipIfDeviceDoesNotHaveFeature('timestamp-query');
213+
214+
const querySet = t.createQuerySetTracked({
215+
type: 'timestamp',
216+
count: kNumQueries,
217+
});
218+
219+
const view = t
220+
.createTextureTracked({
221+
size: [1, 1, 1],
222+
format: 'rgba8unorm',
223+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
224+
})
225+
.createView();
226+
const encoder = t.device.createCommandEncoder();
227+
228+
for (let slot = 0; slot < kNumQueries; slot += 2) {
229+
// skip every other pair so we can test resolving un-used slots
230+
const pair = (slot / 2) | 0;
231+
if (pair % 2) {
232+
continue;
233+
}
234+
encodeTimestampQueries(encoder, view, querySet, stage, slot);
235+
}
236+
237+
const size = kNumQueries * 8;
238+
const resolveBuffer1 = t.createBufferTracked({
239+
size,
240+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
241+
});
242+
const resolveBuffer2 = t.createBufferTracked({
243+
size,
244+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
245+
});
246+
const resultBuffer1 = t.createBufferTracked({
247+
size,
248+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
249+
});
250+
251+
encoder.resolveQuerySet(querySet, 0, kNumQueries, resolveBuffer1, 0);
252+
encoder.resolveQuerySet(querySet, 0, kNumQueries, resolveBuffer2, 0);
253+
encoder.copyBufferToBuffer(resolveBuffer1, 0, resultBuffer1, 0, size);
254+
255+
t.device.queue.submit([encoder.finish()]);
256+
257+
// Read back the first result.
258+
await resultBuffer1.mapAsync(GPUMapMode.READ);
259+
const expected = new Uint32Array(resultBuffer1.getMappedRange());
260+
t.expectGPUBufferValuesEqual(resolveBuffer2, expected);
261+
});
262+
263+
g.test('unused_slots_are_zero')
264+
.desc(`Test that unused slots are resolved to zero`)
265+
.params(u => u.combine('stage', ['compute', 'render'] as const))
266+
.fn(t => {
267+
const { stage } = t.params;
268+
const kNumQueries = 64;
269+
270+
t.skipIfDeviceDoesNotHaveFeature('timestamp-query');
271+
272+
const querySet = t.createQuerySetTracked({
273+
type: 'timestamp',
274+
count: kNumQueries,
275+
});
276+
277+
const view = t
278+
.createTextureTracked({
279+
size: [1, 1, 1],
280+
format: 'rgba8unorm',
281+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
282+
})
283+
.createView();
284+
const usedEncoder = t.device.createCommandEncoder();
285+
const unusedEncoder = t.device.createCommandEncoder();
286+
287+
for (let slot = 0; slot < kNumQueries; slot += 2) {
288+
const pair = (slot / 2) | 0;
289+
const encoder = pair % 2 ? usedEncoder : unusedEncoder;
290+
encodeTimestampQueries(encoder, view, querySet, stage, slot);
291+
}
292+
293+
unusedEncoder.finish(); // don't submit this encoder
294+
295+
const size = kNumQueries * 8;
296+
const resolveBuffer = t.createBufferTracked({
297+
size,
298+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
299+
});
300+
301+
usedEncoder.resolveQuerySet(querySet, 0, kNumQueries, resolveBuffer, 0);
302+
303+
t.device.queue.submit([usedEncoder.finish()]);
304+
305+
// Read back the first result.
306+
t.expectGPUBufferValuesPassCheck(
307+
resolveBuffer,
308+
(actualU32: Uint32Array) => {
309+
// MAINTENANCE_TODO: expectGPUBufferValuesPassCheck doesn't work with BigUint64Array.
310+
const actual = new BigUint64Array(
311+
actualU32.buffer,
312+
actualU32.byteOffset,
313+
actualU32.byteLength / 8
314+
);
315+
const errors: string[] = [];
316+
for (let slot = 0; slot < kNumQueries; ++slot) {
317+
t.debug(() => `slot ${slot}: ${actual[slot]}`);
318+
const pair = (slot / 2) | 0;
319+
if (pair % 2) {
320+
// used slot, implementation defined so don't check
321+
} else {
322+
// unused slot, expect zero
323+
if (actual[slot] !== 0n) {
324+
errors.push(`slot ${slot} expected 0 but got ${actual[slot]}`);
325+
}
326+
}
327+
}
328+
return errors.length === 0 ? undefined : new Error(errors.join('\n'));
329+
},
330+
{
331+
type: Uint32Array,
332+
typedLength: kNumQueries * 2, // because it's really BigUint64Array data
333+
}
334+
);
335+
});

0 commit comments

Comments
 (0)