Skip to content

Commit

Permalink
Test default layout bindgroups (#3374)
Browse files Browse the repository at this point in the history
* Test default layout bind group binding.

According to the current WebGPU spec, a bind group created
with a bind group layout that itself is from a default layout
pipeline (a pipeline created with `layout: 'auto'` is
incompatible with any other bind group created with a different
bind group layout, even if they'd appear to match by their
resource types and binding locations. Further, no exception is made
for empty bind groups.
  • Loading branch information
greggman authored Feb 10, 2024
1 parent 2fdb8e5 commit 726c24a
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ type ComputeCmd = (typeof kComputeCmds)[number];
const kRenderCmds = ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const;
type RenderCmd = (typeof kRenderCmds)[number];

const kPipelineTypes = ['auto0', 'explicit'] as const;
type PipelineType = (typeof kPipelineTypes)[number];
const kBindingTypes = ['auto0', 'auto1', 'explicit'] as const;
type BindingType = (typeof kBindingTypes)[number];

const kEmptyBindGroupNdx = 0;
const kNonEmptyBindGroupNdx = 1;

// Test resource type compatibility in pipeline and bind group
// [1]: Need to add externalTexture
const kResourceTypes: ValidBindableResource[] = [
Expand Down Expand Up @@ -277,6 +285,106 @@ class F extends ValidationTest {

validateFinish(success);
}

runDefaultLayoutBindingTest<T extends GPURenderPipeline | GPUComputePipeline>({
visibility,
empty,
pipelineType,
bindingType,
makePipelinesFn,
doCommandFn,
}: {
visibility: number;
empty: boolean;
pipelineType: PipelineType;
bindingType: BindingType;
makePipelinesFn: (t: F, explicitPipelineLayout: GPUPipelineLayout) => T[];
doCommandFn: (params: {
t: F;
encoder: GPUCommandEncoder;
pipeline: T;
emptyBindGroup: GPUBindGroup;
nonEmptyBindGroup: GPUBindGroup;
}) => void;
}) {
const { device } = this;
const explicitEmptyBindGroupLayout = device.createBindGroupLayout({
entries: [],
});
const explicitBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility,
buffer: {},
},
],
});
const explicitPipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [explicitEmptyBindGroupLayout, explicitBindGroupLayout],
});

const [pipelineAuto0, pipelineAuto1, pipelineExplicit] = makePipelinesFn(
this,
explicitPipelineLayout
);

const buffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM,
});
this.trackForCleanup(buffer);

let emptyBindGroupLayout;
let nonEmptyBindGroupLayout;
let pipeline: T;

if (empty) {
// Testing empty:
// - nonEmptyBindGroupLayout comes from the pipeline we'll render/compute with.
// - emptyBindGroupLayout comes from a possibly incompatible place.
pipeline = pipelineType === 'auto0' ? pipelineAuto0 : pipelineExplicit;
emptyBindGroupLayout =
bindingType === 'explicit'
? explicitEmptyBindGroupLayout
: bindingType === 'auto0'
? pipelineAuto0.getBindGroupLayout(kEmptyBindGroupNdx)
: pipelineAuto1.getBindGroupLayout(kEmptyBindGroupNdx);
nonEmptyBindGroupLayout = pipeline.getBindGroupLayout(kNonEmptyBindGroupNdx);
} else {
// Testing non-empty:
// - emptyBindGroupLayout comes from the pipeline we'll render/compute with.
// - nonEmptyBindGroupLayout comes from a possibly incompatible place.
pipeline = pipelineType === 'auto0' ? pipelineAuto0 : pipelineExplicit;
nonEmptyBindGroupLayout =
bindingType === 'explicit'
? explicitBindGroupLayout
: bindingType === 'auto0'
? pipelineAuto0.getBindGroupLayout(kNonEmptyBindGroupNdx)
: pipelineAuto1.getBindGroupLayout(kNonEmptyBindGroupNdx);
emptyBindGroupLayout = pipeline.getBindGroupLayout(kEmptyBindGroupNdx);
}

const emptyBindGroup = device.createBindGroup({
layout: emptyBindGroupLayout,
entries: [],
});

const nonEmptyBindGroup = device.createBindGroup({
layout: nonEmptyBindGroupLayout,
entries: [{ binding: 0, resource: { buffer } }],
});

const encoder = device.createCommandEncoder();

doCommandFn({ t: this, encoder, pipeline, emptyBindGroup, nonEmptyBindGroup });

const success = bindingType === pipelineType;

this.expectValidationError(() => {
encoder.finish();
}, !success);
}
}

export const g = makeTestGroup(F);
Expand Down Expand Up @@ -793,3 +901,155 @@ g.test('empty_bind_group_layouts_requires_empty_bind_groups,render_pass')
encoder.finish();
}, !success);
});

const kPipelineTypesAndBindingTypeParams = [
{ pipelineType: 'auto0', bindingType: 'auto0' }, // successful case
{ pipelineType: 'explicit', bindingType: 'explicit' }, // successful case
{ pipelineType: 'explicit', bindingType: 'auto0' },
{ pipelineType: 'auto0', bindingType: 'explicit' },
{ pipelineType: 'auto0', bindingType: 'auto1' },
] as const;

g.test('default_bind_group_layouts_never_match,compute_pass')
.desc(
`
Test that bind groups created with default bind group layouts never match other layouts, including empty bind groups.
* Test that a pipeline with an explicit layout can not be used with a bindGroup from an auto layout
* Test that a pipeline with an auto layout can not be used with a bindGroup from an explicit layout
* Test that an auto layout from one pipeline can not be used with an auto layout from a different pipeline.
TODO:
* Test matching bindgroup layouts on the same default layout pipeline are compatible. In other words if
you only define group(2) then group(0)'s empty layout and group(1)'s empty layout should be compatible.
Similarly if group(0) and group(1) have the same types of resources they should be compatible.
`
)
.params(u =>
u
.combineWithParams(kPipelineTypesAndBindingTypeParams)
.combine('empty', [false, true])
.combine('computeCommand', ['dispatchIndirect', 'dispatch'] as const)
)
.fn(t => {
const { pipelineType, bindingType, computeCommand, empty } = t.params;

t.runDefaultLayoutBindingTest<GPUComputePipeline>({
visibility: GPUShaderStage.COMPUTE,
empty,
pipelineType,
bindingType,
makePipelinesFn: (t, explicitPipelineLayout) => {
return (['auto', 'auto', explicitPipelineLayout] as const).map<GPUComputePipeline>(layout =>
t.device.createComputePipeline({
layout,
compute: {
module: t.device.createShaderModule({
code: `
@group(1) @binding(0) var<uniform> u: vec4f;
@compute @workgroup_size(1) fn main() { _ = u; }
`,
}),
entryPoint: 'main',
},
})
);
},
doCommandFn: ({ t, encoder, pipeline, emptyBindGroup, nonEmptyBindGroup }) => {
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(kEmptyBindGroupNdx, emptyBindGroup);
pass.setBindGroup(kNonEmptyBindGroupNdx, nonEmptyBindGroup);
t.doCompute(pass, computeCommand, true);
pass.end();
},
});
});

g.test('default_bind_group_layouts_never_match,render_pass')
.desc(
`
Test that bind groups created with default bind group layouts never match other layouts, including empty bind groups.
* Test that a pipeline with an explicit layout can not be used with a bindGroup from an auto layout
* Test that a pipeline with an auto layout can not be used with a bindGroup from an explicit layout
* Test that an auto layout from one pipeline can not be used with an auto layout from a different pipeline.
TODO:
* Test matching bindgroup layouts on the same default layout pipeline are compatible. In other words if
you only define group(2) then group(0)'s empty layout and group(1)'s empty layout should be compatible.
Similarly if group(0) and group(1) have the same types of resources they should be compatible.
`
)
.params(u =>
u
.combineWithParams(kPipelineTypesAndBindingTypeParams)
.combine('empty', [false, true])
.combine('renderCommand', [
'draw',
'drawIndexed',
'drawIndirect',
'drawIndexedIndirect',
] as const)
)
.fn(t => {
const { pipelineType, bindingType, renderCommand, empty } = t.params;

t.runDefaultLayoutBindingTest<GPURenderPipeline>({
visibility: GPUShaderStage.VERTEX,
empty,
pipelineType,
bindingType,
makePipelinesFn: (t, explicitPipelineLayout) => {
return (['auto', 'auto', explicitPipelineLayout] as const).map<GPURenderPipeline>(
layout => {
const colorFormat = 'rgba8unorm';
return t.device.createRenderPipeline({
layout,
vertex: {
module: t.device.createShaderModule({
code: `
@group(1) @binding(0) var<uniform> u: vec4f;
@vertex fn main() -> @builtin(position) vec4f { return u; }
`,
}),
entryPoint: 'main',
},
fragment: {
module: t.device.createShaderModule({
code: `@fragment fn main() {}`,
}),
entryPoint: 'main',
targets: [{ format: colorFormat, writeMask: 0 }],
},
});
}
);
},
doCommandFn: ({ t, encoder, pipeline, emptyBindGroup, nonEmptyBindGroup }) => {
const attachmentTexture = t.device.createTexture({
format: 'rgba8unorm',
size: { width: 16, height: 16, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
t.trackForCleanup(attachmentTexture);

const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: attachmentTexture.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
});

renderPass.setPipeline(pipeline);
renderPass.setBindGroup(kEmptyBindGroupNdx, emptyBindGroup);
renderPass.setBindGroup(kNonEmptyBindGroupNdx, nonEmptyBindGroup);
t.doRender(renderPass, renderCommand, true);
renderPass.end();
},
});
});
2 changes: 2 additions & 0 deletions src/webgpu/listing_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:bgl_visibility_mismatch:*": { "subcaseMS": 0.608 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:bind_groups_and_pipeline_layout_mismatch:*": { "subcaseMS": 1.535 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:buffer_binding,render_pipeline:*": { "subcaseMS": 1.734 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:default_bind_group_layouts_never_match,compute_pass:*": { "subcaseMS": 1.734 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:default_bind_group_layouts_never_match,render_pass:*": { "subcaseMS": 1.734 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:empty_bind_group_layouts_requires_empty_bind_groups,compute_pass:*": { "subcaseMS": 2.325 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:empty_bind_group_layouts_requires_empty_bind_groups,render_pass:*": { "subcaseMS": 10.838 },
"webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:sampler_binding,render_pipeline:*": { "subcaseMS": 10.523 },
Expand Down

0 comments on commit 726c24a

Please sign in to comment.