export const description = `
API operations tests for occlusion queries.
- test query with
- scissor
- sample mask
- alpha to coverage
- stencil
- depth test
- test empty query (no draw) (should be cleared?)
- test via render bundle
- test resolveQuerySet with non-zero firstIndex
- test no queries is zero
- test 0x0 -> 0x3 sample mask
- test 0 -> 1 alpha to coverage
- test resolving twice in same pass keeps values
- test resolving twice across pass keeps values
- test resolveQuerySet destinationOffset
- test using the same query multiple times in different passes without resolving in between
`;
import { kUnitCaseParamsBuilder } from '../../../../../common/framework/params_builder.js';
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import {
assert,
TypedArrayBufferView,
range,
unreachable,
} from '../../../../../common/util/util.js';
import { kMaxQueryCount } from '../../../../capability_info.js';
import { DepthStencilFormat } from '../../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../../gpu_test.js';
const kRequiredQueryBufferOffsetAlignment = 256;
const kBytesPerQuery = 8;
const kTextureSize = [4, 4];
const kRenderModes = ['direct', 'render-bundle'] as const;
type RenderMode = (typeof kRenderModes)[number];
const kBufferOffsets = ['zero', 'non-zero'] as const;
type BufferOffset = (typeof kBufferOffsets)[number];
type SetupParams = {
numQueries: number;
depthStencilFormat?: DepthStencilFormat;
sampleCount?: number;
sampleMask?: number;
alpha?: number;
writeMask?: number;
bufferOffset?: BufferOffset;
querySetOffset?: BufferOffset;
renderMode?: RenderMode;
};
* This class is used by the RenderPassHelper below to
* abstract calling these 4 functions on a RenderPassEncoder or a RenderBundleEncoder.
*/
interface QueryHelper {
setPipeline(pipeline: GPURenderPipeline): void;
setVertexBuffer(buffer: GPUBuffer): void;
draw(count: number): void;
end(): void;
}
interface QueryStarter {
begin(endFn: () => void): QueryHelper;
}
* This class helps use a render pass encoder or a render bundle encoder
* in the correct way given the order that operations must happen, in order to be
* compatible across both paths.
*/
class RenderPassHelper {
_pass: GPURenderPassEncoder;
_helper: QueryStarter;
_queryHelper?: QueryHelper;
constructor(pass: GPURenderPassEncoder, helper: QueryStarter) {
this._pass = pass;
this._helper = helper;
}
setScissorRect(x: number, y: number, width: number, height: number) {
assert(!this._queryHelper);
this._pass.setScissorRect(x, y, width, height);
}
setStencilReference(ref: number) {
assert(!this._queryHelper);
this._pass.setStencilReference(ref);
}
beginOcclusionQuery(queryIndex: number) {
assert(!this._queryHelper);
this._pass.beginOcclusionQuery(queryIndex);
this._queryHelper = this._helper.begin(() => {
assert(!!this._queryHelper);
this._queryHelper = undefined;
this._pass.endOcclusionQuery();
});
return this._queryHelper;
}
}
* Helper class for using a render pass encoder directly
*/
class QueryHelperDirect implements QueryHelper {
_pass?: GPURenderPassEncoder;
_endFn: () => void;
constructor(pass: GPURenderPassEncoder, endFn: () => void) {
this._pass = pass;
this._endFn = endFn;
}
setPipeline(pipeline: GPURenderPipeline): void {
assert(!!this._pass);
this._pass.setPipeline(pipeline);
}
setVertexBuffer(buffer: GPUBuffer): void {
assert(!!this._pass);
this._pass.setVertexBuffer(0, buffer);
}
draw(count: number): void {
assert(!!this._pass);
this._pass.draw(count);
}
end() {
const fn = this._endFn;
this._endFn = unreachable;
this._pass = undefined;
fn();
}
}
* Helper class for starting a query on a render pass encoder directly
*/
class QueryStarterDirect implements QueryStarter {
_pass: GPURenderPassEncoder;
_helper?: QueryHelperDirect;
constructor(pass: GPURenderPassEncoder) {
this._pass = pass;
}
begin(endFn: () => void) {
assert(!this._helper);
this._helper = new QueryHelperDirect(this._pass, () => {
this._helper = undefined;
endFn();
});
return this._helper;
}
}
* Helper class for using a render bundle encoder.
*/
class QueryHelperRenderBundle implements QueryHelper {
_encoder?: GPURenderBundleEncoder;
_endFn: () => void;
constructor(pass: GPURenderBundleEncoder, endFn: () => void) {
this._encoder = pass;
this._endFn = endFn;
}
setPipeline(pipeline: GPURenderPipeline): void {
assert(!!this._encoder);
this._encoder.setPipeline(pipeline);
}
setVertexBuffer(buffer: GPUBuffer): void {
assert(!!this._encoder);
this._encoder.setVertexBuffer(0, buffer);
}
draw(count: number): void {
assert(!!this._encoder);
this._encoder.draw(count);
}
end() {
const fn = this._endFn;
this._endFn = unreachable;
this._encoder = undefined;
fn();
}
}
* Helper class for starting a query on a render bundle encoder
*/
class QueryStarterRenderBundle implements QueryStarter {
_device: GPUDevice;
_pass: GPURenderPassEncoder;
_renderBundleEncoderDescriptor: GPURenderBundleEncoderDescriptor;
_encoder?: GPURenderBundleEncoder;
_helper?: QueryHelperRenderBundle;
constructor(
device: GPUDevice,
pass: GPURenderPassEncoder,
renderPassDescriptor: GPURenderPassDescriptor
) {
this._device = device;
this._pass = pass;
const colorAttachment = (
renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[]
)[0];
this._renderBundleEncoderDescriptor = {
colorFormats: ['rgba8unorm'],
depthStencilFormat: renderPassDescriptor.depthStencilAttachment?.depthLoadOp
? 'depth24plus'
: renderPassDescriptor.depthStencilAttachment?.stencilLoadOp
? 'stencil8'
: undefined,
sampleCount: colorAttachment.resolveTarget ? 4 : 1,
};
}
begin(endFn: () => void) {
assert(!this._encoder);
this._encoder = this._device.createRenderBundleEncoder(this._renderBundleEncoderDescriptor);
this._helper = new QueryHelperRenderBundle(this._encoder, () => {
assert(!!this._encoder);
assert(!!this._helper);
this._pass.executeBundles([this._encoder.finish()]);
this._helper = undefined;
this._encoder = undefined;
endFn();
});
return this._helper;
}
setPipeline(pipeline: GPURenderPipeline): void {
assert(!!this._encoder);
this._encoder.setPipeline(pipeline);
}
setVertexBuffer(buffer: GPUBuffer): void {
assert(!!this._encoder);
this._encoder.setVertexBuffer(0, buffer);
}
draw(count: number) {
assert(!!this._encoder);
this._encoder.draw(count);
}
}
class OcclusionQueryTest extends AllFeaturesMaxLimitsGPUTest {
createVertexBuffer(data: TypedArrayBufferView) {
return this.makeBufferWithContents(data, GPUBufferUsage.VERTEX);
}
createSingleTriangleVertexBuffer(z: number) {
return this.createVertexBuffer(new Float32Array([
-0.5, -0.5, z,
0.5, -0.5, z,
-0.5, 0.5, z,
]));
}
async readBufferAsBigUint64(buffer: GPUBuffer) {
await buffer.mapAsync(GPUMapMode.READ);
const result = new BigUint64Array(buffer.getMappedRange().slice(0));
buffer.unmap();
return result;
}
setup(params: SetupParams) {
const {
numQueries,
depthStencilFormat,
sampleMask = 0xffffffff,
alpha,
sampleCount,
writeMask = 0xf,
bufferOffset,
renderMode,
} = params;
const { device } = this;
const queryResolveBufferOffset =
bufferOffset === 'non-zero' ? kRequiredQueryBufferOffsetAlignment : 0;
const queryResolveBuffer = this.createBufferTracked({
size: numQueries * 8 + queryResolveBufferOffset,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
});
const readBuffer = this.createBufferTracked({
size: numQueries * kBytesPerQuery,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const vertexBuffer = this.createSingleTriangleVertexBuffer(0);
const renderTargetTexture = this.createTextureTracked({
format: 'rgba8unorm',
size: kTextureSize,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const multisampleRenderTarget = sampleCount
? this.createTextureTracked({
size: kTextureSize,
format: 'rgba8unorm',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount,
})
: null;
const depthStencilTexture = depthStencilFormat
? this.createTextureTracked({
format: depthStencilFormat,
size: kTextureSize,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
})
: undefined;
const module = device.createShaderModule({
code: `
@vertex fn vs(@location(0) pos: vec4f) -> @builtin(position) vec4f {
return pos;
}
@fragment fn fs() -> @location(0) vec4f {
return vec4f(0, 0, 0, ${alpha === undefined ? 1 : alpha});
}
`,
});
const haveDepth = !!depthStencilFormat && depthStencilFormat.includes('depth');
const haveStencil = !!depthStencilFormat && depthStencilFormat.includes('stencil');
assert(!(haveDepth && haveStencil), 'code does not handle mixed depth-stencil');
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
{
arrayStride: 3 * 4,
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x3',
},
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format: 'rgba8unorm', writeMask }],
},
...(sampleCount && {
multisample: {
count: sampleCount,
mask: alpha === undefined ? sampleMask : 0xffffffff,
alphaToCoverageEnabled: alpha !== undefined,
},
}),
...(depthStencilTexture && {
depthStencil: {
format: depthStencilFormat as GPUTextureFormat,
depthWriteEnabled: haveDepth,
depthCompare: haveDepth ? 'less-equal' : 'always',
...(haveStencil && {
stencilFront: {
compare: 'equal',
},
}),
},
}),
});
const querySetOffset = params?.querySetOffset === 'non-zero' ? 7 : 0;
const occlusionQuerySet = this.createQuerySetTracked({
type: 'occlusion',
count: numQueries + querySetOffset,
});
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: sampleCount
? [
{
view: multisampleRenderTarget!.createView(),
resolveTarget: renderTargetTexture.createView(),
loadOp: 'clear',
storeOp: 'store',
},
]
: [
{
view: renderTargetTexture.createView(),
loadOp: 'clear',
storeOp: 'store',
},
],
...(haveDepth && {
depthStencilAttachment: {
view: depthStencilTexture!.createView(),
depthLoadOp: 'clear',
depthStoreOp: 'store',
depthClearValue: 0.5,
},
}),
...(haveStencil && {
depthStencilAttachment: {
view: depthStencilTexture!.createView(),
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
},
}),
occlusionQuerySet,
};
return {
readBuffer,
vertexBuffer,
queryResolveBuffer,
queryResolveBufferOffset,
occlusionQuerySet,
renderTargetTexture,
renderPassDescriptor,
pipeline,
depthStencilTexture,
querySetOffset,
renderMode,
};
}
encodeQueries(
resources: ReturnType<OcclusionQueryTest['setup']>,
encoder: GPUCommandEncoder,
renderPassDescriptor: GPURenderPassDescriptor,
encodePassFn: (helper: RenderPassHelper, queryIndex: number) => void
) {
const { device } = this;
const { occlusionQuerySet, querySetOffset, renderMode = 'direct' } = resources;
const numQueries = occlusionQuerySet.count - querySetOffset;
const queryIndices = range(numQueries, (i: number) => i + querySetOffset);
const pass = encoder.beginRenderPass(renderPassDescriptor);
const helper = new RenderPassHelper(
pass,
renderMode === 'direct'
? new QueryStarterDirect(pass)
: new QueryStarterRenderBundle(device, pass, renderPassDescriptor)
);
for (const queryIndex of queryIndices) {
encodePassFn(helper, queryIndex);
}
pass.end();
}
encodeAndResolveQueries(
resources: ReturnType<OcclusionQueryTest['setup']>,
encoder: GPUCommandEncoder,
renderPassDescriptor: GPURenderPassDescriptor | null,
encodePassFn: (helper: RenderPassHelper, queryIndex: number) => void
) {
const {
readBuffer,
queryResolveBuffer,
queryResolveBufferOffset,
occlusionQuerySet,
querySetOffset,
} = resources;
const numQueries = occlusionQuerySet.count - querySetOffset;
if (renderPassDescriptor) {
this.encodeQueries(resources, encoder, renderPassDescriptor, encodePassFn);
}
encoder.resolveQuerySet(
occlusionQuerySet,
querySetOffset,
numQueries,
queryResolveBuffer,
queryResolveBufferOffset
);
encoder.copyBufferToBuffer(
queryResolveBuffer,
queryResolveBufferOffset,
readBuffer,
0,
readBuffer.size
);
}
async checkQueryResults(
resources: ReturnType<OcclusionQueryTest['setup']>,
checkQueryIndexResultFn: (passed: boolean, queryIndex: number) => void
) {
const { readBuffer, occlusionQuerySet, querySetOffset } = resources;
const numQueries = occlusionQuerySet.count - querySetOffset;
const queryIndices = range(numQueries, (i: number) => i + querySetOffset);
const result = await this.readBufferAsBigUint64(readBuffer);
for (const queryIndex of queryIndices) {
const resultNdx = queryIndex - querySetOffset;
const passed = !!result[resultNdx];
checkQueryIndexResultFn(passed, queryIndex);
}
return result;
}
async runQueryTest(
resources: ReturnType<OcclusionQueryTest['setup']>,
renderPassDescriptor: GPURenderPassDescriptor | null,
encodePassFn: (helper: RenderPassHelper, queryIndex: number) => void,
checkQueryIndexResultFn: (passed: boolean, queryIndex: number) => void
) {
const { device } = this;
const encoder = device.createCommandEncoder();
this.encodeAndResolveQueries(resources, encoder, renderPassDescriptor, encodePassFn);
device.queue.submit([encoder.finish()]);
return this.checkQueryResults(resources, checkQueryIndexResultFn);
}
}
const kQueryTestBaseParams = kUnitCaseParamsBuilder
.combine('writeMask', [0xf, 0x0])
.combine('renderMode', kRenderModes)
.combine('bufferOffset', kBufferOffsets)
.combine('querySetOffset', kBufferOffsets);
export const g = makeTestGroup(OcclusionQueryTest);
g.test('occlusion_query,initial')
.desc(`Test getting contents of QuerySet without any queries.`)
.fn(async t => {
const kNumQueries = kMaxQueryCount;
const resources = t.setup({ numQueries: kNumQueries });
await t.runQueryTest(
resources,
null,
() => {},
(passed: boolean) => {
t.expect(!passed);
}
);
});
g.test('occlusion_query,unsubmitted_render_pass')
.desc(
`
Test getting contents of QuerySet without any queries with an unsubmitted render pass that uses them.
This is a regression test for a bug in Dawn where encoding the pass marked the slots as ok to read
when they should not have been marked as okay to read until the command buffer was submitted.
`
)
.fn(async t => {
const kNumQueries = kMaxQueryCount;
const resources = t.setup({ numQueries: kNumQueries });
{
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: resources.renderTargetTexture.createView(),
loadOp: 'clear',
storeOp: 'store',
},
],
occlusionQuerySet: resources.occlusionQuerySet,
});
for (let i = 0; i < resources.occlusionQuerySet.count; i += 2) {
pass.beginOcclusionQuery(i);
pass.endOcclusionQuery();
}
pass.end();
encoder.finish();
}
await t.runQueryTest(
resources,
null,
() => {},
(passed: boolean) => {
t.expect(!passed);
}
);
});
g.test('occlusion_query,basic')
.desc('Test all queries pass')
.params(kQueryTestBaseParams)
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset } = t.params;
const kNumQueries = 30;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
});
const { renderPassDescriptor, vertexBuffer, pipeline } = resources;
await t.runQueryTest(
resources,
renderPassDescriptor,
(helper, queryIndex) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(vertexBuffer);
queryHelper.draw(3);
queryHelper.end();
},
(passed, queryIndex) => {
const expectPassed = true;
t.expect(
!!passed === expectPassed,
`queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}`
);
}
);
});
g.test('occlusion_query,reuse')
.desc(
`
Test queries can be reused.
This tests that if you write to a query twice in the same command buffer (different passes)
and resolve after, that you get the 2nd result.
`
)
.params(kQueryTestBaseParams.combine('noop', ['before', 'after'] as const))
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset, noop } = t.params;
const kNumQueries = 30;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
});
const { renderPassDescriptor, vertexBuffer, pipeline } = resources;
const opEncodePassFn = (helper: RenderPassHelper, queryIndex: number) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(vertexBuffer);
queryHelper.draw(3);
queryHelper.end();
};
const noopEncodePassFn = (helper: RenderPassHelper, queryIndex: number) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.end();
};
const [firstOp, secondOp] =
noop === 'before' ? [noopEncodePassFn, opEncodePassFn] : [opEncodePassFn, noopEncodePassFn];
const { device } = t;
const encoder = device.createCommandEncoder();
t.encodeQueries(resources, encoder, renderPassDescriptor, firstOp);
t.encodeAndResolveQueries(resources, encoder, renderPassDescriptor, secondOp);
device.queue.submit([encoder.finish()]);
await t.checkQueryResults(resources, (passed, queryIndex) => {
const expectPassed = noop === 'before';
t.expect(
!!passed === expectPassed,
`queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}`
);
});
});
g.test('occlusion_query,empty')
.desc(
`
Test beginOcclusionQuery/endOcclusionQuery with nothing in between clears the queries
Calls beginOcclusionQuery/draw/endOcclusionQuery that should show passing fragments
and validates they passed. Then executes the same queries (same QuerySet) without drawing.
Those queries should have not passed.
`
)
.fn(async t => {
const kNumQueries = 30;
const resources = t.setup({ numQueries: kNumQueries });
const { vertexBuffer, renderPassDescriptor, pipeline } = resources;
const makeQueryRunner = (draw: boolean) => {
return (helper: RenderPassHelper, queryIndex: number) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(vertexBuffer);
if (draw) {
queryHelper.draw(3);
}
queryHelper.end();
};
};
const makeQueryChecker = (draw: boolean) => {
return (passed: boolean, queryIndex: number) => {
const expectPassed = draw;
t.expect(
!!passed === expectPassed,
`draw: ${draw}, queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}`
);
};
};
await t.runQueryTest(
resources,
renderPassDescriptor,
makeQueryRunner(true),
makeQueryChecker(true)
);
await t.runQueryTest(
resources,
renderPassDescriptor,
makeQueryRunner(false),
makeQueryChecker(false)
);
});
g.test('occlusion_query,scissor')
.desc(
`
Test beginOcclusionQuery/endOcclusionQuery using scissor to occlude
`
)
.params(kQueryTestBaseParams)
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset } = t.params;
const kNumQueries = 30;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
});
const { renderPassDescriptor, renderTargetTexture, vertexBuffer, pipeline } = resources;
const getScissorRect = (i: number) => {
const { width, height } = renderTargetTexture;
switch (i % 4) {
case 0:
return {
x: 0,
y: 0,
width,
height,
occluded: false,
name: 'whole target',
};
case 1:
return {
x: width / 4,
y: height / 4,
width: width / 2,
height: height / 2,
occluded: false,
name: 'center',
};
case 2:
return {
x: width / 4,
y: height / 4,
width: 0,
height: 0,
occluded: true,
name: 'none',
};
case 3:
return {
x: 0,
y: 0,
width,
height: height / 2,
occluded: true,
name: 'top quarter',
};
default:
unreachable();
}
};
await t.runQueryTest(
resources,
renderPassDescriptor,
(helper, queryIndex) => {
const { x, y, width, height } = getScissorRect(queryIndex);
helper.setScissorRect(x, y, width, height);
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(vertexBuffer);
queryHelper.draw(3);
queryHelper.end();
},
(passed, queryIndex) => {
const { occluded, name: scissorCase } = getScissorRect(queryIndex);
const expectPassed = !occluded;
t.expect(
!!passed === expectPassed,
`queryIndex: ${queryIndex}, scissorCase: ${scissorCase}, was: ${!!passed}, expected: ${expectPassed}`
);
}
);
});
g.test('occlusion_query,depth')
.desc(
`
Test beginOcclusionQuery/endOcclusionQuery using depth test to occlude
Compares depth against 0.5, with alternating vertex buffers which have a depth
of 0 and 1. When depth check passes, we expect non-zero successful fragments.
`
)
.params(kQueryTestBaseParams)
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset } = t.params;
const kNumQueries = 30;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
depthStencilFormat: 'depth24plus',
});
const { vertexBuffer: vertexBufferAtZ0, renderPassDescriptor, pipeline } = resources;
const vertexBufferAtZ1 = t.createSingleTriangleVertexBuffer(1);
await t.runQueryTest(
resources,
renderPassDescriptor,
(helper, queryIndex) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(queryIndex % 2 ? vertexBufferAtZ1 : vertexBufferAtZ0);
queryHelper.draw(3);
queryHelper.end();
},
(passed, queryIndex) => {
const expectPassed = queryIndex % 2 === 0;
t.expect(
!!passed === expectPassed,
`queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}`
);
}
);
});
g.test('occlusion_query,stencil')
.desc(
`
Test beginOcclusionQuery/endOcclusionQuery using stencil to occlude
Compares stencil against 0, with alternating stencil reference values of
of 0 and 1. When stencil test passes, we expect non-zero successful fragments.
`
)
.params(kQueryTestBaseParams)
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset } = t.params;
const kNumQueries = 30;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
depthStencilFormat: 'stencil8',
});
const { vertexBuffer, renderPassDescriptor, pipeline } = resources;
await t.runQueryTest(
resources,
renderPassDescriptor,
(helper, queryIndex) => {
helper.setStencilReference(queryIndex % 2);
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(vertexBuffer);
queryHelper.draw(3);
queryHelper.end();
},
(passed, queryIndex) => {
const expectPassed = queryIndex % 2 === 0;
t.expect(
!!passed === expectPassed,
`queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}`
);
}
);
});
g.test('occlusion_query,sample_mask')
.desc(
`
Test beginOcclusionQuery/endOcclusionQuery using sample_mask to occlude
Set sampleMask to 0, 2, 4, 6 and draw quads in top right or bottom left corners of the texel.
If the corner we draw to matches the corner masked we expect non-zero successful fragments.
See: https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels
`
)
.params(kQueryTestBaseParams.combine('sampleMask', [0, 2, 4, 6]))
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset, sampleMask } = t.params;
const kNumQueries = 30;
const sampleCount = 4;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
sampleCount,
sampleMask,
});
const { renderPassDescriptor, pipeline } = resources;
const createQuad = (offset: number) => {
return t.createVertexBuffer(new Float32Array([
offset + 0 , offset + 0 , 0,
offset + 0.25, offset + 0 , 0,
offset + 0 , offset + 0.25, 0,
offset + 0 , offset + 0.25, 0,
offset + 0.25, offset + 0 , 0,
offset + 0.25, offset + 0.25, 0,
]));
};
const vertexBufferBL = createQuad(0);
const vertexBufferTR = createQuad(0.25);
await t.runQueryTest(
resources,
renderPassDescriptor,
(helper, queryIndex) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(queryIndex % 2 ? vertexBufferTR : vertexBufferBL);
queryHelper.draw(6);
queryHelper.end();
},
(passed, queryIndex) => {
const drawMask = queryIndex % 2 ? 2 : 4;
const expectPassed = !!(sampleMask & drawMask);
t.expect(
!!passed === expectPassed,
`queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}`
);
}
);
});
g.test('occlusion_query,alpha_to_coverage')
.desc(
`
Test beginOcclusionQuery/endOcclusionQuery using alphaToCoverage to occlude
Set alpha to 0, 0.25, 0.5, 0.75, and 1, draw quads in 4 corners of texel.
Some should be culled. We count how many passed via queries. It's undefined which
will pass but it is defined how many will pass for a given alpha value.
Note: It seems like the result is well defined but if we find some devices/drivers
don't follow this exactly then we can relax check for the expected number of passed
queries.
See: https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f
`
)
.params(kQueryTestBaseParams.combine('alpha', [0, 0.25, 0.5, 0.75, 1.0]))
.fn(async t => {
const { writeMask, renderMode, bufferOffset, querySetOffset, alpha } = t.params;
const kNumQueries = 32;
const sampleCount = 4;
const resources = t.setup({
writeMask,
renderMode,
bufferOffset,
querySetOffset,
numQueries: kNumQueries,
sampleCount,
alpha,
});
const { renderPassDescriptor, pipeline } = resources;
const createQuad = (xOffset: number, yOffset: number) => {
return t.createVertexBuffer(new Float32Array([
xOffset + 0 , yOffset + 0 , 0,
xOffset + 0.25, yOffset + 0 , 0,
xOffset + 0 , yOffset + 0.25, 0,
xOffset + 0 , yOffset + 0.25, 0,
xOffset + 0.25, yOffset + 0 , 0,
xOffset + 0.25, yOffset + 0.25, 0,
]));
};
const vertexBuffers = [
createQuad(0, 0),
createQuad(0.25, 0),
createQuad(0, 0.25),
createQuad(0.25, 0.25),
];
const numPassedPerGroup: number[] = new Array(kNumQueries / 4).fill(0);
let queryCount = 0;
let resultCount = 0;
await t.runQueryTest(
resources,
renderPassDescriptor,
(helper, queryIndex) => {
const queryHelper = helper.beginOcclusionQuery(queryIndex);
queryHelper.setPipeline(pipeline);
queryHelper.setVertexBuffer(vertexBuffers[queryCount++ % 4]);
queryHelper.draw(6);
queryHelper.end();
},
passed => {
const groupIndex = (resultCount++ / 4) | 0;
numPassedPerGroup[groupIndex] += passed ? 1 : 0;
}
);
const expected = (alpha / 0.25) | 0;
numPassedPerGroup.forEach((numPassed, queryGroup) => {
t.expect(
numPassed === expected,
`queryGroup: ${queryGroup}, was: ${numPassed}, expected: ${expected}`
);
});
});
g.test('occlusion_query,multi_resolve')
.desc('Test calling resolveQuerySet more than once does not change results')
.fn(async t => {
const { device } = t;
const kNumQueries = 30;
const {
pipeline,
vertexBuffer,
occlusionQuerySet,
renderPassDescriptor,
renderTargetTexture,
queryResolveBuffer,
readBuffer,
} = t.setup({ numQueries: kNumQueries });
const readBuffer2 = t.createBufferTracked(readBuffer);
const readBuffer3 = t.createBufferTracked(readBuffer);
const renderSomething = (encoder: GPUCommandEncoder) => {
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setScissorRect(0, 0, renderTargetTexture.width, renderTargetTexture.height);
pass.draw(3);
pass.end();
};
{
const encoder = device.createCommandEncoder();
{
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
for (let i = 0; i < kNumQueries; ++i) {
pass.beginOcclusionQuery(i);
if (i % 2) {
pass.setScissorRect(0, 0, renderTargetTexture.width, renderTargetTexture.height);
} else {
pass.setScissorRect(0, 0, 0, 0);
}
pass.draw(3);
pass.endOcclusionQuery();
}
pass.end();
}
encoder.resolveQuerySet(occlusionQuerySet, 0, kNumQueries, queryResolveBuffer, 0);
encoder.resolveQuerySet(occlusionQuerySet, 0, kNumQueries, queryResolveBuffer, 0);
encoder.copyBufferToBuffer(queryResolveBuffer, 0, readBuffer, 0, readBuffer.size);
renderSomething(encoder);
encoder.resolveQuerySet(occlusionQuerySet, 0, kNumQueries, queryResolveBuffer, 0);
encoder.copyBufferToBuffer(queryResolveBuffer, 0, readBuffer2, 0, readBuffer2.size);
device.queue.submit([encoder.finish()]);
}
{
const encoder = device.createCommandEncoder();
renderSomething(encoder);
encoder.resolveQuerySet(occlusionQuerySet, 0, kNumQueries, queryResolveBuffer, 0);
encoder.copyBufferToBuffer(queryResolveBuffer, 0, readBuffer3, 0, readBuffer3.size);
device.queue.submit([encoder.finish()]);
}
const results = await Promise.all([
t.readBufferAsBigUint64(readBuffer),
t.readBufferAsBigUint64(readBuffer2),
t.readBufferAsBigUint64(readBuffer3),
]);
results.forEach((result, r) => {
for (let i = 0; i < kNumQueries; ++i) {
const passed = !!result[i];
const expectPassed = !!(i % 2);
t.expect(
passed === expectPassed,
`result(${r}): queryIndex: ${i}, passed: ${passed}, expected: ${expectPassed}`
);
}
});
});