export const description = `
Test indexing, index format and primitive restart.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import { getTextureCopyLayout } from '../../../util/texture/layout.js';
const kHeight = 4;
const kWidth = 8;
const kTextureFormat = 'r8uint' as const;
type Raster8x4 = readonly [
readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
];
const kBottomLeftTriangle: Raster8x4 = [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0],
];
const kSquare: Raster8x4 = [
[0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1, 1],
];
const kNothing: Raster8x4 = [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
];
const { byteLength, bytesPerRow, rowsPerImage } = getTextureCopyLayout(kTextureFormat, '2d', [
kWidth,
kHeight,
1,
]);
class IndexFormatTest extends AllFeaturesMaxLimitsGPUTest {
makeRenderPipeline(
topology: GPUPrimitiveTopology,
stripIndexFormat?: GPUIndexFormat
): GPURenderPipeline {
const vertexModule = this.device.createShaderModule({
code: `
@vertex
fn main(@builtin(vertex_index) VertexIndex : u32)
-> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 4>(
vec2<f32>(0.01, 0.98),
vec2<f32>(0.99, -0.98),
vec2<f32>(0.99, 0.98),
vec2<f32>(0.01, -0.98));
if (VertexIndex == 0xFFFFu || VertexIndex == 0xFFFFFFFFu) {
return vec4<f32>(-0.99, -0.98, 0.0, 1.0);
}
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
`,
});
const fragmentModule = this.device.createShaderModule({
code: `
@fragment
fn main() -> @location(0) u32 {
return 1u;
}
`,
});
return this.device.createRenderPipeline({
layout: this.device.createPipelineLayout({ bindGroupLayouts: [] }),
vertex: { module: vertexModule, entryPoint: 'main' },
fragment: {
module: fragmentModule,
entryPoint: 'main',
targets: [{ format: kTextureFormat }],
},
primitive: {
topology,
stripIndexFormat,
},
});
}
createIndexBuffer(indices: readonly number[], indexFormat: GPUIndexFormat): GPUBuffer {
const TypedArrayConstructor = { uint16: Uint16Array, uint32: Uint32Array }[indexFormat];
return this.makeBufferWithContents(new TypedArrayConstructor(indices), GPUBufferUsage.INDEX);
}
run(
indexBuffer: GPUBuffer,
indexCount: number,
indexFormat: GPUIndexFormat,
indexOffset: number = 0,
primitiveTopology: GPUPrimitiveTopology = 'triangle-list'
): GPUBuffer {
let pipeline: GPURenderPipeline;
if (primitiveTopology === 'line-strip' || primitiveTopology === 'triangle-strip') {
pipeline = this.makeRenderPipeline(primitiveTopology, indexFormat);
} else {
pipeline = this.makeRenderPipeline(primitiveTopology);
}
const colorAttachment = this.createTextureTracked({
format: kTextureFormat,
size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const result = this.createBufferTracked({
size: byteLength,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const encoder = this.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: colorAttachment.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
pass.drawIndexed(indexCount);
pass.end();
encoder.copyTextureToBuffer(
{ texture: colorAttachment },
{ buffer: result, bytesPerRow, rowsPerImage },
[kWidth, kHeight]
);
this.device.queue.submit([encoder.finish()]);
return result;
}
createExpectedUint8Array(renderShape: Raster8x4): Uint8Array {
const arrayBuffer = new Uint8Array(byteLength);
for (let row = 0; row < renderShape.length; row++) {
for (let col = 0; col < renderShape[row].length; col++) {
const texel: 0 | 1 = renderShape[row][col];
const kBytesPerTexel = 1;
const byteOffset = row * bytesPerRow + col * kBytesPerTexel;
arrayBuffer[byteOffset] = texel;
}
}
return arrayBuffer;
}
}
export const g = makeTestGroup(IndexFormatTest);
g.test('index_format,uint16')
.desc('Test rendering result of indexed draw with index format of uint16.')
.paramsSubcasesOnly([
{ indexOffset: 0, _indexCount: 10, _expectedShape: kSquare },
{ indexOffset: 6, _indexCount: 6, _expectedShape: kBottomLeftTriangle },
{ indexOffset: 18, _indexCount: 0, _expectedShape: kNothing },
])
.fn(t => {
const { indexOffset, _indexCount, _expectedShape } = t.params;
const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
const indexBuffer = t.createIndexBuffer(indices, 'uint16');
const result = t.run(indexBuffer, _indexCount, 'uint16', indexOffset);
const expectedTextureValues = t.createExpectedUint8Array(_expectedShape);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
});
g.test('index_format,uint32')
.desc('Test rendering result of indexed draw with index format of uint32.')
.paramsSubcasesOnly([
{ indexOffset: 0, _indexCount: 10, _expectedShape: kSquare },
{ indexOffset: 12, _indexCount: 7, _expectedShape: kBottomLeftTriangle },
{ indexOffset: 36, _indexCount: 0, _expectedShape: kNothing },
])
.fn(t => {
const { indexOffset, _indexCount, _expectedShape } = t.params;
const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
const indexBuffer = t.createIndexBuffer(indices, 'uint32');
const result = t.run(indexBuffer, _indexCount, 'uint32', indexOffset);
const expectedTextureValues = t.createExpectedUint8Array(_expectedShape);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
});
g.test('index_format,change_pipeline_after_setIndexBuffer')
.desc('Test that setting the index buffer before the pipeline works correctly.')
.params(u => u.combine('setPipelineBeforeSetIndexBuffer', [false, true]))
.fn(t => {
const indexOffset = 12;
const indexCount = 7;
const expectedShape = kBottomLeftTriangle;
const indexFormat16 = 'uint16';
const indexFormat32 = 'uint32';
const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
const indexBuffer = t.createIndexBuffer(indices, indexFormat32);
const kPrimitiveTopology = 'triangle-strip';
const pipeline32 = t.makeRenderPipeline(kPrimitiveTopology, indexFormat32);
const pipeline16 = t.makeRenderPipeline(kPrimitiveTopology, indexFormat16);
const colorAttachment = t.createTextureTracked({
format: kTextureFormat,
size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const result = t.createBufferTracked({
size: byteLength,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: colorAttachment.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
});
if (t.params.setPipelineBeforeSetIndexBuffer) {
pass.setPipeline(pipeline16);
}
pass.setIndexBuffer(indexBuffer, indexFormat32, indexOffset);
pass.setPipeline(pipeline32);
pass.drawIndexed(indexCount);
pass.end();
encoder.copyTextureToBuffer(
{ texture: colorAttachment },
{ buffer: result, bytesPerRow, rowsPerImage },
[kWidth, kHeight]
);
t.device.queue.submit([encoder.finish()]);
const expectedTextureValues = t.createExpectedUint8Array(expectedShape);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
});
g.test('index_format,setIndexBuffer_before_setPipeline')
.desc('Test that setting the index buffer before the pipeline works correctly.')
.params(u => u.combine('setIndexBufferBeforeSetPipeline', [false, true]))
.fn(t => {
const indexOffset = 12;
const indexCount = 7;
const expectedShape = kBottomLeftTriangle;
const indexFormat = 'uint32';
const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
const indexBuffer = t.createIndexBuffer(indices, indexFormat);
const kPrimitiveTopology = 'triangle-strip';
const pipeline = t.makeRenderPipeline(kPrimitiveTopology, indexFormat);
const colorAttachment = t.createTextureTracked({
format: kTextureFormat,
size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const result = t.createBufferTracked({
size: byteLength,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: colorAttachment.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
});
if (t.params.setIndexBufferBeforeSetPipeline) {
pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
pass.setPipeline(pipeline);
} else {
pass.setPipeline(pipeline);
pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
}
pass.drawIndexed(indexCount);
pass.end();
encoder.copyTextureToBuffer(
{ texture: colorAttachment },
{ buffer: result, bytesPerRow, rowsPerImage },
[kWidth, kHeight]
);
t.device.queue.submit([encoder.finish()]);
const expectedTextureValues = t.createExpectedUint8Array(expectedShape);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
});
g.test('index_format,setIndexBuffer_different_formats')
.desc(
`
Test that index buffers of multiple formats can be used with a pipeline that doesn't use strip
primitive topology.
`
)
.fn(t => {
const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
const kPrimitiveTopology = 'triangle-list';
const pipeline = t.makeRenderPipeline(kPrimitiveTopology);
const expectedTextureValues = t.createExpectedUint8Array(kBottomLeftTriangle);
const colorAttachment = t.createTextureTracked({
format: kTextureFormat,
size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const result = t.createBufferTracked({
size: byteLength,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
let encoder = t.device.createCommandEncoder();
{
const indexFormat = 'uint32';
const indexOffset = 12;
const indexCount = 7;
const indexBuffer = t.createIndexBuffer(indices, indexFormat);
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: colorAttachment.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
pass.setPipeline(pipeline);
pass.drawIndexed(indexCount);
pass.end();
encoder.copyTextureToBuffer(
{ texture: colorAttachment },
{ buffer: result, bytesPerRow, rowsPerImage },
[kWidth, kHeight]
);
}
t.device.queue.submit([encoder.finish()]);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
encoder = t.device.createCommandEncoder();
{
const indexFormat = 'uint16';
const indexOffset = 6;
const indexCount = 6;
const indexBuffer = t.createIndexBuffer(indices, indexFormat);
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: colorAttachment.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
pass.setPipeline(pipeline);
pass.drawIndexed(indexCount);
pass.end();
encoder.copyTextureToBuffer(
{ texture: colorAttachment },
{ buffer: result, bytesPerRow, rowsPerImage },
[kWidth, kHeight]
);
}
t.device.queue.submit([encoder.finish()]);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
});
g.test('primitive_restart')
.desc(
`
Test primitive restart with each primitive topology.
Primitive restart should be always active with strip primitive topologies
('line-strip' or 'triangle-strip') and never active for other topologies, where
the primitive restart value isn't special and should be treated as a regular index value.
The value -1 gets uploaded as 0xFFFF or 0xFFFF_FFFF according to the format.
The positions of these points are embedded in the shader above, and look like this:
| 0 2|
| |
-1 3 1|
Below are the indices lists used for each test, and the expected rendering result of each
(approximately, in the case of incorrect results). This shows the expected result (marked '->')
is different from what you would get if the topology were incorrect.
- primitiveTopology: triangle-list
indices: [0, 1, 3, -1, 2, 1, 0, 0],
-> triangle-list: (0, 1, 3), (-1, 2, 1)
| # #|
| ####|
| #####|
| #######|
triangle-list with restart: (0, 1, 3), (2, 1, 0)
triangle-strip: (0, 1, 3), (2, 1, 0), (1, 0, 0)
| ####|
| ####|
| ####|
| ####|
triangle-strip w/o restart: (0, 1, 3), (1, 3, -1), (3, -1, 2), (-1, 2, 1), (2, 1, 0), (1, 0, 0)
| ####|
| ####|
| #####|
| #######|
- primitiveTopology: triangle-strip
indices: [3, 1, 0, -1, 2, 2, 1, 3],
-> triangle-strip: (3, 1, 0), (2, 2, 1), (2, 1, 3)
| # #|
| ####|
| ####|
| ####|
triangle-strip w/o restart: (3, 1, 0), (1, 0, -1), (0, -1, 2), (2, 2, 1), (2, 3, 1)
| ####|
| #####|
| ######|
| #######|
triangle-list: (3, 1, 0), (-1, 2, 2)
triangle-list with restart: (3, 1, 0), (2, 2, 1)
| |
| # |
| ## |
| ### |
- primitiveTopology: point, line-list, line-strip:
indices: [0, 1, -1, 2, -1, 2, 3, 0],
-> point-list: (0), (1), (-1), (2), (3), (0)
| # #|
| |
| |
|# # #|
point-list with restart (0), (1), (2), (3), (0)
| # #|
| |
| |
| # #|
-> line-list: (0, 1), (-1, 2), (3, 0)
| # ##|
| ## |
| ### # |
|## # #|
line-list with restart: (0, 1), (2, 3)
| # #|
| ## |
| ## |
| # #|
-> line-strip: (0, 1), (2, 3), (3, 0)
| # #|
| ### |
| ### |
| # #|
line-strip w/o restart: (0, 1), (1, -1), (-1, 2), (2, 3), (3, 3)
| # ##|
| ### |
| ## ## |
|########|
`
)
.params(u =>
u
.combine('indexFormat', ['uint16', 'uint32'] as const)
.combineWithParams([
{
primitiveTopology: 'point-list',
_indices: [0, 1, -1, 2, 3, 0],
_expectedShape: [
[0, 0, 0, 0, 1, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 1, 0, 0, 1],
],
},
{
primitiveTopology: 'line-list',
_indices: [0, 1, -1, 2, 3, 0],
_expectedShape: [
[0, 0, 0, 0, 1, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 1, 0],
[1, 1, 0, 0, 1, 0, 0, 1],
],
},
{
primitiveTopology: 'line-strip',
_indices: [0, 1, -1, 2, 3, 0],
_expectedShape: [
[0, 0, 0, 0, 1, 0, 0, 1],
[0, 0, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 1],
],
},
{
primitiveTopology: 'triangle-list',
_indices: [0, 1, 3, -1, 2, 1, 0, 0],
_expectedShape: [
[0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1],
],
},
{
primitiveTopology: 'triangle-strip',
_indices: [3, 1, 0, -1, 2, 2, 1, 3],
_expectedShape: [
[0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 1, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1, 1],
],
},
] as const)
)
.fn(t => {
const { indexFormat, primitiveTopology, _indices, _expectedShape } = t.params;
const indexBuffer = t.createIndexBuffer(_indices, indexFormat);
const result = t.run(indexBuffer, _indices.length, indexFormat, 0, primitiveTopology);
const expectedTextureValues = t.createExpectedUint8Array(_expectedShape);
t.expectGPUBufferValuesEqual(result, expectedTextureValues);
});