blob: 5540d1f9391d8ad96691612c9b2a34d0880c6268 [file] [log] [blame]
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);
});