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 { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../gpu_test.js';
import * as ttu from '../../texture_test_utils.js';
class DrawCall {
private test: GPUTest;
private vertexBuffers: GPUBuffer[];
private offsetVertexBuffer: boolean;
private keepInstanceStepModeBufferInRange: boolean;
public vertexCount: number;
public firstVertex: number;
public vertexCountInIndexBuffer: number;
public indexCount: number;
public firstIndex: number;
public baseVertex: number;
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;
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;
this.vertexBuffers = vertexArrays.map((v, i) => {
if (i === 0 && keepInstanceStepModeBufferInRange) {
return this.generateVertexBuffer(v, false);
} else {
return this.generateVertexBuffer(v, partialLastNumber);
}
});
}
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);
}
}
}
public draw(pass: GPURenderPassEncoder) {
this.bindVertexBuffers(pass);
pass.draw(this.vertexCount, this.instanceCount, this.firstVertex, this.firstInstance);
}
public drawIndexed(pass: GPURenderPassEncoder) {
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
);
}
public drawIndirect(pass: GPURenderPassEncoder) {
this.bindVertexBuffers(pass);
pass.drawIndirect(this.generateIndirectBuffer(), 0);
}
public drawIndexedIndirect(pass: GPURenderPassEncoder) {
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);
}
private bindVertexBuffers(pass: GPURenderPassEncoder) {
let currSlot = 0;
for (let i = 0; i < this.vertexBuffers.length; i++) {
if (i === 0 && this.keepInstanceStepModeBufferInRange) {
pass.setVertexBuffer(currSlot++, this.vertexBuffers[i], 0);
} else {
pass.setVertexBuffer(currSlot++, this.vertexBuffers[i], this.offsetVertexBuffer ? 4 : 0);
}
}
}
private generateVertexBuffer(vertexArray: Float32Array, partialLastNumber: boolean): GPUBuffer {
let size = vertexArray.byteLength;
let length = vertexArray.length;
if (partialLastNumber) {
size -= 1;
length -= 1;
}
const buffer = this.test.createBufferTracked({
size,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
this.test.device.queue.writeBuffer(buffer, 0, vertexArray.slice(0, length));
return buffer;
}
private generateIndirectBuffer(): GPUBuffer {
const indirectArray = new Int32Array([
this.vertexCount,
this.instanceCount,
this.firstVertex,
this.firstInstance,
]);
return this.test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
}
private generateIndexedIndirectBuffer(): GPUBuffer {
const indirectArray = new Int32Array([
this.indexCount,
this.instanceCount,
this.firstIndex,
this.baseVertex,
this.firstInstance,
]);
return this.test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
}
}
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 AllFeaturesMaxLimitsGPUTest {
generateBufferContents(
numVertices: number,
attributesPerBuffer: number,
typeInfo: VertexInfo,
arbitraryValues: number[],
bufferCount: number
): Float32Array[] {
const vertexArray = new Float32Array(
numVertices * attributesPerBuffer * (typeInfo.sizeInBytes / 4)
);
for (let i = 0; i < vertexArray.length; ++i) {
vertexArray[i] = arbitraryValues[i % arbitraryValues.length];
}
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];
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 {
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 {
const buffers: GPUVertexBufferLayout[] = this.generateVertexBufferDescriptors(
bufferCount,
attributesPerBuffer,
dataType
);
const pipeline = this.createRenderPipeline({
bufferCount,
attributesPerBuffer,
validValues,
typeInfo: typeInfoMap[dataType],
vertexIndexOffset,
numVertices,
isIndexed,
buffers,
});
const colorAttachment = this.createTextureTracked({
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,
clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
drawCall.insertInto(pass, isIndexed, isIndirect);
pass.end();
this.device.queue.submit([encoder.finish()]);
ttu.expectSinglePixelComparisonsAreOkInTexture(this, { texture: colorAttachment }, [
{ coord: { x: 0, y: 0 }, exp: new Uint8Array([0x00, 0xff, 0x00, 0xff]) },
]);
}
}
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])
.beginSubcases()
.combine('errorScale', [0, 1, 4, 10 ** 2, 10 ** 4, 10 ** 6])
.unless(p => p.drawCallTestParameter === 'instanceCount' && p.errorScale > 10 ** 4)
)
.fn(t => {
const p = t.params;
const typeInfo = typeInfoMap[p.type];
const numVertices = 4;
const attributesPerBuffer = 2;
const arbitraryValues = [990, 685, 446, 175];
const validValues =
p.errorScale === 0 && !p.offsetVertexBuffer && !p.partialLastNumber
? arbitraryValues
: [0, ...arbitraryValues];
const bufferCount = p.additionalBuffers + 2;
const bufferContents = t.generateBufferContents(
numVertices,
attributesPerBuffer,
typeInfo,
arbitraryValues,
bufferCount
);
const draw = new DrawCall({
test: t,
vertexArrays: bufferContents,
vertexCount: numVertices,
partialLastNumber: p.partialLastNumber,
offsetVertexBuffer: p.offsetVertexBuffer,
keepInstanceStepModeBufferInRange: p.indexed && !p.indirect,
});
draw[p.drawCallTestParameter] += p.errorScale;
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,
});
});