summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts610
1 files changed, 610 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts
new file mode 100644
index 0000000000..45f7f5167f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts
@@ -0,0 +1,610 @@
+export const description = `
+Test vertex attributes behave correctly (no crash / data leak) when accessed out of bounds
+
+Test coverage:
+
+The following is parameterized (all combinations tested):
+
+1) Draw call type? (drawIndexed, drawIndirect, drawIndexedIndirect)
+ - Run the draw call using an index buffer and/or an indirect buffer.
+ - Doesn't test direct draw, as vertex buffer OOB are CPU validated and treated as validation errors.
+ - Also the instance step mode vertex buffer OOB are CPU validated for drawIndexed, so we only test
+ robustness access for vertex step mode vertex buffers.
+
+2) Draw call parameter (vertexCount, firstVertex, indexCount, firstIndex, baseVertex, instanceCount,
+ vertexCountInIndexBuffer)
+ - The parameter which goes out of bounds. Filtered depending on the draw call type.
+ - vertexCount, firstVertex: used for drawIndirect only, test for vertex step mode buffer OOB
+ - instanceCount: used for both drawIndirect and drawIndexedIndirect, test for instance step mode buffer OOB
+ - baseVertex, vertexCountInIndexBuffer: used for both drawIndexed and drawIndexedIndirect, test
+ for vertex step mode buffer OOB. vertexCountInIndexBuffer indicates how many vertices are used
+ within the index buffer, i.e. [0, 1, ..., vertexCountInIndexBuffer-1].
+ - indexCount, firstIndex: used for drawIndexedIndirect only, validate the vertex buffer access
+ when the vertex itself is OOB in index buffer. This never happens in drawIndexed as we have index
+ buffer OOB CPU validation for it.
+
+3) Attribute type (float32, float32x2, float32x3, float32x4)
+ - The input attribute type in the vertex shader
+
+4) Error scale (0, 1, 4, 10^2, 10^4, 10^6)
+ - Offset to add to the correct draw call parameter
+ - 0 For control case
+
+5) Additional vertex buffers (0, +4)
+ - Tests that no OOB occurs if more vertex buffers are used
+
+6) Partial last number and offset vertex buffer (false, true)
+ - Tricky cases that make vertex buffer OOB.
+ - With partial last number enabled, vertex buffer size will be 1 byte less than enough, making the
+ last vertex OOB with 1 byte.
+ - Offset vertex buffer will bind the vertex buffer to render pass with 4 bytes offset, causing OOB
+ - For drawIndexed, these two flags are suppressed for instance step mode vertex buffer to make sure
+ it pass the CPU validation.
+
+The tests have one instance step mode vertex buffer bound for instanced attributes, to make sure
+instanceCount / firstInstance are tested.
+
+The tests include multiple attributes per vertex buffer.
+
+The vertex buffers are filled by repeating a few values randomly chosen for each test until the
+end of the buffer.
+
+The tests run a render pipeline which verifies the following:
+1) All vertex attribute values occur in the buffer or are 0 (for control case it can't be 0)
+2) All gl_VertexIndex values are within the index buffer or 0
+
+TODO:
+Currently firstInstance is not tested, as for drawIndexed it is CPU validated, and for drawIndirect
+and drawIndexedIndirect it should always be 0. Once there is an extension to allow making them non-zero,
+it should be added into drawCallTestParameter list.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+
+// Encapsulates a draw call (either indexed or non-indexed)
+class DrawCall {
+ private test: GPUTest;
+ private vertexBuffers: GPUBuffer[];
+
+ // Add a float offset when binding vertex buffer
+ private offsetVertexBuffer: boolean;
+
+ // Keep instance step mode vertex buffer in range, in order to test vertex step
+ // mode buffer OOB in drawIndexed. Setting true will suppress partialLastNumber
+ // and offsetVertexBuffer for instance step mode vertex buffer.
+ private keepInstanceStepModeBufferInRange: boolean;
+
+ // Draw
+ public vertexCount: number;
+ public firstVertex: number;
+
+ // DrawIndexed
+ public vertexCountInIndexBuffer: number; // For generating index buffer in drawIndexed and drawIndexedIndirect
+ public indexCount: number; // For accessing index buffer in drawIndexed and drawIndexedIndirect
+ public firstIndex: number;
+ public baseVertex: number;
+
+ // Both Draw and DrawIndexed
+ public instanceCount: number;
+ public firstInstance: number;
+
+ constructor({
+ test,
+ vertexArrays,
+ vertexCount,
+ partialLastNumber,
+ offsetVertexBuffer,
+ keepInstanceStepModeBufferInRange,
+ }: {
+ test: GPUTest;
+ vertexArrays: Float32Array[];
+ vertexCount: number;
+ partialLastNumber: boolean;
+ offsetVertexBuffer: boolean;
+ keepInstanceStepModeBufferInRange: boolean;
+ }) {
+ this.test = test;
+
+ // Default arguments (valid call)
+ this.vertexCount = vertexCount;
+ this.firstVertex = 0;
+ this.vertexCountInIndexBuffer = vertexCount;
+ this.indexCount = vertexCount;
+ this.firstIndex = 0;
+ this.baseVertex = 0;
+ this.instanceCount = vertexCount;
+ this.firstInstance = 0;
+
+ this.offsetVertexBuffer = offsetVertexBuffer;
+ this.keepInstanceStepModeBufferInRange = keepInstanceStepModeBufferInRange;
+
+ // Since vertexInIndexBuffer is mutable, generation of the index buffer should be deferred to right before calling draw
+
+ // Generate vertex buffer
+ this.vertexBuffers = vertexArrays.map((v, i) => {
+ if (i === 0 && keepInstanceStepModeBufferInRange) {
+ // Suppress partialLastNumber for the first vertex buffer, aka the instance step mode buffer
+ return this.generateVertexBuffer(v, false);
+ } else {
+ return this.generateVertexBuffer(v, partialLastNumber);
+ }
+ });
+ }
+
+ // Insert a draw call into |pass| with specified type
+ public insertInto(pass: GPURenderPassEncoder, indexed: boolean, indirect: boolean) {
+ if (indexed) {
+ if (indirect) {
+ this.drawIndexedIndirect(pass);
+ } else {
+ this.drawIndexed(pass);
+ }
+ } else {
+ if (indirect) {
+ this.drawIndirect(pass);
+ } else {
+ this.draw(pass);
+ }
+ }
+ }
+
+ // Insert a draw call into |pass|
+ public draw(pass: GPURenderPassEncoder) {
+ this.bindVertexBuffers(pass);
+ pass.draw(this.vertexCount, this.instanceCount, this.firstVertex, this.firstInstance);
+ }
+
+ // Insert an indexed draw call into |pass|
+ public drawIndexed(pass: GPURenderPassEncoder) {
+ // Generate index buffer
+ const indexArray = new Uint32Array(this.vertexCountInIndexBuffer).map((_, i) => i);
+ const indexBuffer = this.test.makeBufferWithContents(indexArray, GPUBufferUsage.INDEX);
+ this.bindVertexBuffers(pass);
+ pass.setIndexBuffer(indexBuffer, 'uint32');
+ pass.drawIndexed(
+ this.indexCount,
+ this.instanceCount,
+ this.firstIndex,
+ this.baseVertex,
+ this.firstInstance
+ );
+ }
+
+ // Insert an indirect draw call into |pass|
+ public drawIndirect(pass: GPURenderPassEncoder) {
+ this.bindVertexBuffers(pass);
+ pass.drawIndirect(this.generateIndirectBuffer(), 0);
+ }
+
+ // Insert an indexed indirect draw call into |pass|
+ public drawIndexedIndirect(pass: GPURenderPassEncoder) {
+ // Generate index buffer
+ const indexArray = new Uint32Array(this.vertexCountInIndexBuffer).map((_, i) => i);
+ const indexBuffer = this.test.makeBufferWithContents(indexArray, GPUBufferUsage.INDEX);
+ this.bindVertexBuffers(pass);
+ pass.setIndexBuffer(indexBuffer, 'uint32');
+ pass.drawIndexedIndirect(this.generateIndexedIndirectBuffer(), 0);
+ }
+
+ // Bind all vertex buffers generated
+ private bindVertexBuffers(pass: GPURenderPassEncoder) {
+ let currSlot = 0;
+ for (let i = 0; i < this.vertexBuffers.length; i++) {
+ if (i === 0 && this.keepInstanceStepModeBufferInRange) {
+ // Keep the instance step mode buffer in range
+ pass.setVertexBuffer(currSlot++, this.vertexBuffers[i], 0);
+ } else {
+ pass.setVertexBuffer(currSlot++, this.vertexBuffers[i], this.offsetVertexBuffer ? 4 : 0);
+ }
+ }
+ }
+
+ // Create a vertex buffer from |vertexArray|
+ // If |partialLastNumber| is true, delete one byte off the end
+ private generateVertexBuffer(vertexArray: Float32Array, partialLastNumber: boolean): GPUBuffer {
+ let size = vertexArray.byteLength;
+ let length = vertexArray.length;
+ if (partialLastNumber) {
+ size -= 1; // Shave off one byte from the buffer size.
+ length -= 1; // And one whole element from the writeBuffer.
+ }
+ const buffer = this.test.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, // Ensure that buffer can be used by writeBuffer
+ });
+ this.test.device.queue.writeBuffer(buffer, 0, vertexArray.slice(0, length));
+ return buffer;
+ }
+
+ // Create an indirect buffer containing draw call values
+ private generateIndirectBuffer(): GPUBuffer {
+ const indirectArray = new Int32Array([
+ this.vertexCount,
+ this.instanceCount,
+ this.firstVertex,
+ this.firstInstance,
+ ]);
+ return this.test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ }
+
+ // Create an indirect buffer containing indexed draw call values
+ private generateIndexedIndirectBuffer(): GPUBuffer {
+ const indirectArray = new Int32Array([
+ this.indexCount,
+ this.instanceCount,
+ this.firstIndex,
+ this.baseVertex,
+ this.firstInstance,
+ ]);
+ return this.test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ }
+}
+
+// Parameterize different sized types
+interface VertexInfo {
+ wgslType: string;
+ sizeInBytes: number;
+ validationFunc: string;
+}
+
+const typeInfoMap: { [k: string]: VertexInfo } = {
+ float32: {
+ wgslType: 'f32',
+ sizeInBytes: 4,
+ validationFunc: 'return valid(v);',
+ },
+ float32x2: {
+ wgslType: 'vec2<f32>',
+ sizeInBytes: 8,
+ validationFunc: 'return valid(v.x) && valid(v.y);',
+ },
+ float32x3: {
+ wgslType: 'vec3<f32>',
+ sizeInBytes: 12,
+ validationFunc: 'return valid(v.x) && valid(v.y) && valid(v.z);',
+ },
+ float32x4: {
+ wgslType: 'vec4<f32>',
+ sizeInBytes: 16,
+ validationFunc: `return (valid(v.x) && valid(v.y) && valid(v.z) && valid(v.w)) ||
+ (v.x == 0.0 && v.y == 0.0 && v.z == 0.0 && (v.w == 0.0 || v.w == 1.0));`,
+ },
+};
+
+class F extends GPUTest {
+ generateBufferContents(
+ numVertices: number,
+ attributesPerBuffer: number,
+ typeInfo: VertexInfo,
+ arbitraryValues: number[],
+ bufferCount: number
+ ): Float32Array[] {
+ // Make an array big enough for the vertices, attributes, and size of each element
+ const vertexArray = new Float32Array(
+ numVertices * attributesPerBuffer * (typeInfo.sizeInBytes / 4)
+ );
+
+ for (let i = 0; i < vertexArray.length; ++i) {
+ vertexArray[i] = arbitraryValues[i % arbitraryValues.length];
+ }
+
+ // Only the first buffer is instance step mode, all others are vertex step mode buffer
+ assert(bufferCount >= 2);
+ const bufferContents: Float32Array[] = [];
+ for (let i = 0; i < bufferCount; i++) {
+ bufferContents.push(vertexArray);
+ }
+
+ return bufferContents;
+ }
+
+ generateVertexBufferDescriptors(
+ bufferCount: number,
+ attributesPerBuffer: number,
+ format: GPUVertexFormat
+ ) {
+ const typeInfo = typeInfoMap[format];
+ // Vertex buffer descriptors
+ const buffers: GPUVertexBufferLayout[] = [];
+ {
+ let currAttribute = 0;
+ for (let i = 0; i < bufferCount; i++) {
+ buffers.push({
+ arrayStride: attributesPerBuffer * typeInfo.sizeInBytes,
+ stepMode: i === 0 ? 'instance' : 'vertex',
+ attributes: Array(attributesPerBuffer)
+ .fill(0)
+ .map((_, i) => ({
+ shaderLocation: currAttribute++,
+ offset: i * typeInfo.sizeInBytes,
+ format,
+ })),
+ });
+ }
+ }
+ return buffers;
+ }
+
+ generateVertexShaderCode({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ }: {
+ bufferCount: number;
+ attributesPerBuffer: number;
+ validValues: number[];
+ typeInfo: VertexInfo;
+ vertexIndexOffset: number;
+ numVertices: number;
+ isIndexed: boolean;
+ }): string {
+ // Create layout and attributes listing
+ let layoutStr = 'struct Attributes {';
+ const attributeNames = [];
+ {
+ let currAttribute = 0;
+ for (let i = 0; i < bufferCount; i++) {
+ for (let j = 0; j < attributesPerBuffer; j++) {
+ layoutStr += `@location(${currAttribute}) a_${currAttribute} : ${typeInfo.wgslType},\n`;
+ attributeNames.push(`a_${currAttribute}`);
+ currAttribute++;
+ }
+ }
+ }
+ layoutStr += '};';
+
+ const vertexShaderCode: string = `
+ ${layoutStr}
+
+ fn valid(f : f32) -> bool {
+ return ${validValues.map(v => `f == ${v}.0`).join(' || ')};
+ }
+
+ fn validationFunc(v : ${typeInfo.wgslType}) -> bool {
+ ${typeInfo.validationFunc}
+ }
+
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32,
+ attributes : Attributes
+ ) -> @builtin(position) vec4<f32> {
+ var attributesInBounds = ${attributeNames
+ .map(a => `validationFunc(attributes.${a})`)
+ .join(' && ')};
+
+ var indexInBoundsCountFromBaseVertex =
+ (VertexIndex >= ${vertexIndexOffset}u &&
+ VertexIndex < ${vertexIndexOffset + numVertices}u);
+ var indexInBounds = VertexIndex == 0u || indexInBoundsCountFromBaseVertex;
+
+ var Position : vec4<f32>;
+ if (attributesInBounds && (${!isIndexed} || indexInBounds)) {
+ // Success case, move the vertex to the right of the viewport to show that at least one case succeed
+ Position = vec4<f32>(0.5, 0.0, 0.0, 1.0);
+ } else {
+ // Failure case, move the vertex to the left of the viewport
+ Position = vec4<f32>(-0.5, 0.0, 0.0, 1.0);
+ }
+ return Position;
+ }`;
+ return vertexShaderCode;
+ }
+
+ createRenderPipeline({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ buffers,
+ }: {
+ bufferCount: number;
+ attributesPerBuffer: number;
+ validValues: number[];
+ typeInfo: VertexInfo;
+ vertexIndexOffset: number;
+ numVertices: number;
+ isIndexed: boolean;
+ buffers: GPUVertexBufferLayout[];
+ }): GPURenderPipeline {
+ const pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: this.generateVertexShaderCode({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ }),
+ }),
+ entryPoint: 'main',
+ buffers,
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ return pipeline;
+ }
+
+ doTest({
+ bufferCount,
+ attributesPerBuffer,
+ dataType,
+ validValues,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ isIndirect,
+ drawCall,
+ }: {
+ bufferCount: number;
+ attributesPerBuffer: number;
+ dataType: GPUVertexFormat;
+ validValues: number[];
+ vertexIndexOffset: number;
+ numVertices: number;
+ isIndexed: boolean;
+ isIndirect: boolean;
+ drawCall: DrawCall;
+ }): void {
+ // Vertex buffer descriptors
+ const buffers: GPUVertexBufferLayout[] = this.generateVertexBufferDescriptors(
+ bufferCount,
+ attributesPerBuffer,
+ dataType
+ );
+
+ // Pipeline setup, texture setup
+ const pipeline = this.createRenderPipeline({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo: typeInfoMap[dataType],
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ buffers,
+ });
+
+ const colorAttachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 2, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+
+ // Run the draw variant
+ drawCall.insertInto(pass, isIndexed, isIndirect);
+
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ // Validate we see green on the left pixel, showing that no failure case is detected
+ this.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ 'rgba8unorm',
+ { x: 0, y: 0 },
+ { exp: new Uint8Array([0x00, 0xff, 0x00, 0xff]), layout: { mipLevel: 0 } }
+ );
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('vertex_buffer_access')
+ .params(
+ u =>
+ u
+ .combineWithParams([
+ { indexed: false, indirect: true },
+ { indexed: true, indirect: false },
+ { indexed: true, indirect: true },
+ ])
+ .expand('drawCallTestParameter', function* (p) {
+ if (p.indexed) {
+ yield* ['baseVertex', 'vertexCountInIndexBuffer'] as const;
+ if (p.indirect) {
+ yield* ['indexCount', 'instanceCount', 'firstIndex'] as const;
+ }
+ } else if (p.indirect) {
+ yield* ['vertexCount', 'instanceCount', 'firstVertex'] as const;
+ }
+ })
+ .combine('type', Object.keys(typeInfoMap) as GPUVertexFormat[])
+ .combine('additionalBuffers', [0, 4])
+ .combine('partialLastNumber', [false, true])
+ .combine('offsetVertexBuffer', [false, true])
+ .combine('errorScale', [0, 1, 4, 10 ** 2, 10 ** 4, 10 ** 6])
+ .unless(p => p.drawCallTestParameter === 'instanceCount' && p.errorScale > 10 ** 4) // To avoid timeout
+ )
+ .fn(async t => {
+ const p = t.params;
+ const typeInfo = typeInfoMap[p.type];
+
+ // Number of vertices to draw
+ const numVertices = 4;
+ // Each buffer is bound to this many attributes (2 would mean 2 attributes per buffer)
+ const attributesPerBuffer = 2;
+ // Some arbitrary values to fill our buffer with to avoid collisions with other tests
+ const arbitraryValues = [990, 685, 446, 175];
+
+ // A valid value is 0 or one in the buffer
+ const validValues =
+ p.errorScale === 0 && !p.offsetVertexBuffer && !p.partialLastNumber
+ ? arbitraryValues // Control case with no OOB access, must read back valid values in buffer
+ : [0, ...arbitraryValues]; // Testing case with OOB access, can be 0 for OOB data
+
+ // Generate vertex buffer contents. Only the first buffer is instance step mode, all others are vertex step mode
+ const bufferCount = p.additionalBuffers + 2; // At least one instance step mode and one vertex step mode buffer
+ const bufferContents = t.generateBufferContents(
+ numVertices,
+ attributesPerBuffer,
+ typeInfo,
+ arbitraryValues,
+ bufferCount
+ );
+
+ // Mutable draw call
+ const draw = new DrawCall({
+ test: t,
+ vertexArrays: bufferContents,
+ vertexCount: numVertices,
+ partialLastNumber: p.partialLastNumber,
+ offsetVertexBuffer: p.offsetVertexBuffer,
+ keepInstanceStepModeBufferInRange: p.indexed && !p.indirect, // keep instance step mode buffer in range for drawIndexed
+ });
+
+ // Offset the draw call parameter we are testing by |errorScale|
+ draw[p.drawCallTestParameter] += p.errorScale;
+ // Offset the range checks for gl_VertexIndex in the shader if we use BaseVertex
+ let vertexIndexOffset = 0;
+ if (p.drawCallTestParameter === 'baseVertex') {
+ vertexIndexOffset += p.errorScale;
+ }
+
+ t.doTest({
+ bufferCount,
+ attributesPerBuffer,
+ dataType: p.type,
+ validValues,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed: p.indexed,
+ isIndirect: p.indirect,
+ drawCall: draw,
+ });
+ });