| export const description = `Execution tests for WGSL limits.`; |
| |
| import { makeTestGroup } from '../../../common/framework/test_group.js'; |
| import { keysOf } from '../../../common/util/data_tables.js'; |
| import { iterRange } from '../../../common/util/util.js'; |
| import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../gpu_test.js'; |
| import { CheckElementsGenerator, checkElementsEqualGenerated } from '../../util/check_contents.js'; |
| |
| export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); |
| |
| // The limits that we test. |
| const kMaxStructMembers = 1023; |
| const kMaxCompositeNestingDepth = 15; |
| const kMaxBraceNestingDepth = 127; |
| const kMaxFunctionParameters = 255; |
| const kMaxSwitchCaseSelectors = 1023; |
| const kMaxPrivateStorageSize = 8192; |
| const kMaxFunctionStorageSize = 8192; |
| const kMaxConstArrayElements = 2047; |
| |
| /** |
| * Run a shader and check that the buffer output matches expectations. |
| * |
| * @param t The test object |
| * @param wgsl The shader source |
| * @param input The initial buffer contents |
| * @param expected The expected buffer contents after running the shader |
| * @param constants The optional pipeline overridable constant values |
| */ |
| function runShaderTest( |
| t: GPUTest, |
| wgsl: string, |
| input: Uint32Array, |
| expected: CheckElementsGenerator, |
| constants?: Record<string, GPUPipelineConstantValue> |
| ): void { |
| const pipeline = t.device.createComputePipeline({ |
| layout: 'auto', |
| compute: { |
| module: t.device.createShaderModule({ code: wgsl }), |
| entryPoint: 'main', |
| constants, |
| }, |
| }); |
| |
| // Allocate a buffer and copy the input values to it. |
| const outputBuffer = t.makeBufferWithContents( |
| input, |
| GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC |
| ); |
| const bindGroup = t.device.createBindGroup({ |
| layout: pipeline.getBindGroupLayout(0), |
| entries: [{ binding: 0, resource: { buffer: outputBuffer } }], |
| }); |
| |
| // Run the shader. |
| const encoder = t.device.createCommandEncoder(); |
| const pass = encoder.beginComputePass(); |
| pass.setPipeline(pipeline); |
| pass.setBindGroup(0, bindGroup); |
| pass.dispatchWorkgroups(1); |
| pass.end(); |
| t.queue.submit([encoder.finish()]); |
| |
| // Check that the output matches the expected values. |
| t.expectGPUBufferValuesPassCheck( |
| outputBuffer, |
| data => checkElementsEqualGenerated(data, expected), |
| { |
| type: Uint32Array, |
| typedLength: input.length, |
| } |
| ); |
| } |
| |
| g.test('struct_members') |
| .desc(`Test that structures with the maximum number of members are supported.`) |
| .fn(t => { |
| let code = `struct S {`; |
| for (let m = 0; m < kMaxStructMembers; m++) { |
| code += ` m${m}: u32,\n`; |
| } |
| code += `} |
| |
| @group(0) @binding(0) var<storage, read_write> buffer : S; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| buffer = S(); |
| } |
| `; |
| |
| runShaderTest( |
| t, |
| code, |
| new Uint32Array([...iterRange(kMaxStructMembers, _i => 0xdeadbeef)]), |
| _i => 0 |
| ); |
| }); |
| |
| g.test('nesting_depth_composite_struct') |
| .desc(`Test that composite types can be nested up to the maximum level.`) |
| .fn(t => { |
| let code = `struct S0 { a : u32 }\n`; |
| for (let s = 1; s < kMaxCompositeNestingDepth; s++) { |
| code += `struct S${s} { a : S${s - 1} }\n`; |
| } |
| code += ` |
| @group(0) @binding(0) var<storage, read_write> buffer : S${kMaxCompositeNestingDepth - 1}; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| buffer = S${kMaxCompositeNestingDepth - 1}(); |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([0xdeadbeef]), _i => 0); |
| }); |
| |
| g.test('nesting_depth_composite_array') |
| .desc(`Test that composite types can be nested up to the maximum level.`) |
| .fn(t => { |
| let type = ``; |
| for (let m = 0; m < kMaxCompositeNestingDepth; m++) { |
| type += `array<`; |
| } |
| type += 'u32'; |
| for (let m = 0; m < kMaxCompositeNestingDepth; m++) { |
| type += `, 1>`; |
| } |
| |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : ${type}; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| buffer = ${type}(); |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([0xdeadbeef]), _i => 0); |
| }); |
| |
| g.test('nesting_depth_braces') |
| .desc(`Test that brace-enclosed statements can be nested up to the maximum level.`) |
| .fn(t => { |
| let code = `@group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${kMaxBraceNestingDepth}>; |
| @compute @workgroup_size(1) |
| |
| fn main() { |
| `; |
| // Note: We subtract one from the spec value to account for the function body, and another one |
| // for the nested statement itself. |
| for (let b = 0; b < kMaxBraceNestingDepth - 2; b++) { |
| code += ` {\n`; |
| } |
| code += ` buffer[0] = 42;\n`; |
| for (let b = 0; b < kMaxBraceNestingDepth - 2; b++) { |
| code += ` }\n`; |
| } |
| code += ` |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(kMaxBraceNestingDepth, i => i)]), i => { |
| return i === 0 ? 42 : i; |
| }); |
| }); |
| |
| g.test('function_parameters') |
| .desc(`Test that functions can have the maximum number of parameters.`) |
| .fn(t => { |
| let code = `@group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${kMaxFunctionParameters}>; |
| |
| fn bar(`; |
| for (let p = 0; p < kMaxFunctionParameters; p++) { |
| code += `p${p}: u32, `; |
| } |
| code += `) {`; |
| |
| for (let p = 0; p < kMaxFunctionParameters; p++) { |
| code += `buffer[${p}] = p${p};\n`; |
| } |
| |
| code += `} |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| bar(`; |
| for (let p = 0; p < kMaxFunctionParameters; p++) { |
| code += `${p}, `; |
| } |
| code += `); |
| } |
| `; |
| |
| runShaderTest( |
| t, |
| code, |
| new Uint32Array([...iterRange(kMaxFunctionParameters, _i => 0xdeadbeef)]), |
| i => i |
| ); |
| }); |
| |
| g.test('switch_case_selectors') |
| .desc( |
| `Test that switch statements can have the maximum number of case selectors in separate clauses.` |
| ) |
| .fn(t => { |
| let code = `@group(0) @binding(0) var<storage, read_write> buffer : array<u32, 2>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| switch (buffer[0]) { |
| default {}`; |
| for (let s = 0; s < kMaxSwitchCaseSelectors - 1; s++) { |
| code += ` |
| case ${s} { buffer[1] = ${s}; }`; |
| } |
| code += ` |
| }; |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([42, 0xdeadbeef]), _i => 42); |
| }); |
| |
| g.test('switch_case_selectors_same_clause') |
| .desc( |
| `Test that switch statements can have the maximum number of case selectors in the same clause.` |
| ) |
| .fn(t => { |
| let code = `@group(0) @binding(0) var<storage, read_write> buffer : array<u32, 2>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| switch (buffer[0]) { |
| default {} |
| case `; |
| for (let s = 0; s < kMaxSwitchCaseSelectors - 1; s++) { |
| code += `${s}, `; |
| } |
| code += ` { buffer[1] = 42; } |
| }; |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([999, 0xdeadbeef]), i => { |
| return i === 0 ? 999 : 42; |
| }); |
| }); |
| |
| // A list of types used for array elements. |
| const kArrayElements = { |
| bool: { |
| size: 4, |
| to_u32: (x: string) => `u32(${x})`, |
| }, |
| u32: { |
| size: 4, |
| to_u32: (x: string) => x, |
| }, |
| vec4u: { |
| size: 16, |
| to_u32: (x: string) => `dot(${x}, ${x})`, |
| }, |
| }; |
| |
| g.test('private_array_byte_size') |
| .desc(`Test that arrays in the private address space up to the maximum size are supported.`) |
| .params(u => u.combine('type', keysOf(kArrayElements))) |
| .fn(t => { |
| const type = kArrayElements[t.params.type]; |
| const elements = Math.floor(kMaxPrivateStorageSize / type.size); |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${elements}>; |
| |
| var<private> arr : array<${t.params.type}, ${elements}>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| for (var i = 0; i < ${elements}; i++) { |
| buffer[i] = ${type.to_u32('arr[i]')}; |
| } |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elements, _i => 0xdeadbeef)]), _i => 0); |
| }); |
| |
| g.test('private_array_combined_byte_size') |
| .desc(`Test the combined sizes of variables in the private address space.`) |
| .params(u => u.combine('type', keysOf(kArrayElements))) |
| .fn(t => { |
| const type = kArrayElements[t.params.type]; |
| const elements = Math.floor(kMaxPrivateStorageSize / type.size / 4); |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${elements}>; |
| |
| var<private> arr1 : array<${t.params.type}, ${elements}>; |
| var<private> arr2 : array<${t.params.type}, ${elements}>; |
| var<private> arr3 : array<${t.params.type}, ${elements}>; |
| var<private> arr4 : array<${t.params.type}, ${elements}>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| for (var i = 0; i < ${elements}; i++) { |
| buffer[i] = ${type.to_u32('arr1[i]')} + ${type.to_u32('arr2[i]')} + |
| ${type.to_u32('arr3[i]')} + ${type.to_u32('arr4[i]')}; |
| } |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elements, _i => 0xdeadbeef)]), _i => 0); |
| }); |
| |
| g.test('function_array_byte_size') |
| .desc(`Test that arrays in the function address space up to the maximum size are supported.`) |
| .params(u => u.combine('type', keysOf(kArrayElements))) |
| .fn(t => { |
| const type = kArrayElements[t.params.type]; |
| const elements = Math.floor(kMaxFunctionStorageSize / type.size); |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${elements}>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| var arr : array<${t.params.type}, ${elements}>; |
| for (var i = 0; i < ${elements}; i++) { |
| buffer[i] = ${type.to_u32('arr[i]')}; |
| } |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elements, _i => 0xdeadbeef)]), _i => 0); |
| }); |
| |
| g.test('function_variable_combined_byte_size') |
| .desc(`Test the combined sizes of variables in the function address space.`) |
| .params(u => u.combine('type', keysOf(kArrayElements))) |
| .fn(t => { |
| const type = kArrayElements[t.params.type]; |
| const elements = Math.floor(kMaxFunctionStorageSize / type.size / 4); |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${elements}>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| var arr1 : array<${t.params.type}, ${elements}>; |
| var arr2 : array<${t.params.type}, ${elements}>; |
| var arr3 : array<${t.params.type}, ${elements}>; |
| var arr4 : array<${t.params.type}, ${elements}>; |
| for (var i = 0; i < ${elements}; i++) { |
| buffer[i] = ${type.to_u32('arr1[i]')} + ${type.to_u32('arr2[i]')} + |
| ${type.to_u32('arr3[i]')} + ${type.to_u32('arr4[i]')}; |
| } |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elements, _i => 0xdeadbeef)]), _i => 0); |
| }); |
| |
| g.test('workgroup_array_byte_size') |
| .desc(`Test that arrays in the workgroup address space up to the maximum size are supported.`) |
| .params(u => u.combine('type', keysOf(kArrayElements))) |
| .fn(t => { |
| const maxSize = t.device.limits.maxComputeWorkgroupStorageSize; |
| const type = kArrayElements[t.params.type]; |
| const elements = Math.floor(maxSize / type.size); |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${elements}>; |
| |
| var<workgroup> arr : array<${t.params.type}, ${elements}>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| for (var i = 0; i < ${elements}; i++) { |
| buffer[i] = ${type.to_u32('arr[i]')}; |
| } |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elements, _i => 0xdeadbeef)]), _i => 0); |
| }); |
| |
| g.test('workgroup_array_byte_size_override') |
| .desc(`Test that arrays in the workgroup address space up to the maximum size are supported.`) |
| .params(u => u.combine('type', keysOf(kArrayElements))) |
| .fn(t => { |
| const maxSize = t.device.limits.maxComputeWorkgroupStorageSize; |
| const type = kArrayElements[t.params.type]; |
| const elements = Math.floor(maxSize / type.size); |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : array<u32, ${elements}>; |
| |
| // Set the default element count far too large, which we later override with a valid value. |
| override elements = ${elements} * 1000; |
| var<workgroup> arr : array<${t.params.type}, elements>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| for (var i = 0; i < ${elements}; i++) { |
| buffer[i] = ${type.to_u32('arr[i]')}; |
| } |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elements, _i => 0xdeadbeef)]), _i => 0, { |
| elements, |
| }); |
| }); |
| |
| g.test('const_array_elements') |
| .desc(`Test that constant array expressions with the maximum number of elements are supported.`) |
| .params( |
| u => |
| // Some backend shader compilers take too long to compile the maximum |
| // size. In that case the browser GPU process can time out. The WGSL |
| // spec allows this as an 'uncategorized error'. |
| // To get some useful signal from this test, also check a const array |
| // with a significant size even though it may not be the maximum supported |
| // size listed in the spec. |
| u.combine('sizeDivisor', [64, 8, 1]) // Must include 1, to test largest size. |
| ) |
| .fn(t => { |
| const elementCount = Math.ceil(kMaxConstArrayElements / t.params.sizeDivisor); |
| const type = `array<u32, ${elementCount}>`; |
| |
| let expr = `${type}(`; |
| for (let i = 0; i < elementCount; i++) { |
| expr += `${i}, `; |
| } |
| expr += `)`; |
| |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> buffer : ${type}; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| buffer = ${expr}; |
| } |
| `; |
| |
| runShaderTest(t, code, new Uint32Array([...iterRange(elementCount, _i => 0xdeadbeef)]), i => i); |
| }); |