Skip to content

Commit 0f4e899

Browse files
greggmankainino0x
andauthored
Test uninitialized queries in unsubmitted command buffers (#4624)
* Test uninitialized queries in unsubmitted command buffers Query slots that have never been used at required to resolve to zero. Some implementaitons might wrongly mark them as used at encoding time instead of submit time. * Update src/webgpu/api/operation/command_buffer/queries/timestampQuery.spec.ts Co-authored-by: Kai Ninomiya <kainino1@gmail.com> --------- Co-authored-by: Kai Ninomiya <kainino1@gmail.com>
1 parent ad254c6 commit 0f4e899

2 files changed

Lines changed: 145 additions & 46 deletions

File tree

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,49 @@ g.test('occlusion_query,initial')
520520
);
521521
});
522522

523+
g.test('occlusion_query,unsubmitted_render_pass')
524+
.desc(
525+
`
526+
Test getting contents of QuerySet without any queries with an unsubmitted render pass that uses them.
527+
This is a regression test for a bug in Dawn where encoding the pass marked the slots as ok to read
528+
when they should not have been marked as okay to read until the command buffer was submitted.
529+
`
530+
)
531+
.fn(async t => {
532+
const kNumQueries = kMaxQueryCount;
533+
const resources = t.setup({ numQueries: kNumQueries });
534+
535+
// Encode a render pass that uses the QuerySet but don't submit it.
536+
{
537+
const encoder = t.device.createCommandEncoder();
538+
const pass = encoder.beginRenderPass({
539+
colorAttachments: [
540+
{
541+
view: resources.renderTargetTexture.createView(),
542+
loadOp: 'clear',
543+
storeOp: 'store',
544+
},
545+
],
546+
occlusionQuerySet: resources.occlusionQuerySet,
547+
});
548+
for (let i = 0; i < resources.occlusionQuerySet.count; i += 2) {
549+
pass.beginOcclusionQuery(i);
550+
pass.endOcclusionQuery();
551+
}
552+
pass.end();
553+
encoder.finish();
554+
}
555+
556+
await t.runQueryTest(
557+
resources,
558+
null,
559+
() => {},
560+
(passed: boolean) => {
561+
t.expect(!passed);
562+
}
563+
);
564+
});
565+
523566
g.test('occlusion_query,basic')
524567
.desc('Test all queries pass')
525568
.params(kQueryTestBaseParams)

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

Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ there is not much we can test except that there are no errors.
88
- compute pass
99
- render pass
1010
- 64k query objects
11+
- resolving unused slots
1112
`;
1213

1314
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
14-
import { AllFeaturesMaxLimitsGPUTest } from '../../../../gpu_test.js';
15+
import { range } from '../../../../../common/util/util.js';
16+
import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../../../gpu_test.js';
1517

1618
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
1719

@@ -95,6 +97,63 @@ and prevent pages from running.
9597
}
9698
});
9799

100+
function encoderQueryUsage(
101+
t: GPUTest,
102+
stage: 'compute' | 'render',
103+
numQuerySets: number,
104+
numSlots: number
105+
) {
106+
const encoder = t.device.createCommandEncoder();
107+
108+
const view = t
109+
.createTextureTracked({
110+
size: [1, 1, 1],
111+
format: 'rgba8unorm',
112+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
113+
})
114+
.createView();
115+
116+
const querySets = range(numQuerySets, _ => {
117+
const querySet = t.createQuerySetTracked({
118+
type: 'timestamp',
119+
count: numSlots,
120+
});
121+
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+
}
150+
}
151+
152+
return querySet;
153+
});
154+
return { encoder, querySets };
155+
}
156+
98157
g.test('many_slots')
99158
.desc(
100159
`
@@ -112,52 +171,49 @@ So, test we can use 4k slots across a few QuerySets
112171
const kNumSlots = 4096;
113172
const kNumQuerySets = 4;
114173

115-
const view = t
116-
.createTextureTracked({
117-
size: [1, 1, 1],
118-
format: 'rgba8unorm',
119-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
120-
})
121-
.createView();
122-
const encoder = t.device.createCommandEncoder();
174+
const { encoder } = encoderQueryUsage(t, stage, kNumQuerySets, kNumSlots);
175+
t.device.queue.submit([encoder.finish()]);
176+
});
123177

124-
for (let i = 0; i < kNumQuerySets; ++i) {
125-
const querySet = t.createQuerySetTracked({
126-
type: 'timestamp',
127-
count: kNumSlots,
128-
});
178+
g.test('resolve_unused_slots')
179+
.desc(
180+
`
181+
Test resolving query sets with unused slots.
129182
130-
switch (stage) {
131-
case 'compute': {
132-
for (let slot = 0; slot < kNumSlots; slot += 2) {
133-
const pass = encoder.beginComputePass({
134-
timestampWrites: {
135-
querySet,
136-
beginningOfPassWriteIndex: slot,
137-
endOfPassWriteIndex: slot + 1,
138-
},
139-
});
140-
pass.end();
141-
}
142-
break;
143-
}
144-
case 'render': {
145-
for (let slot = 0; slot < kNumSlots; slot += 2) {
146-
const pass = encoder.beginRenderPass({
147-
colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }],
148-
timestampWrites: {
149-
querySet,
150-
beginningOfPassWriteIndex: slot,
151-
endOfPassWriteIndex: slot + 1,
152-
},
153-
});
154-
pass.end();
155-
}
156-
break;
157-
}
158-
}
159-
}
183+
We create a command buffer that uses the slots but don't actually submit it
184+
to make sure the implementation doesn't mistakenly mark them as used.
185+
`
186+
)
187+
.params(u => u.combine('stage', ['compute', 'render'] as const))
188+
.fn(t => {
189+
const { stage } = t.params;
160190

161-
const shouldError = false; // just expect no error
162-
t.expectValidationError(() => t.device.queue.submit([encoder.finish()]), shouldError);
191+
t.skipIfDeviceDoesNotHaveFeature('timestamp-query');
192+
193+
const kNumSlots = 4096;
194+
const kNumQuerySets = 2;
195+
196+
// Create a encoder and encode usage of every query and slot but do not submit it.
197+
const querySets = (() => {
198+
const { encoder, querySets } = encoderQueryUsage(t, stage, kNumQuerySets, kNumSlots);
199+
encoder.finish();
200+
return querySets;
201+
})();
202+
203+
// Read the slots, they should all be zero.
204+
const encoder = t.device.createCommandEncoder();
205+
const buffers = querySets.map((querySet, i) => {
206+
const resolveBuffer = t.createBufferTracked({
207+
size: kNumSlots * 8,
208+
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE,
209+
});
210+
encoder.resolveQuerySet(querySet, 0, kNumSlots, resolveBuffer, 0);
211+
return resolveBuffer;
212+
});
213+
t.device.queue.submit([encoder.finish()]);
214+
215+
for (const buffer of buffers) {
216+
const expected = new Uint8Array(buffer.size);
217+
t.expectGPUBufferValuesEqual(buffer, expected);
218+
}
163219
});

0 commit comments

Comments
 (0)