export const description = `Test primitive topology rendering.
Draw a primitive using 6 vertices with each topology and check if the pixel is covered.
Vertex sequence and coordinates are the same for each topology:
- Vertex buffer = [v1, v2, v3, v4, v5, v6]
- Topology = [point-list, line-list, line-strip, triangle-list, triangle-strip]
Test locations are framebuffer coordinates:
- Pixel value { valid: green, invalid: black, format: 'rgba8unorm'}
- Test point is valid if the pixel value equals the covered pixel value at the test location.
- Primitive restart occurs for strips (line-strip and triangle-strip) between [v3, v4].
Topology: point-list Valid test location(s) Invalid test location(s)
v2 v4 v6 Every vertex. Line-strip locations.
Triangle-list locations.
Triangle-strip locations.
v1 v3 v5
Topology: line-list (3 lines)
v2 v4 v6 Center of three line segments: Line-strip locations.
* * * {v1,V2}, {v3,v4}, and {v4,v5}. Triangle-list locations.
* * * Triangle-strip locations.
* * *
v1 v3 v5
Topology: line-strip (5 lines)
v2 v4 v6
** ** *
* * * * * Line-list locations Triangle-list locations.
* ** ** + Center of two line segments: Triangle-strip locations.
v1 v3 v5 {v2,v3} and {v4,v5}.
With primitive restart:
Line segment {v3, v4}.
Topology: triangle-list (2 triangles)
v2 v4 v6
** ****** Center of two triangle(s): Triangle-strip locations.
**** **** {v1,v2,v3} and {v4,v5,v6}.
****** **
v1 v3 v5
Topology: triangle-strip (4 triangles)
v2 v4 v6
** ****** ** ****** Triangle-list locations None.
**** **** **** **** + Center of two triangle(s):
****** ** ****** ** {v2,v3,v4} and {v3,v4,v5}. With primitive restart:
v1 v3 v5 Triangle {v2, v3, v4}
and {v3, v4, v5}.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as ttu from '../../../texture_test_utils.js';
import { PerPixelComparison } from '../../../util/texture/texture_ok.js';
const kRTSize: number = 56;
const kColorFormat = 'rgba8unorm';
const kValidPixelColor = new Uint8Array([0x00, 0xff, 0x00, 0xff]);
const kInvalidPixelColor = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
class Point2D {
x: number;
y: number;
z: number;
w: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
this.z = 0;
this.w = 1;
}
toNDC(): Point2D {
return new Point2D((2 * (this.x + 0.5)) / kRTSize - 1, (-2 * (this.y + 0.5)) / kRTSize + 1);
}
static getMidpoint(a: Point2D, b: Point2D) {
return new Point2D((a.x + b.x) / 2, (a.y + b.y) / 2);
}
static getCentroid(a: Point2D, b: Point2D, c: Point2D) {
return new Point2D((a.x + b.x + c.x) / 3, (a.y + b.y + c.y) / 3);
}
}
type TestLocation = PerPixelComparison<Uint8Array>;
const VertexLocations = [
new Point2D(8, 24),
new Point2D(16, 8),
new Point2D(24, 24),
new Point2D(32, 8),
new Point2D(40, 24),
new Point2D(48, 8),
];
function getPointTestLocations(expectedColor: Uint8Array): TestLocation[] {
const testLocations: TestLocation[] = [];
for (const location of VertexLocations) {
testLocations.push({ coord: location, exp: expectedColor });
}
return testLocations;
}
function getLineTestLocations(expectedColor: Uint8Array): TestLocation[] {
return [
{
coord: Point2D.getMidpoint(VertexLocations[0], VertexLocations[1]),
exp: expectedColor,
},
{
coord: Point2D.getMidpoint(VertexLocations[2], VertexLocations[3]),
exp: expectedColor,
},
{
coord: Point2D.getMidpoint(VertexLocations[4], VertexLocations[5]),
exp: expectedColor,
},
];
}
function getPrimitiveRestartLineTestLocations(expectedColor: Uint8Array): TestLocation[] {
return [
{
coord: Point2D.getMidpoint(VertexLocations[0], VertexLocations[1]),
exp: expectedColor,
},
{
coord: Point2D.getMidpoint(VertexLocations[4], VertexLocations[5]),
exp: expectedColor,
},
];
}
function getLineStripTestLocations(expectedColor: Uint8Array): TestLocation[] {
return [
{
coord: Point2D.getMidpoint(VertexLocations[1], VertexLocations[2]),
exp: expectedColor,
},
{
coord: Point2D.getMidpoint(VertexLocations[3], VertexLocations[4]),
exp: expectedColor,
},
];
}
function getTriangleListTestLocations(expectedColor: Uint8Array): TestLocation[] {
return [
{
coord: Point2D.getCentroid(VertexLocations[0], VertexLocations[1], VertexLocations[2]),
exp: expectedColor,
},
{
coord: Point2D.getCentroid(VertexLocations[3], VertexLocations[4], VertexLocations[5]),
exp: expectedColor,
},
];
}
function getTriangleStripTestLocations(expectedColor: Uint8Array): TestLocation[] {
return [
{
coord: Point2D.getCentroid(VertexLocations[1], VertexLocations[2], VertexLocations[3]),
exp: expectedColor,
},
{
coord: Point2D.getCentroid(VertexLocations[2], VertexLocations[3], VertexLocations[4]),
exp: expectedColor,
},
];
}
function getDefaultTestLocations({
topology,
primitiveRestart = false,
invalidateLastInList = false,
}: {
topology: GPUPrimitiveTopology;
primitiveRestart?: boolean;
invalidateLastInList?: boolean;
}) {
function maybeInvalidateLast(locations: TestLocation[]) {
if (!invalidateLastInList) return locations;
return locations.map((tl, i) => {
if (i === locations.length - 1) {
return {
coord: tl.coord,
exp: kInvalidPixelColor,
};
} else {
return tl;
}
});
}
let testLocations: TestLocation[];
switch (topology) {
case 'point-list':
testLocations = [
...getPointTestLocations(kValidPixelColor),
...getLineStripTestLocations(kInvalidPixelColor),
...getTriangleListTestLocations(kInvalidPixelColor),
...getTriangleStripTestLocations(kInvalidPixelColor),
];
break;
case 'line-list':
testLocations = [
...maybeInvalidateLast(getLineTestLocations(kValidPixelColor)),
...getLineStripTestLocations(kInvalidPixelColor),
...getTriangleListTestLocations(kInvalidPixelColor),
...getTriangleStripTestLocations(kInvalidPixelColor),
];
break;
case 'line-strip':
testLocations = [
...(primitiveRestart
? getPrimitiveRestartLineTestLocations(kValidPixelColor)
: getLineTestLocations(kValidPixelColor)),
...getLineStripTestLocations(kValidPixelColor),
...getTriangleListTestLocations(kInvalidPixelColor),
...getTriangleStripTestLocations(kInvalidPixelColor),
];
break;
case 'triangle-list':
testLocations = [
...maybeInvalidateLast(getTriangleListTestLocations(kValidPixelColor)),
...getTriangleStripTestLocations(kInvalidPixelColor),
];
break;
case 'triangle-strip':
testLocations = [
...getTriangleListTestLocations(kValidPixelColor),
...getTriangleStripTestLocations(primitiveRestart ? kInvalidPixelColor : kValidPixelColor),
];
break;
}
return testLocations;
}
function generateVertexBuffer(vertexLocations: Point2D[]): Float32Array {
const vertexCoords = new Float32Array(vertexLocations.length * 4);
for (let i = 0; i < vertexLocations.length; i++) {
const point = vertexLocations[i].toNDC();
vertexCoords[i * 4 + 0] = point.x;
vertexCoords[i * 4 + 1] = point.y;
vertexCoords[i * 4 + 2] = point.z;
vertexCoords[i * 4 + 3] = point.w;
}
return vertexCoords;
}
const kDefaultDrawCount = 6;
class PrimitiveTopologyTest extends AllFeaturesMaxLimitsGPUTest {
makeAttachmentTexture(): GPUTexture {
return this.createTextureTracked({
format: kColorFormat,
size: { width: kRTSize, height: kRTSize, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
}
run({
topology,
indirect,
testLocations,
primitiveRestart = false,
drawCount = kDefaultDrawCount,
}: {
topology: GPUPrimitiveTopology;
indirect: boolean;
testLocations: TestLocation[];
primitiveRestart?: boolean;
drawCount?: number;
}): void {
const colorAttachment = this.makeAttachmentTexture();
const encoder = this.device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: colorAttachment.createView(),
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
});
let stripIndexFormat = undefined;
if (topology === 'triangle-strip' || topology === 'line-strip') {
stripIndexFormat = 'uint32' as const;
}
renderPass.setPipeline(
this.device.createRenderPipeline({
layout: 'auto',
vertex: {
module: this.device.createShaderModule({
code: `
@vertex fn main(
@location(0) pos : vec4<f32>
) -> @builtin(position) vec4<f32> {
return pos;
}`,
}),
entryPoint: 'main',
buffers: [
{
arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT,
attributes: [
{
format: 'float32x4',
offset: 0,
shaderLocation: 0,
},
],
},
],
},
fragment: {
module: this.device.createShaderModule({
code: `
@fragment fn main() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 1.0, 0.0, 1.0);
}`,
}),
entryPoint: 'main',
targets: [{ format: kColorFormat }],
},
primitive: {
topology,
stripIndexFormat,
},
})
);
const vertexCoords = generateVertexBuffer(VertexLocations);
const vertexBuffer = this.makeBufferWithContents(vertexCoords, GPUBufferUsage.VERTEX);
renderPass.setVertexBuffer(0, vertexBuffer);
if (primitiveRestart) {
const indexBuffer = this.makeBufferWithContents(
new Uint32Array([0, 1, 2, -1, 3, 4, 5]),
GPUBufferUsage.INDEX
);
renderPass.setIndexBuffer(indexBuffer, 'uint32');
if (indirect) {
renderPass.drawIndexedIndirect(
this.makeBufferWithContents(
new Uint32Array([drawCount + 1, 1, 0, 0, 0]),
GPUBufferUsage.INDIRECT
),
0
);
} else {
renderPass.drawIndexed(drawCount + 1);
}
} else {
if (indirect) {
renderPass.drawIndirect(
this.makeBufferWithContents(
new Uint32Array([drawCount, 1, 0, 0]),
GPUBufferUsage.INDIRECT
),
0
);
} else {
renderPass.draw(drawCount);
}
}
renderPass.end();
this.device.queue.submit([encoder.finish()]);
ttu.expectSinglePixelComparisonsAreOkInTexture(
this,
{ texture: colorAttachment },
testLocations
);
}
}
export const g = makeTestGroup(PrimitiveTopologyTest);
const topologies: GPUPrimitiveTopology[] = [
'point-list',
'line-list',
'line-strip',
'triangle-list',
'triangle-strip',
];
g.test('basic')
.desc(
`Compute test locations for valid and invalid pixels for each topology.
If the primitive covers the pixel, the color value will be |kValidPixelColor|.
Otherwise, a non-covered pixel will be |kInvalidPixelColor|.
Params:
- topology= {...all topologies}
- indirect= {true, false}
- primitiveRestart= { true, false } - always false for non-strip topologies
`
)
.params(u =>
u
.combine('topology', topologies)
.combine('indirect', [false, true])
.combine('primitiveRestart', [false, true])
.unless(
p => p.primitiveRestart && p.topology !== 'line-strip' && p.topology !== 'triangle-strip'
)
)
.fn(t => {
t.run({
...t.params,
testLocations: getDefaultTestLocations(t.params),
});
});
g.test('unaligned_vertex_count')
.desc(
`Test that drawing with a number of vertices that's not a multiple of the vertices a given primitive list topology is not an error. The last primitive is not drawn.
Params:
- topology= {line-list, triangle-list}
- indirect= {true, false}
- drawCount - number of vertices to draw. A value smaller than the test's default of ${kDefaultDrawCount}.
One smaller for line-list. One or two smaller for triangle-list.
`
)
.params(u =>
u
.combine('topology', ['line-list', 'triangle-list'] as const)
.combine('indirect', [false, true])
.expand('drawCount', function* (p) {
switch (p.topology) {
case 'line-list':
yield kDefaultDrawCount - 1;
break;
case 'triangle-list':
yield kDefaultDrawCount - 1;
yield kDefaultDrawCount - 2;
break;
}
})
)
.fn(t => {
const testLocations = getDefaultTestLocations({ ...t.params, invalidateLastInList: true });
t.run({
...t.params,
testLocations,
});
});