blob: 6c6147fd4cf776ee65226a40cdf6c0336a396bc3 [file] [log] [blame]
export const description = `
setBindGroup validation tests.
TODO: merge these notes and implement.
> (Note: If there are errors with using certain binding types in certain passes, test those in the file for that pass type, not here.)
>
> - state tracking (probably separate file)
> - x= {compute pass, render pass}
> - {null, compatible, incompatible} current pipeline (should have no effect without draw/dispatch)
> - setBindGroup in different orders (e.g. 0,1,2 vs 2,0,1)
`;
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import { makeValueTestVariant, range, unreachable } from '../../../../../common/util/util.js';
import {
kBufferBindingTypes,
kMinDynamicBufferOffsetAlignment,
} from '../../../../capability_info.js';
import { GPUConst } from '../../../../constants.js';
import {
kResourceStates,
ResourceState,
AllFeaturesMaxLimitsGPUTest,
} from '../../../../gpu_test.js';
import {
kProgrammableEncoderTypes,
ProgrammableEncoderType,
} from '../../../../util/command_buffer_maker.js';
import * as vtu from '../../validation_test_utils.js';
class F extends AllFeaturesMaxLimitsGPUTest {
encoderTypeToStageFlag(encoderType: ProgrammableEncoderType): GPUShaderStageFlags {
switch (encoderType) {
case 'compute pass':
return GPUShaderStage.COMPUTE;
case 'render pass':
case 'render bundle':
return GPUShaderStage.FRAGMENT;
default:
unreachable('Unknown encoder type');
}
}
createBindingResourceWithState(
resourceType: 'texture' | 'buffer',
state: 'valid' | 'destroyed'
): GPUBindingResource {
switch (resourceType) {
case 'texture': {
const texture = vtu.createTextureWithState(this, 'valid');
const view = texture.createView();
if (state === 'destroyed') {
texture.destroy();
}
return view;
}
case 'buffer':
return {
buffer: vtu.createBufferWithState(this, state, {
size: 4,
usage: GPUBufferUsage.UNIFORM,
}),
};
default:
unreachable('unknown resource type');
}
}
/**
* If state is 'invalid', creates an invalid bind group with valid resources.
* If state is 'destroyed', creates a valid bind group with destroyed resources.
*/
createBindGroup(
state: ResourceState,
resourceType: 'buffer' | 'texture',
encoderType: ProgrammableEncoderType,
indices: number[]
) {
if (state === 'invalid') {
this.device.pushErrorScope('validation');
indices = new Array<number>(indices.length + 1).fill(0);
}
const layout = this.device.createBindGroupLayout({
entries: indices.map(binding => ({
binding,
visibility: this.encoderTypeToStageFlag(encoderType),
...(resourceType === 'buffer' ? { buffer: { type: 'uniform' } } : { texture: {} }),
})),
});
const bindGroup = this.device.createBindGroup({
layout,
entries: indices.map(binding => ({
binding,
resource: this.createBindingResourceWithState(
resourceType,
state === 'destroyed' ? state : 'valid'
),
})),
});
if (state === 'invalid') {
void this.device.popErrorScope();
}
return bindGroup;
}
}
export const g = makeTestGroup(F);
g.test('state_and_binding_index')
.desc('Tests that setBindGroup correctly handles {valid, invalid, destroyed} bindGroups.')
.params(u =>
u
.combine('encoderType', kProgrammableEncoderTypes)
.combine('state', kResourceStates)
.combine('resourceType', ['buffer', 'texture'] as const)
)
.fn(t => {
const { encoderType, state, resourceType } = t.params;
const maxBindGroups = t.device.limits.maxBindGroups;
function runTest(index: number) {
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
encoder.setBindGroup(index, t.createBindGroup(state, resourceType, encoderType, [index]));
validateFinishAndSubmit(state !== 'invalid' && index < maxBindGroups, state !== 'destroyed');
}
// MAINTENANCE_TODO: move to subcases() once we can query the device limits
for (const index of [1, maxBindGroups - 1, maxBindGroups]) {
t.debug(`test bind group index ${index}`);
runTest(index);
}
});
g.test('bind_group,device_mismatch')
.desc(
`
Tests setBindGroup cannot be called with a bind group created from another device
- x= setBindGroup {sequence overload, Uint32Array overload}
`
)
.params(u =>
u
.combine('encoderType', kProgrammableEncoderTypes)
.beginSubcases()
.combine('useU32Array', [true, false])
.combine('mismatched', [true, false])
)
.beforeAllSubcases(t => t.usesMismatchedDevice())
.fn(t => {
const { encoderType, useU32Array, mismatched } = t.params;
const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
const buffer = t.trackForCleanup(
sourceDevice.createBuffer({
size: 4,
usage: GPUBufferUsage.UNIFORM,
})
);
const layout = sourceDevice.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: t.encoderTypeToStageFlag(encoderType),
buffer: { type: 'uniform', hasDynamicOffset: useU32Array },
},
],
});
const bindGroup = sourceDevice.createBindGroup({
layout,
entries: [
{
binding: 0,
resource: { buffer },
},
],
});
const { encoder, validateFinish } = t.createEncoder(encoderType);
if (useU32Array) {
encoder.setBindGroup(0, bindGroup, new Uint32Array([0]), 0, 1);
} else {
encoder.setBindGroup(0, bindGroup);
}
validateFinish(!mismatched);
});
g.test('dynamic_offsets_passed_but_not_expected')
.desc('Tests that setBindGroup correctly errors on unexpected dynamicOffsets.')
.params(u => u.combine('encoderType', kProgrammableEncoderTypes))
.fn(t => {
const { encoderType } = t.params;
const bindGroup = t.createBindGroup('valid', 'buffer', encoderType, []);
const dynamicOffsets = [0];
const { encoder, validateFinish } = t.createEncoder(encoderType);
encoder.setBindGroup(0, bindGroup, dynamicOffsets);
validateFinish(false);
});
g.test('dynamic_offsets_match_expectations_in_pass_encoder')
.desc('Tests that given dynamicOffsets match the specified bindGroup.')
.params(u =>
u
.combine('encoderType', kProgrammableEncoderTypes)
.combineWithParams([
{ dynamicOffsets: [256, 0], _success: true }, // Dynamic offsets aligned
{ dynamicOffsets: [1, 2], _success: false }, // Dynamic offsets not aligned
// Wrong number of dynamic offsets
{ dynamicOffsets: [256, 0, 0], _success: false },
{ dynamicOffsets: [256], _success: false },
{ dynamicOffsets: [], _success: false },
// Dynamic uniform buffer out of bounds because of binding size
{ dynamicOffsets: [512, 0], _success: false },
{ dynamicOffsets: [1024, 0], _success: false },
{ dynamicOffsets: [0xffffffff, 0], _success: false },
// Dynamic storage buffer out of bounds because of binding size
{ dynamicOffsets: [0, 512], _success: false },
{ dynamicOffsets: [0, 1024], _success: false },
{ dynamicOffsets: [0, 0xffffffff], _success: false },
])
.combine('useU32array', [false, true])
.beginSubcases()
.combine('visibility', [
GPUConst.ShaderStage.COMPUTE,
GPUConst.ShaderStage.COMPUTE | GPUConst.ShaderStage.FRAGMENT,
] as const)
.combine('useStorage', [false, true] as const)
)
.fn(t => {
const { visibility, useStorage } = t.params;
t.skipIf(
t.isCompatibility &&
(visibility & GPUShaderStage.FRAGMENT) !== 0 &&
!(t.device.limits.maxStorageBuffersInFragmentStage! >= 1),
`maxStorageBuffersInFragmentStage${t.device.limits.maxStorageBuffersInFragmentStage} < 1`
);
const kBindingSize = 12;
const bindGroupLayout = t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility,
buffer: {
type: 'uniform',
hasDynamicOffset: true,
},
},
{
binding: 1,
visibility,
buffer: {
type: useStorage ? 'storage' : 'uniform',
hasDynamicOffset: true,
},
},
],
});
const uniformBuffer = t.createBufferTracked({
size: 2 * kMinDynamicBufferOffsetAlignment + 8,
usage: GPUBufferUsage.UNIFORM,
});
const storageOrUniformBuffer = t.createBufferTracked({
size: 2 * kMinDynamicBufferOffsetAlignment + 8,
usage: useStorage ? GPUBufferUsage.STORAGE : GPUBufferUsage.UNIFORM,
});
const bindGroup = t.device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer,
size: kBindingSize,
},
},
{
binding: 1,
resource: {
buffer: storageOrUniformBuffer,
size: kBindingSize,
},
},
],
});
const { encoderType, dynamicOffsets, useU32array, _success } = t.params;
const { encoder, validateFinish } = t.createEncoder(encoderType);
if (useU32array) {
encoder.setBindGroup(0, bindGroup, new Uint32Array(dynamicOffsets), 0, dynamicOffsets.length);
} else {
encoder.setBindGroup(0, bindGroup, dynamicOffsets);
}
validateFinish(_success);
});
g.test('u32array_start_and_length')
.desc('Tests that dynamicOffsetsData(Start|Length) apply to the given Uint32Array.')
.paramsSubcasesOnly([
// dynamicOffsetsDataLength > offsets.length
{
offsets: [0] as const,
dynamicOffsetsDataStart: 0,
dynamicOffsetsDataLength: 2,
_success: false,
},
// dynamicOffsetsDataStart + dynamicOffsetsDataLength > offsets.length
{
offsets: [0] as const,
dynamicOffsetsDataStart: 1,
dynamicOffsetsDataLength: 1,
_success: false,
},
{
offsets: [0, 0] as const,
dynamicOffsetsDataStart: 1,
dynamicOffsetsDataLength: 1,
_success: true,
},
{
offsets: [0, 0, 0] as const,
dynamicOffsetsDataStart: 1,
dynamicOffsetsDataLength: 1,
_success: true,
},
{
offsets: [0, 0] as const,
dynamicOffsetsDataStart: 0,
dynamicOffsetsDataLength: 2,
_success: true,
},
])
.fn(t => {
const { offsets, dynamicOffsetsDataStart, dynamicOffsetsDataLength, _success } = t.params;
const kBindingSize = 8;
const bindGroupLayout = t.device.createBindGroupLayout({
entries: range(dynamicOffsetsDataLength, i => ({
binding: i,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: 'uniform',
hasDynamicOffset: true,
},
})),
});
const bindGroup = t.device.createBindGroup({
layout: bindGroupLayout,
entries: range(dynamicOffsetsDataLength, i => ({
binding: i,
resource: {
buffer: vtu.createBufferWithState(t, 'valid', {
size: kBindingSize,
usage: GPUBufferUsage.UNIFORM,
}),
size: kBindingSize,
},
})),
});
const { encoder, validateFinish } = t.createEncoder('render pass');
const doSetBindGroup = () => {
encoder.setBindGroup(
0,
bindGroup,
new Uint32Array(offsets),
dynamicOffsetsDataStart,
dynamicOffsetsDataLength
);
};
if (_success) {
doSetBindGroup();
} else {
t.shouldThrow('RangeError', doSetBindGroup);
}
// RangeError in setBindGroup does not cause the encoder to become invalid.
validateFinish(true);
});
g.test('buffer_dynamic_offsets')
.desc(
`
Test that the dynamic offsets of the BufferLayout is a multiple of
'minUniformBufferOffsetAlignment|minStorageBufferOffsetAlignment' if the BindGroup entry defines
buffer and the buffer type is 'uniform|storage|read-only-storage'.
`
)
.params(u =>
u //
.combine('type', kBufferBindingTypes)
.combine('encoderType', kProgrammableEncoderTypes)
.beginSubcases()
.combine('dynamicOffsetVariant', [
{ mult: 1, add: 0 },
{ mult: 0.5, add: 0 },
{ mult: 1.5, add: 0 },
{ mult: 2, add: 0 },
{ mult: 1, add: 2 },
])
)
.fn(t => {
const { type, dynamicOffsetVariant, encoderType } = t.params;
const kBindingSize = 12;
const minAlignment =
t.device.limits[
type === 'uniform' ? 'minUniformBufferOffsetAlignment' : 'minStorageBufferOffsetAlignment'
];
const dynamicOffset = makeValueTestVariant(minAlignment, dynamicOffsetVariant);
const bindGroupLayout = t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: { type, hasDynamicOffset: true },
},
],
});
const usage = type === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE;
const isValid = dynamicOffset % minAlignment === 0;
const buffer = t.createBufferTracked({
size: 3 * kMinDynamicBufferOffsetAlignment,
usage,
});
const bindGroup = t.device.createBindGroup({
entries: [{ binding: 0, resource: { buffer, size: kBindingSize } }],
layout: bindGroupLayout,
});
const { encoder, validateFinish } = t.createEncoder(encoderType);
encoder.setBindGroup(0, bindGroup, [dynamicOffset]);
validateFinish(isValid);
});