Draw vertex buffer access test for vertex step mode buffer (#1450)

In this PR the vertex step mode vertex buffer OOB validation in draw calls is tested. It test that encoder.draw will validate vertex step mode vertex buffer satisfing `bound size >= (strideCount - 1) * arrayStride + lastStride` iif strideCount > 0, and other draw call types do not validate vertex step mode vertex buffer OOB.

Issue: #906
diff --git a/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts b/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
index b89dafe..78a962e 100644
--- a/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
+++ b/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
@@ -5,9 +5,22 @@
 `;
 
 import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { kVertexFormatInfo } from '../../../../../capability_info.js';
 import { GPUTest } from '../../../../../gpu_test.js';
 import { ValidationTest } from '../../../validation_test.js';
 
+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
+>;
+
 interface DrawIndexedParameter {
   indexCount: number;
   instanceCount?: number;
@@ -47,6 +60,76 @@
     }
   }
 }
+interface DrawParameter {
+  vertexCount: number;
+  instanceCount?: number;
+  firstVertex?: number;
+  firstInstance?: number;
+}
+
+function callDraw(
+  test: GPUTest,
+  encoder: GPURenderCommandsMixin,
+  drawType: 'draw' | 'drawIndirect',
+  param: DrawParameter
+) {
+  switch (drawType) {
+    case 'draw': {
+      encoder.draw(
+        param.vertexCount,
+        param.instanceCount ?? 1,
+        param.firstVertex ?? 0,
+        param.firstInstance ?? 0
+      );
+      break;
+    }
+    case 'drawIndirect': {
+      const indirectArray = new Int32Array([
+        param.vertexCount,
+        param.instanceCount ?? 1,
+        param.firstVertex ?? 0,
+        param.firstInstance ?? 0,
+      ]);
+      const indirectBuffer = test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+      encoder.drawIndirect(indirectBuffer, 0);
+      break;
+    }
+  }
+}
+
+function makeTestPipeline(
+  test: ValidationTest,
+  buffers: VertexState<
+    { stepMode: GPUVertexStepMode; arrayStride: number },
+    {
+      offset: number;
+      format: GPUVertexFormat;
+    }
+  >
+): GPURenderPipeline {
+  const bufferLayouts: GPUVertexBufferLayout[] = [];
+  for (const b of buffers) {
+    bufferLayouts[b.slot] = b;
+  }
+
+  return test.device.createRenderPipeline({
+    vertex: {
+      module: test.device.createShaderModule({
+        code: test.getNoOpShaderCode('VERTEX'),
+      }),
+      entryPoint: 'main',
+      buffers: bufferLayouts,
+    },
+    fragment: {
+      module: test.device.createShaderModule({
+        code: test.getNoOpShaderCode('FRAGMENT'),
+      }),
+      entryPoint: 'main',
+      targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+    },
+    primitive: { topology: 'triangle-list' },
+  });
+}
 
 export const g = makeTestGroup(ValidationTest);
 
@@ -83,7 +166,7 @@
       .combine('indexFormat', ['uint16', 'uint32'] as const)
       .combine('useBundle', [false, true])
   )
-  .fn(t => {
+  .fn(async t => {
     const {
       indexFormat,
       bindingSizeInElements,
@@ -126,7 +209,7 @@
     commandBufferMaker.validateFinishAndSubmit(isFinishSuccess, true);
   });
 
-g.test(`vertex_buffer_OOB`)
+g.test(`vertex_buffer_OOB,vertex_step_mode`)
   .desc(
     `
 In this test we test the vertex buffer OOB validation in draw calls. Specifically, only vertex step
@@ -136,7 +219,8 @@
     - Draw call needs to read {=, >} any bound vertex buffer range, with GPUBuffer that is {large
       enough, exactly the size of bound range}
         - Binding size = 0 (ensure it's not treated as a special case)
-        - x= weird offset values
+        - x= weird buffer offset values
+        - x= weird attribute offset values
         - x= weird arrayStride values
         - x= {render pass, render bundle}
 - For vertex step mode vertex buffer,
@@ -144,25 +228,215 @@
         - vertexCount largeish
         - firstVertex {=, >} 0
         - arrayStride is 0 and bound buffer size too small
+        - (vertexCount + firstVertex) is zero
     - Validation error occurs in:
         - draw
         - drawIndexed with a zero array stride vertex step mode buffer OOB
     - Otherwise no validation error in drawIndexed, draIndirect and drawIndexedIndirect
+
+In this test, we use a a render pipeline requiring one vertex step mode with different vertex buffer
+layouts (attribute offset, array stride, vertex format). Then for a given drawing parameter set (e.g.,
+vertexCount, instanceCount, firstVertex, indexCount), we calculate the exactly required size for
+vertex step mode vertex buffer. Then, we generate buffer parameters (i.e. GPU buffer size,
+binding offset and binding size) for all buffers, covering both (bound size == required size),
+(bound size == required size - 1), and (bound size == 0), and test that draw and drawIndexed will
+success/error as expected. Such set of buffer parameters should include cases like weird offset values.
+`
+  )
+  .params(u =>
+    u //
+      .combine('drawType', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+      .combine('zeroVertexStrideCount', [false, true] as const)
+      .combine('zeroInstanceStrideCount', [false, true] as const)
+      .combine('arrayStrideState', ['zero', 'exact', 'oversize'] as const)
+      .combine('attributeOffsetFactor', [0, 1, 2, 7]) // the offset of attribute will be factor * MIN(4, sizeof(vertexFormat))
+      .combine('boundVertexBufferSizeState', ['zero', 'exile', 'enough'] as const)
+      .beginSubcases()
+      .combine('setBufferOffset', [0, 200]) // must be a multiple of 4
+      .combine('attributeFormat', ['snorm8x2', 'float32', 'float16x4'] as GPUVertexFormat[])
+      .combine('vertexCount', [0, 10, 10000])
+      .combine('firstVertex', [0, 10000])
+      .filter(p => p.zeroVertexStrideCount === (p.firstVertex + p.vertexCount === 0))
+      .combine('instanceCount', [0, 10, 10000])
+      .combine('firstInstance', [0, 10000])
+      .filter(p => p.zeroInstanceStrideCount === (p.firstInstance + p.instanceCount === 0))
+      .unless(p => p.vertexCount === 10000 && p.instanceCount === 10000)
+      .combine('setBufferBeforePipeline', [false, true])
+      .combine('useBundle', [false, true])
+  )
+  .fn(async t => {
+    const {
+      drawType,
+      zeroVertexStrideCount,
+      arrayStrideState,
+      attributeOffsetFactor,
+      boundVertexBufferSizeState,
+      setBufferOffset,
+      attributeFormat,
+      vertexCount,
+      instanceCount,
+      firstVertex,
+      firstInstance,
+      setBufferBeforePipeline,
+      useBundle,
+    } = t.params;
+
+    const attributeFormatInfo = kVertexFormatInfo[attributeFormat];
+    const formatSize = attributeFormatInfo.bytesPerComponent * attributeFormatInfo.componentCount;
+    const attributeOffset = attributeOffsetFactor * Math.min(4, formatSize);
+    const lastStride = attributeOffset + formatSize;
+    let arrayStride = 0;
+    if (arrayStrideState !== 'zero') {
+      arrayStride = lastStride;
+      if (arrayStrideState === 'oversize') {
+        arrayStride = arrayStride + 20;
+      }
+      arrayStride = arrayStride + (-arrayStride & 3); // Make sure arrayStride is a multiple of 4
+    }
+
+    const calcSetBufferSize = (
+      boundBufferSizeState: 'zero' | 'exile' | 'enough',
+      strideCount: number
+    ): number => {
+      let requiredBufferSize: number;
+      if (strideCount > 0) {
+        requiredBufferSize = arrayStride * (strideCount - 1) + lastStride;
+      } else {
+        // Spec do not validate bounded buffer size if strideCount == 0.
+        requiredBufferSize = lastStride;
+      }
+      let setBufferSize: number;
+      switch (boundBufferSizeState) {
+        case 'zero': {
+          setBufferSize = 0;
+          break;
+        }
+        case 'exile': {
+          setBufferSize = requiredBufferSize - 1;
+          break;
+        }
+        case 'enough': {
+          setBufferSize = requiredBufferSize;
+          break;
+        }
+      }
+      return setBufferSize;
+    };
+
+    const strideCountForVertexBuffer = firstVertex + vertexCount;
+    const setVertexBufferSize = calcSetBufferSize(
+      boundVertexBufferSizeState,
+      strideCountForVertexBuffer
+    );
+    const vertexBufferSize = setBufferOffset + setVertexBufferSize;
+
+    const vertexBuffer = t.device.createBuffer({
+      size: vertexBufferSize,
+      usage: GPUBufferUsage.VERTEX,
+    });
+
+    const vertexBufferLayouts: VertexLayoutState<{}, {}> = [
+      {
+        slot: 1,
+        stepMode: 'vertex',
+        arrayStride,
+        attributes: [
+          {
+            shaderLocation: 2,
+            format: attributeFormat,
+            offset: attributeOffset,
+          },
+        ],
+      },
+    ];
+
+    const renderPipeline = makeTestPipeline(t, vertexBufferLayouts);
+
+    const commandBufferMaker = t.createEncoder(useBundle ? 'render bundle' : 'render pass');
+    const renderEncoder = commandBufferMaker.encoder;
+
+    if (setBufferBeforePipeline) {
+      renderEncoder.setVertexBuffer(1, vertexBuffer, setBufferOffset, setVertexBufferSize);
+    }
+    renderEncoder.setPipeline(renderPipeline);
+    if (!setBufferBeforePipeline) {
+      renderEncoder.setVertexBuffer(1, vertexBuffer, setBufferOffset, setVertexBufferSize);
+    }
+
+    if (drawType === 'draw' || drawType === 'drawIndirect') {
+      const drawParam: DrawParameter = {
+        vertexCount,
+        instanceCount,
+        firstVertex,
+        firstInstance,
+      };
+
+      callDraw(t, renderEncoder, drawType, drawParam);
+    } else {
+      const indexFormat = 'uint16';
+      const indexElementSize = 2;
+      const indexCount = 12;
+      const indexBufferSize = indexElementSize * indexCount;
+
+      const desc: GPUBufferDescriptor = {
+        size: indexBufferSize,
+        usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
+      };
+      const indexBuffer = t.device.createBuffer(desc);
+
+      const drawParam: DrawIndexedParameter = {
+        indexCount,
+        instanceCount,
+        firstIndex: 0,
+        baseVertex: firstVertex,
+        firstInstance,
+      };
+
+      renderEncoder.setIndexBuffer(indexBuffer, indexFormat, 0, indexBufferSize);
+      callDrawIndexed(t, renderEncoder, drawType, drawParam);
+    }
+
+    const isVertexBufferOOB =
+      boundVertexBufferSizeState !== 'enough' &&
+      drawType === 'draw' && // drawIndirect, drawIndexed, and drawIndexedIndirect do not validate vertex step mode buffer
+      !zeroVertexStrideCount; // vertex step mode buffer never OOB if stride count = 0
+    const isFinishSuccess = !isVertexBufferOOB;
+
+    commandBufferMaker.validateFinishAndSubmit(isFinishSuccess, true);
+  });
+
+g.test(`vertex_buffer_OOB,instance_step_mode`)
+  .desc(
+    `
+TODO: merge this test into vertex_buffer_OOB,vertex_step_mode.
+
+In this test we test the vertex buffer OOB validation in draw calls. Specifically, only vertex step
+mode buffer OOB in draw and instance step mode buffer OOB in draw and drawIndexed are CPU-validated.
+Other cases are handled by robust access and no validation error occurs.
+- Test that:
+    - Draw call needs to read {=, >} any bound vertex buffer range, with GPUBuffer that is {large
+      enough, exactly the size of bound range}
+        - Binding size = 0 (ensure it's not treated as a special case)
+        - x= weird buffer offset values
+        - x= weird attribute offset values
+        - x= weird arrayStride values
+        - x= {render pass, render bundle}
 - For instance step mode vertex buffer,
     - Test with draw and drawIndexed:
         - instanceCount largeish
         - firstInstance {=, >} 0
         - arrayStride is 0 and bound buffer size too small
+        - (instanceCount + firstInstance) is zero
     - Validation error occurs in draw and drawIndexed
     - No validation error in drawIndirect and drawIndexedIndirect
 
-In this test, we use a a render pipeline requiring one vertex step mode and one instance step mode
-vertex buffer. Then for a given drawing parameter set (e.g., vertexCount, instanceCount, firstVertex,
-indexCount), we calculate the exactly required size for vertex step mode vertex buffer, instance
-step mode vertex buffer and index buffer. Then, we generate buffer parameters (i.e. GPU buffer size,
-binding offset and binding size) for all three buffer, covering both (bound size == required size)
-and (bound size == required size - 1), and test that draw and drawIndexed will success/error as
-expected. Such set of buffer parameters should include cases like weird offset values.
+In this test, we use a a render pipeline requiring one instance step mode with different vertex buffer
+layouts (attribute offset, array stride, vertex format). Then for a given drawing parameter set (e.g.,
+vertexCount, instanceCount, firstVertex, indexCount), we calculate the exactly required size for
+vertex step mode vertex buffer. Then, we generate buffer parameters (i.e. GPU buffer size,
+binding offset and binding size) for all buffers, covering both (bound size == required size),
+(bound size == required size - 1), and (bound size == 0), and test that draw and drawIndexed will
+success/error as expected. Such set of buffer parameters should include cases like weird offset values.
 `
   )
   .unimplemented();