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

export const description = `
Validation tests for indexed draws accessing the index buffer.
`;

import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../../gpu_test.js';

class F extends AllFeaturesMaxLimitsGPUTest {
  createIndexBuffer(indexData: Iterable<number>): GPUBuffer {
    return this.makeBufferWithContents(new Uint32Array(indexData), GPUBufferUsage.INDEX);
  }

  createRenderPipeline(): GPURenderPipeline {
    return this.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: this.device.createShaderModule({
          code: `
            @vertex fn main() -> @builtin(position) vec4<f32> {
              return vec4<f32>(0.0, 0.0, 0.0, 1.0);
            }`,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: this.device.createShaderModule({
          code: `
            @fragment fn main() -> @location(0) vec4<f32> {
              return vec4<f32>(0.0, 1.0, 0.0, 1.0);
            }`,
        }),
        entryPoint: 'main',
        targets: [{ format: 'rgba8unorm' }],
      },
      primitive: {
        topology: 'triangle-strip',
        stripIndexFormat: 'uint32',
      },
    });
  }

  beginRenderPass(encoder: GPUCommandEncoder) {
    const colorAttachment = this.createTextureTracked({
      format: 'rgba8unorm',
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    return encoder.beginRenderPass({
      colorAttachments: [
        {
          view: colorAttachment.createView(),
          clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });
  }

  drawIndexed(
    indexBuffer: GPUBuffer,
    indexCount: number,
    instanceCount: number,
    firstIndex: number,
    baseVertex: number,
    firstInstance: number,
    isSuccess: boolean
  ) {
    const pipeline = this.createRenderPipeline();

    const encoder = this.device.createCommandEncoder();
    const pass = this.beginRenderPass(encoder);
    pass.setPipeline(pipeline);
    pass.setIndexBuffer(indexBuffer, 'uint32');
    pass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
    pass.end();

    if (isSuccess) {
      this.device.queue.submit([encoder.finish()]);
    } else {
      this.expectValidationError(() => {
        encoder.finish();
      });
    }
  }
}

export const g = makeTestGroup(F);

g.test('out_of_bounds')
  .desc(
    `Test drawing with out of bound index access to make sure encoder validation catch the
    following indexCount and firstIndex OOB conditions
    - either is within bound but indexCount + firstIndex is out of bound
    - only firstIndex is out of bound
    - only indexCount is out of bound
    - firstIndex much larger than indexCount
    - indexCount much larger than firstIndex
    - max uint32 value for both to make sure the sum doesn't overflow
    - max uint32 indexCount and small firstIndex
    - max uint32 firstIndex and small indexCount
    Together with normal and large instanceCount`
  )
  .params(
    u =>
      u
        .combineWithParams([
          { indexCount: 6, firstIndex: 0 }, // draw all 6 out of 6 index
          { indexCount: 5, firstIndex: 1 }, // draw the last 5 out of 6 index
          { indexCount: 1, firstIndex: 5 }, // draw the last 1 out of 6 index
          { indexCount: 0, firstIndex: 6 }, // firstIndex point to the one after last, but (indexCount + firstIndex) * stride <= bufferSize, valid
          { indexCount: 0, firstIndex: 7 }, // (indexCount + firstIndex) * stride > bufferSize, invalid
          { indexCount: 7, firstIndex: 0 }, // only indexCount out of bound
          { indexCount: 6, firstIndex: 1 }, // indexCount + firstIndex out of bound
          { indexCount: 1, firstIndex: 6 }, // indexCount valid, but (indexCount + firstIndex) out of bound
          { indexCount: 6, firstIndex: 10000 }, // firstIndex much larger than the bound
          { indexCount: 10000, firstIndex: 0 }, // indexCount much larger than the bound
          { indexCount: 0xffffffff, firstIndex: 0xffffffff }, // max uint32 value
          { indexCount: 0xffffffff, firstIndex: 2 }, // max uint32 indexCount and small firstIndex
          { indexCount: 2, firstIndex: 0xffffffff }, // small indexCount and max uint32 firstIndex
        ] as const)
        .combine('instanceCount', [1, 10000]) // normal and large instanceCount
  )
  .fn(t => {
    const { indexCount, firstIndex, instanceCount } = t.params;

    const indexBuffer = t.createIndexBuffer([0, 1, 2, 3, 1, 2]);
    const isSuccess: boolean = indexCount + firstIndex <= 6;

    t.drawIndexed(indexBuffer, indexCount, instanceCount, firstIndex, 0, 0, isSuccess);
  });

g.test('out_of_bounds_zero_sized_index_buffer')
  .desc(
    `Test drawing with an empty index buffer to make sure the encoder validation catch the
    following indexCount and firstIndex conditions
    - indexCount + firstIndex is out of bound
    - indexCount is 0 but firstIndex is out of bound
    - only indexCount is out of bound
    - both are 0s (not out of bound) but index buffer size is 0
    Together with normal and large instanceCount`
  )
  .params(
    u =>
      u
        .combineWithParams([
          { indexCount: 3, firstIndex: 1 }, // indexCount + firstIndex out of bound
          { indexCount: 0, firstIndex: 1 }, // indexCount is 0 but firstIndex out of bound
          { indexCount: 3, firstIndex: 0 }, // only indexCount out of bound
          { indexCount: 0, firstIndex: 0 }, // just zeros, valid
        ] as const)
        .combine('instanceCount', [1, 10000]) // normal and large instanceCount
  )
  .fn(t => {
    const { indexCount, firstIndex, instanceCount } = t.params;

    const indexBuffer = t.createIndexBuffer([]);
    const isSuccess: boolean = indexCount + firstIndex <= 0;

    t.drawIndexed(indexBuffer, indexCount, instanceCount, firstIndex, 0, 0, isSuccess);
  });