blob: 22059201c0263f5506c057839737f9e9e44ea560 [file] [edit]
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<storage, read_write> 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<string, RequiredSizeCase> = {
ro_storage_32bytes: {
code: `
@group(0) @binding(0) var<storage> v : array<vec4u, 2>;
@compute @workgroup_size(1)
fn main() {
_ = v;
}`,
size: 32,
binding_type: 'read-only-storage',
},
storage_32bytes: {
code: `
@group(0) @binding(0) var<storage, read_write> v : array<vec4u, 2>;
@compute @workgroup_size(1)
fn main() {
_ = v;
}`,
size: 32,
binding_type: 'storage',
},
uniform_32bytes: {
code: `
@group(0) @binding(0) var<uniform> v : array<vec4u, 2>;
@compute @workgroup_size(1)
fn main() {
_ = v;
}`,
size: 32,
binding_type: 'uniform',
},
ro_storage_sized_buffer: {
code: `
@group(0) @binding(0) var<storage> 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<storage, read_write> 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<uniform> 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<storage> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<array<u32, 16>>(&v, 0);
}`,
size: 16 * 4,
binding_type: 'read-only-storage',
requires: ['buffer_view'],
},
ro_storage_unsized_buffer_bufferView2: {
code: `
@group(0) @binding(0) var<storage> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<array<u32, 16>>(&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<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let x = 16u;
let p = bufferView<S>(&v, x);
}`,
size: 32,
binding_type: 'storage',
requires: ['buffer_view'],
},
storage_unsized_buffer_bufferView4: {
code: `
@group(0) @binding(0) var<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<array<vec2u>>(&v, 0);
}`,
size: 8,
binding_type: 'storage',
requires: ['buffer_view'],
},
storage_unsized_buffer_bufferView5: {
code: `
struct S {
a: array<u32, 4>
}
@group(0) @binding(0) var<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<vec2u>(&v, 0);
let q = bufferView<S>(&v, 0);
}`,
size: 16,
binding_type: 'storage',
requires: ['buffer_view'],
},
storage_unsized_buffer_bufferView6: {
code: `
struct S {
a: array<u32, 4>
}
@group(0) @binding(0) var<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<u32>(&v, 16);
let q = bufferView<S>(&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<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<vec2i>(&v, 16);
}
@fragment
fn main2() {
let q = bufferView<vec4u>(&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<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<vec2f>(&v, 16);
}
fn foo() {
let q = bufferView<vec4u>(&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<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferView<u32>(&v, 16);
foo(&v);
}
fn foo(p : ptr<storage, buffer, read_write>) {
let q = bufferView<vec4u>(&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<storage> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferArrayView<array<vec2u>>(&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<storage> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferArrayView<array<vec2f>>(&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<storage> v : buffer;
@compute @workgroup_size(1)
fn main() {
let o = 16;
let s = 32;
let p = bufferArrayView<array<vec2u>>(&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<storage> v : buffer;
@compute @workgroup_size(1)
fn main() {
let o = 16;
let s = 32;
let p = bufferArrayView<array<S>>(&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<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let o = 0u;
let s = 64;
let p1 = bufferArrayView<array<u32>>(&v, o, s);
let p2 = bufferArrayView<array<S>>(&v, o, s);
}`,
size: 32,
binding_type: 'storage',
requires: ['buffer_view'],
},
storage_unsized_buffer_bufferArrayView6: {
code: `
struct S {
a : vec4f,
b : vec4f,
}
var<private> o : i32;
var<private> s : i32;
@group(0) @binding(0) var<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferArrayView<array<vec4u>>(&v, o, s);
}
fn foo() {
let p = bufferArrayView<array<S>>(&v, o, s);
}`,
size: 16,
binding_type: 'storage',
requires: ['buffer_view'],
},
storage_unsized_buffer_bufferArrayView7: {
code: `
struct S {
a : vec4f,
b : vec4f,
}
var<private> o : i32;
var<private> s : i32;
@group(0) @binding(0) var<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
let p = bufferArrayView<array<vec4u>>(&v, o, s);
}
@fragment
fn foo() {
let p = bufferArrayView<array<S>>(&v, o, s);
}`,
size: 16,
binding_type: 'storage',
requires: ['buffer_view'],
},
storage_unsized_buffer_bufferArrayView8: {
code: `
struct S {
a : vec4f,
b : vec4f,
}
var<private> o : i32;
var<private> s : i32;
@group(0) @binding(0) var<storage, read_write> v : buffer;
@compute @workgroup_size(1)
fn main() {
bar();
foo();
}
fn bar() {
let p = bufferArrayView<array<vec4u>>(&v, o, s);
}
fn foo() {
let p = bufferArrayView<array<S>>(&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);
}
});