diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts new file mode 100644 index 0000000000..a7b292bed0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts @@ -0,0 +1,790 @@ +export const description = ` +TODO: +- test compatibility between bind groups and pipelines + - the binding resource in bindGroups[i].layout is "group-equivalent" (value-equal) to pipelineLayout.bgls[i]. + - in the test fn, test once without the dispatch/draw (should always be valid) and once with + the dispatch/draw, to make sure the validation happens in dispatch/draw. + - x= {dispatch, all draws} (dispatch/draw should be size 0 to make sure validation still happens if no-op) + - x= all relevant stages + +TODO: subsume existing test, rewrite fixture as needed. +TODO: Add externalTexture to kResourceTypes [1] +`; + +import { kUnitCaseParamsBuilder } from '../../../../../common/framework/params_builder.js'; +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { memcpy, unreachable } from '../../../../../common/util/util.js'; +import { + kSamplerBindingTypes, + kShaderStageCombinations, + kBufferBindingTypes, + ValidBindableResource, +} from '../../../../capability_info.js'; +import { GPUConst } from '../../../../constants.js'; +import { + ProgrammableEncoderType, + kProgrammableEncoderTypes, +} from '../../../../util/command_buffer_maker.js'; +import { ValidationTest } from '../../validation_test.js'; + +const kComputeCmds = ['dispatch', 'dispatchIndirect'] as const; +type ComputeCmd = typeof kComputeCmds[number]; +const kRenderCmds = ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const; +type RenderCmd = typeof kRenderCmds[number]; + +// Test resource type compatibility in pipeline and bind group +// [1]: Need to add externalTexture +const kResourceTypes: ValidBindableResource[] = [ + 'uniformBuf', + 'filtSamp', + 'sampledTex', + 'storageTex', +]; + +function getTestCmds( + encoderType: ProgrammableEncoderType +): readonly ComputeCmd[] | readonly RenderCmd[] { + return encoderType === 'compute pass' ? kComputeCmds : kRenderCmds; +} + +const kCompatTestParams = kUnitCaseParamsBuilder + .combine('encoderType', kProgrammableEncoderTypes) + .expand('call', p => getTestCmds(p.encoderType)) + .combine('callWithZero', [true, false]); + +class F extends ValidationTest { + getIndexBuffer(): GPUBuffer { + return this.device.createBuffer({ + size: 8 * Uint32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.INDEX, + }); + } + + getIndirectBuffer(indirectParams: Array<number>): GPUBuffer { + const buffer = this.device.createBuffer({ + mappedAtCreation: true, + size: indirectParams.length * Uint32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST, + }); + memcpy({ src: new Uint32Array(indirectParams) }, { dst: buffer.getMappedRange() }); + buffer.unmap(); + return buffer; + } + + getBindingResourceType(entry: GPUBindGroupLayoutEntry): ValidBindableResource { + if (entry.buffer !== undefined) return 'uniformBuf'; + if (entry.sampler !== undefined) return 'filtSamp'; + if (entry.texture !== undefined) return 'sampledTex'; + if (entry.storageTexture !== undefined) return 'storageTex'; + unreachable(); + } + + createRenderPipelineWithLayout( + bindGroups: Array<Array<GPUBindGroupLayoutEntry>> + ): GPURenderPipeline { + const shader = ` + @vertex fn vs_main() -> @builtin(position) vec4<f32> { + return vec4<f32>(1.0, 1.0, 0.0, 1.0); + } + + @fragment fn fs_main() -> @location(0) vec4<f32> { + return vec4<f32>(0.0, 1.0, 0.0, 1.0); + } + `; + const module = this.device.createShaderModule({ code: shader }); + const pipeline = this.device.createRenderPipeline({ + layout: this.device.createPipelineLayout({ + bindGroupLayouts: bindGroups.map(entries => this.device.createBindGroupLayout({ entries })), + }), + vertex: { + module, + entryPoint: 'vs_main', + }, + fragment: { + module, + entryPoint: 'fs_main', + targets: [{ format: 'rgba8unorm' }], + }, + primitive: { topology: 'triangle-list' }, + }); + return pipeline; + } + + createComputePipelineWithLayout( + bindGroups: Array<Array<GPUBindGroupLayoutEntry>> + ): GPUComputePipeline { + const shader = ` + @compute @workgroup_size(1) + fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { + } + `; + + const module = this.device.createShaderModule({ code: shader }); + const pipeline = this.device.createComputePipeline({ + layout: this.device.createPipelineLayout({ + bindGroupLayouts: bindGroups.map(entries => this.device.createBindGroupLayout({ entries })), + }), + compute: { + module, + entryPoint: 'main', + }, + }); + return pipeline; + } + + createBindGroupWithLayout(bglEntries: Array<GPUBindGroupLayoutEntry>): GPUBindGroup { + const bgEntries: Array<GPUBindGroupEntry> = []; + for (const entry of bglEntries) { + const resource = this.getBindingResource(this.getBindingResourceType(entry)); + bgEntries.push({ + binding: entry.binding, + resource, + }); + } + + return this.device.createBindGroup({ + entries: bgEntries, + layout: this.device.createBindGroupLayout({ entries: bglEntries }), + }); + } + + doCompute(pass: GPUComputePassEncoder, call: ComputeCmd | undefined, callWithZero: boolean) { + const x = callWithZero ? 0 : 1; + switch (call) { + case 'dispatch': + pass.dispatchWorkgroups(x, 1, 1); + break; + case 'dispatchIndirect': + pass.dispatchWorkgroupsIndirect(this.getIndirectBuffer([x, 1, 1]), 0); + break; + default: + break; + } + } + + doRender( + pass: GPURenderPassEncoder | GPURenderBundleEncoder, + call: RenderCmd | undefined, + callWithZero: boolean + ) { + const vertexCount = callWithZero ? 0 : 3; + switch (call) { + case 'draw': + pass.draw(vertexCount, 1, 0, 0); + break; + case 'drawIndexed': + pass.setIndexBuffer(this.getIndexBuffer(), 'uint32'); + pass.drawIndexed(vertexCount, 1, 0, 0, 0); + break; + case 'drawIndirect': + pass.drawIndirect(this.getIndirectBuffer([vertexCount, 1, 0, 0, 0]), 0); + break; + case 'drawIndexedIndirect': + pass.setIndexBuffer(this.getIndexBuffer(), 'uint32'); + pass.drawIndexedIndirect(this.getIndirectBuffer([vertexCount, 1, 0, 0, 0]), 0); + break; + default: + break; + } + } + + createBindGroupLayoutEntry( + encoderType: ProgrammableEncoderType, + resourceType: ValidBindableResource, + useU32Array: boolean + ): GPUBindGroupLayoutEntry { + const entry: GPUBindGroupLayoutEntry = { + binding: 0, + visibility: encoderType === 'compute pass' ? GPUShaderStage.COMPUTE : GPUShaderStage.FRAGMENT, + }; + + switch (resourceType) { + case 'uniformBuf': + entry.buffer = { hasDynamicOffset: useU32Array }; // default type: uniform + break; + case 'filtSamp': + entry.sampler = {}; // default type: filtering + break; + case 'sampledTex': + entry.texture = {}; // default sampleType: float + break; + case 'storageTex': + entry.storageTexture = { access: 'write-only', format: 'rgba8unorm' }; + break; + } + + return entry; + } + + runTest( + encoderType: ProgrammableEncoderType, + pipeline: GPUComputePipeline | GPURenderPipeline, + bindGroups: Array<GPUBindGroup | undefined>, + dynamicOffsets: Array<number> | undefined, + call: ComputeCmd | RenderCmd | undefined, + callWithZero: boolean, + success: boolean + ) { + const { encoder, validateFinish } = this.createEncoder(encoderType); + + if (encoder instanceof GPUComputePassEncoder) { + encoder.setPipeline(pipeline as GPUComputePipeline); + } else { + encoder.setPipeline(pipeline as GPURenderPipeline); + } + + for (let i = 0; i < bindGroups.length; i++) { + const bindGroup = bindGroups[i]; + if (!bindGroup) { + break; + } + if (dynamicOffsets) { + encoder.setBindGroup( + i, + bindGroup, + new Uint32Array(dynamicOffsets), + 0, + dynamicOffsets.length + ); + } else { + encoder.setBindGroup(i, bindGroup); + } + } + + if (encoder instanceof GPUComputePassEncoder) { + this.doCompute(encoder, call as ComputeCmd, callWithZero); + } else { + this.doRender(encoder, call as RenderCmd, callWithZero); + } + + validateFinish(success); + } +} + +export const g = makeTestGroup(F); + +g.test('bind_groups_and_pipeline_layout_mismatch') + .desc( + ` + Tests the bind groups must match the requirements of the pipeline layout. + - bind groups required by the pipeline layout are required. + - bind groups unused by the pipeline layout can be set or not. + ` + ) + .params( + kCompatTestParams + .beginSubcases() + .combineWithParams([ + { setBindGroup0: true, setBindGroup1: true, setUnusedBindGroup2: true, _success: true }, + { setBindGroup0: true, setBindGroup1: true, setUnusedBindGroup2: false, _success: true }, + { setBindGroup0: true, setBindGroup1: false, setUnusedBindGroup2: true, _success: false }, + { setBindGroup0: false, setBindGroup1: true, setUnusedBindGroup2: true, _success: false }, + { setBindGroup0: false, setBindGroup1: false, setUnusedBindGroup2: false, _success: false }, + ]) + .combine('useU32Array', [false, true]) + ) + .fn(t => { + const { + encoderType, + call, + callWithZero, + setBindGroup0, + setBindGroup1, + setUnusedBindGroup2, + _success, + useU32Array, + } = t.params; + const visibility = + encoderType === 'compute pass' ? GPUShaderStage.COMPUTE : GPUShaderStage.VERTEX; + + const bindGroupLayouts: Array<Array<GPUBindGroupLayoutEntry>> = [ + // bind group layout 0 + [ + { + binding: 0, + visibility, + buffer: { hasDynamicOffset: useU32Array }, // default type: uniform + }, + ], + // bind group layout 1 + [ + { + binding: 0, + visibility, + buffer: { hasDynamicOffset: useU32Array }, // default type: uniform + }, + ], + ]; + + // Create required bind groups + const bindGroup0 = setBindGroup0 ? t.createBindGroupWithLayout(bindGroupLayouts[0]) : undefined; + const bindGroup1 = setBindGroup1 ? t.createBindGroupWithLayout(bindGroupLayouts[1]) : undefined; + const unusedBindGroup2 = setUnusedBindGroup2 + ? t.createBindGroupWithLayout(bindGroupLayouts[1]) + : undefined; + + // Create fixed pipeline + const pipeline = + encoderType === 'compute pass' + ? t.createComputePipelineWithLayout(bindGroupLayouts) + : t.createRenderPipelineWithLayout(bindGroupLayouts); + + const dynamicOffsets = useU32Array ? [0] : undefined; + + // Test without the dispatch/draw (should always be valid) + t.runTest( + encoderType, + pipeline, + [bindGroup0, bindGroup1, unusedBindGroup2], + dynamicOffsets, + undefined, + false, + true + ); + + // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw. + t.runTest( + encoderType, + pipeline, + [bindGroup0, bindGroup1, unusedBindGroup2], + dynamicOffsets, + call, + callWithZero, + _success + ); + }); + +g.test('buffer_binding,render_pipeline') + .desc( + ` + The GPUBufferBindingLayout bindings configure should be exactly + same in PipelineLayout and bindgroup. + - TODO: test more draw functions, e.g. indirect + - TODO: test more visibilities, e.g. vertex + - TODO: bind group should be created with different layout + ` + ) + .params(u => u.combine('type', kBufferBindingTypes)) + .fn(async t => { + const { type } = t.params; + + // Create fixed bindGroup + const uniformBuffer = t.getUniformBuffer(); + + const bindGroup = t.device.createBindGroup({ + entries: [ + { + binding: 0, + resource: { + buffer: uniformBuffer, + }, + }, + ], + layout: t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: {}, // default type: uniform + }, + ], + }), + }); + + // Create pipeline with different layouts + const pipeline = t.createRenderPipelineWithLayout([ + [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + type, + }, + }, + ], + ]); + + const { encoder, validateFinish } = t.createEncoder('render pass'); + encoder.setPipeline(pipeline); + encoder.setBindGroup(0, bindGroup); + encoder.draw(3); + + validateFinish(type === undefined || type === 'uniform'); + }); + +g.test('sampler_binding,render_pipeline') + .desc( + ` + The GPUSamplerBindingLayout bindings configure should be exactly + same in PipelineLayout and bindgroup. + - TODO: test more draw functions, e.g. indirect + - TODO: test more visibilities, e.g. vertex + ` + ) + .params(u => + u // + .combine('bglType', kSamplerBindingTypes) + .combine('bgType', kSamplerBindingTypes) + ) + .fn(async t => { + const { bglType, bgType } = t.params; + const bindGroup = t.device.createBindGroup({ + entries: [ + { + binding: 0, + resource: + bgType === 'comparison' + ? t.device.createSampler({ compare: 'always' }) + : t.device.createSampler(), + }, + ], + layout: t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + sampler: { type: bgType }, + }, + ], + }), + }); + + // Create pipeline with different layouts + const pipeline = t.createRenderPipelineWithLayout([ + [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + sampler: { + type: bglType, + }, + }, + ], + ]); + + const { encoder, validateFinish } = t.createEncoder('render pass'); + encoder.setPipeline(pipeline); + encoder.setBindGroup(0, bindGroup); + encoder.draw(3); + + validateFinish(bglType === bgType); + }); + +g.test('bgl_binding_mismatch') + .desc( + 'Tests the binding number must exist or not exist in both bindGroups[i].layout and pipelineLayout.bgls[i]' + ) + .params( + kCompatTestParams + .beginSubcases() + .combineWithParams([ + { bgBindings: [0, 1, 2], plBindings: [0, 1, 2], _success: true }, + { bgBindings: [0, 1, 2], plBindings: [0, 1, 3], _success: false }, + { bgBindings: [0, 2], plBindings: [0, 2], _success: true }, + { bgBindings: [0, 2], plBindings: [2, 0], _success: true }, + { bgBindings: [0, 1, 2], plBindings: [0, 1], _success: false }, + { bgBindings: [0, 1], plBindings: [0, 1, 2], _success: false }, + ]) + .combine('useU32Array', [false, true]) + ) + .fn(t => { + const { + encoderType, + call, + callWithZero, + bgBindings, + plBindings, + _success, + useU32Array, + } = t.params; + const visibility = + encoderType === 'compute pass' ? GPUShaderStage.COMPUTE : GPUShaderStage.VERTEX; + + const bglEntries: Array<GPUBindGroupLayoutEntry> = []; + for (const binding of bgBindings) { + bglEntries.push({ + binding, + visibility, + buffer: { hasDynamicOffset: useU32Array }, // default type: uniform + }); + } + const bindGroup = t.createBindGroupWithLayout(bglEntries); + + const plEntries: Array<Array<GPUBindGroupLayoutEntry>> = [[]]; + for (const binding of plBindings) { + plEntries[0].push({ + binding, + visibility, + buffer: { hasDynamicOffset: useU32Array }, // default type: uniform + }); + } + const pipeline = + encoderType === 'compute pass' + ? t.createComputePipelineWithLayout(plEntries) + : t.createRenderPipelineWithLayout(plEntries); + + const dynamicOffsets = useU32Array ? new Array(bgBindings.length).fill(0) : undefined; + + // Test without the dispatch/draw (should always be valid) + t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, undefined, false, true); + + // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw. + t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, call, callWithZero, _success); + }); + +g.test('bgl_visibility_mismatch') + .desc('Tests the visibility in bindGroups[i].layout and pipelineLayout.bgls[i] must be matched') + .params( + kCompatTestParams + .beginSubcases() + .combine('bgVisibility', kShaderStageCombinations) + .expand('plVisibility', p => + p.encoderType === 'compute pass' + ? ([GPUConst.ShaderStage.COMPUTE] as const) + : ([ + GPUConst.ShaderStage.VERTEX, + GPUConst.ShaderStage.FRAGMENT, + GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT, + ] as const) + ) + .combine('useU32Array', [false, true]) + ) + .fn(t => { + const { encoderType, call, callWithZero, bgVisibility, plVisibility, useU32Array } = t.params; + + const bglEntries: Array<GPUBindGroupLayoutEntry> = [ + { + binding: 0, + visibility: bgVisibility, + buffer: { hasDynamicOffset: useU32Array }, // default type: uniform + }, + ]; + const bindGroup = t.createBindGroupWithLayout(bglEntries); + + const plEntries: Array<Array<GPUBindGroupLayoutEntry>> = [ + [ + { + binding: 0, + visibility: plVisibility, + buffer: { hasDynamicOffset: useU32Array }, // default type: uniform + }, + ], + ]; + const pipeline = + encoderType === 'compute pass' + ? t.createComputePipelineWithLayout(plEntries) + : t.createRenderPipelineWithLayout(plEntries); + + const dynamicOffsets = useU32Array ? [0] : undefined; + + // Test without the dispatch/draw (should always be valid) + t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, undefined, false, true); + + // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw. + t.runTest( + encoderType, + pipeline, + [bindGroup], + dynamicOffsets, + call, + callWithZero, + bgVisibility === plVisibility + ); + }); + +g.test('bgl_resource_type_mismatch') + .desc( + ` + Tests the binding resource type in bindGroups[i].layout and pipelineLayout.bgls[i] must be matched + - TODO: Test externalTexture + ` + ) + .params( + kCompatTestParams + .beginSubcases() + .combine('bgResourceType', kResourceTypes) + .combine('plResourceType', kResourceTypes) + .expand('useU32Array', p => (p.bgResourceType === 'uniformBuf' ? [true, false] : [false])) + ) + .fn(t => { + const { + encoderType, + call, + callWithZero, + bgResourceType, + plResourceType, + useU32Array, + } = t.params; + + const bglEntries: Array<GPUBindGroupLayoutEntry> = [ + t.createBindGroupLayoutEntry(encoderType, bgResourceType, useU32Array), + ]; + const bindGroup = t.createBindGroupWithLayout(bglEntries); + + const plEntries: Array<Array<GPUBindGroupLayoutEntry>> = [ + [t.createBindGroupLayoutEntry(encoderType, plResourceType, useU32Array)], + ]; + const pipeline = + encoderType === 'compute pass' + ? t.createComputePipelineWithLayout(plEntries) + : t.createRenderPipelineWithLayout(plEntries); + + const dynamicOffsets = useU32Array ? [0] : undefined; + + // Test without the dispatch/draw (should always be valid) + t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, undefined, false, true); + + // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw. + t.runTest( + encoderType, + pipeline, + [bindGroup], + dynamicOffsets, + call, + callWithZero, + bgResourceType === plResourceType + ); + }); + +g.test('empty_bind_group_layouts_requires_empty_bind_groups,compute_pass') + .desc( + ` + Test that a compute pipeline with empty bind groups layouts requires empty bind groups to be set. + ` + ) + .params(u => + u + .combine('bindGroupLayoutEntryCount', [3, 4]) + .combine('computeCommand', ['dispatchIndirect', 'dispatch'] as const) + ) + .fn(async t => { + const { bindGroupLayoutEntryCount, computeCommand } = t.params; + + const emptyBGLCount = 4; + const emptyBGL = t.device.createBindGroupLayout({ entries: [] }); + const emptyBGLs = []; + for (let i = 0; i < emptyBGLCount; i++) { + emptyBGLs.push(emptyBGL); + } + + const pipelineLayout = t.device.createPipelineLayout({ + bindGroupLayouts: emptyBGLs, + }); + + const pipeline = t.device.createComputePipeline({ + layout: pipelineLayout, + compute: { + module: t.device.createShaderModule({ + code: '@compute @workgroup_size(1) fn main() {}', + }), + entryPoint: 'main', + }, + }); + + const emptyBindGroup = t.device.createBindGroup({ + layout: emptyBGL, + entries: [], + }); + + const encoder = t.device.createCommandEncoder(); + const computePass = encoder.beginComputePass(); + computePass.setPipeline(pipeline); + for (let i = 0; i < bindGroupLayoutEntryCount; i++) { + computePass.setBindGroup(i, emptyBindGroup); + } + + t.doCompute(computePass, computeCommand, true); + computePass.end(); + + const success = bindGroupLayoutEntryCount === emptyBGLCount; + + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('empty_bind_group_layouts_requires_empty_bind_groups,render_pass') + .desc( + ` + Test that a render pipeline with empty bind groups layouts requires empty bind groups to be set. + ` + ) + .params(u => + u + .combine('bindGroupLayoutEntryCount', [3, 4]) + .combine('renderCommand', [ + 'draw', + 'drawIndexed', + 'drawIndirect', + 'drawIndexedIndirect', + ] as const) + ) + .fn(async t => { + const { bindGroupLayoutEntryCount, renderCommand } = t.params; + + const emptyBGLCount = 4; + const emptyBGL = t.device.createBindGroupLayout({ entries: [] }); + const emptyBGLs = []; + for (let i = 0; i < emptyBGLCount; i++) { + emptyBGLs.push(emptyBGL); + } + + const pipelineLayout = t.device.createPipelineLayout({ + bindGroupLayouts: emptyBGLs, + }); + + const colorFormat = 'rgba8unorm'; + const pipeline = t.device.createRenderPipeline({ + layout: pipelineLayout, + vertex: { + module: t.device.createShaderModule({ + code: `@vertex fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(); }`, + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: `@fragment fn main() {}`, + }), + entryPoint: 'main', + targets: [{ format: colorFormat, writeMask: 0 }], + }, + }); + + const emptyBindGroup = t.device.createBindGroup({ + layout: emptyBGL, + entries: [], + }); + + const encoder = t.device.createCommandEncoder(); + + const attachmentTexture = t.device.createTexture({ + format: 'rgba8unorm', + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const renderPass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: attachmentTexture.createView(), + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + + renderPass.setPipeline(pipeline); + for (let i = 0; i < bindGroupLayoutEntryCount; i++) { + renderPass.setBindGroup(i, emptyBindGroup); + } + t.doRender(renderPass, renderCommand, true); + renderPass.end(); + + const success = bindGroupLayoutEntryCount === emptyBGLCount; + + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); |