export const description = `
Validation for attachment compatibility between render passes, bundles, and pipelines
TODO(#3363): Make this into a MaxLimitTest and increase kMaxColorAttachments.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { range } from '../../../../common/util/util.js';
import { getDefaultLimits, kTextureSampleCounts } from '../../../capability_info.js';
import {
kSizedDepthStencilFormats,
kUnsizedDepthStencilFormats,
filterFormatsByFeature,
getFeaturesForFormats,
isDepthTextureFormat,
isStencilTextureFormat,
kPossibleColorRenderableTextureFormats,
} from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
const kMaxColorAttachments = getDefaultLimits('core').maxColorAttachments.default;
const kColorAttachmentCounts = range(kMaxColorAttachments, i => i + 1);
const kColorAttachments = kColorAttachmentCounts
.map(count => {
if (count === 1) {
return [[1]];
}
if (count === 2) {
return [
[1, 1],
[0, 1],
[1, 0],
];
}
let result = [new Array<boolean>(count).fill(true)];
result = result.concat(
range(count, i => {
const r = new Array<boolean>(count).fill(true);
r[i] = false;
return r;
})
);
if (count <= 4) {
result = result.concat(
range(count - 1, i => {
const cases = [] as boolean[][];
for (let j = i + 1; j < count; j++) {
const r = new Array<boolean>(count).fill(true);
r[i] = false;
r[j] = false;
cases.push(r);
}
return cases;
}).flat()
);
}
return result;
})
.flat() as boolean[][];
const kDepthStencilAttachmentFormats = [
undefined,
...kSizedDepthStencilFormats,
...kUnsizedDepthStencilFormats,
] as const;
const kFeaturesForDepthStencilAttachmentFormats = getFeaturesForFormats([
...kSizedDepthStencilFormats,
...kUnsizedDepthStencilFormats,
]);
class F extends AllFeaturesMaxLimitsGPUTest {
createAttachmentTextureView(format: GPUTextureFormat, sampleCount?: number) {
return this.createTextureTracked({
size: [16, 16, 1],
format,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount,
}).createView();
}
createColorAttachment(
format: GPUTextureFormat | null,
sampleCount?: number
): GPURenderPassColorAttachment | null {
return format === null
? null
: {
view: this.createAttachmentTextureView(format, sampleCount),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
};
}
createDepthAttachment(
format: GPUTextureFormat,
sampleCount?: number
): GPURenderPassDepthStencilAttachment {
const attachment: GPURenderPassDepthStencilAttachment = {
view: this.createAttachmentTextureView(format, sampleCount),
};
if (isDepthTextureFormat(format)) {
attachment.depthClearValue = 0;
attachment.depthLoadOp = 'clear';
attachment.depthStoreOp = 'discard';
}
if (isStencilTextureFormat(format)) {
attachment.stencilClearValue = 1;
attachment.stencilLoadOp = 'clear';
attachment.stencilStoreOp = 'discard';
}
return attachment;
}
createRenderPipeline(
targets: Iterable<GPUColorTargetState | null>,
depthStencil?: GPUDepthStencilState,
sampleCount?: number,
cullMode?: GPUCullMode
) {
return this.device.createRenderPipeline({
layout: 'auto',
vertex: {
module: this.device.createShaderModule({
code: `
@vertex fn main() -> @builtin(position) vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
}`,
}),
entryPoint: 'main',
},
fragment: {
module: this.device.createShaderModule({
code: '@fragment fn main() {}',
}),
entryPoint: 'main',
targets,
},
primitive: { topology: 'triangle-list', cullMode },
depthStencil,
multisample: { count: sampleCount },
});
}
}
export const g = makeTestGroup(F);
g.test('render_pass_and_bundle,color_format')
.desc('Test that color attachment formats in render passes and bundles must match.')
.paramsSubcasesOnly(u =>
u
.combine('passFormat', kPossibleColorRenderableTextureFormats)
.combine('bundleFormat', kPossibleColorRenderableTextureFormats)
)
.fn(t => {
const { passFormat, bundleFormat } = t.params;
t.skipIfTextureFormatNotSupported(passFormat, bundleFormat);
t.skipIfTextureFormatNotUsableAsRenderAttachment(passFormat, bundleFormat);
const bundleEncoder = t.device.createRenderBundleEncoder({
colorFormats: [bundleFormat],
});
const bundle = bundleEncoder.finish();
const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
const pass = encoder.beginRenderPass({
colorAttachments: [t.createColorAttachment(passFormat)],
});
pass.executeBundles([bundle]);
pass.end();
validateFinishAndSubmit(passFormat === bundleFormat, true);
});
g.test('render_pass_and_bundle,color_count')
.desc(
`
Test that the number of color attachments in render passes and bundles must match.
`
)
.paramsSubcasesOnly(u =>
u
.combine('passCount', kColorAttachmentCounts)
.combine('bundleCount', kColorAttachmentCounts)
)
.fn(t => {
const { passCount, bundleCount } = t.params;
const { maxColorAttachments } = t.device.limits;
t.skipIf(
passCount > maxColorAttachments,
`passCount: ${passCount} > maxColorAttachments for device: ${maxColorAttachments}`
);
t.skipIf(
bundleCount > maxColorAttachments,
`bundleCount: ${bundleCount} > maxColorAttachments for device: ${maxColorAttachments}`
);
const bundleEncoder = t.device.createRenderBundleEncoder({
colorFormats: range(bundleCount, () => 'rgba8uint'),
});
const bundle = bundleEncoder.finish();
const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
const pass = encoder.beginRenderPass({
colorAttachments: range(passCount, () => t.createColorAttachment('rgba8uint')),
});
pass.executeBundles([bundle]);
pass.end();
validateFinishAndSubmit(passCount === bundleCount, true);
});
g.test('render_pass_and_bundle,color_sparse')
.desc(
`
Test that each of color attachments in render passes and bundles must match.
`
)
.params(u =>
u
.combine('attachmentCount', kColorAttachmentCounts)
.beginSubcases()
.expand('iPass', p =>
range(kColorAttachments.length, i => i).filter(
i => kColorAttachments[i].length === p.attachmentCount
)
)
.expand('iBundle', p =>
range(kColorAttachments.length, i => i).filter(
i => kColorAttachments[i].length === p.attachmentCount
)
)
)
.fn(t => {
const passAttachments = kColorAttachments[t.params.iPass];
const bundleAttachments = kColorAttachments[t.params.iBundle];
const { maxColorAttachments } = t.device.limits;
t.skipIf(passAttachments.length > maxColorAttachments);
t.skipIf(bundleAttachments.length > maxColorAttachments);
const colorFormats = bundleAttachments.map(i => (i ? 'rgba8uint' : null));
const bundleEncoder = t.device.createRenderBundleEncoder({
colorFormats,
});
const bundle = bundleEncoder.finish();
const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
const colorAttachments = passAttachments.map(i =>
t.createColorAttachment(i ? 'rgba8uint' : null)
);
const pass = encoder.beginRenderPass({
colorAttachments,
});
pass.executeBundles([bundle]);
pass.end();
validateFinishAndSubmit(
passAttachments.every((v, i) => v === bundleAttachments[i]),
true
);
});
g.test('render_pass_and_bundle,depth_format')
.desc('Test that the depth attachment format in render passes and bundles must match.')
.params(u =>
u
.combine('passFeature', kFeaturesForDepthStencilAttachmentFormats)
.combine('bundleFeature', kFeaturesForDepthStencilAttachmentFormats)
.beginSubcases()
.expand('passFormat', ({ passFeature }) =>
filterFormatsByFeature(passFeature, kDepthStencilAttachmentFormats)
)
.expand('bundleFormat', ({ bundleFeature }) =>
filterFormatsByFeature(bundleFeature, kDepthStencilAttachmentFormats)
)
)
.fn(t => {
const { passFormat, bundleFormat } = t.params;
t.skipIfTextureFormatNotSupported(passFormat, bundleFormat);
const bundleEncoder = t.device.createRenderBundleEncoder({
colorFormats: ['rgba8unorm'],
depthStencilFormat: bundleFormat,
});
const bundle = bundleEncoder.finish();
const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
const pass = encoder.beginRenderPass({
colorAttachments: [t.createColorAttachment('rgba8unorm')],
depthStencilAttachment:
passFormat !== undefined ? t.createDepthAttachment(passFormat) : undefined,
});
pass.executeBundles([bundle]);
pass.end();
validateFinishAndSubmit(passFormat === bundleFormat, true);
});
g.test('render_pass_and_bundle,sample_count')
.desc('Test that the sample count in render passes and bundles must match.')
.paramsSubcasesOnly(u =>
u
.combine('renderSampleCount', kTextureSampleCounts)
.combine('bundleSampleCount', kTextureSampleCounts)
)
.fn(t => {
const { renderSampleCount, bundleSampleCount } = t.params;
const bundleEncoder = t.device.createRenderBundleEncoder({
colorFormats: ['rgba8unorm'],
sampleCount: bundleSampleCount,
});
const bundle = bundleEncoder.finish();
const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
const pass = encoder.beginRenderPass({
colorAttachments: [t.createColorAttachment('rgba8unorm', renderSampleCount)],
});
pass.executeBundles([bundle]);
pass.end();
validateFinishAndSubmit(renderSampleCount === bundleSampleCount, true);
});
g.test('render_pass_and_bundle,device_mismatch')
.desc('Test that render passes cannot be called with bundles created from another device.')
.paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
.beforeAllSubcases(t => t.usesMismatchedDevice())
.fn(t => {
const { mismatched } = t.params;
const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
const format = 'r16uint';
const bundleEncoder = sourceDevice.createRenderBundleEncoder({
colorFormats: [format],
});
const bundle = bundleEncoder.finish();
const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
const pass = encoder.beginRenderPass({
colorAttachments: [t.createColorAttachment(format)],
});
pass.executeBundles([bundle]);
pass.end();
validateFinishAndSubmit(!mismatched, true);
});
g.test('render_pass_or_bundle_and_pipeline,color_format')
.desc(
`
Test that color attachment formats in render passes or bundles match the pipeline color format.
`
)
.params(u =>
u
.combine('encoderType', ['render pass', 'render bundle'] as const)
.beginSubcases()
.combine('encoderFormat', kPossibleColorRenderableTextureFormats)
.combine('pipelineFormat', kPossibleColorRenderableTextureFormats)
)
.fn(t => {
const { encoderType, encoderFormat, pipelineFormat } = t.params;
t.skipIfTextureFormatNotSupported(encoderFormat, pipelineFormat);
t.skipIfTextureFormatNotUsableAsRenderAttachment(encoderFormat, pipelineFormat);
const pipeline = t.createRenderPipeline([{ format: pipelineFormat, writeMask: 0 }]);
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
attachmentInfo: { colorFormats: [encoderFormat] },
});
encoder.setPipeline(pipeline);
validateFinishAndSubmit(encoderFormat === pipelineFormat, true);
});
g.test('render_pass_or_bundle_and_pipeline,color_count')
.desc(
`
Test that the number of color attachments in render passes or bundles match the pipeline color
count.
`
)
.params(u =>
u
.combine('encoderType', ['render pass', 'render bundle'] as const)
.beginSubcases()
.combine('encoderCount', kColorAttachmentCounts)
.combine('pipelineCount', kColorAttachmentCounts)
)
.fn(t => {
const { encoderType, encoderCount, pipelineCount } = t.params;
const { maxColorAttachments } = t.device.limits;
t.skipIf(
pipelineCount > maxColorAttachments,
`pipelineCount: ${pipelineCount} > maxColorAttachments for device: ${maxColorAttachments}`
);
t.skipIf(
encoderCount > maxColorAttachments,
`encoderCount: ${encoderCount} > maxColorAttachments for device: ${maxColorAttachments}`
);
const pipeline = t.createRenderPipeline(
range(pipelineCount, () => ({ format: 'rgba8uint', writeMask: 0 }))
);
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
attachmentInfo: { colorFormats: range(encoderCount, () => 'rgba8uint') },
});
encoder.setPipeline(pipeline);
validateFinishAndSubmit(encoderCount === pipelineCount, true);
});
g.test('render_pass_or_bundle_and_pipeline,color_sparse')
.desc(
`
Test that each of color attachments in render passes or bundles match that of the pipeline.
`
)
.params(u =>
u
.combine('encoderType', ['render pass', 'render bundle'] as const)
.combine('attachmentCount', kColorAttachmentCounts)
.beginSubcases()
.expand('iEncoder', p =>
range(kColorAttachments.length, i => i).filter(
i => kColorAttachments[i].length === p.attachmentCount
)
)
.expand('iPipeline', p =>
range(kColorAttachments.length, i => i).filter(
i => kColorAttachments[i].length === p.attachmentCount
)
)
)
.fn(t => {
const { encoderType } = t.params;
const encoderAttachments = kColorAttachments[t.params.iEncoder];
const pipelineAttachments = kColorAttachments[t.params.iPipeline];
const { maxColorAttachments } = t.device.limits;
t.skipIf(encoderAttachments.length > maxColorAttachments);
t.skipIf(pipelineAttachments.length > maxColorAttachments);
const colorTargets = pipelineAttachments.map(i =>
i ? ({ format: 'rgba8uint', writeMask: 0 } as GPUColorTargetState) : null
);
const pipeline = t.createRenderPipeline(colorTargets);
const colorFormats = encoderAttachments.map(i => (i ? 'rgba8uint' : null));
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
attachmentInfo: { colorFormats },
});
encoder.setPipeline(pipeline);
validateFinishAndSubmit(
encoderAttachments.every((v, i) => v === pipelineAttachments[i]),
true
);
});
g.test('render_pass_or_bundle_and_pipeline,depth_format')
.desc(
`
Test that the depth attachment format in render passes or bundles match the pipeline depth format.
`
)
.params(u =>
u
.combine('encoderType', ['render pass', 'render bundle'] as const)
.combine('encoderFormatFeature', kFeaturesForDepthStencilAttachmentFormats)
.combine('pipelineFormatFeature', kFeaturesForDepthStencilAttachmentFormats)
.beginSubcases()
.expand('encoderFormat', ({ encoderFormatFeature }) =>
filterFormatsByFeature(encoderFormatFeature, kDepthStencilAttachmentFormats)
)
.expand('pipelineFormat', ({ pipelineFormatFeature }) =>
filterFormatsByFeature(pipelineFormatFeature, kDepthStencilAttachmentFormats)
)
)
.fn(t => {
const { encoderType, encoderFormat, pipelineFormat } = t.params;
t.skipIfTextureFormatNotSupported(encoderFormat, pipelineFormat);
const pipeline = t.createRenderPipeline(
[{ format: 'rgba8unorm', writeMask: 0 }],
pipelineFormat !== undefined
? { format: pipelineFormat, depthCompare: 'always', depthWriteEnabled: false }
: undefined
);
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
attachmentInfo: { colorFormats: ['rgba8unorm'], depthStencilFormat: encoderFormat },
});
encoder.setPipeline(pipeline);
validateFinishAndSubmit(encoderFormat === pipelineFormat, true);
});
const kStencilFaceStates = [
{ failOp: 'keep', depthFailOp: 'keep', passOp: 'keep' },
{ failOp: 'zero', depthFailOp: 'zero', passOp: 'zero' },
] as GPUStencilFaceState[];
g.test('render_pass_or_bundle_and_pipeline,depth_stencil_read_only_write_state')
.desc(
`
Test that the depth stencil read only state in render passes or bundles is compatible with the depth stencil write state of the pipeline.
`
)
.params(u =>
u
.combine('encoderType', ['render pass', 'render bundle'] as const)
.combine('format', kDepthStencilAttachmentFormats)
.beginSubcases()
.combine('depthReadOnly', [false, true])
.combine('stencilReadOnly', [false, true])
.combine('stencilFront', kStencilFaceStates)
.combine('stencilBack', kStencilFaceStates)
.combine('depthWriteEnabled', [false, true])
.combine('stencilWriteMask', [0, 0xffffffff])
.combine('cullMode', ['none', 'front', 'back'] as const)
.filter(p => {
if (p.format) {
if (!isDepthTextureFormat(p.format) && p.depthWriteEnabled) {
return false;
}
if (
!isStencilTextureFormat(p.format) &&
(p.stencilFront.failOp !== 'keep' || p.stencilBack.failOp !== 'keep')
) {
return false;
}
}
return true;
})
)
.fn(t => {
const {
encoderType,
format,
depthReadOnly,
stencilReadOnly,
depthWriteEnabled,
stencilWriteMask,
cullMode,
stencilFront,
stencilBack,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
const pipeline = t.createRenderPipeline(
[{ format: 'rgba8unorm', writeMask: 0 }],
format === undefined
? undefined
: {
format,
depthWriteEnabled,
depthCompare: 'always',
stencilWriteMask,
stencilFront,
stencilBack,
},
1,
cullMode
);
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
attachmentInfo: {
colorFormats: ['rgba8unorm'],
depthStencilFormat: format,
depthReadOnly,
stencilReadOnly,
},
});
encoder.setPipeline(pipeline);
let writesDepth = false;
let writesStencil = false;
if (format) {
writesDepth = depthWriteEnabled;
if (stencilWriteMask !== 0) {
if (
cullMode !== 'front' &&
(stencilFront.passOp !== 'keep' ||
stencilFront.depthFailOp !== 'keep' ||
stencilFront.failOp !== 'keep')
) {
writesStencil = true;
}
if (
cullMode !== 'back' &&
(stencilBack.passOp !== 'keep' ||
stencilBack.depthFailOp !== 'keep' ||
stencilBack.failOp !== 'keep')
) {
writesStencil = true;
}
}
}
let isValid = true;
if (writesDepth) {
isValid &&= !depthReadOnly;
}
if (writesStencil) {
isValid &&= !stencilReadOnly;
}
validateFinishAndSubmit(isValid, true);
});
g.test('render_pass_or_bundle_and_pipeline,sample_count')
.desc(
`
Test that the sample count in render passes or bundles match the pipeline sample count for both color texture and depthstencil texture.
`
)
.params(u =>
u
.combine('encoderType', ['render pass', 'render bundle'] as const)
.combine('attachmentType', ['color', 'depthstencil'] as const)
.beginSubcases()
.combine('encoderSampleCount', kTextureSampleCounts)
.combine('pipelineSampleCount', kTextureSampleCounts)
)
.fn(t => {
const { encoderType, attachmentType, encoderSampleCount, pipelineSampleCount } = t.params;
const colorFormats = attachmentType === 'color' ? ['rgba8unorm' as const] : [];
const depthStencilFormat =
attachmentType === 'depthstencil' ? ('depth24plus-stencil8' as const) : undefined;
const pipeline = t.createRenderPipeline(
colorFormats.map(format => ({ format, writeMask: 0 })),
depthStencilFormat
? { format: depthStencilFormat, depthWriteEnabled: false, depthCompare: 'always' }
: undefined,
pipelineSampleCount
);
const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
attachmentInfo: { colorFormats, depthStencilFormat, sampleCount: encoderSampleCount },
});
encoder.setPipeline(pipeline);
validateFinishAndSubmit(encoderSampleCount === pipelineSampleCount, true);
});