summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts446
1 files changed, 446 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts
new file mode 100644
index 0000000000..476ad576e1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts
@@ -0,0 +1,446 @@
+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 { range, unreachable } from '../../../../../common/util/util.js';
+import {
+ kBufferBindingTypes,
+ kMinDynamicBufferOffsetAlignment,
+ kLimitInfo,
+} from '../../../../capability_info.js';
+import { kResourceStates, ResourceState } from '../../../../gpu_test.js';
+import {
+ kProgrammableEncoderTypes,
+ ProgrammableEncoderType,
+} from '../../../../util/command_buffer_maker.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ 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 = this.createTextureWithState('valid');
+ const view = texture.createView();
+ if (state === 'destroyed') {
+ texture.destroy();
+ }
+ return view;
+ }
+ case 'buffer':
+ return {
+ buffer: this.createBufferWithState(state, {
+ size: 4,
+ usage: GPUBufferUsage.STORAGE,
+ }),
+ };
+ 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: 'storage' } } : { 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(async t => {
+ const { encoderType, state, resourceType } = t.params;
+ const maxBindGroups = t.device.limits.maxBindGroups;
+
+ async 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}`);
+ await 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.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, useU32Array, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const buffer = sourceDevice.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ const layout = sourceDevice.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: t.encoderTypeToStageFlag(encoderType),
+ buffer: { type: 'storage', 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(async 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])
+ )
+ .fn(async t => {
+ const kBindingSize = 12;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'uniform',
+ hasDynamicOffset: true,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'storage',
+ hasDynamicOffset: true,
+ },
+ },
+ ],
+ });
+
+ const uniformBuffer = t.device.createBuffer({
+ size: 2 * kMinDynamicBufferOffsetAlignment + 8,
+ usage: GPUBufferUsage.UNIFORM,
+ });
+
+ const storageBuffer = t.device.createBuffer({
+ size: 2 * kMinDynamicBufferOffsetAlignment + 8,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: uniformBuffer,
+ size: kBindingSize,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: storageBuffer,
+ 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: 'storage',
+ hasDynamicOffset: true,
+ },
+ })),
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: range(dynamicOffsetsDataLength, i => ({
+ binding: i,
+ resource: {
+ buffer: t.createBufferWithState('valid', {
+ size: kBindingSize,
+ usage: GPUBufferUsage.STORAGE,
+ }),
+ 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()
+ .expand('dynamicOffset', ({ type }) =>
+ type === 'uniform'
+ ? [
+ kLimitInfo.minUniformBufferOffsetAlignment.default,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 0.5,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 1.5,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 2,
+ kLimitInfo.minUniformBufferOffsetAlignment.default + 2,
+ ]
+ : [
+ kLimitInfo.minStorageBufferOffsetAlignment.default,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 0.5,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 1.5,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 2,
+ kLimitInfo.minStorageBufferOffsetAlignment.default + 2,
+ ]
+ )
+ )
+ .fn(async t => {
+ const { type, dynamicOffset, encoderType } = t.params;
+ const kBindingSize = 12;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type, hasDynamicOffset: true },
+ },
+ ],
+ });
+
+ let usage, isValid;
+ if (type === 'uniform') {
+ usage = GPUBufferUsage.UNIFORM;
+ isValid = dynamicOffset % kLimitInfo.minUniformBufferOffsetAlignment.default === 0;
+ } else {
+ usage = GPUBufferUsage.STORAGE;
+ isValid = dynamicOffset % kLimitInfo.minStorageBufferOffsetAlignment.default === 0;
+ }
+
+ const buffer = t.device.createBuffer({
+ 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);
+ });