| /** |
| * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts |
| **/ export const description = ` |
| Test vertex attributes behave correctly (no crash / data leak) when accessed out of bounds |
| |
| Test coverage: |
| |
| The following will be parameterized (all combinations tested): |
| |
| 1) Draw call indexed? (false / true) |
| - Run the draw call using an index buffer |
| |
| 2) Draw call indirect? (false / true) |
| - Run the draw call using an indirect buffer |
| |
| 3) Draw call parameter (vertexCount, firstVertex, indexCount, firstIndex, baseVertex, instanceCount, |
| firstInstance) |
| - The parameter which will go out of bounds. Filtered depending on if the draw call is indexed. |
| |
| 4) Attribute type (float, vec2, vec3, vec4) |
| - The input attribute type in the vertex shader |
| |
| 5) Error scale (1, 4, 10^2, 10^4, 10^6) |
| - Offset to add to the correct draw call parameter |
| |
| 6) Additional vertex buffers (0, +4) |
| - Tests that no OOB occurs if more vertex buffers are used |
| |
| The tests will also have another vertex buffer bound for an instanced attribute, to make sure |
| instanceCount / firstInstance are tested. |
| |
| The tests will include multiple attributes per vertex buffer. |
| |
| The vertex buffers will be filled by repeating a few chosen values until the end of the buffer. |
| |
| The test will run a render pipeline which verifies the following: |
| 1) All vertex attribute values occur in the buffer or are zero |
| 2) All gl_VertexIndex values are within the index buffer or 0 |
| |
| TODO: |
| |
| A suppression may be needed for d3d12 on tests that have non-zero baseVertex, since d3d12 counts |
| from 0 instead of from baseVertex (will fail check for gl_VertexIndex). |
| |
| Vertex buffer contents could be randomized to prevent the case where a previous test creates |
| a similar buffer to ours and the OOB-read seems valid. This should be deterministic, which adds |
| more complexity that we may not need.`; |
| import { params, pbool, poptions } from '../../../common/framework/params_builder.js'; |
| import { makeTestGroup } from '../../../common/framework/test_group.js'; |
| import { GPUTest } from '../../gpu_test.js'; |
| |
| export const g = makeTestGroup(GPUTest); |
| |
| // Encapsulates a draw call (either indexed or non-indexed) |
| class DrawCall { |
| // Add a float offset when binding vertex buffer |
| |
| // Draw |
| |
| // DrawIndexed |
| |
| // Both Draw and DrawIndexed |
| |
| constructor(device, vertexArrays, vertexCount, partialLastNumber, offsetVertexBuffer) { |
| this.device = device; |
| this.vertexBuffers = vertexArrays.map(v => this.generateVertexBuffer(v, partialLastNumber)); |
| |
| const indexArray = new Uint16Array(vertexCount).fill(0).map((_, i) => i); |
| this.indexBuffer = this.generateIndexBuffer(indexArray); |
| |
| // Default arguments (valid call) |
| this.vertexCount = vertexCount; |
| this.firstVertex = 0; |
| this.indexCount = vertexCount; |
| this.firstIndex = 0; |
| this.baseVertex = 0; |
| this.instanceCount = vertexCount; |
| this.firstInstance = 0; |
| |
| this.offsetVertexBuffer = offsetVertexBuffer; |
| } |
| |
| // Insert a draw call into |pass| with specified type |
| insertInto(pass, indexed, indirect) { |
| 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| |
| draw(pass) { |
| this.bindVertexBuffers(pass); |
| pass.draw(this.vertexCount, this.instanceCount, this.firstVertex, this.firstInstance); |
| } |
| |
| // Insert an indexed draw call into |pass| |
| drawIndexed(pass) { |
| this.bindVertexBuffers(pass); |
| pass.setIndexBuffer(this.indexBuffer, 'uint16'); |
| pass.drawIndexed( |
| this.indexCount, |
| this.instanceCount, |
| this.firstIndex, |
| this.baseVertex, |
| this.firstInstance |
| ); |
| } |
| |
| // Insert an indirect draw call into |pass| |
| drawIndirect(pass) { |
| this.bindVertexBuffers(pass); |
| pass.drawIndirect(this.generateIndirectBuffer(), 0); |
| } |
| |
| // Insert an indexed indirect draw call into |pass| |
| drawIndexedIndirect(pass) { |
| this.bindVertexBuffers(pass); |
| pass.setIndexBuffer(this.indexBuffer, 'uint16'); |
| pass.drawIndexedIndirect(this.generateIndexedIndirectBuffer(), 0); |
| } |
| |
| // Bind all vertex buffers generated |
| bindVertexBuffers(pass) { |
| let currSlot = 0; |
| for (let i = 0; i < this.vertexBuffers.length; i++) { |
| 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 |
| generateVertexBuffer(vertexArray, partialLastNumber) { |
| let size = vertexArray.byteLength; |
| if (partialLastNumber) { |
| size -= 1; |
| } |
| const vertexBuffer = this.device.createBuffer({ |
| size, |
| usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, |
| }); |
| |
| if (partialLastNumber) { |
| size -= 3; |
| } |
| this.device.defaultQueue.writeBuffer(vertexBuffer, 0, vertexArray, size); |
| return vertexBuffer; |
| } |
| |
| // Create an index buffer from |indexArray| |
| generateIndexBuffer(indexArray) { |
| const indexBuffer = this.device.createBuffer({ |
| size: indexArray.byteLength, |
| usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, |
| }); |
| |
| this.device.defaultQueue.writeBuffer(indexBuffer, 0, indexArray); |
| return indexBuffer; |
| } |
| |
| // Create an indirect buffer containing draw call values |
| generateIndirectBuffer() { |
| const indirectArray = new Int32Array([ |
| this.vertexCount, |
| this.instanceCount, |
| this.firstVertex, |
| this.firstInstance, |
| ]); |
| |
| const indirectBuffer = this.device.createBuffer({ |
| mappedAtCreation: true, |
| size: indirectArray.byteLength, |
| usage: GPUBufferUsage.INDIRECT, |
| }); |
| |
| new Int32Array(indirectBuffer.getMappedRange()).set(indirectArray); |
| indirectBuffer.unmap(); |
| return indirectBuffer; |
| } |
| |
| // Create an indirect buffer containing indexed draw call values |
| generateIndexedIndirectBuffer() { |
| const indirectArray = new Int32Array([ |
| this.indexCount, |
| this.instanceCount, |
| this.firstVertex, |
| this.baseVertex, |
| this.firstInstance, |
| ]); |
| |
| const indirectBuffer = this.device.createBuffer({ |
| mappedAtCreation: true, |
| size: indirectArray.byteLength, |
| usage: GPUBufferUsage.INDIRECT, |
| }); |
| |
| new Int32Array(indirectBuffer.getMappedRange()).set(indirectArray); |
| indirectBuffer.unmap(); |
| return indirectBuffer; |
| } |
| } |
| |
| // Parameterize different sized types |
| |
| const typeInfoMap = { |
| float: { |
| wgslType: 'f32', |
| size: 4, |
| validationFunc: 'return valid(v);', |
| }, |
| |
| float2: { |
| wgslType: 'vec2<f32>', |
| size: 8, |
| validationFunc: 'return valid(v.x) && valid(v.y);', |
| }, |
| |
| float3: { |
| wgslType: 'vec3<f32>', |
| size: 12, |
| validationFunc: 'return valid(v.x) && valid(v.y) && valid(v.z);', |
| }, |
| |
| float4: { |
| wgslType: 'vec4<f32>', |
| size: 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);`, |
| }, |
| }; |
| |
| g.test('vertexAccess') |
| .params( |
| params() |
| .combine(pbool('indexed')) |
| .combine(pbool('indirect')) |
| .expand(p => |
| poptions( |
| 'drawCallTestParameter', |
| p.indexed |
| ? ['indexCount', 'instanceCount', 'firstIndex', 'baseVertex', 'firstInstance'] |
| : ['vertexCount', 'instanceCount', 'firstVertex', 'firstInstance'] |
| ) |
| ) |
| .combine(poptions('type', Object.keys(typeInfoMap))) |
| .combine(poptions('additionalBuffers', [0, 4])) |
| .combine(pbool('partialLastNumber')) |
| .combine(pbool('offsetVertexBuffer')) |
| .combine(poptions('errorScale', [1, 4, 10 ** 2, 10 ** 4, 10 ** 6])) |
| ) |
| .fn(async t => { |
| const p = t.params; |
| const typeInfo = typeInfoMap[p.type]; |
| |
| // Number of vertices to draw, odd so that uint16 index buffers are aligned to size 4 |
| const numVertices = 4; |
| // Each buffer will be bound to this many attributes (2 would mean 2 attributes per buffer) |
| const attributesPerBuffer = 2; |
| // Make an array big enough for the vertices, attributes, and size of each element |
| const vertexArray = new Float32Array(numVertices * attributesPerBuffer * (typeInfo.size / 4)); |
| |
| // Sufficiently unusual values to fill our buffer with to avoid collisions with other tests |
| const arbitraryValues = [759, 329, 908]; |
| for (let i = 0; i < vertexArray.length; ++i) { |
| vertexArray[i] = arbitraryValues[i % arbitraryValues.length]; |
| } |
| // A valid value is 0 or one in the buffer |
| const validValues = [0, ...arbitraryValues]; |
| |
| // Instance step mode buffer, vertex step mode buffer |
| const bufferContents = [vertexArray, vertexArray]; |
| // Additional buffers (vertex step mode) |
| for (let i = 0; i < p.additionalBuffers; i++) { |
| bufferContents.push(vertexArray); |
| } |
| |
| // Mutable draw call |
| const draw = new DrawCall( |
| t.device, |
| bufferContents, |
| numVertices, |
| p.partialLastNumber, |
| p.offsetVertexBuffer |
| ); |
| |
| // Create attributes listing |
| let layoutStr = ''; |
| const attributeNames = []; |
| { |
| let currAttribute = 0; |
| for (let i = 0; i < bufferContents.length; i++) { |
| for (let j = 0; j < attributesPerBuffer; j++) { |
| layoutStr += `[[location(${currAttribute})]] var<in> a_${currAttribute} : ${typeInfo.wgslType};\n`; |
| attributeNames.push(`a_${currAttribute}`); |
| currAttribute++; |
| } |
| } |
| } |
| |
| // Vertex buffer descriptors |
| const vertexBuffers = []; |
| { |
| let currAttribute = 0; |
| for (let i = 0; i < bufferContents.length; i++) { |
| vertexBuffers.push({ |
| arrayStride: attributesPerBuffer * typeInfo.size, |
| stepMode: i === 0 ? 'instance' : 'vertex', |
| attributes: Array(attributesPerBuffer) |
| .fill(0) |
| .map((_, i) => ({ |
| shaderLocation: currAttribute++, |
| offset: i * typeInfo.size, |
| format: p.type, |
| })), |
| }); |
| } |
| } |
| |
| // Offset the range checks for gl_VertexIndex in the shader if we use BaseVertex |
| let vertexIndexOffset = 0; |
| if (p.drawCallTestParameter === 'baseVertex') { |
| vertexIndexOffset += p.errorScale; |
| } |
| |
| const pipeline = t.device.createRenderPipeline({ |
| vertexStage: { |
| module: t.device.createShaderModule({ |
| code: ` |
| [[builtin(position)]] var<out> Position : vec4<f32>; |
| [[builtin(vertex_idx)]] var<in> VertexIndex : u32; |
| ${layoutStr} |
| |
| fn valid(f : f32) -> bool { |
| return ${validValues.map(v => `f == ${v}.0`).join(' || ')}; |
| } |
| |
| fn validationFunc(v : ${typeInfo.wgslType}) -> bool { |
| ${typeInfo.validationFunc} |
| } |
| |
| [[stage(vertex)]] fn main() -> void { |
| var attributesInBounds : bool = ${attributeNames |
| .map(a => `validationFunc(${a})`) |
| .join(' && ')}; |
| var indexInBounds : bool = VertexIndex == 0u || |
| (VertexIndex >= ${vertexIndexOffset}u && |
| VertexIndex < ${vertexIndexOffset + numVertices}u); |
| |
| if (attributesInBounds && (${!p.indexed} || indexInBounds)) { |
| // Success case, move the vertex out of the viewport |
| Position = vec4<f32>(-1.0, 0.0, 0.0, 1.0); |
| } else { |
| // Failure case, move the vertex inside the viewport |
| Position = vec4<f32>(0.0, 0.0, 0.0, 1.0); |
| } |
| }`, |
| }), |
| |
| entryPoint: 'main', |
| }, |
| |
| fragmentStage: { |
| module: t.device.createShaderModule({ |
| code: ` |
| [[location(0)]] var<out> fragColor : vec4<f32>; |
| [[stage(fragment)]] fn main() -> void { |
| fragColor = vec4<f32>(1.0, 0.0, 0.0, 1.0); |
| }`, |
| }), |
| |
| entryPoint: 'main', |
| }, |
| |
| primitiveTopology: 'point-list', |
| colorStates: [{ format: 'rgba8unorm' }], |
| vertexState: { |
| vertexBuffers, |
| }, |
| }); |
| |
| // Pipeline setup, texture setup |
| const colorAttachment = t.device.createTexture({ |
| format: 'rgba8unorm', |
| size: { width: 1, height: 1, depth: 1 }, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.OUTPUT_ATTACHMENT, |
| }); |
| |
| const colorAttachmentView = colorAttachment.createView(); |
| |
| // Offset the draw call parameter we are testing by |errorScale| |
| draw[p.drawCallTestParameter] += p.errorScale; |
| |
| const encoder = t.device.createCommandEncoder(); |
| const pass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| attachment: colorAttachmentView, |
| storeOp: 'store', |
| loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, |
| }, |
| ], |
| }); |
| |
| pass.setPipeline(pipeline); |
| |
| // Run the draw variant |
| draw.insertInto(pass, p.indexed, p.indirect); |
| |
| pass.endPass(); |
| t.device.defaultQueue.submit([encoder.finish()]); |
| |
| // Validate we see green instead of red, meaning no fragment ended up on-screen |
| t.expectSinglePixelIn2DTexture( |
| colorAttachment, |
| 'rgba8unorm', |
| { x: 0, y: 0 }, |
| { exp: new Uint8Array([0x00, 0xff, 0x00, 0xff]), layout: { mipLevel: 0 } } |
| ); |
| }); |