diff --git a/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts b/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts index bef33968b04f..28ac441ba21a 100644 --- a/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts +++ b/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts @@ -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[] = [ @@ -277,6 +285,106 @@ class F extends ValidationTest { validateFinish(success); } + + runDefaultLayoutBindingTest({ + 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); @@ -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({ + visibility: GPUShaderStage.COMPUTE, + empty, + pipelineType, + bindingType, + makePipelinesFn: (t, explicitPipelineLayout) => { + return (['auto', 'auto', explicitPipelineLayout] as const).map(layout => + t.device.createComputePipeline({ + layout, + compute: { + module: t.device.createShaderModule({ + code: ` + @group(1) @binding(0) var 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({ + visibility: GPUShaderStage.VERTEX, + empty, + pipelineType, + bindingType, + makePipelinesFn: (t, explicitPipelineLayout) => { + return (['auto', 'auto', explicitPipelineLayout] as const).map( + layout => { + const colorFormat = 'rgba8unorm'; + return t.device.createRenderPipeline({ + layout, + vertex: { + module: t.device.createShaderModule({ + code: ` + @group(1) @binding(0) var 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(); + }, + }); + }); diff --git a/src/webgpu/listing_meta.json b/src/webgpu/listing_meta.json index cf8a195fc372..6a268d7f728f 100644 --- a/src/webgpu/listing_meta.json +++ b/src/webgpu/listing_meta.json @@ -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 },