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

export const description = `
Tests for the indirect-specific aspects of drawIndirect/drawIndexedIndirect.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import {
  kDrawIndirectParametersSize,
  kDrawIndexedIndirectParametersSize,
} from '../../../capability_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as ttu from '../../../texture_test_utils.js';

const filled = new Uint8Array([0, 255, 0, 255]);
const notFilled = new Uint8Array([0, 0, 0, 0]);

const kRenderTargetFormat = 'rgba8unorm';

class F extends AllFeaturesMaxLimitsGPUTest {
  makeIndexBuffer(): GPUBuffer {
    return this.makeBufferWithContents(
      /* prettier-ignore */ new Uint32Array([
        0, 1, 2, // The bottom left triangle
        1, 2, 3, // The top right triangle
      ]),
      GPUBufferUsage.INDEX
    );
  }

  makeVertexBuffer(isIndexed: boolean): GPUBuffer {
    /* prettier-ignore */
    const vertices = isIndexed
      ? [
          -1.0, -1.0,
          -1.0,  1.0,
           1.0, -1.0,
           1.0,  1.0,
        ]
      : [
          // The bottom left triangle
          -1.0,  1.0,
           1.0, -1.0,
          -1.0, -1.0,

          // The top right triangle
          -1.0,  1.0,
           1.0, -1.0,
           1.0,  1.0,
        ];
    return this.makeBufferWithContents(new Float32Array(vertices), GPUBufferUsage.VERTEX);
  }

  makeIndirectBuffer(isIndexed: boolean, indirectOffset: number): GPUBuffer {
    const o = indirectOffset / Uint32Array.BYTES_PER_ELEMENT;

    const parametersSize = isIndexed
      ? kDrawIndexedIndirectParametersSize
      : kDrawIndirectParametersSize;
    const arraySize = o + parametersSize * 2;

    const indirectBuffer = [...Array(arraySize)].map(() => Math.floor(Math.random() * 100));

    if (isIndexed) {
      // draw args that will draw the left bottom triangle (expected call)
      indirectBuffer[o] = 3; // indexCount
      indirectBuffer[o + 1] = 1; // instanceCount
      indirectBuffer[o + 2] = 0; // firstIndex
      indirectBuffer[o + 3] = 0; // baseVertex
      indirectBuffer[o + 4] = 0; // firstInstance

      // draw args that will draw both triangles
      indirectBuffer[o + 5] = 6; // indexCount
      indirectBuffer[o + 6] = 1; // instanceCount
      indirectBuffer[o + 7] = 0; // firstIndex
      indirectBuffer[o + 8] = 0; // baseVertex
      indirectBuffer[o + 9] = 0; // firstInstance

      if (o >= parametersSize) {
        // draw args that will draw the right top triangle
        indirectBuffer[o - 5] = 3; // indexCount
        indirectBuffer[o - 4] = 1; // instanceCount
        indirectBuffer[o - 3] = 3; // firstIndex
        indirectBuffer[o - 2] = 0; // baseVertex
        indirectBuffer[o - 1] = 0; // firstInstance
      }

      if (o >= parametersSize * 2) {
        // draw args that will draw nothing
        indirectBuffer[0] = 0; // indexCount
        indirectBuffer[1] = 0; // instanceCount
        indirectBuffer[2] = 0; // firstIndex
        indirectBuffer[3] = 0; // baseVertex
        indirectBuffer[4] = 0; // firstInstance
      }
    } else {
      // draw args that will draw the left bottom triangle (expected call)
      indirectBuffer[o] = 3; // vertexCount
      indirectBuffer[o + 1] = 1; // instanceCount
      indirectBuffer[o + 2] = 0; // firstVertex
      indirectBuffer[o + 3] = 0; // firstInstance

      // draw args that will draw both triangles
      indirectBuffer[o + 4] = 6; // vertexCount
      indirectBuffer[o + 5] = 1; // instanceCount
      indirectBuffer[o + 6] = 0; // firstVertex
      indirectBuffer[o + 7] = 0; // firstInstance

      if (o >= parametersSize) {
        // draw args that will draw the right top triangle
        indirectBuffer[o - 4] = 3; // vertexCount
        indirectBuffer[o - 3] = 1; // instanceCount
        indirectBuffer[o - 2] = 3; // firstVertex
        indirectBuffer[o - 1] = 0; // firstInstance
      }

      if (o >= parametersSize * 2) {
        // draw args that will draw nothing
        indirectBuffer[0] = 0; // vertexCount
        indirectBuffer[1] = 0; // instanceCount
        indirectBuffer[2] = 0; // firstVertex
        indirectBuffer[3] = 0; // firstInstance
      }
    }

    return this.makeBufferWithContents(new Uint32Array(indirectBuffer), GPUBufferUsage.INDIRECT);
  }
}

export const g = makeTestGroup(F);

g.test('basics')
  .desc(
    `Test that the indirect draw parameters are tightly packed for drawIndirect and drawIndexedIndirect.
An indirectBuffer is created based on indirectOffset. The actual draw args being used indicated by the
indirectOffset is going to draw a left bottom triangle.
While the remaining indirectBuffer is populated with random numbers or draw args
that draw right top triangle, both, or nothing which will fail the color check.
The test will check render target to see if only the left bottom area is filled,
meaning the expected draw args is uploaded correctly by the indirectBuffer and indirectOffset.

Params:
    - draw{Indirect, IndexedIndirect}
    - indirectOffset= {0, 4, k * sizeof(args struct), k * sizeof(args struct) + 4}
    `
  )
  .params(u =>
    u
      .combine('isIndexed', [true, false])
      .beginSubcases()
      .expand('indirectOffset', p => {
        const indirectDrawParametersSize = p.isIndexed
          ? kDrawIndexedIndirectParametersSize * Uint32Array.BYTES_PER_ELEMENT
          : kDrawIndirectParametersSize * Uint32Array.BYTES_PER_ELEMENT;
        return [
          0,
          Uint32Array.BYTES_PER_ELEMENT,
          1 * indirectDrawParametersSize,
          1 * indirectDrawParametersSize + Uint32Array.BYTES_PER_ELEMENT,
          3 * indirectDrawParametersSize,
          3 * indirectDrawParametersSize + Uint32Array.BYTES_PER_ELEMENT,
          99 * indirectDrawParametersSize,
          99 * indirectDrawParametersSize + Uint32Array.BYTES_PER_ELEMENT,
        ] as const;
      })
  )
  .fn(t => {
    const { isIndexed, indirectOffset } = t.params;

    const vertexBuffer = t.makeVertexBuffer(isIndexed);
    const indirectBuffer = t.makeIndirectBuffer(isIndexed, indirectOffset);

    const pipeline = t.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: t.device.createShaderModule({
          code: `@vertex fn main(@location(0) pos : vec2<f32>) -> @builtin(position) vec4<f32> {
              return vec4<f32>(pos, 0.0, 1.0);
          }`,
        }),
        entryPoint: 'main',
        buffers: [
          {
            attributes: [
              {
                shaderLocation: 0,
                format: 'float32x2',
                offset: 0,
              },
            ],
            arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT,
          },
        ],
      },
      fragment: {
        module: t.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: kRenderTargetFormat,
          },
        ],
      },
    });

    const renderTarget = t.createTextureTracked({
      size: [4, 4],
      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
      format: kRenderTargetFormat,
    });

    const commandEncoder = t.device.createCommandEncoder();
    const renderPass = commandEncoder.beginRenderPass({
      colorAttachments: [
        {
          view: renderTarget.createView(),
          clearValue: [0, 0, 0, 0],
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });
    renderPass.setPipeline(pipeline);
    renderPass.setVertexBuffer(0, vertexBuffer, 0);

    if (isIndexed) {
      renderPass.setIndexBuffer(t.makeIndexBuffer(), 'uint32', 0);
      renderPass.drawIndexedIndirect(indirectBuffer, indirectOffset);
    } else {
      renderPass.drawIndirect(indirectBuffer, indirectOffset);
    }
    renderPass.end();
    t.queue.submit([commandEncoder.finish()]);

    ttu.expectSinglePixelComparisonsAreOkInTexture(t, { texture: renderTarget }, [
      // The bottom left area is filled
      { coord: { x: 0, y: 1 }, exp: filled },
      // The top right area is not filled
      { coord: { x: 1, y: 0 }, exp: notFilled },
    ]);
  });