export const description = `
API validation test for compute pass
Does **not** test usage scopes (resource_usages/) or programmable pass stuff (programmable_pass).
`;
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import { makeValueTestVariant } from '../../../../../common/util/util.js';
import { kBufferUsages } from '../../../../capability_info.js';
import { GPUConst } from '../../../../constants.js';
import {
kResourceStates,
ResourceState,
AllFeaturesMaxLimitsGPUTest,
} from '../../../../gpu_test.js';
import * as vtu from '../../validation_test_utils.js';
class F extends AllFeaturesMaxLimitsGPUTest {
createComputePipeline(state: 'valid' | 'invalid'): GPUComputePipeline {
if (state === 'valid') {
return vtu.createNoOpComputePipeline(this);
}
return vtu.createErrorComputePipeline(this);
}
createIndirectBuffer(state: ResourceState, data: Uint32Array): GPUBuffer {
const descriptor: GPUBufferDescriptor = {
size: data.byteLength,
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
};
if (state === 'invalid') {
descriptor.usage = 0xffff;
}
this.device.pushErrorScope('validation');
const buffer = this.createBufferTracked(descriptor);
void this.device.popErrorScope();
if (state === 'valid') {
this.queue.writeBuffer(buffer, 0, data);
}
if (state === 'destroyed') {
buffer.destroy();
}
return buffer;
}
}
export const g = makeTestGroup(F);
g.test('set_pipeline')
.desc(
`
setPipeline should generate an error iff using an 'invalid' pipeline.
`
)
.params(u => u.beginSubcases().combine('state', ['valid', 'invalid'] as const))
.fn(t => {
const { state } = t.params;
const pipeline = t.createComputePipeline(state);
const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder('compute pass');
encoder.setPipeline(pipeline);
validateFinishAndSubmitGivenState(state);
});
g.test('pipeline,device_mismatch')
.desc('Tests setPipeline cannot be called with a compute pipeline created from another device')
.paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
.beforeAllSubcases(t => t.usesMismatchedDevice())
.fn(t => {
const { mismatched } = t.params;
const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
const pipeline = sourceDevice.createComputePipeline({
layout: 'auto',
compute: {
module: sourceDevice.createShaderModule({
code: '@compute @workgroup_size(1) fn main() {}',
}),
entryPoint: 'main',
},
});
const { encoder, validateFinish } = t.createEncoder('compute pass');
encoder.setPipeline(pipeline);
validateFinish(!mismatched);
});
g.test('dispatch_sizes')
.desc(
`Test 'direct' and 'indirect' dispatch with various sizes.
Only direct dispatches can produce validation errors.
Workgroup sizes:
- valid: { zero, one, just under limit }
- invalid: { just over limit, way over limit }
TODO: Verify that the invalid cases don't execute any invocations at all.
`
)
.params(u =>
u
.combine('dispatchType', ['direct', 'indirect'] as const)
.combine('largeDimValueVariant', [
{ mult: 0, add: 0 },
{ mult: 0, add: 1 },
{ mult: 1, add: 0 },
{ mult: 1, add: 1 },
{ mult: 0, add: 0x7fff_ffff },
{ mult: 0, add: 0xffff_ffff },
])
.beginSubcases()
.combine('largeDimIndex', [0, 1, 2] as const)
.combine('smallDimValue', [0, 1])
)
.fn(t => {
const { dispatchType, largeDimIndex, smallDimValue, largeDimValueVariant } = t.params;
const maxDispatch = t.device.limits.maxComputeWorkgroupsPerDimension;
const largeDimValue = makeValueTestVariant(maxDispatch, largeDimValueVariant);
const pipeline = vtu.createNoOpComputePipeline(t);
const workSizes = [smallDimValue, smallDimValue, smallDimValue];
workSizes[largeDimIndex] = largeDimValue;
const { encoder, validateFinishAndSubmit } = t.createEncoder('compute pass');
encoder.setPipeline(pipeline);
if (dispatchType === 'direct') {
const [x, y, z] = workSizes;
encoder.dispatchWorkgroups(x, y, z);
} else if (dispatchType === 'indirect') {
encoder.dispatchWorkgroupsIndirect(
t.createIndirectBuffer('valid', new Uint32Array(workSizes)),
0
);
}
const shouldError =
dispatchType === 'direct' &&
(workSizes[0] > maxDispatch || workSizes[1] > maxDispatch || workSizes[2] > maxDispatch);
validateFinishAndSubmit(!shouldError, true);
});
const kBufferData = new Uint32Array(6).fill(1);
g.test('indirect_dispatch_buffer_state')
.desc(
`
Test dispatchWorkgroupsIndirect validation by submitting various dispatches with a no-op pipeline
and an indirectBuffer with 6 elements.
- indirectBuffer: {'valid', 'invalid', 'destroyed'}
- indirectOffset:
- valid, within the buffer: {beginning, middle, end} of the buffer
- invalid, non-multiple of 4
- invalid, the last element is outside the buffer
`
)
.paramsSubcasesOnly(u =>
u
.combine('state', kResourceStates)
.combine('offset', [
0,
Uint32Array.BYTES_PER_ELEMENT,
kBufferData.byteLength - 3 * Uint32Array.BYTES_PER_ELEMENT,
1,
kBufferData.byteLength - 2 * Uint32Array.BYTES_PER_ELEMENT,
])
)
.fn(t => {
const { state, offset } = t.params;
const pipeline = vtu.createNoOpComputePipeline(t);
const buffer = t.createIndirectBuffer(state, kBufferData);
const { encoder, validateFinishAndSubmit } = t.createEncoder('compute pass');
encoder.setPipeline(pipeline);
encoder.dispatchWorkgroupsIndirect(buffer, offset);
const finishShouldError =
state === 'invalid' ||
offset % 4 !== 0 ||
offset + 3 * Uint32Array.BYTES_PER_ELEMENT > kBufferData.byteLength;
validateFinishAndSubmit(!finishShouldError, state !== 'destroyed');
});
g.test('indirect_dispatch_buffer,device_mismatch')
.desc(
`Tests dispatchWorkgroupsIndirect cannot be called with an indirect buffer created from another device`
)
.paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
.beforeAllSubcases(t => t.usesMismatchedDevice())
.fn(t => {
const { mismatched } = t.params;
const pipeline = vtu.createNoOpComputePipeline(t);
const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
const buffer = t.trackForCleanup(
sourceDevice.createBuffer({
size: 16,
usage: GPUBufferUsage.INDIRECT,
})
);
const { encoder, validateFinish } = t.createEncoder('compute pass');
encoder.setPipeline(pipeline);
encoder.dispatchWorkgroupsIndirect(buffer, 0);
validateFinish(!mismatched);
});
g.test('indirect_dispatch_buffer,usage')
.desc(
`
Tests dispatchWorkgroupsIndirect generates a validation error if the buffer usage does not
contain INDIRECT usage.
`
)
.paramsSubcasesOnly(u =>
u
.combine('bufferUsage0', kBufferUsages)
.combine('bufferUsage1', kBufferUsages)
.unless(
({ bufferUsage0, bufferUsage1 }) =>
((bufferUsage0 | bufferUsage1) &
(GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE)) !==
0
)
)
.fn(t => {
const { bufferUsage0, bufferUsage1 } = t.params;
const bufferUsage = bufferUsage0 | bufferUsage1;
const layout = t.device.createPipelineLayout({ bindGroupLayouts: [] });
const pipeline = vtu.createNoOpComputePipeline(t, layout);
const buffer = t.createBufferTracked({
size: 16,
usage: bufferUsage,
});
const success = (GPUBufferUsage.INDIRECT & bufferUsage) !== 0;
const { encoder, validateFinish } = t.createEncoder('compute pass');
encoder.setPipeline(pipeline);
encoder.dispatchWorkgroupsIndirect(buffer, 0);
validateFinish(success);
});