blob: a711b5062a4268439fd4b0ffeed332568d818d27 [file] [log] [blame]
/**
* 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 } }
);
});