export const description = ` Compute dispatch validation tests. `; import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js'; import { makeTestGroup } from '../../../common/framework/test_group.js'; import { keysOf } from '../../../common/util/data_tables.js'; import { WGSLLanguageFeature } from '../../capability_info.js'; export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); g.test('dispatch,linear_indexing_range') .desc('Tests validation of total invocations for linear_indexing built-in values') .params(u => u .combine('builtin', ['global_invocation_index', 'workgroup_index'] as const) .beginSubcases() .combine('size', ['max', 'valid'] as const) ) .fn(t => { // Other builtins are not tested due to onerous runtimes. t.skipIf(!t.hasLanguageFeature('linear_indexing'), 'Missing linear_indexing language feature'); // Spec limits: // - maxComputeWorkgroupsPerDimension = 65535 const { maxComputeWorkgroupsPerDimension } = t.device.limits; const x = t.params.builtin === 'global_invocation_index' ? 2 : 1, y = 1, z = 1; const wgSize = x * y * z; const countX = maxComputeWorkgroupsPerDimension; const countY = t.params.size === 'max' ? maxComputeWorkgroupsPerDimension : 1; const countZ = t.params.builtin === 'workgroup_index' ? 2 : 1; const totalInvocations = wgSize * countX * countY * countZ; t.skipIf(t.params.size === 'max' && totalInvocations <= 0xffffffff, 'Uninteresting test'); const code = ` @compute @workgroup_size(${x}, ${y}, ${z}) fn main(@builtin(${t.params.builtin}) input : u32) { _ = input; }`; const shaderModule = t.device.createShaderModule({ code }); const computePipeline = t.device.createComputePipeline({ layout: 'auto', compute: { module: shaderModule, }, }); const commandEncoder = t.device.createCommandEncoder(); const computePassEncoder = commandEncoder.beginComputePass(); computePassEncoder.setPipeline(computePipeline); computePassEncoder.dispatchWorkgroups(countX, countY, countZ); computePassEncoder.end(); t.expectValidationError(() => { commandEncoder.finish(); }, t.params.size === 'max'); }); g.test('dispatchIndirect,linear_indexing_range') .desc('Tests dispatchIndirect skips when linear_indexing is out of range') .params(u => u .combine('builtin', ['global_invocation_index', 'workgroup_index'] as const) .beginSubcases() .combine('size', ['max', 'valid'] as const) ) .fn(t => { // Other builtins are not tested due to onerous runtimes. t.skipIf(!t.hasLanguageFeature('linear_indexing'), 'Missing linear_indexing language feature'); // Spec limits: // - maxComputeWorkgroupsPerDimension = 65535 const { maxComputeWorkgroupsPerDimension } = t.device.limits; const x = t.params.builtin === 'global_invocation_index' ? 2 : 1, y = 1, z = 1; const wgSize = x * y * z; const countX = maxComputeWorkgroupsPerDimension; const countY = t.params.size === 'max' ? maxComputeWorkgroupsPerDimension : 1; const countZ = t.params.builtin === 'workgroup_index' ? 2 : 1; const totalInvocations = wgSize * countX * countY * countZ; t.skipIf(t.params.size === 'max' && totalInvocations <= 0xffffffff, 'Uninteresting test'); const kMagic = 0xdeadbeef; const code = ` @group(0) @binding(0) var out : u32; @compute @workgroup_size(${x}, ${y}, ${z}) fn main(@builtin(${t.params.builtin}) input : u32, @builtin(global_invocation_id) gid : vec3u) { _ = input; if (gid.x == 0 && gid.y == 0 && gid.z == 0) { out = ${kMagic}; } }`; const dispatchIndirectCounts = new Uint32Array(3); dispatchIndirectCounts[0] = countX; dispatchIndirectCounts[1] = countY; dispatchIndirectCounts[2] = countZ; const indirectBuffer = t.makeBufferWithContents( dispatchIndirectCounts, GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.INDIRECT ); t.trackForCleanup(indirectBuffer); const outputBuffer = t.makeBufferWithContents( new Uint32Array([0]), GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE ); t.trackForCleanup(outputBuffer); const shaderModule = t.device.createShaderModule({ code }); const computePipeline = t.device.createComputePipeline({ layout: 'auto', compute: { module: shaderModule, }, }); const bg = t.device.createBindGroup({ layout: computePipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: outputBuffer, }, }, ], }); const commandEncoder = t.device.createCommandEncoder(); const computePassEncoder = commandEncoder.beginComputePass(); computePassEncoder.setPipeline(computePipeline); computePassEncoder.setBindGroup(0, bg); computePassEncoder.dispatchWorkgroupsIndirect(indirectBuffer, 0); computePassEncoder.end(); t.queue.submit([commandEncoder.finish()]); const expected = t.params.size === 'max' ? 0 : kMagic; t.expectGPUBufferValuesEqual(outputBuffer, new Uint32Array([expected])); }); interface RequiredSizeCase { code: string; size: number; // Size must be greater than 4 binding_type: GPUBufferBindingType; requires?: WGSLLanguageFeature[]; } const kRequiredSizeCases: Record = { ro_storage_32bytes: { code: ` @group(0) @binding(0) var v : array; @compute @workgroup_size(1) fn main() { _ = v; }`, size: 32, binding_type: 'read-only-storage', }, storage_32bytes: { code: ` @group(0) @binding(0) var v : array; @compute @workgroup_size(1) fn main() { _ = v; }`, size: 32, binding_type: 'storage', }, uniform_32bytes: { code: ` @group(0) @binding(0) var v : array; @compute @workgroup_size(1) fn main() { _ = v; }`, size: 32, binding_type: 'uniform', }, ro_storage_sized_buffer: { code: ` @group(0) @binding(0) var v : buffer<32>; @compute @workgroup_size(1) fn main() { _ = &v; }`, size: 32, binding_type: 'read-only-storage', requires: ['buffer_view'], }, storage_sized_buffer: { code: ` @group(0) @binding(0) var v : buffer<32>; @compute @workgroup_size(1) fn main() { _ = &v; }`, size: 32, binding_type: 'storage', requires: ['buffer_view'], }, uniform_sized_buffer: { code: ` @group(0) @binding(0) var v : buffer<32>; @compute @workgroup_size(1) fn main() { _ = &v; }`, size: 32, binding_type: 'uniform', requires: ['buffer_view'], }, ro_storage_unsized_buffer_bufferView1: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView>(&v, 0); }`, size: 16 * 4, binding_type: 'read-only-storage', requires: ['buffer_view'], }, ro_storage_unsized_buffer_bufferView2: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView>(&v, 56); }`, // Offset does not count toward min binding size. size: 16 * 4, binding_type: 'read-only-storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView3: { code: ` struct S { a: vec4u, b: vec2u, } @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let x = 16u; let p = bufferView(&v, x); }`, size: 32, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView4: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView>(&v, 0); }`, size: 8, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView5: { code: ` struct S { a: array } @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView(&v, 0); let q = bufferView(&v, 0); }`, size: 16, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView6: { code: ` struct S { a: array } @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView(&v, 16); let q = bufferView(&v, 0); }`, // Offset does not count toward min binding size. size: 16, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView7: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView(&v, 16); } @fragment fn main2() { let q = bufferView(&v, 32); }`, // Offset does not count toward min binding size. size: 8, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView8: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView(&v, 16); } fn foo() { let q = bufferView(&v, 32); }`, // Offset does not count toward min binding size. size: 8, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferView9: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferView(&v, 16); foo(&v); } fn foo(p : ptr) { let q = bufferView(&v, 32); }`, // Offset does not count toward min binding size. size: 16, binding_type: 'storage', requires: ['buffer_view', 'unrestricted_pointer_parameters'], }, ro_storage_unsized_buffer_bufferArrayView1: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferArrayView>(&v, 0, 32); }`, // Size does not count toward min binding size. size: 8, binding_type: 'read-only-storage', requires: ['buffer_view'], }, ro_storage_unsized_buffer_bufferArrayView2: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferArrayView>(&v, 16, 32); }`, // Offset and size do not count toward min binding size. size: 8, binding_type: 'read-only-storage', requires: ['buffer_view'], }, ro_storage_unsized_buffer_bufferArrayView3: { code: ` @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let o = 16; let s = 32; let p = bufferArrayView>(&v, o, s); }`, size: 8, binding_type: 'read-only-storage', requires: ['buffer_view'], }, ro_storage_unsized_buffer_bufferArrayView4: { code: ` struct S { a: vec4u, b: u32, } @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let o = 16; let s = 32; let p = bufferArrayView>(&v, o, s); }`, size: 32, binding_type: 'read-only-storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferArrayView5: { code: ` struct S { a : vec4f, b : vec4f, } @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let o = 0u; let s = 64; let p1 = bufferArrayView>(&v, o, s); let p2 = bufferArrayView>(&v, o, s); }`, size: 32, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferArrayView6: { code: ` struct S { a : vec4f, b : vec4f, } var o : i32; var s : i32; @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferArrayView>(&v, o, s); } fn foo() { let p = bufferArrayView>(&v, o, s); }`, size: 16, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferArrayView7: { code: ` struct S { a : vec4f, b : vec4f, } var o : i32; var s : i32; @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { let p = bufferArrayView>(&v, o, s); } @fragment fn foo() { let p = bufferArrayView>(&v, o, s); }`, size: 16, binding_type: 'storage', requires: ['buffer_view'], }, storage_unsized_buffer_bufferArrayView8: { code: ` struct S { a : vec4f, b : vec4f, } var o : i32; var s : i32; @group(0) @binding(0) var v : buffer; @compute @workgroup_size(1) fn main() { bar(); foo(); } fn bar() { let p = bufferArrayView>(&v, o, s); } fn foo() { let p = bufferArrayView>(&v, o, s); }`, size: 32, binding_type: 'storage', requires: ['buffer_view'], }, }; g.test('shader_required_buffer_size') .desc('Test that dispatch time validation occurs about the required buffer size') .params(u => u .combine('case', keysOf(kRequiredSizeCases)) .beginSubcases() .combine('valid', [false, true] as const) .combine('layout', ['auto', 'explicit'] as const) ) .fn(t => { const testcase = kRequiredSizeCases[t.params.case]; const features = testcase.requires ?? []; features.forEach(f => { t.skipIfLanguageFeatureNotSupported(f); }); const buffer = t.createBufferTracked({ size: t.params.valid ? testcase.size : testcase.size - 4, usage: testcase.binding_type === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE, }); const bgLayout = t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: testcase.binding_type, minBindingSize: 0, }, }, ], }); const layout = t.device.createPipelineLayout({ bindGroupLayouts: [bgLayout] }); const pipeline = t.device.createComputePipeline({ layout: t.params.layout === 'auto' ? 'auto' : layout, compute: { module: t.device.createShaderModule({ code: testcase.code }), }, }); if (t.params.layout === 'auto' && !t.params.valid) { // 'auto' layout get minBindingSize from the shader. t.expectValidationError(() => { t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer, }, }, ], }), true; }); } else { // Expect dispatch time validation. const bg = t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer, }, }, ], }); const commandEncoder = t.device.createCommandEncoder(); const computePassEncoder = commandEncoder.beginComputePass(); computePassEncoder.setPipeline(pipeline); computePassEncoder.setBindGroup(0, bg); computePassEncoder.dispatchWorkgroups(1); computePassEncoder.end(); t.expectValidationError(() => { commandEncoder.finish(); }, !t.params.valid); } });