blob: 2720cbac1c0547814f3a7b58509a90439e27bb17 [file] [log] [blame]
export const description = `
Execution tests for discard.
The discard statement converts invocations into helpers.
This results in the following conditions:
* No outputs are written
* No resources are written
* Atomics are undefined
Conditions that still occur:
* Derivative calculations are correct
* Reads
* Writes to non-external memory
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { iterRange } from '../../../../common/util/util.js';
import { AllFeaturesMaxLimitsGPUTest, GPUTestBase } from '../../../gpu_test.js';
import { checkElementsPassPredicate } from '../../../util/check_contents.js';
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
// Framebuffer dimensions
const kWidth = 64;
const kHeight = 64;
const kSharedCode = `
@group(0) @binding(0) var<storage, read_write> output: array<vec2f>;
@group(0) @binding(1) var<storage, read_write> atomicIndex : atomic<u32>;
@group(0) @binding(2) var<uniform> uniformValues : array<vec4u, 5>;
@vertex
fn vsMain(@builtin(vertex_index) index : u32) -> @builtin(position) vec4f {
const vertices = array(
vec2(-1, -1), vec2(-1, 0), vec2( 0, -1),
vec2(-1, 0), vec2( 0, 0), vec2( 0, -1),
vec2( 0, -1), vec2( 0, 0), vec2( 1, -1),
vec2( 0, 0), vec2( 1, 0), vec2( 1, -1),
vec2(-1, 0), vec2(-1, 1), vec2( 0, 0),
vec2(-1, 1), vec2( 0, 1), vec2( 0, 0),
vec2( 0, 0), vec2( 0, 1), vec2( 1, 0),
vec2( 0, 1), vec2( 1, 1), vec2( 1, 0),
);
return vec4f(vec2f(vertices[index]), 0, 1);
}
`;
function drawFullScreen(
t: GPUTestBase,
code: string,
useStorageBuffers: boolean,
dataChecker: (a: Float32Array) => Error | undefined,
framebufferChecker: (a: Uint32Array) => Error | undefined
) {
t.skipIf(
useStorageBuffers &&
t.isCompatibility &&
!(t.device.limits.maxStorageBuffersInFragmentStage! >= 2),
`maxStorageBuffersInFragmentStage${t.device.limits.maxStorageBuffersInFragmentStage} is less than 2`
);
const pipeline = t.device.createRenderPipeline({
layout: 'auto',
vertex: {
module: t.device.createShaderModule({ code }),
entryPoint: 'vsMain',
},
fragment: {
module: t.device.createShaderModule({ code }),
entryPoint: 'fsMain',
targets: [{ format: 'rg32uint' }],
},
primitive: {
topology: 'triangle-list',
},
});
const bytesPerWord = 4;
const framebuffer = t.createTextureTracked({
size: [kWidth, kHeight],
usage:
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING,
format: 'rg32uint',
});
// Create a buffer to copy the framebuffer contents into.
// Initialize with a sentinel value and load this buffer to detect unintended writes.
const fbBuffer = t.makeBufferWithContents(
new Uint32Array([...iterRange(kWidth * kHeight * 2, x => kWidth * kHeight)]),
GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
);
// Create a buffer to hold the storage shader resources.
// (0,0) = vec2u width * height
// (0,1) = u32
const dataSize = 2 * kWidth * kHeight * bytesPerWord;
const dataBufferSize = dataSize + bytesPerWord;
const dataBuffer = t.createBufferTracked({
size: dataBufferSize,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
});
const uniformSize = bytesPerWord * 5 * 4;
const uniformBuffer = t.makeBufferWithContents(
// Loop bound, [derivative constants].
// prettier-ignore
new Uint32Array([
4, 0, 0, 0,
1, 0, 0, 0,
4, 0, 0, 0,
4, 0, 0, 0,
7, 0, 0, 0,
]),
GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM
);
// 'atomicIndex' packed at the end of the buffer.
const bg = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
...(useStorageBuffers
? [
{
binding: 0,
resource: {
buffer: dataBuffer,
offset: 0,
size: dataSize,
},
},
{
binding: 1,
resource: {
buffer: dataBuffer,
offset: dataSize,
size: bytesPerWord,
},
},
]
: []),
{
binding: 2,
resource: {
buffer: uniformBuffer,
offset: 0,
size: uniformSize,
},
},
],
});
const encoder = t.device.createCommandEncoder();
encoder.copyBufferToTexture(
{
buffer: fbBuffer,
offset: 0,
bytesPerRow: kWidth * bytesPerWord * 2,
rowsPerImage: kHeight,
},
{ texture: framebuffer },
{ width: kWidth, height: kHeight }
);
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: framebuffer.createView(),
loadOp: 'load',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bg);
pass.draw(24);
pass.end();
encoder.copyTextureToBuffer(
{ texture: framebuffer },
{
buffer: fbBuffer,
offset: 0,
bytesPerRow: kWidth * bytesPerWord * 2,
rowsPerImage: kHeight,
},
{ width: kWidth, height: kHeight }
);
t.queue.submit([encoder.finish()]);
if (useStorageBuffers) {
t.expectGPUBufferValuesPassCheck(dataBuffer, dataChecker, {
type: Float32Array,
typedLength: dataSize / bytesPerWord,
});
}
t.expectGPUBufferValuesPassCheck(fbBuffer, framebufferChecker, {
type: Uint32Array,
typedLength: kWidth * kHeight * 2,
});
}
g.test('all')
.desc('Test a shader that discards all fragments')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
_ = uniformValues[0];
discard;
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = pos.xy;
`
: ''
}
return vec2u(1);
}
`;
// No storage writes occur.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === 0;
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number) => {
return 0;
},
},
],
}
);
};
// No fragment outputs occur.
const fbChecker = (a: Uint32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === kWidth * kHeight;
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
return 0;
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});
g.test('three_quarters')
.desc('Test a shader that discards all but the upper-left quadrant fragments')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
_ = uniformValues[0];
if (pos.x >= 0.5 * ${kWidth} || pos.y >= 0.5 * ${kHeight}) {
discard;
}
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = pos.xy;
return vec2u(idx);
`
: `
return vec2(u32(pos.x), u32(pos.y));
`
}
}
`;
// Only the the upper left quadrant is kept.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
const is_x = idx % 2 === 0;
if (is_x) {
return value < 0.5 * kWidth;
} else {
return value < 0.5 * kHeight;
}
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number): number | string => {
const is_x = idx % 2 === 0;
if (is_x) {
const x = Math.floor(idx / 2) % kWidth;
if (x >= kWidth / 2) {
return 0;
}
} else {
const y = Math.floor((idx - 1) / kWidth);
if (y >= kHeight / 2) {
return 0;
}
}
if (is_x) {
return `< ${0.5 * kWidth}`;
} else {
return `< ${0.5 * kHeight}`;
}
},
},
],
}
);
};
const fbChecker = (a: Uint32Array) => {
const discarded = (x: number, y: number) => x >= kWidth / 2 || y >= kHeight / 2;
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
const fragId = (idx / 2) | 0;
const x = fragId % kWidth;
const y = (fragId / kWidth) | 0;
if (discarded(x, y)) {
return value === kWidth * kHeight;
} else {
if (useStorageBuffers) {
return value < (kWidth * kHeight) / 4;
} else {
return value === (idx % 2 ? y : x);
}
}
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
const x = idx % kWidth;
const y = Math.floor(idx / kWidth);
if (discarded(x, y)) {
return 0;
} else {
return useStorageBuffers ? 'any' : idx % 2 ? y : x;
}
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});
g.test('function_call')
.desc('Test discards happening in a function call')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
fn foo(pos : vec2f) {
let p = vec2i(pos);
if p.x <= ${kWidth} / 2 && p.y <= ${kHeight} / 2 {
discard;
}
if p.x >= ${kWidth} / 2 && p.y >= ${kHeight} / 2 {
discard;
}
}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
_ = uniformValues[0];
foo(pos.xy);
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = pos.xy;
return vec2u(idx);
`
: `
return vec2u(u32(pos.x), u32(pos.y));
`
}
}
`;
// Only the upper right and bottom left quadrants are kept.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
const is_x = idx % 2 === 0;
if (value === 0.0) {
return is_x ? a[idx + 1] === 0 : a[idx - 1] === 0;
}
let expect = is_x ? kWidth : kHeight;
expect = 0.5 * expect;
if (value < expect) {
return is_x ? a[idx + 1] > 0.5 * kWidth : a[idx - 1] > 0.5 * kHeight;
} else {
return is_x ? a[idx + 1] < 0.5 * kWidth : a[idx - 1] < 0.5 * kHeight;
}
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number): number | string => {
if (idx < (kWidth * kHeight) / 2) {
return 'any';
} else {
return 0;
}
},
},
],
}
);
};
const fbChecker = (a: Uint32Array) => {
const discarded = (x: number, y: number) =>
(x >= kWidth / 2 && y >= kHeight / 2) || (x <= kWidth / 2 && y <= kHeight / 2);
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
const fragId = (idx / 2) | 0;
const x = fragId % kWidth;
const y = (fragId / kWidth) | 0;
if (discarded(x, y)) {
return value === kWidth * kHeight;
} else {
if (useStorageBuffers) {
return value < (kWidth * kHeight) / 2;
} else {
return value === (idx % 2 ? y : x);
}
}
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
const x = idx % kWidth;
const y = Math.floor(idx / kWidth);
if (discarded(x, y)) {
return kWidth * kHeight;
}
return useStorageBuffers ? 'any' : idx % 2 ? y : x;
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});
g.test('loop')
.desc('Test discards in a loop')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
_ = uniformValues[0];
for (var i = 0; i < 2; i++) {
if i > 0 {
discard;
}
}
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = pos.xy;
`
: ''
}
return vec2u(1);
}
`;
// No storage writes occur.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === 0;
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number) => {
return 0;
},
},
],
}
);
};
// No fragment outputs occur.
const fbChecker = (a: Uint32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === kWidth * kHeight;
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
return kWidth * kHeight;
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});
g.test('continuing')
.desc('Test discards in a loop')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
_ = uniformValues[0];
var i = 0;
loop {
continuing {
if i > 0 {
discard;
}
i++;
break if i >= 2;
}
}
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = pos.xy;
`
: ''
}
return vec2u(1);
}
`;
// No storage writes occur.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === 0;
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number) => {
return 0;
},
},
],
}
);
};
// No fragment outputs occur.
const fbChecker = (a: Uint32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === kWidth * kHeight;
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
return kWidth * kHeight;
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});
g.test('uniform_read_loop')
.desc('Test that helpers read a uniform value in a loop')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
discard;
for (var i = 0u; i < uniformValues[0].x; i++) {
}
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = pos.xy;
`
: ''
}
return vec2u(1);
}
`;
// No storage writes occur.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === 0;
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number) => {
return 0;
},
},
],
}
);
};
// No fragment outputs occur.
const fbChecker = (a: Uint32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
return value === kWidth * kHeight;
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
return kWidth * kHeight;
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});
g.test('derivatives')
.desc('Test that derivatives are correct in the presence of discard')
.params(u => u.combine('useStorageBuffers', [false, true]))
.fn(t => {
const { useStorageBuffers } = t.params;
const code = `
${kSharedCode}
@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
let ipos = vec2i(pos.xy);
let lsb = ipos & vec2(0x1);
let left_sel = select(2, 4, lsb.y == 1);
let right_sel = select(1, 3, lsb.y == 1);
let uidx = select(left_sel, right_sel, lsb.x == 1);
if ((lsb.x | lsb.y) & 0x1) == 0 {
discard;
}
let v = uniformValues[uidx].x;
let dx = dpdx(f32(v));
let dy = dpdy(f32(v));
${
useStorageBuffers
? `
let idx = atomicAdd(&atomicIndex, 1);
output[idx] = vec2(dx, dy);
return vec2u(idx);
`
: `
return bitcast<vec2u>(vec2f(dx, dy));
`
}
}
`;
// One pixel per quad is discarded. The derivatives values are always the same +/- 3.
const dataChecker = (a: Float32Array) => {
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
if (idx < (3 * (2 * kWidth * kHeight)) / 4) {
return value === -3 || value === 3;
} else {
return value === 0;
}
},
{
predicatePrinter: [
{
leftHeader: 'data exp ==',
getValueForCell: (idx: number) => {
if (idx < (3 * (2 * kWidth * kHeight)) / 4) {
return '+/- 3';
} else {
return 0;
}
},
},
],
}
);
};
// 3/4 of the fragments are written.
const fbChecker = (a: Uint32Array) => {
const discarded = (x: number, y: number) => ((x | y) & 0x1) === 0;
const asF32 = new Float32Array(1);
const asU32 = new Uint32Array(asF32.buffer);
return checkElementsPassPredicate(
a,
(idx: number, value: number | bigint) => {
const fragId = (idx / 2) | 0;
const x = fragId % kWidth;
const y = (fragId / kWidth) | 0;
if (discarded(x, y)) {
return value === kWidth * kHeight;
} else {
if (useStorageBuffers) {
return value < (3 * (kWidth * kHeight)) / 4;
} else {
asU32[0] = value as number;
const v = asF32[0];
return v === -3 || v === 3;
}
}
},
{
predicatePrinter: [
{
leftHeader: 'fb exp ==',
getValueForCell: (idx: number) => {
const x = idx % kWidth;
const y = Math.floor(idx / kWidth);
if (((x | y) & 0x1) === 0) {
return kWidth * kHeight;
} else {
return useStorageBuffers ? 'any' : '+/- 3';
}
},
},
],
}
);
};
drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
});