🏡 index : cold-air-networks/mirror-webgpu-cts.git

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);
    }
  });