summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts862
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts319
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts202
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts29
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts124
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts62
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts141
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts184
8 files changed, 1923 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
new file mode 100644
index 0000000000..913ea86f33
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
@@ -0,0 +1,862 @@
+export const description = `
+Here we test the validation for draw functions, mainly the buffer access validation. All four types
+of draw calls are tested, and test that validation errors do / don't occur for certain call type
+and parameters as expect.
+`;
+
+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;
+ firstIndex?: number;
+ baseVertex?: number;
+ firstInstance?: number;
+}
+
+function callDrawIndexed(
+ test: GPUTest,
+ encoder: GPURenderCommandsMixin,
+ drawType: 'drawIndexed' | 'drawIndexedIndirect',
+ param: DrawIndexedParameter
+) {
+ switch (drawType) {
+ case 'drawIndexed': {
+ encoder.drawIndexed(
+ param.indexCount,
+ param.instanceCount ?? 1,
+ param.firstIndex ?? 0,
+ param.baseVertex ?? 0,
+ param.firstInstance ?? 0
+ );
+ break;
+ }
+ case 'drawIndexedIndirect': {
+ const indirectArray = new Int32Array([
+ param.indexCount,
+ param.instanceCount ?? 1,
+ param.firstIndex ?? 0,
+ param.baseVertex ?? 0,
+ param.firstInstance ?? 0,
+ ]);
+ const indirectBuffer = test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ break;
+ }
+ }
+}
+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({
+ layout: 'auto',
+ 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' },
+ });
+}
+
+function makeTestPipelineWithVertexAndInstanceBuffer(
+ test: ValidationTest,
+ arrayStride: number,
+ attributeFormat: GPUVertexFormat,
+ attributeOffset: number = 0
+): GPURenderPipeline {
+ const vertexBufferLayouts: VertexLayoutState<{}, {}> = [
+ {
+ slot: 1,
+ stepMode: 'vertex',
+ arrayStride,
+ attributes: [
+ {
+ shaderLocation: 2,
+ format: attributeFormat,
+ offset: attributeOffset,
+ },
+ ],
+ },
+ {
+ slot: 7,
+ stepMode: 'instance',
+ arrayStride,
+ attributes: [
+ {
+ shaderLocation: 6,
+ format: attributeFormat,
+ offset: attributeOffset,
+ },
+ ],
+ },
+ ];
+
+ return makeTestPipeline(test, vertexBufferLayouts);
+}
+
+// Default parameters for all kind of draw call, arbitrary non-zero values that is not very large.
+const kDefaultParameterForDraw = {
+ instanceCount: 100,
+ firstInstance: 100,
+};
+
+// Default parameters for non-indexed draw, arbitrary non-zero values that is not very large.
+const kDefaultParameterForNonIndexedDraw = {
+ vertexCount: 100,
+ firstVertex: 100,
+};
+
+// Default parameters for indexed draw call and required index buffer, arbitrary non-zero values
+// that is not very large.
+const kDefaultParameterForIndexedDraw = {
+ indexCount: 100,
+ firstIndex: 100,
+ baseVertex: 100,
+ indexFormat: 'uint16' as GPUIndexFormat,
+ indexBufferSize: 2 * 200, // exact required bound size for index buffer
+};
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test(`unused_buffer_bound`)
+ .desc(
+ `
+In this test we test that a small buffer bound to unused buffer slot won't cause validation error.
+- All draw commands,
+ - An unused {index , vertex} buffer with uselessly small range is bound (immediately before draw
+ call)
+`
+ )
+ .params(u =>
+ u //
+ .combine('smallIndexBuffer', [false, true])
+ .combine('smallVertexBuffer', [false, true])
+ .combine('smallInstanceBuffer', [false, true])
+ .beginSubcases()
+ .combine('drawType', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+ .unless(
+ // Always provide index buffer of enough size if it is used by indexed draw
+ p =>
+ p.smallIndexBuffer &&
+ (p.drawType === 'drawIndexed' || p.drawType === 'drawIndexedIndirect')
+ )
+ .combine('bufferOffset', [0, 4])
+ .combine('boundSize', [0, 1])
+ )
+ .fn(async t => {
+ const {
+ smallIndexBuffer,
+ smallVertexBuffer,
+ smallInstanceBuffer,
+ drawType,
+ bufferOffset,
+ boundSize,
+ } = t.params;
+ const renderPipeline = t.createNoOpRenderPipeline();
+ const bufferSize = bufferOffset + boundSize;
+ const smallBuffer = t.createBufferWithState('valid', {
+ size: bufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.VERTEX,
+ });
+
+ // An index buffer of enough size, used if smallIndexBuffer === false
+ const { indexFormat, indexBufferSize } = kDefaultParameterForIndexedDraw;
+ const indexBuffer = t.createBufferWithState('valid', {
+ size: indexBufferSize,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'drawIndexed' || drawType === 'drawIndexedIndirect') {
+ // Always use large enough index buffer for indexed draw. Index buffer OOB validation is
+ // tested in index_buffer_OOB.
+ renderEncoder.setIndexBuffer(indexBuffer, indexFormat, 0, indexBufferSize);
+ } else if (smallIndexBuffer) {
+ renderEncoder.setIndexBuffer(smallBuffer, indexFormat, bufferOffset, boundSize);
+ }
+ if (smallVertexBuffer) {
+ renderEncoder.setVertexBuffer(1, smallBuffer, bufferOffset, boundSize);
+ }
+ if (smallInstanceBuffer) {
+ renderEncoder.setVertexBuffer(7, smallBuffer, bufferOffset, boundSize);
+ }
+
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'draw' || drawType === 'drawIndirect') {
+ const drawParam: DrawParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForNonIndexedDraw,
+ };
+ callDraw(t, renderEncoder, drawType, drawParam);
+ } else {
+ const drawParam: DrawIndexedParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForIndexedDraw,
+ };
+ callDrawIndexed(t, renderEncoder, drawType, drawParam);
+ }
+
+ // Binding a unused small index/vertex buffer will never cause validation error.
+ commandBufferMaker.validateFinishAndSubmit(true, true);
+ }
+ }
+ });
+
+g.test(`index_buffer_OOB`)
+ .desc(
+ `
+In this test we test that index buffer OOB is caught as a validation error in drawIndexed, but not in
+drawIndexedIndirect as it is GPU-validated.
+- Issue an indexed draw call, with the following index buffer states, for {all index formats}:
+ - range and GPUBuffer are exactly the required size for the draw call
+ - range is too small but GPUBuffer is still large enough
+ - range and GPUBuffer are both too small
+`
+ )
+ .params(u =>
+ u
+ .combine('bufferSizeInElements', [10, 100])
+ // Binding size is always no larger than buffer size, make sure that setIndexBuffer succeed
+ .combine('bindingSizeInElements', [10])
+ .combine('drawIndexCount', [10, 11])
+ .combine('drawType', ['drawIndexed', 'drawIndexedIndirect'] as const)
+ .beginSubcases()
+ .combine('indexFormat', ['uint16', 'uint32'] as GPUIndexFormat[])
+ )
+ .fn(async t => {
+ const {
+ indexFormat,
+ bindingSizeInElements,
+ bufferSizeInElements,
+ drawIndexCount,
+ drawType,
+ } = t.params;
+
+ const indexElementSize = indexFormat === 'uint16' ? 2 : 4;
+ const bindingSize = bindingSizeInElements * indexElementSize;
+ const bufferSize = bufferSizeInElements * indexElementSize;
+
+ const desc: GPUBufferDescriptor = {
+ size: bufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
+ };
+ const indexBuffer = t.createBufferWithState('valid', desc);
+
+ const drawCallParam: DrawIndexedParameter = {
+ indexCount: drawIndexCount,
+ };
+
+ // Encoder finish will succeed if no index buffer access OOB when calling drawIndexed,
+ // and always succeed when calling drawIndexedIndirect.
+ const isFinishSuccess =
+ drawIndexCount <= bindingSizeInElements || drawType === 'drawIndexedIndirect';
+
+ const renderPipeline = t.createNoOpRenderPipeline();
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+ renderEncoder.setIndexBuffer(indexBuffer, indexFormat, 0, bindingSize);
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ callDrawIndexed(t, renderEncoder, drawType, drawCallParam);
+
+ commandBufferMaker.validateFinishAndSubmit(isFinishSuccess, true);
+ }
+ }
+ });
+
+g.test(`vertex_buffer_OOB`)
+ .desc(
+ `
+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 vertex step mode vertex buffer,
+ - Test that:
+ - 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
+- 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 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
+ // type of draw call
+ .combine('type', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+ // the state of vertex step mode vertex buffer bound size
+ .combine('VBSize', ['zero', 'exile', 'enough'] as const)
+ // the state of instance step mode vertex buffer bound size
+ .combine('IBSize', ['zero', 'exile', 'enough'] as const)
+ // should the vertex stride count be zero
+ .combine('VStride0', [false, true] as const)
+ // should the instance stride count be zero
+ .combine('IStride0', [false, true] as const)
+ // the state of array stride
+ .combine('AStride', ['zero', 'exact', 'oversize'] as const)
+ // the factor for offset of attributes in vertex layout
+ .combine('offset', [0, 1, 2, 7]) // the offset of attribute will be factor * MIN(4, sizeof(vertexFormat))
+ .beginSubcases()
+ .combine('setBufferOffset', [0, 200]) // must be a multiple of 4
+ .combine('attributeFormat', ['snorm8x2', 'float32', 'float16x4'] as GPUVertexFormat[])
+ .combine('vertexCount', [0, 1, 10000])
+ .combine('firstVertex', [0, 10000])
+ .filter(p => p.VStride0 === (p.firstVertex + p.vertexCount === 0))
+ .combine('instanceCount', [0, 1, 10000])
+ .combine('firstInstance', [0, 10000])
+ .filter(p => p.IStride0 === (p.firstInstance + p.instanceCount === 0))
+ .unless(p => p.vertexCount === 10000 && p.instanceCount === 10000)
+ )
+ .fn(async t => {
+ const {
+ type: drawType,
+ VBSize: boundVertexBufferSizeState,
+ IBSize: boundInstanceBufferSizeState,
+ VStride0: zeroVertexStrideCount,
+ IStride0: zeroInstanceStrideCount,
+ AStride: arrayStrideState,
+ offset: attributeOffsetFactor,
+ setBufferOffset,
+ attributeFormat,
+ vertexCount,
+ instanceCount,
+ firstVertex,
+ firstInstance,
+ } = 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') {
+ // Add an arbitrary number to array stride to make it larger than required by attributes
+ 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 strideCountForInstanceBuffer = firstInstance + instanceCount;
+ const setInstanceBufferSize = calcSetBufferSize(
+ boundInstanceBufferSizeState,
+ strideCountForInstanceBuffer
+ );
+ const instanceBufferSize = setBufferOffset + setInstanceBufferSize;
+
+ const vertexBuffer = t.createBufferWithState('valid', {
+ size: vertexBufferSize,
+ usage: GPUBufferUsage.VERTEX,
+ });
+ const instanceBuffer = t.createBufferWithState('valid', {
+ size: instanceBufferSize,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const renderPipeline = makeTestPipelineWithVertexAndInstanceBuffer(
+ t,
+ arrayStride,
+ attributeFormat,
+ attributeOffset
+ );
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+ renderEncoder.setVertexBuffer(1, vertexBuffer, setBufferOffset, setVertexBufferSize);
+ renderEncoder.setVertexBuffer(7, instanceBuffer, setBufferOffset, setInstanceBufferSize);
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'draw' || drawType === 'drawIndirect') {
+ const drawParam: DrawParameter = {
+ vertexCount,
+ instanceCount,
+ firstVertex,
+ firstInstance,
+ };
+
+ callDraw(t, renderEncoder, drawType, drawParam);
+ } else {
+ const {
+ indexFormat,
+ indexCount,
+ firstIndex,
+ indexBufferSize,
+ } = kDefaultParameterForIndexedDraw;
+
+ const desc: GPUBufferDescriptor = {
+ size: indexBufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
+ };
+ const indexBuffer = t.createBufferWithState('valid', desc);
+
+ const drawParam: DrawIndexedParameter = {
+ indexCount,
+ instanceCount,
+ firstIndex,
+ 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 isInstanceBufferOOB =
+ boundInstanceBufferSizeState !== 'enough' &&
+ (drawType === 'draw' || drawType === 'drawIndexed') && // drawIndirect and drawIndexedIndirect do not validate instance step mode buffer
+ !zeroInstanceStrideCount; // vertex step mode buffer never OOB if stride count = 0
+ const isFinishSuccess = !isVertexBufferOOB && !isInstanceBufferOOB;
+
+ commandBufferMaker.validateFinishAndSubmit(isFinishSuccess, true);
+ }
+ }
+ });
+
+g.test(`buffer_binding_overlap`)
+ .desc(
+ `
+In this test we test that binding one GPU buffer to multiple vertex buffer slot or both vertex
+buffer slot and index buffer will cause no validation error, with completely/partial overlap.
+ - x= all draw types
+`
+ )
+ .params(u =>
+ u //
+ .combine('drawType', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+ .beginSubcases()
+ .combine('vertexBoundOffestFactor', [0, 0.5, 1, 1.5, 2])
+ .combine('instanceBoundOffestFactor', [0, 0.5, 1, 1.5, 2])
+ .combine('indexBoundOffestFactor', [0, 0.5, 1, 1.5, 2])
+ .combine('arrayStrideState', ['zero', 'exact', 'oversize'] as const)
+ )
+ .fn(async t => {
+ const {
+ drawType,
+ vertexBoundOffestFactor,
+ instanceBoundOffestFactor,
+ indexBoundOffestFactor,
+ arrayStrideState,
+ } = t.params;
+
+ // Compute the array stride for vertex step mode and instance step mode attribute
+ const attributeFormat = 'float32x4';
+ const attributeFormatInfo = kVertexFormatInfo[attributeFormat];
+ const formatSize = attributeFormatInfo.bytesPerComponent * attributeFormatInfo.componentCount;
+ const attributeOffset = 0;
+ const lastStride = attributeOffset + formatSize;
+ let arrayStride = 0;
+ if (arrayStrideState !== 'zero') {
+ arrayStride = lastStride;
+ if (arrayStrideState === 'oversize') {
+ // Add an arbitrary number to array stride
+ arrayStride = arrayStride + 20;
+ }
+ arrayStride = arrayStride + (-arrayStride & 3); // Make sure arrayStride is a multiple of 4
+ }
+
+ const calcAttributeBufferSize = (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;
+ }
+ return requiredBufferSize;
+ };
+
+ const calcSetBufferOffset = (requiredSetBufferSize: number, offsetFactor: number): number => {
+ const offset = Math.ceil(requiredSetBufferSize * offsetFactor);
+ const alignedOffset = offset + (-offset & 3); // Make sure offset is a multiple of 4
+ return alignedOffset;
+ };
+
+ // Compute required bound range for all vertex and index buffer to ensure the shared GPU buffer
+ // has enough size.
+ const { vertexCount, firstVertex } = kDefaultParameterForNonIndexedDraw;
+ const strideCountForVertexBuffer = firstVertex + vertexCount;
+ const setVertexBufferSize = calcAttributeBufferSize(strideCountForVertexBuffer);
+ const setVertexBufferOffset = calcSetBufferOffset(setVertexBufferSize, vertexBoundOffestFactor);
+ let requiredBufferSize = setVertexBufferOffset + setVertexBufferSize;
+
+ const { instanceCount, firstInstance } = kDefaultParameterForDraw;
+ const strideCountForInstanceBuffer = firstInstance + instanceCount;
+ const setInstanceBufferSize = calcAttributeBufferSize(strideCountForInstanceBuffer);
+ const setInstanceBufferOffset = calcSetBufferOffset(
+ setInstanceBufferSize,
+ instanceBoundOffestFactor
+ );
+ requiredBufferSize = Math.max(
+ requiredBufferSize,
+ setInstanceBufferOffset + setInstanceBufferSize
+ );
+
+ const { indexBufferSize: setIndexBufferSize, indexFormat } = kDefaultParameterForIndexedDraw;
+ const setIndexBufferOffset = calcSetBufferOffset(setIndexBufferSize, indexBoundOffestFactor);
+ requiredBufferSize = Math.max(requiredBufferSize, setIndexBufferOffset + setIndexBufferSize);
+
+ // Create the shared GPU buffer with both vertetx and index usage
+ const sharedBuffer = t.createBufferWithState('valid', {
+ size: requiredBufferSize,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.INDEX,
+ });
+
+ const renderPipeline = makeTestPipelineWithVertexAndInstanceBuffer(
+ t,
+ arrayStride,
+ attributeFormat
+ );
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+ renderEncoder.setVertexBuffer(1, sharedBuffer, setVertexBufferOffset, setVertexBufferSize);
+ renderEncoder.setVertexBuffer(
+ 7,
+ sharedBuffer,
+ setInstanceBufferOffset,
+ setInstanceBufferSize
+ );
+ renderEncoder.setIndexBuffer(
+ sharedBuffer,
+ indexFormat,
+ setIndexBufferOffset,
+ setIndexBufferSize
+ );
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'draw' || drawType === 'drawIndirect') {
+ const drawParam: DrawParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForNonIndexedDraw,
+ };
+ callDraw(t, renderEncoder, drawType, drawParam);
+ } else {
+ const drawParam: DrawIndexedParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForIndexedDraw,
+ };
+ callDrawIndexed(t, renderEncoder, drawType, drawParam);
+ }
+
+ // Since all bound buffer are of enough size, draw call should always succeed.
+ commandBufferMaker.validateFinishAndSubmit(true, true);
+ }
+ }
+ });
+
+g.test(`last_buffer_setting_take_account`)
+ .desc(
+ `
+In this test we test that only the last setting for a buffer slot take account.
+- All (non/indexed, in/direct) draw commands
+ - setPl, setVB, setIB, draw, {setPl,setVB,setIB,nothing (control)}, then a larger draw that
+ wouldn't have been valid before that
+`
+ )
+ .unimplemented();
+
+g.test(`max_draw_count`)
+ .desc(
+ `
+In this test we test that draw count which exceeds
+GPURenderPassDescriptor.maxDrawCount causes validation error on
+GPUCommandEncoder.finish(). The test sets specified maxDrawCount,
+calls specified draw call specified times with or without bundles,
+and checks whether GPUCommandEncoder.finish() causes a validation error.
+ - x= whether to use a bundle for the first half of the draw calls
+ - x= whether to use a bundle for the second half of the draw calls
+ - x= several different draw counts
+ - x= several different maxDrawCounts
+`
+ )
+ .params(u =>
+ u
+ .combine('bundleFirstHalf', [false, true])
+ .combine('bundleSecondHalf', [false, true])
+ .combine('maxDrawCount', [0, 1, 4, 16])
+ .beginSubcases()
+ .expand('drawCount', p => new Set([0, p.maxDrawCount, p.maxDrawCount + 1]))
+ )
+ .fn(async t => {
+ const { bundleFirstHalf, bundleSecondHalf, maxDrawCount, drawCount } = t.params;
+
+ const colorFormat = 'rgba8unorm';
+ const colorTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: colorFormat,
+ mipLevelCount: 1,
+ sampleCount: 1,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() {}`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: colorFormat, writeMask: 0 }],
+ },
+ });
+
+ const indexBuffer = t.makeBufferWithContents(new Uint16Array([0, 0, 0]), GPUBufferUsage.INDEX);
+ const indirectBuffer = t.makeBufferWithContents(
+ new Uint32Array([3, 1, 0, 0]),
+ GPUBufferUsage.INDIRECT
+ );
+ const indexedIndirectBuffer = t.makeBufferWithContents(
+ new Uint32Array([3, 1, 0, 0, 0]),
+ GPUBufferUsage.INDIRECT
+ );
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPassEncoder = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ maxDrawCount,
+ });
+
+ const firstHalfEncoder = bundleFirstHalf
+ ? t.device.createRenderBundleEncoder({
+ colorFormats: [colorFormat],
+ })
+ : renderPassEncoder;
+
+ const secondHalfEncoder = bundleSecondHalf
+ ? t.device.createRenderBundleEncoder({
+ colorFormats: [colorFormat],
+ })
+ : renderPassEncoder;
+
+ firstHalfEncoder.setPipeline(pipeline);
+ firstHalfEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ secondHalfEncoder.setPipeline(pipeline);
+ secondHalfEncoder.setIndexBuffer(indexBuffer, 'uint16');
+
+ const halfDrawCount = Math.floor(drawCount / 2);
+ for (let i = 0; i < drawCount; i++) {
+ const encoder = i < halfDrawCount ? firstHalfEncoder : secondHalfEncoder;
+ if (i % 4 === 0) {
+ encoder.draw(3);
+ }
+ if (i % 4 === 1) {
+ encoder.drawIndexed(3);
+ }
+ if (i % 4 === 2) {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+ if (i % 4 === 3) {
+ encoder.drawIndexedIndirect(indexedIndirectBuffer, 0);
+ }
+ }
+
+ const bundles = [];
+ if (bundleFirstHalf) {
+ bundles.push((firstHalfEncoder as GPURenderBundleEncoder).finish());
+ }
+ if (bundleSecondHalf) {
+ bundles.push((secondHalfEncoder as GPURenderBundleEncoder).finish());
+ }
+
+ if (bundles.length > 0) {
+ renderPassEncoder.executeBundles(bundles);
+ }
+
+ renderPassEncoder.end();
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ }, drawCount > maxDrawCount);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts
new file mode 100644
index 0000000000..d7bdec6ba5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts
@@ -0,0 +1,319 @@
+export const description = `
+API validation tests for dynamic state commands (setViewport/ScissorRect/BlendColor...).
+
+TODO: ensure existing tests cover these notes. Note many of these may be operation tests instead.
+> - setViewport
+> - {x, y} = {0, invalid values if any}
+> - {width, height, minDepth, maxDepth} = {
+> - least possible value that's valid
+> - greatest possible negative value that's invalid
+> - greatest possible positive value that's valid
+> - least possible positive value that's invalid if any
+> - }
+> - minDepth {<, =, >} maxDepth
+> - setScissorRect
+> - {width, height} = 0
+> - {x+width, y+height} = attachment size + 1
+> - setBlendConstant
+> - color {slightly, very} out of range
+> - used with a simple pipeline that {does, doesn't} use it
+> - setStencilReference
+> - {0, max}
+> - used with a simple pipeline that {does, doesn't} use it
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+interface ViewportCall {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ minDepth: number;
+ maxDepth: number;
+}
+
+interface ScissorCall {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+}
+
+class F extends ValidationTest {
+ testViewportCall(
+ success: boolean,
+ v: ViewportCall,
+ attachmentSize: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }
+ ) {
+ const attachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: attachmentSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachment.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setViewport(v.x, v.y, v.w, v.h, v.minDepth, v.maxDepth);
+ pass.end();
+
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+
+ testScissorCall(
+ success: boolean | 'type-error',
+ s: ScissorCall,
+ attachmentSize: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }
+ ) {
+ const attachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: attachmentSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachment.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ if (success === 'type-error') {
+ this.shouldThrow('TypeError', () => {
+ pass.setScissorRect(s.x, s.y, s.w, s.h);
+ });
+ } else {
+ pass.setScissorRect(s.x, s.y, s.w, s.h);
+ pass.end();
+
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+ }
+
+ createDummyRenderPassEncoder(): { encoder: GPUCommandEncoder; pass: GPURenderPassEncoder } {
+ const attachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachment.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ return { encoder, pass };
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('setViewport,x_y_width_height_nonnegative')
+ .desc(
+ `Test that the parameters of setViewport to define the box must be non-negative.
+
+TODO Test -0 (it should be valid) but can't be tested because the harness complains about duplicate parameters.
+TODO Test the first value smaller than -0`
+ )
+ .paramsSubcasesOnly([
+ // Control case: everything to 0 is ok, covers the empty viewport case.
+ { x: 0, y: 0, w: 0, h: 0 },
+
+ // Test -1
+ { x: -1, y: 0, w: 0, h: 0 },
+ { x: 0, y: -1, w: 0, h: 0 },
+ { x: 0, y: 0, w: -1, h: 0 },
+ { x: 0, y: 0, w: 0, h: -1 },
+ ])
+ .fn(t => {
+ const { x, y, w, h } = t.params;
+ const success = x >= 0 && y >= 0 && w >= 0 && h >= 0;
+ t.testViewportCall(success, { x, y, w, h, minDepth: 0, maxDepth: 1 });
+ });
+
+g.test('setViewport,xy_rect_contained_in_attachment')
+ .desc(
+ 'Test that the rectangle defined by x, y, width, height must be contained in the attachments'
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combineWithParams([
+ { attachmentWidth: 3, attachmentHeight: 5 },
+ { attachmentWidth: 5, attachmentHeight: 3 },
+ { attachmentWidth: 1024, attachmentHeight: 1 },
+ { attachmentWidth: 1, attachmentHeight: 1024 },
+ ])
+ .combineWithParams([
+ // Control case: a full viewport is valid.
+ { dx: 0, dy: 0, dw: 0, dh: 0 },
+
+ // Other valid cases with a partial viewport.
+ { dx: 1, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: -1 },
+ { dx: 0, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: -1 },
+
+ // Test with a small value that causes the viewport to go outside the attachment.
+ { dx: 1, dy: 0, dw: 0, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: 0 },
+ { dx: 0, dy: 0, dw: 1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: 1 },
+ ])
+ )
+ .fn(t => {
+ const { attachmentWidth, attachmentHeight, dx, dy, dw, dh } = t.params;
+ const x = dx;
+ const y = dy;
+ const w = attachmentWidth + dw;
+ const h = attachmentWidth + dh;
+
+ const success = x + w <= attachmentWidth && y + h <= attachmentHeight;
+ t.testViewportCall(
+ success,
+ { x, y, w, h, minDepth: 0, maxDepth: 1 },
+ { width: attachmentWidth, height: attachmentHeight, depthOrArrayLayers: 1 }
+ );
+ });
+
+g.test('setViewport,depth_rangeAndOrder')
+ .desc('Test that 0 <= minDepth <= maxDepth <= 1')
+ .paramsSubcasesOnly([
+ // Success cases
+ { minDepth: 0, maxDepth: 1 },
+ { minDepth: -0, maxDepth: -0 },
+ { minDepth: 1, maxDepth: 1 },
+ { minDepth: 0.3, maxDepth: 0.7 },
+ { minDepth: 0.7, maxDepth: 0.7 },
+ { minDepth: 0.3, maxDepth: 0.3 },
+
+ // Invalid cases
+ { minDepth: -0.1, maxDepth: 1 },
+ { minDepth: 0, maxDepth: 1.1 },
+ { minDepth: 0.5, maxDepth: 0.49999 },
+ ])
+ .fn(t => {
+ const { minDepth, maxDepth } = t.params;
+ const success =
+ 0 <= minDepth && minDepth <= 1 && 0 <= maxDepth && maxDepth <= 1 && minDepth <= maxDepth;
+ t.testViewportCall(success, { x: 0, y: 0, w: 1, h: 1, minDepth, maxDepth });
+ });
+
+g.test('setScissorRect,x_y_width_height_nonnegative')
+ .desc(
+ `Test that the parameters of setScissorRect to define the box must be non-negative or a TypeError is thrown.
+
+TODO Test -0 (it should be valid) but can't be tested because the harness complains about duplicate parameters.
+TODO Test the first value smaller than -0`
+ )
+ .paramsSubcasesOnly([
+ // Control case: everything to 0 is ok, covers the empty scissor case.
+ { x: 0, y: 0, w: 0, h: 0 },
+
+ // Test -1
+ { x: -1, y: 0, w: 0, h: 0 },
+ { x: 0, y: -1, w: 0, h: 0 },
+ { x: 0, y: 0, w: -1, h: 0 },
+ { x: 0, y: 0, w: 0, h: -1 },
+ ])
+ .fn(t => {
+ const { x, y, w, h } = t.params;
+ const success = x >= 0 && y >= 0 && w >= 0 && h >= 0;
+ t.testScissorCall(success ? true : 'type-error', { x, y, w, h });
+ });
+
+g.test('setScissorRect,xy_rect_contained_in_attachment')
+ .desc(
+ 'Test that the rectangle defined by x, y, width, height must be contained in the attachments'
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combineWithParams([
+ { attachmentWidth: 3, attachmentHeight: 5 },
+ { attachmentWidth: 5, attachmentHeight: 3 },
+ { attachmentWidth: 1024, attachmentHeight: 1 },
+ { attachmentWidth: 1, attachmentHeight: 1024 },
+ ])
+ .combineWithParams([
+ // Control case: a full scissor is valid.
+ { dx: 0, dy: 0, dw: 0, dh: 0 },
+
+ // Other valid cases with a partial scissor.
+ { dx: 1, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: -1 },
+ { dx: 0, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: -1 },
+
+ // Test with a small value that causes the scissor to go outside the attachment.
+ { dx: 1, dy: 0, dw: 0, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: 0 },
+ { dx: 0, dy: 0, dw: 1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: 1 },
+ ])
+ )
+ .fn(t => {
+ const { attachmentWidth, attachmentHeight, dx, dy, dw, dh } = t.params;
+ const x = dx;
+ const y = dy;
+ const w = attachmentWidth + dw;
+ const h = attachmentWidth + dh;
+
+ const success = x + w <= attachmentWidth && y + h <= attachmentHeight;
+ t.testScissorCall(
+ success,
+ { x, y, w, h },
+ { width: attachmentWidth, height: attachmentHeight, depthOrArrayLayers: 1 }
+ );
+ });
+
+g.test('setBlendConstant')
+ .desc('Test that almost any color value is valid for setBlendConstant')
+ .paramsSubcasesOnly([
+ { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
+ { r: -1.0, g: -1.0, b: -1.0, a: -1.0 },
+ { r: Number.MAX_SAFE_INTEGER, g: Number.MIN_SAFE_INTEGER, b: -0, a: 100000 },
+ ])
+ .fn(t => {
+ const { r, g, b, a } = t.params;
+ const encoders = t.createDummyRenderPassEncoder();
+ encoders.pass.setBlendConstant({ r, g, b, a });
+ encoders.pass.end();
+ encoders.encoder.finish();
+ });
+
+g.test('setStencilReference')
+ .desc('Test that almost any stencil reference value is valid for setStencilReference')
+ .paramsSubcasesOnly([
+ { value: 1 }, //
+ { value: 0 },
+ { value: 1000 },
+ { value: 0xffffffff },
+ ])
+ .fn(t => {
+ const { value } = t.params;
+ const encoders = t.createDummyRenderPassEncoder();
+ encoders.pass.setStencilReference(value);
+ encoders.pass.end();
+ encoders.encoder.finish();
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts
new file mode 100644
index 0000000000..017c1aa24f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts
@@ -0,0 +1,202 @@
+export const description = `
+Validation tests for drawIndirect/drawIndexedIndirect on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../../../constants.js';
+import { kResourceStates } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams } from './render.js';
+
+const kIndirectDrawTestParams = kRenderEncodeTypeParams.combine('indexed', [true, false] as const);
+
+class F extends ValidationTest {
+ makeIndexBuffer(): GPUBuffer {
+ return this.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('indirect_buffer_state')
+ .desc(
+ `
+Tests indirect buffer must be valid.
+ `
+ )
+ .paramsSubcasesOnly(kIndirectDrawTestParams.combine('state', kResourceStates))
+ .fn(t => {
+ const { encoderType, indexed, state } = t.params;
+ const pipeline = t.createNoOpRenderPipeline();
+ const indirectBuffer = t.createBufferWithState(state, {
+ size: 256,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('indirect_buffer,device_mismatch')
+ .desc(
+ 'Tests draw(Indexed)Indirect cannot be called with an indirect buffer created from another device'
+ )
+ .paramsSubcasesOnly(kIndirectDrawTestParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, indexed, mismatched } = t.params;
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const indirectBuffer = sourceDevice.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+ t.trackForCleanup(indirectBuffer);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(t.createNoOpRenderPipeline());
+
+ if (indexed) {
+ encoder.setIndexBuffer(t.makeIndexBuffer(), 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+ validateFinish(!mismatched);
+ });
+
+g.test('indirect_buffer_usage')
+ .desc(
+ `
+Tests indirect buffer must have 'Indirect' usage.
+ `
+ )
+ .paramsSubcasesOnly(
+ kIndirectDrawTestParams.combine('usage', [
+ GPUConst.BufferUsage.INDIRECT, // control case
+ GPUConst.BufferUsage.COPY_DST,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.INDIRECT,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, indexed, usage } = t.params;
+ const indirectBuffer = t.device.createBuffer({
+ size: 256,
+ usage,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(t.createNoOpRenderPipeline());
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+ validateFinish((usage & GPUBufferUsage.INDIRECT) !== 0);
+ });
+
+g.test('indirect_offset_alignment')
+ .desc(
+ `
+Tests indirect offset must be a multiple of 4.
+ `
+ )
+ .paramsSubcasesOnly(kIndirectDrawTestParams.combine('indirectOffset', [0, 2, 4] as const))
+ .fn(t => {
+ const { encoderType, indexed, indirectOffset } = t.params;
+ const pipeline = t.createNoOpRenderPipeline();
+ const indirectBuffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, indirectOffset);
+ } else {
+ encoder.drawIndirect(indirectBuffer, indirectOffset);
+ }
+
+ validateFinish(indirectOffset % 4 === 0);
+ });
+
+g.test('indirect_offset_oob')
+ .desc(
+ `
+Tests indirect draw calls with various indirect offsets and buffer sizes.
+- (offset, b.size) is
+ - (0, 0)
+ - (0, min size) (control case)
+ - (0, min size + 1) (control case)
+ - (0, min size - 1)
+ - (0, min size - min alignment)
+ - (min alignment, min size + min alignment)
+ - (min alignment, min size + min alignment - 1)
+ - (min alignment / 2, min size + min alignment)
+ - (min alignment +/- 1, min size + min alignment)
+ - (min size, min size)
+ - (min size + min alignment, min size)
+ - min size = indirect draw parameters size
+ - x =(drawIndirect, drawIndexedIndirect)
+ `
+ )
+ .paramsSubcasesOnly(
+ kIndirectDrawTestParams.expandWithParams(p => {
+ const indirectParamsSize = p.indexed ? 20 : 16;
+ return [
+ { indirectOffset: 0, bufferSize: 0, _valid: false },
+ { indirectOffset: 0, bufferSize: indirectParamsSize, _valid: true },
+ { indirectOffset: 0, bufferSize: indirectParamsSize + 1, _valid: true },
+ { indirectOffset: 0, bufferSize: indirectParamsSize - 1, _valid: false },
+ { indirectOffset: 0, bufferSize: indirectParamsSize - 4, _valid: false },
+ { indirectOffset: 4, bufferSize: indirectParamsSize + 4, _valid: true },
+ { indirectOffset: 4, bufferSize: indirectParamsSize + 3, _valid: false },
+ { indirectOffset: 2, bufferSize: indirectParamsSize + 4, _valid: false },
+ { indirectOffset: 3, bufferSize: indirectParamsSize + 4, _valid: false },
+ { indirectOffset: 5, bufferSize: indirectParamsSize + 4, _valid: false },
+ { indirectOffset: indirectParamsSize, bufferSize: indirectParamsSize, _valid: false },
+ { indirectOffset: indirectParamsSize + 4, bufferSize: indirectParamsSize, _valid: false },
+ ] as const;
+ })
+ )
+ .fn(t => {
+ const { encoderType, indexed, indirectOffset, bufferSize, _valid } = t.params;
+ const pipeline = t.createNoOpRenderPipeline();
+ const indirectBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, indirectOffset);
+ } else {
+ encoder.drawIndirect(indirectBuffer, indirectOffset);
+ }
+
+ validateFinish(_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts
new file mode 100644
index 0000000000..0df9ec6365
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts
@@ -0,0 +1,29 @@
+import { kUnitCaseParamsBuilder } from '../../../../../../common/framework/params_builder.js';
+import { kRenderEncodeTypes } from '../../../../../util/command_buffer_maker.js';
+
+export const kRenderEncodeTypeParams = kUnitCaseParamsBuilder.combine(
+ 'encoderType',
+ kRenderEncodeTypes
+);
+
+export function buildBufferOffsetAndSizeOOBTestParams(minAlignment: number, bufferSize: number) {
+ return kRenderEncodeTypeParams.combineWithParams([
+ // Explicit size
+ { offset: 0, size: 0, _valid: true },
+ { offset: 0, size: 1, _valid: true },
+ { offset: 0, size: 4, _valid: true },
+ { offset: 0, size: 5, _valid: true },
+ { offset: 0, size: bufferSize, _valid: true },
+ { offset: 0, size: bufferSize + 4, _valid: false },
+ { offset: minAlignment, size: bufferSize, _valid: false },
+ { offset: minAlignment, size: bufferSize - minAlignment, _valid: true },
+ { offset: bufferSize - minAlignment, size: minAlignment, _valid: true },
+ { offset: bufferSize, size: 1, _valid: false },
+ // Implicit size: buffer.size - offset
+ { offset: 0, size: undefined, _valid: true },
+ { offset: minAlignment, size: undefined, _valid: true },
+ { offset: bufferSize - minAlignment, size: undefined, _valid: true },
+ { offset: bufferSize, size: undefined, _valid: true },
+ { offset: bufferSize + minAlignment, size: undefined, _valid: false },
+ ]);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts
new file mode 100644
index 0000000000..1aacd8de90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts
@@ -0,0 +1,124 @@
+export const description = `
+Validation tests for setIndexBuffer on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../../../constants.js';
+import { kResourceStates } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams, buildBufferOffsetAndSizeOOBTestParams } from './render.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('index_buffer_state')
+ .desc(
+ `
+Tests index buffer must be valid.
+ `
+ )
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('state', kResourceStates))
+ .fn(t => {
+ const { encoderType, state } = t.params;
+ const indexBuffer = t.createBufferWithState(state, {
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('index_buffer,device_mismatch')
+ .desc('Tests setIndexBuffer cannot be called with an index buffer created from another device')
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const indexBuffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+ t.trackForCleanup(indexBuffer);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ validateFinish(!mismatched);
+ });
+
+g.test('index_buffer_usage')
+ .desc(
+ `
+Tests index buffer must have 'Index' usage.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams.combine('usage', [
+ GPUConst.BufferUsage.INDEX, // control case
+ GPUConst.BufferUsage.COPY_DST,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.INDEX,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, usage } = t.params;
+ const indexBuffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ validateFinish((usage & GPUBufferUsage.INDEX) !== 0);
+ });
+
+g.test('offset_alignment')
+ .desc(
+ `
+Tests offset must be a multiple of index format’s byte size.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams
+ .combine('indexFormat', ['uint16', 'uint32'] as const)
+ .expand('offset', p => {
+ return p.indexFormat === 'uint16' ? ([0, 1, 2] as const) : ([0, 2, 4] as const);
+ })
+ )
+ .fn(t => {
+ const { encoderType, indexFormat, offset } = t.params;
+ const indexBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, indexFormat, offset);
+
+ const alignment =
+ indexFormat === 'uint16' ? Uint16Array.BYTES_PER_ELEMENT : Uint32Array.BYTES_PER_ELEMENT;
+ validateFinish(offset % alignment === 0);
+ });
+
+g.test('offset_and_size_oob')
+ .desc(
+ `
+Tests offset and size cannot be larger than index buffer size.
+ `
+ )
+ .paramsSubcasesOnly(buildBufferOffsetAndSizeOOBTestParams(4, 256))
+ .fn(t => {
+ const { encoderType, offset, size, _valid } = t.params;
+ const indexBuffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32', offset, size);
+ validateFinish(_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts
new file mode 100644
index 0000000000..6fcd8015d3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts
@@ -0,0 +1,62 @@
+export const description = `
+Validation tests for setPipeline on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { kRenderEncodeTypes } from '../../../../../util/command_buffer_maker.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams } from './render.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('invalid_pipeline')
+ .desc(
+ `
+Tests setPipeline should generate an error iff using an 'invalid' pipeline.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('encoderType', kRenderEncodeTypes).combine('state', ['valid', 'invalid'] as const)
+ )
+ .fn(t => {
+ const { encoderType, state } = t.params;
+ const pipeline = t.createRenderPipelineWithState(state);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ validateFinish(state !== 'invalid');
+ });
+
+g.test('pipeline,device_mismatch')
+ .desc('Tests setPipeline cannot be called with a render pipeline created from another device')
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const pipeline = sourceDevice.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: sourceDevice.createShaderModule({
+ code: `@vertex fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(); }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: sourceDevice.createShaderModule({
+ code: '@fragment fn main() {}',
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ validateFinish(!mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts
new file mode 100644
index 0000000000..453281dbdd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts
@@ -0,0 +1,141 @@
+export const description = `
+Validation tests for setVertexBuffer on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { kLimitInfo } from '../../../../../capability_info.js';
+import { GPUConst } from '../../../../../constants.js';
+import { kResourceStates } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams, buildBufferOffsetAndSizeOOBTestParams } from './render.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('slot')
+ .desc(
+ `
+Tests slot must be less than the maxVertexBuffers in device limits.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams.combine('slot', [
+ 0,
+ kLimitInfo.maxVertexBuffers.default - 1,
+ kLimitInfo.maxVertexBuffers.default,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, slot } = t.params;
+ const vertexBuffer = t.createBufferWithState('valid', {
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(slot, vertexBuffer);
+ validateFinish(slot < kLimitInfo.maxVertexBuffers.default);
+ });
+
+g.test('vertex_buffer_state')
+ .desc(
+ `
+Tests vertex buffer must be valid.
+ `
+ )
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('state', kResourceStates))
+ .fn(t => {
+ const { encoderType, state } = t.params;
+ const vertexBuffer = t.createBufferWithState(state, {
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer);
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('vertex_buffer,device_mismatch')
+ .desc('Tests setVertexBuffer cannot be called with a vertex buffer created from another device')
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const vertexBuffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+ t.trackForCleanup(vertexBuffer);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer);
+ validateFinish(!mismatched);
+ });
+
+g.test('vertex_buffer_usage')
+ .desc(
+ `
+Tests vertex buffer must have 'Vertex' usage.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams.combine('usage', [
+ GPUConst.BufferUsage.VERTEX, // control case
+ GPUConst.BufferUsage.COPY_DST,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.VERTEX,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, usage } = t.params;
+ const vertexBuffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer);
+ validateFinish((usage & GPUBufferUsage.VERTEX) !== 0);
+ });
+
+g.test('offset_alignment')
+ .desc(
+ `
+Tests offset must be a multiple of 4.
+ `
+ )
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('offset', [0, 2, 4] as const))
+ .fn(t => {
+ const { encoderType, offset } = t.params;
+ const vertexBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinish: finish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer, offset);
+ finish(offset % 4 === 0);
+ });
+
+g.test('offset_and_size_oob')
+ .desc(
+ `
+Tests offset and size cannot be larger than vertex buffer size.
+ `
+ )
+ .paramsSubcasesOnly(buildBufferOffsetAndSizeOOBTestParams(4, 256))
+ .fn(t => {
+ const { encoderType, offset, size, _valid } = t.params;
+ const vertexBuffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer, offset, size);
+ validateFinish(_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts
new file mode 100644
index 0000000000..310f96a9df
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts
@@ -0,0 +1,184 @@
+export const description = `
+Validation tests for setVertexBuffer/setIndexBuffer state (not validation). See also operation tests.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { range } from '../../../../../../common/util/util.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+class F extends ValidationTest {
+ getVertexBuffer(): GPUBuffer {
+ return this.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.VERTEX,
+ });
+ }
+
+ createRenderPipeline(bufferCount: number): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Inputs {
+ ${range(bufferCount, i => `\n@location(${i}) a_position${i} : vec3<f32>,`).join('')}
+ };
+ @vertex fn main(input : Inputs
+ ) -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ arrayStride: 3 * 4,
+ attributes: range(bufferCount, i => ({
+ format: 'float32x3',
+ offset: 0,
+ shaderLocation: i,
+ })),
+ },
+ ],
+ },
+ 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-list' },
+ });
+ }
+
+ beginRenderPass(commandEncoder: GPUCommandEncoder): GPURenderPassEncoder {
+ const attachmentTexture = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ return commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachmentTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test(`all_needed_vertex_buffer_should_be_bound`)
+ .desc(
+ `
+In this test we test that any missing vertex buffer for a used slot will cause validation errors when drawing.
+- All (non/indexed, in/direct) draw commands
+ - A needed vertex buffer is not bound
+ - Was bound in another render pass but not the current one
+`
+ )
+ .unimplemented();
+
+g.test(`all_needed_index_buffer_should_be_bound`)
+ .desc(
+ `
+In this test we test that missing index buffer for a used slot will cause validation errors when drawing.
+- All indexed in/direct draw commands
+ - No index buffer is bound
+`
+ )
+ .unimplemented();
+
+g.test('vertex_buffers_inherit_from_previous_pipeline').fn(async t => {
+ const pipeline1 = t.createRenderPipeline(1);
+ const pipeline2 = t.createRenderPipeline(2);
+
+ const vertexBuffer1 = t.getVertexBuffer();
+ const vertexBuffer2 = t.getVertexBuffer();
+
+ {
+ // Check failure when vertex buffer is not set
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline1);
+ renderPass.draw(3);
+ renderPass.end();
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ }
+ {
+ // Check success when vertex buffer is inherited from previous pipeline
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.setVertexBuffer(1, vertexBuffer2);
+ renderPass.draw(3);
+ renderPass.setPipeline(pipeline1);
+ renderPass.draw(3);
+ renderPass.end();
+
+ commandEncoder.finish();
+ }
+});
+
+g.test('vertex_buffers_do_not_inherit_between_render_passes').fn(async t => {
+ const pipeline1 = t.createRenderPipeline(1);
+ const pipeline2 = t.createRenderPipeline(2);
+
+ const vertexBuffer1 = t.getVertexBuffer();
+ const vertexBuffer2 = t.getVertexBuffer();
+
+ {
+ // Check success when vertex buffer is set for each render pass
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.setVertexBuffer(1, vertexBuffer2);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline1);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ commandEncoder.finish();
+ }
+ {
+ // Check failure because vertex buffer is not inherited in second subpass
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.setVertexBuffer(1, vertexBuffer2);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline1);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ }
+});