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

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