export const description = `
TODO: Test more corner case values for Float16 / Float32 (INF, NaN, ...) and reduce the
float tolerance.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import {
  assert,
  filterUniqueValueTestVariants,
  makeValueTestVariant,
  memcpy,
  unreachable,
} from '../../../../common/util/util.js';
import { kVertexFormatInfo, kVertexFormats } from '../../../capability_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import { float32ToFloat16Bits, normalizedIntegerAsFloat } from '../../../util/conversion.js';
import { align, clamp } from '../../../util/math.js';

// These types mirror the structure of GPUVertexBufferLayout but allow defining the extra
// dictionary members at the GPUVertexBufferLayout and GPUVertexAttribute level. The are used
// like so:
//
//   VertexState<{arrayStride: number}, {format: VertexFormat}>
//   VertexBuffer<{arrayStride: number}, {format: VertexFormat}>
//   VertexAttrib<{format: VertexFormat}>
type VertexAttrib<A> = A & { shaderLocation: number };
type VertexBuffer<V, A> = V & {
  slot: number;
  attributes: VertexAttrib<A>[];
};
type VertexState<V, A> = VertexBuffer<V, A>[];

type VertexLayoutState<V, A> = VertexState<
  { stepMode: GPUVertexStepMode; arrayStride: number } & V,
  { format: GPUVertexFormat; offset: number } & A
>;

function mapBufferAttribs<V, A1, A2>(
  buffer: VertexBuffer<V, A1>,
  f: (v: V, a: VertexAttrib<A1>) => A2
): VertexBuffer<V, A2> {
  const newAttributes: VertexAttrib<A2>[] = [];
  for (const a of buffer.attributes) {
    newAttributes.push({
      shaderLocation: a.shaderLocation,
      ...f(buffer, a),
    });
  }

  return { ...buffer, attributes: newAttributes };
}

function mapStateAttribs<V, A1, A2>(
  buffers: VertexState<V, A1>,
  f: (v: V, a: VertexAttrib<A1>) => A2
): VertexState<V, A2> {
  return buffers.map(b => mapBufferAttribs(b, f));
}

function makeRgb10a2(rgba: Array<number>): number {
  const [r, g, b, a] = rgba;
  assert((r & 0x3ff) === r);
  assert((g & 0x3ff) === g);
  assert((b & 0x3ff) === b);
  assert((a & 0x3) === a);
  return r | (g << 10) | (b << 20) | (a << 30);
}

function normalizeRgb10a2(rgba: number, index: number): number {
  const normalizationFactor = index % 4 === 3 ? 3 : 1023;
  return rgba / normalizationFactor;
}

type TestData = {
  shaderBaseType: string;
  floatTolerance?: number;
  // The number of vertex components in the vertexData (expectedData might contain more because
  // it is padded to 4 components).
  testComponentCount: number;
  // The data that will be in the uniform buffer and used to check the vertex inputs.
  expectedData: ArrayBuffer;
  // The data that will be in the vertex buffer.
  vertexData: ArrayBuffer;
};

class VertexStateTest extends AllFeaturesMaxLimitsGPUTest {
  // Generate for VS + FS (entrypoints vsMain / fsMain) that for each attribute will check that its
  // value corresponds to what's expected (as provided by a uniform buffer per attribute) and then
  // renders each vertex at position (vertexIndex, instanceindex) with either 1 (success) or
  // a negative number corresponding to the check number (in case you need to debug a failure).
  makeTestWGSL(
    buffers: VertexState<
      { stepMode: GPUVertexStepMode },
      {
        format: GPUVertexFormat;
        shaderBaseType: string;
        shaderComponentCount?: number;
        floatTolerance?: number;
      }
    >,
    vertexCount: number,
    instanceCount: number
  ): string {
    let vsInputs = '';
    let vsChecks = '';
    let providedDataDefs = '';

    for (const b of buffers) {
      for (const a of b.attributes) {
        const format = kVertexFormatInfo[a.format];
        const shaderComponentCount = a.shaderComponentCount ?? format.componentCount;
        const i = a.shaderLocation;

        // shaderType is either a scalar type like f32 or a vecN<scalarType>
        let shaderType = a.shaderBaseType;
        if (shaderComponentCount !== 1) {
          shaderType = `vec${shaderComponentCount}<${shaderType}>`;
        }

        let maxCount = `${vertexCount}`;
        let indexBuiltin = `input.vertexIndex`;
        if (b.stepMode === 'instance') {
          maxCount = `${instanceCount}`;
          indexBuiltin = `input.instanceIndex`;
        }

        vsInputs += `  @location(${i}) attrib${i} : ${shaderType},\n`;
        providedDataDefs += `  data${i}: array<vec4<${a.shaderBaseType}>, ${maxCount}>,\n`;

        // Generate the all the checks for the attributes.
        for (let component = 0; component < shaderComponentCount; component++) {
          // Components are filled with (0, 0, 0, 1) if they aren't provided data from the pipeline.
          if (component >= format.componentCount) {
            const expected = component === 3 ? '1' : '0';
            vsChecks += `  check(input.attrib${i}[${component}] == ${a.shaderBaseType}(${expected}));\n`;
            continue;
          }

          // Check each component individually, with special handling of tolerance for floats.
          const attribComponent =
            shaderComponentCount === 1 ? `input.attrib${i}` : `input.attrib${i}[${component}]`;
          const providedData = `providedData.data${i}[${indexBuiltin}][${component}]`;
          if (format.type === 'uint' || format.type === 'sint') {
            vsChecks += `  check(${attribComponent} == ${providedData});\n`;
          } else {
            vsChecks += `  check(floatsSimilar(${attribComponent}, ${providedData}, f32(${
              a.floatTolerance ?? 0
            })));\n`;
          }
        }
      }
    }

    return `
struct Inputs {
${vsInputs}
  @builtin(vertex_index) vertexIndex: u32,
  @builtin(instance_index) instanceIndex: u32,
};

struct ProvidedData {
${providedDataDefs}
};

@group(0) @binding(0) var<uniform> providedData: ProvidedData;

var<private> vsResult : i32 = 1;
var<private> checkIndex : i32 = 0;
fn check(success : bool) {
  if (!success) {
    vsResult = -checkIndex;
  }
  checkIndex = checkIndex + 1;
}

fn floatsSimilar(a : f32, b : f32, tolerance : f32) -> bool {
  // Note: -0.0 and 0.0 have different bit patterns, but compare as equal.
  return abs(a - b) < tolerance;
}

fn doTest(input : Inputs) {
${vsChecks}
}

struct VSOutputs {
  @location(0) @interpolate(flat, either) result : i32,
  @builtin(position) position : vec4<f32>,
};

@vertex fn vsMain(input : Inputs) -> VSOutputs {
  doTest(input);

  // Place that point at pixel (vertexIndex, instanceIndex) in a framebuffer of size
  // (vertexCount , instanceCount).
  var output : VSOutputs;
  output.position = vec4<f32>(
    ((f32(input.vertexIndex) + 0.5) / ${vertexCount}.0 * 2.0) - 1.0,
    ((f32(input.instanceIndex) + 0.5) / ${instanceCount}.0 * 2.0) - 1.0,
    0.0, 1.0
  );
  output.result = vsResult;
  return output;
}

@fragment fn fsMain(@location(0) @interpolate(flat, either) result : i32)
  -> @location(0) i32 {
  return result;
}
    `;
  }

  makeTestPipeline(
    buffers: VertexState<
      { stepMode: GPUVertexStepMode; arrayStride: number },
      {
        offset: number;
        format: GPUVertexFormat;
        shaderBaseType: string;
        shaderComponentCount?: number;
        floatTolerance?: number;
      }
    >,
    vertexCount: number,
    instanceCount: number
  ): GPURenderPipeline {
    const module = this.device.createShaderModule({
      code: this.makeTestWGSL(buffers, vertexCount, instanceCount),
    });

    const bufferLayouts: GPUVertexBufferLayout[] = [];
    for (const b of buffers) {
      bufferLayouts[b.slot] = b;
    }

    return this.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module,
        entryPoint: 'vsMain',
        buffers: bufferLayouts,
      },
      primitive: {
        topology: 'point-list',
      },
      fragment: {
        module,
        entryPoint: 'fsMain',
        targets: [
          {
            format: 'r32sint',
          },
        ],
      },
    });
  }

  // Runs the render pass drawing points in a vertexCount*instanceCount rectangle, then check each
  // of produced a value of 1 which means that the tests in the shader passed.
  submitRenderPass(
    pipeline: GPURenderPipeline,
    buffers: VertexState<{ buffer: GPUBuffer; vbOffset?: number }, {}>,
    expectedData: GPUBindGroup,
    vertexCount: number,
    instanceCount: number
  ) {
    const testTexture = this.createTextureTracked({
      format: 'r32sint',
      size: [vertexCount, instanceCount],
      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
    });

    const encoder = this.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: testTexture.createView(),
          clearValue: [0, 0, 0, 0],
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });

    pass.setPipeline(pipeline);
    pass.setBindGroup(0, expectedData);
    for (const buffer of buffers) {
      pass.setVertexBuffer(buffer.slot, buffer.buffer, buffer.vbOffset ?? 0);
    }
    pass.draw(vertexCount, instanceCount);
    pass.end();

    this.device.queue.submit([encoder.finish()]);

    this.expectSingleColor(testTexture, 'r32sint', {
      size: [vertexCount, instanceCount, 1],
      exp: { R: 1 },
    });
  }

  // Generate TestData for the format with interesting test values.
  // MAINTENANCE_TODO cache the result on the fixture?
  // Note that the test data always starts with an interesting value, so that using the first
  // test value in a test is still meaningful.
  generateTestData(format: GPUVertexFormat): TestData {
    const formatInfo = kVertexFormatInfo[format];
    const bitSize =
      formatInfo.bytesPerComponent === 'packed' ? 0 : formatInfo.bytesPerComponent * 8;

    switch (formatInfo.type) {
      case 'float': {
        // -0.0 and +0.0 have different bit patterns, but compare as equal.
        const data = [42.42, 0.0, -0.0, 1.0, -1.0, 1000, -18.7, 25.17];
        const expectedData = new Float32Array(data).buffer;
        const vertexData =
          bitSize === 32
            ? expectedData
            : bitSize === 16
            ? new Uint16Array(data.map(float32ToFloat16Bits)).buffer
            : unreachable();

        return {
          shaderBaseType: 'f32',
          testComponentCount: data.length,
          expectedData,
          vertexData,
          floatTolerance: 0.05,
        };
      }

      case 'sint': {
        /* prettier-ignore */
        const data = [
          42,
          0, 1, 2, 3, 4, 5,
          -1, -2, -3, -4, -5,
          Math.pow(2, bitSize - 2),
          Math.pow(2, bitSize - 1) - 1, // max value
          -Math.pow(2, bitSize - 2),
          -Math.pow(2, bitSize - 1), // min value
        ];
        const expectedData = new Int32Array(data).buffer;
        const vertexData =
          bitSize === 32
            ? expectedData
            : bitSize === 16
            ? new Int16Array(data).buffer
            : new Int8Array(data).buffer;

        return {
          shaderBaseType: 'i32',
          testComponentCount: data.length,
          expectedData,
          vertexData,
        };
      }

      case 'uint': {
        /* prettier-ignore */
        const data = [
          42,
          0, 1, 2, 3, 4, 5,
          Math.pow(2, bitSize - 1),
          Math.pow(2, bitSize) - 1, // max value
        ];
        const expectedData = new Uint32Array(data).buffer;
        const vertexData =
          bitSize === 32
            ? expectedData
            : bitSize === 16
            ? new Uint16Array(data).buffer
            : new Uint8Array(data).buffer;

        return {
          shaderBaseType: 'u32',
          testComponentCount: data.length,
          expectedData,
          vertexData,
        };
      }

      case 'snorm': {
        /* prettier-ignore */
        const data = [
          42,
          0, 1, 2, 3, 4, 5,
          -1, -2, -3, -4, -5,
          Math.pow(2,bitSize - 2),
          Math.pow(2,bitSize - 1) - 1, // max value
          -Math.pow(2,bitSize - 2),
          -Math.pow(2,bitSize - 1), // min value
        ];
        const vertexData =
          bitSize === 16
            ? new Int16Array(data).buffer
            : bitSize === 8
            ? new Int8Array(data).buffer
            : unreachable();

        return {
          shaderBaseType: 'f32',
          testComponentCount: data.length,
          expectedData: new Float32Array(data.map(v => normalizedIntegerAsFloat(v, bitSize, true)))
            .buffer,
          vertexData,
          floatTolerance: 0.1 * normalizedIntegerAsFloat(1, bitSize, true),
        };
      }

      case 'unorm': {
        if (formatInfo.bytesPerComponent === 'packed') {
          assert(bitSize === 0);

          switch (format) {
            case 'unorm10-10-10-2': {
              /* prettier-ignore */
              const data = [
              [   0,    0,    0, 0],
              [1023, 1023, 1023, 3],
              [ 243,  567,  765, 2],
            ];
              const vertexData = new Uint32Array(data.map(makeRgb10a2)).buffer;
              const expectedData = new Float32Array(data.flat().map(normalizeRgb10a2)).buffer;

              return {
                shaderBaseType: 'f32',
                testComponentCount: data.flat().length,
                expectedData,
                vertexData,
                floatTolerance: 0.1 / 1023,
              };
            }

            case 'unorm8x4-bgra': {
              const data = [42, 0, 1, 2, 3, 4, 128, 255];
              const vertexData = new Uint8Array(data).buffer;
              const expectedData = new Float32Array(
                data.map(v => normalizedIntegerAsFloat(v, 8, false))
              );

              for (let i = 0; i + 2 < expectedData.length; i += 4) {
                const r = expectedData[i + 0];
                const b = expectedData[i + 2];
                expectedData[i + 0] = b;
                expectedData[i + 2] = r;
              }

              return {
                shaderBaseType: 'f32',
                testComponentCount: data.length,
                expectedData: expectedData.buffer,
                vertexData,
                floatTolerance: 0.1 / 255,
              };
            }
          }
        }

        /* prettier-ignore */
        const data = [
          42,
          0, 1, 2, 3, 4, 5,
          Math.pow(2, bitSize - 1),
          Math.pow(2, bitSize) - 1, // max value
        ];
        const vertexData =
          bitSize === 16
            ? new Uint16Array(data).buffer
            : bitSize === 8
            ? new Uint8Array(data).buffer
            : unreachable();

        return {
          shaderBaseType: 'f32',
          testComponentCount: data.length,
          expectedData: new Float32Array(data.map(v => normalizedIntegerAsFloat(v, bitSize, false)))
            .buffer,
          vertexData,
          floatTolerance: 0.1 * normalizedIntegerAsFloat(1, bitSize, false),
        };
      }
    }
  }

  // The TestData generated for a format might not contain enough data for all the vertices we are
  // going to draw, so we expand them by adding additional copies of the vertexData as needed.
  // expectedData is a bit different because it also needs to be unpacked to have `componentCount`
  // components every 4 components (because the shader uses vec4 for the expected data).
  expandTestData(data: TestData, maxCount: number, componentCount: number): TestData {
    const vertexComponentSize = data.vertexData.byteLength / data.testComponentCount;
    const expectedComponentSize = data.expectedData.byteLength / data.testComponentCount;

    const expandedVertexData = new Uint8Array(maxCount * componentCount * vertexComponentSize);
    const expandedExpectedData = new Uint8Array(4 * maxCount * expectedComponentSize);

    for (let index = 0; index < maxCount; index++) {
      for (let component = 0; component < componentCount; component++) {
        // If only we had some builtin JS memcpy function between ArrayBuffers...
        const targetVertexOffset = (index * componentCount + component) * vertexComponentSize;
        const sourceVertexOffset = targetVertexOffset % data.vertexData.byteLength;
        memcpy(
          { src: data.vertexData, start: sourceVertexOffset, length: vertexComponentSize },
          { dst: expandedVertexData, start: targetVertexOffset }
        );

        const targetExpectedOffset = (index * 4 + component) * expectedComponentSize;
        const sourceExpectedOffset =
          ((index * componentCount + component) * expectedComponentSize) %
          data.expectedData.byteLength;
        memcpy(
          { src: data.expectedData, start: sourceExpectedOffset, length: expectedComponentSize },
          { dst: expandedExpectedData, start: targetExpectedOffset }
        );
      }
    }

    return {
      shaderBaseType: data.shaderBaseType,
      testComponentCount: maxCount * componentCount,
      floatTolerance: data.floatTolerance,
      expectedData: expandedExpectedData.slice(0, maxCount * 4 * 4),
      vertexData: expandedVertexData.buffer,
    };
  }

  // Copies `size` bytes from `source` to `target` starting at `offset` each `targetStride`.
  // (the data in `source` is assumed packed)
  interleaveVertexDataInto(
    target: ArrayBuffer,
    src: ArrayBuffer,
    { targetStride, offset, size }: { targetStride: number; offset: number; size: number }
  ) {
    const dst = new Uint8Array(target);
    for (
      let srcStart = 0, dstStart = offset;
      srcStart < src.byteLength;
      srcStart += size, dstStart += targetStride
    ) {
      memcpy({ src, start: srcStart, length: size }, { dst, start: dstStart });
    }
  }

  createTestAndPipelineData<V, A>(
    state: VertexLayoutState<V, A>,
    vertexCount: number,
    instanceCount: number
  ): VertexLayoutState<V, A & TestData> {
    // Gather the test data and some additional test state for attribs.
    return mapStateAttribs(state, (buffer, attrib) => {
      const maxCount = buffer.stepMode === 'instance' ? instanceCount : vertexCount;
      const formatInfo = kVertexFormatInfo[attrib.format];

      let testData = this.generateTestData(attrib.format);
      testData = this.expandTestData(testData, maxCount, formatInfo.componentCount);

      return {
        ...testData,
        ...attrib,
      };
    });
  }

  createExpectedBG(state: VertexState<{}, TestData>, pipeline: GPURenderPipeline): GPUBindGroup {
    // Create the bindgroup for the expected test data

    // Concat expectedData into one buffer
    let numBytes = 0;
    const arrayBuffers = [];
    for (const buffer of state) {
      for (const attrib of buffer.attributes) {
        numBytes += attrib.expectedData.byteLength;
        arrayBuffers.push(attrib.expectedData);
      }
    }

    const allExpectedData = new Uint8Array(numBytes);
    let offset = 0;
    for (const arrayBuffer of arrayBuffers) {
      allExpectedData.set(new Uint8Array(arrayBuffer), offset);
      offset += arrayBuffer.byteLength;
    }
    const expectedDataBuffer = this.makeBufferWithContents(allExpectedData, GPUBufferUsage.UNIFORM);

    return this.device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [{ binding: 0, resource: { buffer: expectedDataBuffer } }],
    });
  }

  createVertexBuffers(
    state: VertexLayoutState<{ vbOffset?: number }, TestData>,
    vertexCount: number,
    instanceCount: number
  ): VertexState<{ buffer: GPUBuffer; vbOffset?: number }, {}> {
    // Create the vertex buffers
    const vertexBuffers: VertexState<{ buffer: GPUBuffer; vbOffset?: number }, {}> = [];

    for (const buffer of state) {
      const maxCount = buffer.stepMode === 'instance' ? instanceCount : vertexCount;

      // Fill the vertex data with garbage so that we don't get `0` (which could be a test value)
      // if the vertex shader loads the vertex data incorrectly.
      const vertexData = new ArrayBuffer(
        align(buffer.arrayStride * maxCount + (buffer.vbOffset ?? 0), 4)
      );
      new Uint8Array(vertexData).fill(0xc4);

      for (const attrib of buffer.attributes) {
        const formatInfo = kVertexFormatInfo[attrib.format];
        this.interleaveVertexDataInto(vertexData, attrib.vertexData, {
          targetStride: buffer.arrayStride,
          offset: (buffer.vbOffset ?? 0) + attrib.offset,
          size: formatInfo.byteSize,
        });
      }

      vertexBuffers.push({
        slot: buffer.slot,
        buffer: this.makeBufferWithContents(new Uint8Array(vertexData), GPUBufferUsage.VERTEX),
        vbOffset: buffer.vbOffset,
        attributes: [],
      });
    }

    return vertexBuffers;
  }

  runTest(
    buffers: VertexLayoutState<{ vbOffset?: number }, { shaderComponentCount?: number }>,
    // Default to using 20 vertices and 20 instances so that we cover each of the test data at least
    // once (at the time of writing the largest testData has 16 values).
    vertexCount: number = 20,
    instanceCount: number = 20
  ) {
    const testData = this.createTestAndPipelineData(buffers, vertexCount, instanceCount);
    const pipeline = this.makeTestPipeline(testData, vertexCount, instanceCount);
    const expectedDataBG = this.createExpectedBG(testData, pipeline);
    const vertexBuffers = this.createVertexBuffers(testData, vertexCount, instanceCount);
    this.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, vertexCount, instanceCount);
  }
}

export const g = makeTestGroup(VertexStateTest);

g.test('vertex_format_to_shader_format_conversion')
  .desc(
    `Test that the raw data passed in vertex buffers is correctly converted to the input type in the shader. Test for:
  - all formats
  - 1 to 4 components in the shader's input type (unused components are filled with 0 and except the 4th with 1)
  - various locations
  - various slots`
  )
  .params(u =>
    u //
      .combine('format', kVertexFormats)
      .combine('shaderComponentCount', [1, 2, 3, 4])
      .beginSubcases()
      .combine('slotVariant', [
        { mult: 0, add: 0 },
        { mult: 0, add: 1 },
        { mult: 1, add: -1 },
      ])
      .combine('shaderLocationVariant', [
        { mult: 0, add: 0 },
        { mult: 0, add: 1 },
        { mult: 1, add: -1 },
      ])
  )
  .fn(t => {
    const { format, shaderComponentCount, slotVariant, shaderLocationVariant } = t.params;
    const slot = t.makeLimitVariant('maxVertexBuffers', slotVariant);
    const shaderLocation = t.makeLimitVariant('maxVertexAttributes', shaderLocationVariant);
    t.runTest([
      {
        slot,
        arrayStride: 16,
        stepMode: 'vertex',
        attributes: [
          {
            shaderLocation,
            format,
            offset: 0,
            shaderComponentCount,
          },
        ],
      },
    ]);
  });

g.test('setVertexBuffer_offset_and_attribute_offset')
  .desc(
    `Test that the vertex buffer offset and attribute offset in the vertex state are applied correctly. Test for:
  - all formats
  - various setVertexBuffer offsets
  - various attribute offsets in a fixed arrayStride`
  )
  .params(u =>
    u //
      .combine('format', kVertexFormats)
      .beginSubcases()
      .combine('vbOffset', [0, 4, 400, 1004])
      .combine('arrayStride', [128])
      .expand('offset', p => {
        const formatInfo = kVertexFormatInfo[p.format];
        const formatSize = formatInfo.byteSize;
        return new Set([
          0,
          4,
          8,
          formatSize,
          formatSize * 2,
          p.arrayStride / 2,
          p.arrayStride - formatSize - 4,
          p.arrayStride - formatSize - 8,
          p.arrayStride - formatSize - formatSize,
          p.arrayStride - formatSize - formatSize * 2,
          p.arrayStride - formatSize,
        ]);
      })
  )
  .fn(t => {
    const { format, vbOffset, arrayStride, offset } = t.params;
    t.runTest([
      {
        slot: 0,
        arrayStride,
        stepMode: 'vertex',
        vbOffset,
        attributes: [
          {
            shaderLocation: 0,
            format,
            offset,
          },
        ],
      },
    ]);
  });

g.test('non_zero_array_stride_and_attribute_offset')
  .desc(
    `Test that the array stride and attribute offset in the vertex state are applied correctly. Test for:
  - all formats
  - various array strides
  - various attribute offsets in a fixed arrayStride`
  )
  .params(u =>
    u //
      .combine('format', kVertexFormats)
      .beginSubcases()
      .expand('arrayStrideVariant', p => {
        const formatInfo = kVertexFormatInfo[p.format];
        const formatSize = formatInfo.byteSize;

        return [
          { mult: 0, add: align(formatSize, 4) },
          { mult: 0, add: align(formatSize, 4) + 4 },
          { mult: 1, add: 0 },
        ];
      })
      .expand('offsetVariant', function* (p) {
        const formatInfo = kVertexFormatInfo[p.format];
        const formatSize = formatInfo.byteSize;
        yield { mult: 0, add: 0 };
        yield { mult: 0, add: 4 };
        if (formatSize !== 4) yield { mult: 0, add: formatSize };
        yield { mult: 0.5, add: 0 };
        yield { mult: 1, add: -formatSize * 2 };
        if (formatSize !== 4) yield { mult: 1, add: -formatSize - 4 };
        yield { mult: 1, add: -formatSize };
      })
  )
  .fn(t => {
    const { format, arrayStrideVariant, offsetVariant } = t.params;
    const arrayStride = t.makeLimitVariant('maxVertexBufferArrayStride', arrayStrideVariant);
    const formatInfo = kVertexFormatInfo[format];
    const formatSize = formatInfo.byteSize;
    const makeMultipleOf = Math.min(formatSize, 4);
    const offset =
      Math.floor(
        clamp(makeValueTestVariant(arrayStride, offsetVariant), {
          min: 0,
          max: arrayStride - formatSize,
        }) / makeMultipleOf
      ) * makeMultipleOf;

    t.runTest([
      {
        slot: 0,
        arrayStride,
        stepMode: 'vertex',
        attributes: [
          {
            shaderLocation: 0,
            format,
            offset,
          },
        ],
      },
    ]);
  });

g.test('buffers_with_varying_step_mode')
  .desc(
    `Test buffers with varying step modes in the same vertex state.
  - Various combination of step modes`
  )
  .paramsSubcasesOnly(u =>
    u //
      .combine('stepModes', [
        ['instance'],
        ['vertex', 'vertex', 'instance'],
        ['instance', 'vertex', 'instance'],
        ['vertex', 'instance', 'vertex', 'vertex'],
      ])
  )
  .fn(t => {
    const { stepModes } = t.params;
    const state = (stepModes as GPUVertexStepMode[]).map((stepMode, i) => ({
      slot: i,
      arrayStride: 4,
      stepMode,
      attributes: [
        {
          shaderLocation: i,
          format: 'float32' as const,
          offset: 0,
        },
      ],
    }));
    t.runTest(state);
  });

g.test('vertex_buffer_used_multiple_times_overlapped')
  .desc(
    `Test using the same vertex buffer in for multiple "vertex buffers", with data from each buffer overlapping.
  - For each vertex format.
  - For various numbers of vertex buffers [2, 3, max]`
  )
  .params(u =>
    u //
      .combine('format', kVertexFormats)
      .beginSubcases()
      .combine('vbCountVariant', [
        { mult: 0, add: 2 },
        { mult: 0, add: 3 },
        { mult: 1, add: 0 },
      ])
      .combine('additionalVBOffset', [0, 4, 120])
  )
  .fn(t => {
    const { format, vbCountVariant, additionalVBOffset } = t.params;
    const vbCount = t.makeLimitVariant('maxVertexBuffers', vbCountVariant);
    const kVertexCount = 20;
    const kInstanceCount = 1;
    const formatInfo = kVertexFormatInfo[format];
    const formatByteSize = formatInfo.byteSize;
    // We need to align so the offset for non-0 setVertexBuffer don't fail validation.
    const alignedFormatByteSize = align(formatByteSize, 4);

    // In this test we want to test using the same vertex buffer for multiple different attributes.
    // For example if vbCount is 3, we will create a vertex buffer containing the following data:
    //    a0, a1, a2, a3, ..., a<baseDataVertexCount>
    // We also create the expected data for the vertex fetching from that buffer so we can modify it
    // below.
    const baseDataVertexCount = kVertexCount + vbCount - 1;
    const baseData = t.createTestAndPipelineData(
      [
        {
          slot: 0,
          arrayStride: alignedFormatByteSize,
          stepMode: 'vertex',
          vbOffset: additionalVBOffset,
          attributes: [{ shaderLocation: 0, format, offset: 0 }],
        },
      ],
      baseDataVertexCount,
      kInstanceCount
    );
    const vertexBuffer = t.createVertexBuffers(baseData, baseDataVertexCount, kInstanceCount)[0]
      .buffer;

    // We are going to bind the vertex buffer multiple times, each time at a different offset that's
    // a multiple of the data size. So what should be fetched by the vertex shader is:
    //    - attrib0: a0, a1, ..., a19
    //    - attrib1: a1, a2, ..., a20
    //    - attrib2: a2, a3, ..., a21
    //    etc.
    // We re-create the test data by:
    //   1) creating multiple "vertex buffers" that all point at the GPUBuffer above but at
    //      different offsets.
    //   2) selecting what parts of the expectedData each attribute will see in the expectedData for
    //      the full vertex buffer.
    const baseTestData = baseData[0].attributes[0];
    assert(baseTestData.testComponentCount === formatInfo.componentCount * baseDataVertexCount);
    const expectedDataBytesPerVertex = baseTestData.expectedData.byteLength / baseDataVertexCount;

    const testData: VertexLayoutState<{}, TestData> = [];
    const vertexBuffers: VertexState<{ buffer: GPUBuffer; vbOffset: number }, {}> = [];
    for (let i = 0; i < vbCount; i++) {
      vertexBuffers.push({
        buffer: vertexBuffer,
        slot: i,
        vbOffset: additionalVBOffset + i * alignedFormatByteSize,
        attributes: [],
      });

      testData.push({
        slot: i,
        arrayStride: alignedFormatByteSize,
        stepMode: 'vertex',
        attributes: [
          {
            shaderLocation: i,
            format,
            offset: 0,

            shaderBaseType: baseTestData.shaderBaseType,
            floatTolerance: baseTestData.floatTolerance,
            // Select vertices [i, i + kVertexCount]
            testComponentCount: kVertexCount * formatInfo.componentCount,
            expectedData: baseTestData.expectedData.slice(
              expectedDataBytesPerVertex * i,
              expectedDataBytesPerVertex * (kVertexCount + i)
            ),
            vertexData: new ArrayBuffer(0),
          },
        ],
      });
    }

    // Run the test with the modified test data.
    const pipeline = t.makeTestPipeline(testData, kVertexCount, kInstanceCount);
    const expectedDataBG = t.createExpectedBG(testData, pipeline);
    t.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, kVertexCount, kInstanceCount);
  });

g.test('vertex_buffer_used_multiple_times_interleaved')
  .desc(
    `Test using the same vertex buffer in for multiple "vertex buffers", with data from each buffer interleaved.
  - For each vertex format.
  - For various numbers of vertex buffers [2, 3, max]`
  )
  .params(u =>
    u //
      .combine('format', kVertexFormats)
      .beginSubcases()
      .combine('vbCountVariant', [
        { mult: 0, add: 2 },
        { mult: 0, add: 3 },
        { mult: 1, add: 0 },
      ])
      .combine('additionalVBOffset', [0, 4, 120])
  )
  .fn(t => {
    const { format, vbCountVariant, additionalVBOffset } = t.params;
    const vbCount = t.makeLimitVariant('maxVertexBuffers', vbCountVariant);
    const kVertexCount = 20;
    const kInstanceCount = 1;
    const formatInfo = kVertexFormatInfo[format];
    const formatByteSize = formatInfo.byteSize;
    // We need to align so the offset for non-0 setVertexBuffer don't fail validation.
    const alignedFormatByteSize = align(formatByteSize, 4);

    // Create data for a single vertex buffer with many attributes, that will be split between
    // many vertex buffers set at different offsets.

    // In this test we want to test using the same vertex buffer for multiple different attributes.
    // For example if vbCount is 3, we will create a vertex buffer containing the following data:
    //    a0, a0, a0, a1, a1, a1, ...
    // To do that we create a single vertex buffer with `vbCount` attributes that all have the same
    // format.
    const attribs: GPUVertexAttribute[] = [];
    for (let i = 0; i < vbCount; i++) {
      attribs.push({ format, offset: i * alignedFormatByteSize, shaderLocation: i });
    }
    const baseData = t.createTestAndPipelineData(
      [
        {
          slot: 0,
          arrayStride: alignedFormatByteSize * vbCount,
          stepMode: 'vertex',
          vbOffset: additionalVBOffset,
          attributes: attribs,
        },
      ],
      kVertexCount,
      kInstanceCount
    );
    const vertexBuffer = t.createVertexBuffers(baseData, kVertexCount, kInstanceCount)[0].buffer;

    // Then we recreate test data by:
    //   1) creating multiple "vertex buffers" that all point at the GPUBuffer above but at
    //      different offsets.
    //   2) have multiple vertex buffer, each with one attributes that will expect a0, a1, ...
    const testData: VertexLayoutState<{}, TestData> = [];
    const vertexBuffers: VertexState<{ buffer: GPUBuffer; vbOffset: number }, {}> = [];
    for (let i = 0; i < vbCount; i++) {
      vertexBuffers.push({
        slot: i,
        buffer: vertexBuffer,
        vbOffset: additionalVBOffset + i * alignedFormatByteSize,
        attributes: [],
      });
      testData.push({
        ...baseData[0],
        slot: i,
        attributes: [{ ...baseData[0].attributes[i], offset: 0 }],
      });
    }

    // Run the test with the modified test data.
    const pipeline = t.makeTestPipeline(testData, kVertexCount, kInstanceCount);
    const expectedDataBG = t.createExpectedBG(testData, pipeline);
    t.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, kVertexCount, kInstanceCount);
  });

g.test('max_buffers_and_attribs')
  .desc(
    `Test a vertex state that loads as many attributes and buffers as possible.
  - For each format.
  `
  )
  .params(u => u.combine('format', kVertexFormats))
  .fn(t => {
    const { format } = t.params;
    // In compat mode, @builtin(vertex_index) and @builtin(instance_index) each take an attribute
    const maxVertexBuffers = t.device.limits.maxVertexBuffers;
    const deviceMaxVertexAttributes = t.device.limits.maxVertexAttributes;
    const maxVertexAttributes = deviceMaxVertexAttributes - (t.isCompatibility ? 2 : 0);
    const attributesPerBuffer = Math.ceil(maxVertexAttributes / maxVertexBuffers);
    let attributesEmitted = 0;

    const state: VertexLayoutState<{}, {}> = [];
    for (let i = 0; i < maxVertexBuffers; i++) {
      const attributes: GPUVertexAttribute[] = [];
      for (let j = 0; j < attributesPerBuffer && attributesEmitted < maxVertexAttributes; j++) {
        attributes.push({ format, offset: 0, shaderLocation: attributesEmitted });
        attributesEmitted++;
      }
      state.push({
        slot: i,
        stepMode: 'vertex',
        arrayStride: 32,
        attributes,
      });
    }
    t.runTest(state);
  });

g.test('array_stride_zero')
  .desc(
    `Test that arrayStride 0 correctly uses the same data for all vertex/instances, while another test vertex buffer with arrayStride != 0 gets different data.
  - Test for all formats
  - Test for both step modes`
  )
  .params(u =>
    u //
      .combine('format', kVertexFormats)
      .beginSubcases()
      .combine('stepMode', ['vertex', 'instance'] as const)
      .expand('offsetVariant', p => {
        const formatInfo = kVertexFormatInfo[p.format];
        const formatSize = formatInfo.byteSize;
        return filterUniqueValueTestVariants([
          { mult: 0, add: 0 },
          { mult: 0, add: 4 },
          { mult: 0, add: 8 },
          { mult: 0, add: formatSize },
          { mult: 0, add: formatSize * 2 },
          { mult: 0.5, add: 0 },
          { mult: 1, add: -formatSize - 4 },
          { mult: 1, add: -formatSize - 8 },
          { mult: 1, add: -formatSize },
          { mult: 1, add: -formatSize * 2 },
        ]);
      })
  )
  .fn(t => {
    const { format, stepMode, offsetVariant } = t.params;
    const formatInfo = kVertexFormatInfo[format];
    const formatSize = formatInfo.byteSize;
    const makeMultipleOf = Math.min(formatSize, 4);
    const offset =
      Math.floor(t.makeLimitVariant('maxVertexBufferArrayStride', offsetVariant) / makeMultipleOf) *
      makeMultipleOf;
    const kCount = 10;

    // Create the stride 0 part of the test, first by faking a single vertex being drawn and
    // then expanding the data to cover kCount vertex / instances
    const stride0TestData = t.createTestAndPipelineData(
      [
        {
          slot: 0,
          arrayStride: 2048,
          stepMode,
          vbOffset: offset, // used to push data in the vertex buffer
          attributes: [{ format, offset: 0, shaderLocation: 0 }],
        },
      ],
      1,
      1
    )[0];
    const stride0VertexBuffer = t.createVertexBuffers([stride0TestData], kCount, kCount)[0];

    // Expand the stride0 test data to have kCount values for expectedData.
    const originalData = stride0TestData.attributes[0].expectedData;
    const expandedData = new ArrayBuffer(kCount * originalData.byteLength);
    for (let i = 0; i < kCount; i++) {
      new Uint8Array(expandedData, originalData.byteLength * i).set(new Uint8Array(originalData));
    }

    // Fixup stride0TestData to use arrayStride 0.
    stride0TestData.attributes[0].offset = offset;
    stride0TestData.attributes[0].expectedData = expandedData;
    stride0TestData.attributes[0].testComponentCount *= kCount;
    stride0TestData.arrayStride = 0;
    stride0VertexBuffer.vbOffset = 0;

    // Create the part of the state that will be varying for each vertex / instance
    const varyingTestData = t.createTestAndPipelineData(
      [
        {
          slot: 1,
          arrayStride: 32,
          stepMode,
          attributes: [{ format, offset: 0, shaderLocation: 1 }],
        },
      ],
      kCount,
      kCount
    )[0];
    const varyingVertexBuffer = t.createVertexBuffers([varyingTestData], kCount, kCount)[0];

    // Run the test with the merged test state.
    const state = [stride0TestData, varyingTestData];
    const vertexBuffers = [stride0VertexBuffer, varyingVertexBuffer];

    const pipeline = t.makeTestPipeline(state, kCount, kCount);
    const expectedDataBG = t.createExpectedBG(state, pipeline);
    t.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, kCount, kCount);
  });

g.test('discontiguous_location_and_attribs')
  .desc('Test that using far away slots / shaderLocations works as expected')
  .fn(t => {
    t.runTest([
      {
        slot: t.device.limits.maxVertexBuffers - 1,
        arrayStride: 4,
        stepMode: 'vertex',
        attributes: [
          { format: 'uint8x2', offset: 2, shaderLocation: 0 },
          { format: 'uint8x2', offset: 0, shaderLocation: 8 },
        ],
      },
      {
        slot: 1,
        arrayStride: 16,
        stepMode: 'instance',
        vbOffset: 1000,
        attributes: [
          {
            format: 'uint32x4',
            offset: 0,
            shaderLocation: t.device.limits.maxVertexAttributes - 1,
          },
        ],
      },
    ]);
  });

g.test('overlapping_attributes')
  .desc(
    `Test that overlapping attributes in the same vertex buffer works
   - Test for all formats`
  )
  .params(u => u.combine('format', kVertexFormats))
  .fn(t => {
    const { format } = t.params;

    // In compat mode, @builtin(vertex_index) and @builtin(instance_index) each take an attribute
    const maxVertexAttributes = t.device.limits.maxVertexAttributes - (t.isCompatibility ? 2 : 0);
    const attributes: GPUVertexAttribute[] = [];
    for (let i = 0; i < maxVertexAttributes; i++) {
      attributes.push({ format, offset: 0, shaderLocation: i });
    }

    t.runTest([
      {
        slot: 0,
        stepMode: 'vertex',
        arrayStride: 32,
        attributes,
      },
    ]);
  });
