export const description = `
Tests for depth clipping, depth clamping (at various points in the pipeline), and maybe extended
depth ranges as well.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import {
canCopyFromAspectOfTextureFormat,
getBlockInfoForTextureFormat,
isStencilTextureFormat,
kDepthTextureFormats,
} from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import {
checkElementsBetween,
checkElementsPassPredicate,
CheckElementsSupplementalTableRows,
} from '../../../util/check_contents.js';
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
g.test('depth_clamp_and_clip')
.desc(
`
Depth written to the depth attachment should always be in the range of the viewport depth,
even if it was written by the fragment shader (using frag_depth). If depth clipping is enabled,
primitives should be clipped to the viewport depth before rasterization; if not, these fragments
should be rasterized, and the fragment shader should receive out-of-viewport position.z values.
To test this, render NxN points, with N vertex depth values, by (if writeDepth=true) N
frag_depth values with the viewport depth set to [0.25,0.75].
While rendering, check the fragment input position.z has the expected value (for all fragments that
were produced by the rasterizer) by writing the diff to a storage buffer, which is later checked to
be all (near) 0.
Then, run another pass (which outputs every point at z=0.5 to avoid clipping) to verify the depth
buffer contents by outputting the expected depth with depthCompare:'not-equal': any fragments that
have unexpected values then get drawn to the color buffer, which is later checked to be empty.`
)
.params(u =>
u
.combine('format', kDepthTextureFormats)
.combine('unclippedDepth', [undefined, false, true])
.combine('writeDepth', [false, true])
.combine('multisampled', [false, true])
)
.fn(async t => {
const { format, unclippedDepth, writeDepth, multisampled } = t.params;
t.skipIfTextureFormatNotSupported(format);
if (unclippedDepth) {
t.skipIfDeviceDoesNotHaveFeature('depth-clip-control');
}
const kNumDepthValues = 8;
const kNumTestPoints = kNumDepthValues * kNumDepthValues;
const kViewportMinDepth = 0.25;
const kViewportMaxDepth = 0.75;
const shaderSource = `
// Test depths, with viewport range corresponding to [0,1].
var<private> kDepths: array<f32, ${kNumDepthValues}> = array<f32, ${kNumDepthValues}>(
-1.0, -0.5, 0.0, 0.25, 0.75, 1.0, 1.5, 2.0);
const vpMin: f32 = ${kViewportMinDepth};
const vpMax: f32 = ${kViewportMaxDepth};
// Draw the points in a straight horizontal row, one per pixel.
fn vertexX(idx: u32) -> f32 {
return (f32(idx) + 0.5) * 2.0 / ${kNumTestPoints}.0 - 1.0;
}
// Test vertex shader's position.z output.
// Here, the viewport range corresponds to position.z in [0,1].
fn vertexZ(idx: u32) -> f32 {
return kDepths[idx / ${kNumDepthValues}u];
}
// Test fragment shader's expected position.z input.
// Here, the viewport range corresponds to position.z in [vpMin,vpMax], but
// unclipped values extend beyond that range.
fn expectedFragPosZ(idx: u32) -> f32 {
return vpMin + vertexZ(idx) * (vpMax - vpMin);
}
//////// "Test" entry points
struct VFTest {
@builtin(position) pos: vec4<f32>,
@location(0) @interpolate(flat, either) vertexIndex: u32,
};
@vertex
fn vtest(@builtin(vertex_index) idx: u32) -> VFTest {
var vf: VFTest;
vf.pos = vec4<f32>(vertexX(idx), 0.0, vertexZ(idx), 1.0);
vf.vertexIndex = idx;
return vf;
}
struct Output {
// Each fragment (that didn't get clipped) writes into one element of this output.
// (Anything that doesn't get written is already zero.)
fragInputZDiff: array<f32, ${kNumTestPoints}>
};
@group(0) @binding(0) var <storage, read_write> output: Output;
fn checkZ(vf: VFTest) {
output.fragInputZDiff[vf.vertexIndex] = vf.pos.z - expectedFragPosZ(vf.vertexIndex);
}
@fragment
fn ftest_WriteDepth(vf: VFTest) -> @builtin(frag_depth) f32 {
checkZ(vf);
return kDepths[vf.vertexIndex % ${kNumDepthValues}u];
}
@fragment
fn ftest_NoWriteDepth(vf: VFTest) {
checkZ(vf);
}
//////// "Check" entry points
struct VFCheck {
@builtin(position) pos: vec4<f32>,
@location(0) @interpolate(flat, either) vertexIndex: u32,
};
@vertex
fn vcheck(@builtin(vertex_index) idx: u32) -> VFCheck {
var vf: VFCheck;
// Depth=0.5 because we want to render every point, not get clipped.
vf.pos = vec4<f32>(vertexX(idx), 0.0, 0.5, 1.0);
vf.vertexIndex = idx;
return vf;
}
struct FCheck {
@location(0) color: f32,
};
${
multisampled
? '@group(0) @binding(0) var depthTex: texture_multisampled_2d<f32>;'
: '@group(0) @binding(0) var depthTex: texture_2d<f32>;'
}
@fragment
fn fcheck(vf: VFCheck) -> FCheck {
let vertZ = vertexZ(vf.vertexIndex);
let outOfRange = vertZ < 0.0 || vertZ > 1.0;
let expFragPosZ = expectedFragPosZ(vf.vertexIndex);
let writtenDepth = kDepths[vf.vertexIndex % ${kNumDepthValues}u];
let expectedDepthWriteInput = ${writeDepth ? 'writtenDepth' : 'expFragPosZ'};
var expectedDepthBufferValue = clamp(expectedDepthWriteInput, vpMin, vpMax);
if (${!unclippedDepth} && outOfRange) {
// Test fragment should have been clipped; expect the depth attachment to
// have its clear value (0.5).
expectedDepthBufferValue = 0.5;
}
let actualDepthBufferValue = textureLoad(depthTex, vec2u(vf.vertexIndex, 0), 0).r;
let actualVsExpectedDiff = abs(expectedDepthBufferValue - actualDepthBufferValue);
var f: FCheck;
f.color = 1.0; // Color written if the resulting depth is unexpected.
if (actualVsExpectedDiff < 1e-5) {
f.color = 0.0;
}
return f;
}
`;
const module = t.device.createShaderModule({ code: shaderSource });
const testPipeline = t.device.createRenderPipeline({
label: 'testPipeline',
layout: 'auto',
vertex: { module, entryPoint: 'vtest' },
primitive: {
topology: 'point-list',
unclippedDepth,
},
depthStencil: { format, depthWriteEnabled: true, depthCompare: 'always' },
multisample: multisampled ? { count: 4 } : undefined,
fragment: {
module,
entryPoint: writeDepth ? 'ftest_WriteDepth' : 'ftest_NoWriteDepth',
targets: [],
},
});
const checkBindGroupLayout = t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: 'unfilterable-float',
multisampled,
},
},
],
});
const checkPipelineLayout = t.device.createPipelineLayout({
bindGroupLayouts: [checkBindGroupLayout],
});
const checkPipeline = t.device.createRenderPipeline({
label: 'checkPipeline',
layout: checkPipelineLayout,
vertex: { module, entryPoint: 'vcheck' },
primitive: { topology: 'point-list' },
multisample: multisampled ? { count: 4 } : undefined,
fragment: { module, entryPoint: 'fcheck', targets: [{ format: 'r8unorm' }] },
});
const dsTexture = t.createTextureTracked({
format,
size: [kNumTestPoints],
usage:
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_SRC,
sampleCount: multisampled ? 4 : 1,
});
const dsTextureView = dsTexture.createView();
const checkTextureDesc = {
format: 'r8unorm' as const,
size: [kNumTestPoints],
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
};
const checkTexture = t.createTextureTracked(checkTextureDesc);
const checkTextureView = checkTexture.createView();
const checkTextureMSView = multisampled
? t.createTextureTracked({ ...checkTextureDesc, sampleCount: 4 }).createView()
: undefined;
const { bytesPerBlock } = getBlockInfoForTextureFormat(format);
const dsActual =
canCopyFromAspectOfTextureFormat(format, 'depth-only') && !multisampled && bytesPerBlock
? t.createBufferTracked({
size: kNumTestPoints * bytesPerBlock,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
})
: undefined;
const dsExpected =
canCopyFromAspectOfTextureFormat(format, 'depth-only') && !multisampled && bytesPerBlock
? t.createBufferTracked({
size: kNumTestPoints * bytesPerBlock,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
})
: undefined;
const checkBuffer = t.createBufferTracked({
size: kNumTestPoints,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const fragInputZFailedBuffer = t.createBufferTracked({
size: 4 * kNumTestPoints,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const testBindGroup = t.device.createBindGroup({
layout: testPipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: fragInputZFailedBuffer } }],
});
const enc = t.device.createCommandEncoder();
{
const pass = enc.beginRenderPass({
colorAttachments: [],
depthStencilAttachment: {
view: dsTextureView,
depthClearValue: 0.5,
depthLoadOp: 'clear',
depthStoreOp: 'store',
...(isStencilTextureFormat(format) && {
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'discard',
}),
},
});
pass.setPipeline(testPipeline);
pass.setBindGroup(0, testBindGroup);
pass.setViewport(0, 0, kNumTestPoints, 1, kViewportMinDepth, kViewportMaxDepth);
pass.draw(kNumTestPoints);
pass.end();
}
if (dsActual) {
enc.copyTextureToBuffer({ texture: dsTexture, aspect: 'depth-only' }, { buffer: dsActual }, [
kNumTestPoints,
]);
}
{
const checkBindGroup = t.device.createBindGroup({
layout: checkBindGroupLayout,
entries: [{ binding: 0, resource: dsTexture.createView({ aspect: 'depth-only' }) }],
});
const clearValue = [0.5, 0.5, 0.5, 0.5];
const pass = enc.beginRenderPass({
colorAttachments: [
checkTextureMSView
? {
view: checkTextureMSView,
resolveTarget: checkTextureView,
clearValue,
loadOp: 'clear',
storeOp: 'discard',
}
: { view: checkTextureView, clearValue, loadOp: 'clear', storeOp: 'store' },
],
});
pass.setPipeline(checkPipeline);
pass.setBindGroup(0, checkBindGroup);
pass.setViewport(0, 0, kNumTestPoints, 1, 0.0, 1.0);
pass.draw(kNumTestPoints);
pass.end();
}
enc.copyTextureToBuffer({ texture: checkTexture }, { buffer: checkBuffer }, [kNumTestPoints]);
if (dsExpected) {
enc.copyTextureToBuffer(
{ texture: dsTexture, aspect: 'depth-only' },
{ buffer: dsExpected },
[kNumTestPoints]
);
}
t.device.queue.submit([enc.finish()]);
t.expectGPUBufferValuesPassCheck(
fragInputZFailedBuffer,
a => checkElementsBetween(a, [() => -1e-5, () => 1e-5]),
{ type: Float32Array, typedLength: kNumTestPoints }
);
const kCheckPassedValue = 0;
const predicatePrinter: CheckElementsSupplementalTableRows = [
{ leftHeader: 'expected ==', getValueForCell: _index => kCheckPassedValue },
];
if (dsActual && dsExpected && format === 'depth32float') {
await Promise.all([dsActual.mapAsync(GPUMapMode.READ), dsExpected.mapAsync(GPUMapMode.READ)]);
const act = new Float32Array(dsActual.getMappedRange());
const exp = new Float32Array(dsExpected.getMappedRange());
predicatePrinter.push(
{ leftHeader: 'act ==', getValueForCell: index => act[index].toFixed(2) },
{ leftHeader: 'exp ==', getValueForCell: index => exp[index].toFixed(2) }
);
}
t.expectGPUBufferValuesPassCheck(
checkBuffer,
a =>
checkElementsPassPredicate(a, (_index, value) => value === kCheckPassedValue, {
predicatePrinter,
}),
{ type: Uint8Array, typedLength: kNumTestPoints, method: 'map' }
);
});
g.test('depth_test_input_clamped')
.desc(
`
Input to the depth test should always be in the range of viewport depth, even if it was written by
the fragment shader (using frag_depth).
To test this, first initialize the depth buffer with N expected values (by writing frag_depth, with
the default viewport). These expected values are clamped by the shader to [0.25, 0.75].
Then, run another pass with the viewport depth set to [0.25,0.75], and output various (unclamped)
frag_depth values from its fragment shader with depthCompare:'not-equal'. These should get clamped;
any fragments that have unexpected values then get drawn to the color buffer, which is later checked
to be empty.`
)
.params(u =>
u
.combine('format', kDepthTextureFormats)
.combine('unclippedDepth', [false, true])
.combine('multisampled', [false, true])
)
.fn(t => {
const { format, unclippedDepth, multisampled } = t.params;
t.skipIfTextureFormatNotSupported(format);
if (unclippedDepth) {
t.skipIfDeviceDoesNotHaveFeature('depth-clip-control');
}
const kNumDepthValues = 8;
const kViewportMinDepth = 0.25;
const kViewportMaxDepth = 0.75;
const shaderSource = `
// Test depths, with viewport range corresponding to [0,1].
var<private> kDepths: array<f32, ${kNumDepthValues}> = array<f32, ${kNumDepthValues}>(
-1.0, -0.5, 0.0, 0.25, 0.75, 1.0, 1.5, 2.0);
const vpMin: f32 = ${kViewportMinDepth};
const vpMax: f32 = ${kViewportMaxDepth};
// Draw the points in a straight horizontal row, one per pixel.
fn vertexX(idx: u32) -> f32 {
return (f32(idx) + 0.5) * 2.0 / ${kNumDepthValues}.0 - 1.0;
}
struct VF {
@builtin(position) pos: vec4<f32>,
@location(0) @interpolate(flat, either) vertexIndex: u32,
};
@vertex
fn vmain(@builtin(vertex_index) idx: u32) -> VF {
var vf: VF;
// Depth=0.5 because we want to render every point, not get clipped.
vf.pos = vec4<f32>(vertexX(idx), 0.0, 0.5, 1.0);
vf.vertexIndex = idx;
return vf;
}
@fragment
fn finit(vf: VF) -> @builtin(frag_depth) f32 {
// Expected values of the ftest pipeline.
return clamp(kDepths[vf.vertexIndex], vpMin, vpMax);
}
struct FTest {
@builtin(frag_depth) depth: f32,
@location(0) color: f32,
};
@fragment
fn ftest(vf: VF) -> FTest {
var f: FTest;
f.depth = kDepths[vf.vertexIndex]; // Should get clamped to the viewport.
f.color = 1.0; // Color written if the resulting depth is unexpected.
return f;
}
`;
const module = t.device.createShaderModule({ code: shaderSource });
const initPipeline = t.device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vmain' },
primitive: { topology: 'point-list' },
depthStencil: { format, depthWriteEnabled: true, depthCompare: 'always' },
multisample: multisampled ? { count: 4 } : undefined,
fragment: { module, entryPoint: 'finit', targets: [] },
});
const testPipeline = t.device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vmain' },
primitive: {
topology: 'point-list',
unclippedDepth,
},
depthStencil: { format, depthCompare: 'not-equal', depthWriteEnabled: false },
multisample: multisampled ? { count: 4 } : undefined,
fragment: { module, entryPoint: 'ftest', targets: [{ format: 'r8unorm' }] },
});
const dsTexture = t.createTextureTracked({
format,
size: [kNumDepthValues],
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
sampleCount: multisampled ? 4 : 1,
});
const dsTextureView = dsTexture.createView();
const testTextureDesc = {
format: 'r8unorm' as const,
size: [kNumDepthValues],
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
};
const testTexture = t.createTextureTracked(testTextureDesc);
const testTextureView = testTexture.createView();
const testTextureMSView = multisampled
? t.createTextureTracked({ ...testTextureDesc, sampleCount: 4 }).createView()
: undefined;
const resultBuffer = t.createBufferTracked({
size: kNumDepthValues,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const enc = t.device.createCommandEncoder();
{
const pass = enc.beginRenderPass({
colorAttachments: [],
depthStencilAttachment: {
view: dsTextureView,
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
...(isStencilTextureFormat(format) && {
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'discard',
}),
},
});
pass.setPipeline(initPipeline);
pass.draw(kNumDepthValues);
pass.end();
}
{
const clearValue = [0, 0, 0, 0];
const pass = enc.beginRenderPass({
colorAttachments: [
testTextureMSView
? {
view: testTextureMSView,
resolveTarget: testTextureView,
clearValue,
loadOp: 'clear',
storeOp: 'discard',
}
: { view: testTextureView, clearValue, loadOp: 'clear', storeOp: 'store' },
],
depthStencilAttachment: {
view: dsTextureView,
depthLoadOp: 'load',
depthStoreOp: 'store',
...(isStencilTextureFormat(format) && {
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'discard',
}),
},
});
pass.setPipeline(testPipeline);
pass.setViewport(0, 0, kNumDepthValues, 1, kViewportMinDepth, kViewportMaxDepth);
pass.draw(kNumDepthValues);
pass.end();
}
enc.copyTextureToBuffer({ texture: testTexture }, { buffer: resultBuffer }, [kNumDepthValues]);
t.device.queue.submit([enc.finish()]);
t.expectGPUBufferValuesEqual(resultBuffer, new Uint8Array(kNumDepthValues), 0, {
method: 'map',
});
});