Skip to content

Commit

Permalink
Test default bindgrouplayouts match in the same pipeline (#3382)
Browse files Browse the repository at this point in the history
Tests 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(2) and group(3)
have the same types of resources they should be compatible.
  • Loading branch information
greggman authored Feb 13, 2024
1 parent a42c759 commit b87bbf6
Showing 1 changed file with 117 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,17 @@ type PipelineType = (typeof kPipelineTypes)[number];
const kBindingTypes = ['auto0', 'auto1', 'explicit'] as const;
type BindingType = (typeof kBindingTypes)[number];

const kEmptyBindGroupNdx = 0;
const kNonEmptyBindGroupNdx = 1;
const kEmptyBindGroup0Ndx = 0;
const kEmptyBindGroup1Ndx = 1;
const kNonEmptyBindGroup0Ndx = 2;
const kNonEmptyBindGroup1Ndx = 3;

// Swaps 2 array elements in place.
function swapArrayElements<T>(array: T[], ndx1: number, ndx2: number) {
const t = array[ndx1];
array[ndx1] = array[ndx2];
array[ndx2] = t;
}

// Test resource type compatibility in pipeline and bind group
// [1]: Need to add externalTexture
Expand Down Expand Up @@ -291,20 +300,24 @@ class F extends ValidationTest {
empty,
pipelineType,
bindingType,
swap,
success,
makePipelinesFn,
doCommandFn,
}: {
visibility: number;
empty: boolean;
pipelineType: PipelineType;
bindingType: BindingType;
swap: boolean;
success: boolean;
makePipelinesFn: (t: F, explicitPipelineLayout: GPUPipelineLayout) => T[];
doCommandFn: (params: {
t: F;
encoder: GPUCommandEncoder;
pipeline: T;
emptyBindGroup: GPUBindGroup;
nonEmptyBindGroup: GPUBindGroup;
emptyBindGroups: GPUBindGroup[];
nonEmptyBindGroups: GPUBindGroup[];
}) => void;
}) {
const { device } = this;
Expand All @@ -321,7 +334,12 @@ class F extends ValidationTest {
],
});
const explicitPipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [explicitEmptyBindGroupLayout, explicitBindGroupLayout],
bindGroupLayouts: [
explicitEmptyBindGroupLayout,
explicitEmptyBindGroupLayout,
explicitBindGroupLayout,
explicitBindGroupLayout,
],
});

const [pipelineAuto0, pipelineAuto1, pipelineExplicit] = makePipelinesFn(
Expand All @@ -335,51 +353,71 @@ class F extends ValidationTest {
});
this.trackForCleanup(buffer);

let emptyBindGroupLayout;
let nonEmptyBindGroupLayout;
let pipeline: T;
let emptyBindGroupLayouts;
let nonEmptyBindGroupLayouts;
const pipeline = pipelineType === 'auto0' ? pipelineAuto0 : pipelineExplicit;

// Gets a bindGroupLayout either from the explicit layout passed in
// or one of the 2 `layout: 'auto'` pipelines.
const getBindGroupLayout = (
explicitBindGroupLayout: GPUBindGroupLayout,
bindGroupIndex: number
) =>
bindingType === 'explicit'
? explicitBindGroupLayout
: bindingType === 'auto0'
? pipelineAuto0.getBindGroupLayout(bindGroupIndex)
: pipelineAuto1.getBindGroupLayout(bindGroupIndex);

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);
// - nonEmptyBindGroupLayout comes from the pipeline we'll render/compute with.
emptyBindGroupLayouts = [
getBindGroupLayout(explicitEmptyBindGroupLayout, kEmptyBindGroup0Ndx),
getBindGroupLayout(explicitEmptyBindGroupLayout, kEmptyBindGroup1Ndx),
];
if (swap) {
swapArrayElements(emptyBindGroupLayouts, 0, 1);
}
nonEmptyBindGroupLayouts = [
pipeline.getBindGroupLayout(kNonEmptyBindGroup0Ndx),
pipeline.getBindGroupLayout(kNonEmptyBindGroup1Ndx),
];
} 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);
// - emptyBindGroupLayout comes from the pipeline we'll render/compute with.
nonEmptyBindGroupLayouts = [
getBindGroupLayout(explicitBindGroupLayout, kNonEmptyBindGroup0Ndx),
getBindGroupLayout(explicitBindGroupLayout, kNonEmptyBindGroup1Ndx),
];
if (swap) {
swapArrayElements(nonEmptyBindGroupLayouts, 0, 1);
}
emptyBindGroupLayouts = [
pipeline.getBindGroupLayout(kEmptyBindGroup0Ndx),
pipeline.getBindGroupLayout(kEmptyBindGroup1Ndx),
];
}

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

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

const encoder = device.createCommandEncoder();

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

const success = bindingType === pipelineType;
doCommandFn({ t: this, encoder, pipeline, emptyBindGroups, nonEmptyBindGroups });

this.expectValidationError(() => {
encoder.finish();
Expand Down Expand Up @@ -902,12 +940,25 @@ g.test('empty_bind_group_layouts_requires_empty_bind_groups,render_pass')
}, !success);
});

// pipelineType specifies which pipeline to try to render/compute with
// auto0 = the first `layout: 'auto'` pipeline
// explicit = a pipeline crated with an explicit pipeline layout using explicit bind group layouts
//
// bindingType specifies where to get bindGroupLayouts to use to create bindGroups
// auto0 = the first `layout: 'auto'` pipeline
// auto1 = the second `layout: 'auto'` pipeline
// explicit = a pipeline crated with an explicit pipeline layout using explicit bind group layouts
//
// swap specifies to swap the bindgroups we're testing. We test 2 of each type, 2 empty bindgroups and
// 2 non-empty bindgroups. The 2 empty bindgroups, when swapped should still be compatible. Similarly
// the 2 non-empty bindgroups, when swapped, should still be compatible.
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' },
{ pipelineType: 'auto0', bindingType: 'auto0', swap: false, _success: true },
{ pipelineType: 'explicit', bindingType: 'explicit', swap: false, _success: true },
{ pipelineType: 'explicit', bindingType: 'auto0', swap: false, _success: false },
{ pipelineType: 'auto0', bindingType: 'explicit', swap: false, _success: false },
{ pipelineType: 'auto0', bindingType: 'auto1', swap: false, _success: false },
{ pipelineType: 'auto0', bindingType: 'auto0', swap: true, _success: true },
] as const;

g.test('default_bind_group_layouts_never_match,compute_pass')
Expand All @@ -918,11 +969,9 @@ g.test('default_bind_group_layouts_never_match,compute_pass')
* 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.
Similarly if group(2) and group(3) have the same types of resources they should be compatible.
`
)
.params(u =>
Expand All @@ -932,34 +981,39 @@ g.test('default_bind_group_layouts_never_match,compute_pass')
.combine('computeCommand', ['dispatchIndirect', 'dispatch'] as const)
)
.fn(t => {
const { pipelineType, bindingType, computeCommand, empty } = t.params;
const { pipelineType, bindingType, swap, _success: success, computeCommand, empty } = t.params;

t.runDefaultLayoutBindingTest<GPUComputePipeline>({
visibility: GPUShaderStage.COMPUTE,
empty,
pipelineType,
bindingType,
swap,
success,
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; }
@group(2) @binding(0) var<uniform> u1: vec4f;
@group(3) @binding(0) var<uniform> u2: vec4f;
@compute @workgroup_size(2) fn main() { _ = u1; _ = u2; }
`,
}),
entryPoint: 'main',
},
})
);
},
doCommandFn: ({ t, encoder, pipeline, emptyBindGroup, nonEmptyBindGroup }) => {
doCommandFn: ({ t, encoder, pipeline, emptyBindGroups, nonEmptyBindGroups }) => {
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(kEmptyBindGroupNdx, emptyBindGroup);
pass.setBindGroup(kNonEmptyBindGroupNdx, nonEmptyBindGroup);
pass.setBindGroup(kEmptyBindGroup0Ndx, emptyBindGroups[0]);
pass.setBindGroup(kEmptyBindGroup1Ndx, emptyBindGroups[1]);
pass.setBindGroup(kNonEmptyBindGroup0Ndx, nonEmptyBindGroups[0]);
pass.setBindGroup(kNonEmptyBindGroup1Ndx, nonEmptyBindGroups[1]);
t.doCompute(pass, computeCommand, true);
pass.end();
},
Expand All @@ -974,11 +1028,9 @@ g.test('default_bind_group_layouts_never_match,render_pass')
* 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.
Similarly if group(2) and group(3) have the same types of resources they should be compatible.
`
)
.params(u =>
Expand All @@ -993,13 +1045,15 @@ g.test('default_bind_group_layouts_never_match,render_pass')
] as const)
)
.fn(t => {
const { pipelineType, bindingType, renderCommand, empty } = t.params;
const { pipelineType, bindingType, swap, _success: success, renderCommand, empty } = t.params;

t.runDefaultLayoutBindingTest<GPURenderPipeline>({
visibility: GPUShaderStage.VERTEX,
empty,
pipelineType,
bindingType,
swap,
success,
makePipelinesFn: (t, explicitPipelineLayout) => {
return (['auto', 'auto', explicitPipelineLayout] as const).map<GPURenderPipeline>(
layout => {
Expand All @@ -1009,8 +1063,9 @@ g.test('default_bind_group_layouts_never_match,render_pass')
vertex: {
module: t.device.createShaderModule({
code: `
@group(1) @binding(0) var<uniform> u: vec4f;
@vertex fn main() -> @builtin(position) vec4f { return u; }
@group(2) @binding(0) var<uniform> u1: vec4f;
@group(3) @binding(0) var<uniform> u2: vec4f;
@vertex fn main() -> @builtin(position) vec4f { return u1 + u2; }
`,
}),
entryPoint: 'main',
Expand All @@ -1026,7 +1081,7 @@ g.test('default_bind_group_layouts_never_match,render_pass')
}
);
},
doCommandFn: ({ t, encoder, pipeline, emptyBindGroup, nonEmptyBindGroup }) => {
doCommandFn: ({ t, encoder, pipeline, emptyBindGroups, nonEmptyBindGroups }) => {
const attachmentTexture = t.device.createTexture({
format: 'rgba8unorm',
size: { width: 16, height: 16, depthOrArrayLayers: 1 },
Expand All @@ -1046,8 +1101,10 @@ g.test('default_bind_group_layouts_never_match,render_pass')
});

renderPass.setPipeline(pipeline);
renderPass.setBindGroup(kEmptyBindGroupNdx, emptyBindGroup);
renderPass.setBindGroup(kNonEmptyBindGroupNdx, nonEmptyBindGroup);
renderPass.setBindGroup(kEmptyBindGroup0Ndx, emptyBindGroups[0]);
renderPass.setBindGroup(kEmptyBindGroup1Ndx, emptyBindGroups[1]);
renderPass.setBindGroup(kNonEmptyBindGroup0Ndx, nonEmptyBindGroups[0]);
renderPass.setBindGroup(kNonEmptyBindGroup1Ndx, nonEmptyBindGroups[1]);
t.doRender(renderPass, renderCommand, true);
renderPass.end();
},
Expand Down

0 comments on commit b87bbf6

Please sign in to comment.