summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts690
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts1097
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts192
4 files changed, 1980 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt
new file mode 100644
index 0000000000..a5797c2b63
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt
@@ -0,0 +1 @@
+Render pass stuff other than commands (which are in encoding/cmds/).
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts
new file mode 100644
index 0000000000..c0ab23b91c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts
@@ -0,0 +1,690 @@
+export const description = `
+Validation for attachment compatibility between render passes, bundles, and pipelines
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import { kMaxColorAttachmentsToTest, kTextureSampleCounts } from '../../../capability_info.js';
+import {
+ kRegularTextureFormats,
+ kSizedDepthStencilFormats,
+ kUnsizedDepthStencilFormats,
+ kTextureFormatInfo,
+ filterFormatsByFeature,
+ getFeaturesForFormats,
+} from '../../../format_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+const kColorAttachmentCounts = range(kMaxColorAttachmentsToTest, i => i + 1);
+const kColorAttachments = kColorAttachmentCounts
+ .map(count => {
+ // generate cases with 0..1 null attachments at different location
+ // e.g. count == 2
+ // [
+ // [1, 1],
+ // [0, 1],
+ // [1, 0],
+ // ]
+ // 0 (false) means null attachment, 1 (true) means non-null attachment, at the slot
+
+ // Special cases: we need at least a color attachment, when we don't have depth stencil attachment
+ if (count === 1) {
+ return [[1]];
+ }
+ if (count === 2) {
+ return [
+ [1, 1],
+ [0, 1],
+ [1, 0],
+ ];
+ }
+
+ // [1, 1, ..., 1]: all color attachment are used
+ let result = [new Array<boolean>(count).fill(true)];
+
+ // [1, 0, 1, ..., 1]: generate cases with one null attachment at different locations
+ result = result.concat(
+ range(count, i => {
+ const r = new Array<boolean>(count).fill(true);
+ r[i] = false;
+ return r;
+ })
+ );
+
+ // [1, 0, 1, ..., 0, 1]: generate cases with two null attachments at different locations
+ // To reduce test run time, limit the attachment count to <= 4
+ 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 ValidationTest {
+ createAttachmentTextureView(format: GPUTextureFormat, sampleCount?: number) {
+ return this.device
+ .createTexture({
+ // Size matching the "arbitrary" size used by ValidationTest helpers.
+ 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 (kTextureFormatInfo[format].depth) {
+ attachment.depthClearValue = 0;
+ attachment.depthLoadOp = 'clear';
+ attachment.depthStoreOp = 'discard';
+ }
+ if (kTextureFormatInfo[format].stencil) {
+ 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);
+
+const kColorAttachmentFormats = kRegularTextureFormats.filter(
+ format => !!kTextureFormatInfo[format].colorRender
+);
+
+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', kColorAttachmentFormats)
+ .combine('bundleFormat', kColorAttachmentFormats)
+ )
+ .fn(t => {
+ const { passFormat, bundleFormat } = t.params;
+
+ t.skipIfTextureFormatNotSupported(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 //
+ // introduce attachmentCount to make it easier to split the test
+ .combine('attachmentCount', kColorAttachmentCounts)
+ .beginSubcases()
+ .combine('passAttachments', kColorAttachments)
+ .combine('bundleAttachments', kColorAttachments)
+ .filter(
+ p =>
+ p.attachmentCount === p.passAttachments.length &&
+ p.attachmentCount === p.bundleAttachments.length
+ )
+ )
+ .fn(t => {
+ const { passAttachments, bundleAttachments } = t.params;
+
+ const { maxColorAttachments } = t.device.limits;
+ t.skipIf(
+ passAttachments.length > maxColorAttachments,
+ `num passAttachments: ${passAttachments.length} > maxColorAttachments for device: ${maxColorAttachments}`
+ );
+ t.skipIf(
+ bundleAttachments.length > maxColorAttachments,
+ `num bundleAttachments: ${bundleAttachments.length} > maxColorAttachments for device: ${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)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { passFeature, bundleFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([passFeature, bundleFeature]);
+ })
+ .fn(t => {
+ const { passFormat, bundleFormat } = t.params;
+
+ 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.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const format = 'r16float';
+ 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', kColorAttachmentFormats)
+ .combine('pipelineFormat', kColorAttachmentFormats)
+ )
+ .fn(t => {
+ const { encoderType, encoderFormat, pipelineFormat } = t.params;
+
+ t.skipIfTextureFormatNotSupported(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)
+ // introduce attachmentCount to make it easier to split the test
+ .combine('attachmentCount', kColorAttachmentCounts)
+ .beginSubcases()
+ .combine('encoderAttachments', kColorAttachments)
+ .combine('pipelineAttachments', kColorAttachments)
+ .filter(
+ p =>
+ p.attachmentCount === p.encoderAttachments.length &&
+ p.attachmentCount === p.pipelineAttachments.length
+ )
+ )
+ .fn(t => {
+ const { encoderType, encoderAttachments, pipelineAttachments } = t.params;
+ const { maxColorAttachments } = t.device.limits;
+ t.skipIf(
+ encoderAttachments.length > maxColorAttachments,
+ `num encoderAttachments: ${encoderAttachments.length} > maxColorAttachments for device: ${maxColorAttachments}`
+ );
+ t.skipIf(
+ pipelineAttachments.length > maxColorAttachments,
+ `num pipelineAttachments: ${pipelineAttachments.length} > maxColorAttachments for device: ${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)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { encoderFormatFeature, pipelineFormatFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([encoderFormatFeature, pipelineFormatFeature]);
+ })
+ .fn(t => {
+ const { encoderType, encoderFormat, pipelineFormat } = t.params;
+
+ 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()
+ // pass/bundle state
+ .combine('depthReadOnly', [false, true])
+ .combine('stencilReadOnly', [false, true])
+ .combine('stencilFront', kStencilFaceStates)
+ .combine('stencilBack', kStencilFaceStates)
+ // pipeline state
+ .combine('depthWriteEnabled', [false, true])
+ .combine('stencilWriteMask', [0, 0xffffffff])
+ .combine('cullMode', ['none', 'front', 'back'] as const)
+ .filter(p => {
+ if (p.format) {
+ const depthStencilInfo = kTextureFormatInfo[p.format];
+ // For combined depth/stencil formats the depth and stencil read only state must match
+ // in order to create a valid render bundle or render pass.
+ if (depthStencilInfo.depth && depthStencilInfo.stencil) {
+ if (p.depthReadOnly !== p.stencilReadOnly) {
+ return false;
+ }
+ }
+ // If the format has no depth aspect, the depthReadOnly, depthWriteEnabled of the pipeline must not be true
+ // in order to create a valid render pipeline.
+ if (!depthStencilInfo.depth && p.depthWriteEnabled) {
+ return false;
+ }
+ // If the format has no stencil aspect, the stencil state operation must be 'keep'
+ // in order to create a valid render pipeline.
+ if (
+ !depthStencilInfo.stencil &&
+ (p.stencilFront.failOp !== 'keep' || p.stencilBack.failOp !== 'keep')
+ ) {
+ return false;
+ }
+ }
+ // No depthStencil attachment
+ return true;
+ })
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(t => {
+ const {
+ encoderType,
+ format,
+ depthReadOnly,
+ stencilReadOnly,
+ depthWriteEnabled,
+ stencilWriteMask,
+ cullMode,
+ stencilFront,
+ stencilBack,
+ } = t.params;
+
+ 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);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts
new file mode 100644
index 0000000000..9713beea52
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts
@@ -0,0 +1,1097 @@
+export const description = `
+render pass descriptor validation tests.
+
+TODO: review for completeness
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import { kMaxColorAttachmentsToTest, kQueryTypes } from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import {
+ computeBytesPerSampleFromFormats,
+ kDepthStencilFormats,
+ kRenderableColorTextureFormats,
+ kTextureFormatInfo,
+} from '../../../format_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+class F extends ValidationTest {
+ createTexture(
+ options: {
+ format?: GPUTextureFormat;
+ width?: number;
+ height?: number;
+ arrayLayerCount?: number;
+ mipLevelCount?: number;
+ sampleCount?: number;
+ usage?: GPUTextureUsageFlags;
+ } = {}
+ ): GPUTexture {
+ const {
+ format = 'rgba8unorm',
+ width = 16,
+ height = 16,
+ arrayLayerCount = 1,
+ mipLevelCount = 1,
+ sampleCount = 1,
+ usage = GPUTextureUsage.RENDER_ATTACHMENT,
+ } = options;
+
+ return this.device.createTexture({
+ size: { width, height, depthOrArrayLayers: arrayLayerCount },
+ format,
+ mipLevelCount,
+ sampleCount,
+ usage,
+ });
+ }
+
+ getColorAttachment(
+ texture: GPUTexture,
+ textureViewDescriptor?: GPUTextureViewDescriptor
+ ): GPURenderPassColorAttachment {
+ const view = texture.createView(textureViewDescriptor);
+
+ return {
+ view,
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ };
+ }
+
+ getDepthStencilAttachment(
+ texture: GPUTexture,
+ textureViewDescriptor?: GPUTextureViewDescriptor
+ ): GPURenderPassDepthStencilAttachment {
+ const view = texture.createView(textureViewDescriptor);
+
+ return {
+ view,
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilClearValue: 0,
+ stencilLoadOp: 'clear',
+ stencilStoreOp: 'store',
+ };
+ }
+
+ tryRenderPass(success: boolean, descriptor: GPURenderPassDescriptor): void {
+ const commandEncoder = this.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass(descriptor);
+ renderPass.end();
+
+ this.expectValidationError(() => {
+ commandEncoder.finish();
+ }, !success);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('attachments,one_color_attachment')
+ .desc(`Test that a render pass works with only one color attachment.`)
+ .fn(t => {
+ const colorTexture = t.createTexture({ format: 'rgba8unorm' });
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ };
+
+ t.tryRenderPass(true, descriptor);
+ });
+
+g.test('attachments,one_depth_stencil_attachment')
+ .desc(`Test that a render pass works with only one depthStencil attachment.`)
+ .fn(t => {
+ const depthStencilTexture = t.createTexture({ format: 'depth24plus-stencil8' });
+ const descriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ });
+
+g.test('color_attachments,empty')
+ .desc(
+ `
+ Test that when colorAttachments has all values be 'undefined' or the sequence is empty, the
+ depthStencilAttachment must not be 'undefined'.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('unclampedColorAttachments', [
+ [],
+ [undefined],
+ [undefined, undefined],
+ new Array(8).fill(undefined),
+ [{ format: 'rgba8unorm' }],
+ ])
+ .combine('hasDepthStencilAttachment', [false, true])
+ )
+ .fn(t => {
+ const { unclampedColorAttachments, hasDepthStencilAttachment } = t.params;
+ const colorAttachments = unclampedColorAttachments.slice(
+ 0,
+ t.device.limits.maxColorAttachments
+ );
+
+ let isEmptyColorTargets = true;
+ for (let i = 0; i < colorAttachments.length; i++) {
+ if (colorAttachments[i] !== undefined) {
+ isEmptyColorTargets = false;
+ const colorTexture = t.createTexture();
+ colorAttachments[i] = t.getColorAttachment(colorTexture);
+ }
+ }
+
+ const _success = !isEmptyColorTargets || hasDepthStencilAttachment;
+ t.tryRenderPass(_success, {
+ colorAttachments,
+ depthStencilAttachment: hasDepthStencilAttachment
+ ? t.getDepthStencilAttachment(t.createTexture({ format: 'depth24plus-stencil8' }))
+ : undefined,
+ });
+ });
+
+g.test('color_attachments,limits,maxColorAttachments')
+ .desc(
+ `
+ Test that the out of bound of color attachment indexes are handled.
+ - a validation error is generated when color attachments exceed the maximum limit(8).
+ `
+ )
+ .paramsSimple([
+ { colorAttachmentsCountVariant: { mult: 1, add: 0 }, _success: true }, // Control case
+ { colorAttachmentsCountVariant: { mult: 1, add: 1 }, _success: false }, // Out of bounds
+ ])
+ .fn(t => {
+ const { colorAttachmentsCountVariant, _success } = t.params;
+ const colorAttachmentsCount = t.makeLimitVariant(
+ 'maxColorAttachments',
+ colorAttachmentsCountVariant
+ );
+
+ const colorAttachments = [];
+ for (let i = 0; i < colorAttachmentsCount; i++) {
+ const colorTexture = t.createTexture({ format: 'r8unorm' });
+ colorAttachments.push(t.getColorAttachment(colorTexture));
+ }
+
+ t.tryRenderPass(_success, { colorAttachments });
+ });
+
+g.test('color_attachments,limits,maxColorAttachmentBytesPerSample,aligned')
+ .desc(
+ `
+ Test that the total bytes per sample of the formats of the color attachments must be no greater
+ than maxColorAttachmentBytesPerSample when the components are aligned (same format).
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine(
+ 'attachmentCount',
+ range(kMaxColorAttachmentsToTest, i => i + 1)
+ )
+ )
+ .beforeAllSubcases(t => {
+ t.skipIfTextureFormatNotSupported(t.params.format);
+ })
+ .fn(t => {
+ const { format, attachmentCount } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ t.skipIf(
+ attachmentCount > t.device.limits.maxColorAttachments,
+ `attachmentCount: ${attachmentCount} > maxColorAttachments: ${t.device.limits.maxColorAttachments}`
+ );
+
+ const colorAttachments = [];
+ for (let i = 0; i < attachmentCount; i++) {
+ const colorTexture = t.createTexture({ format });
+ colorAttachments.push(t.getColorAttachment(colorTexture));
+ }
+ const shouldError =
+ info.colorRender === undefined ||
+ computeBytesPerSampleFromFormats(range(attachmentCount, () => format)) >
+ t.device.limits.maxColorAttachmentBytesPerSample;
+
+ t.tryRenderPass(!shouldError, { colorAttachments });
+ });
+
+g.test('color_attachments,limits,maxColorAttachmentBytesPerSample,unaligned')
+ .desc(
+ `
+ Test that the total bytes per sample of the formats of the color attachments must be no greater
+ than maxColorAttachmentBytesPerSample when the components are (potentially) unaligned.
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ // Alignment causes the first 1 byte R8Unorm to become 4 bytes. So even though
+ // 1+4+8+16+1 < 32, the 4 byte alignment requirement of R32Float makes the first R8Unorm
+ // become 4 and 4+4+8+16+1 > 32. Re-ordering this so the R8Unorm's are at the end, however
+ // is allowed: 4+8+16+1+1 < 32.
+ {
+ formats: [
+ 'r8unorm',
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ },
+ {
+ formats: [
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ },
+ ])
+ )
+ .fn(t => {
+ const { formats } = t.params;
+
+ t.skipIf(
+ formats.length > t.device.limits.maxColorAttachments,
+ `numColorAttachments: ${formats.length} > maxColorAttachments: ${t.device.limits.maxColorAttachments}`
+ );
+
+ const colorAttachments = [];
+ for (const format of formats) {
+ const colorTexture = t.createTexture({ format });
+ colorAttachments.push(t.getColorAttachment(colorTexture));
+ }
+
+ const success =
+ computeBytesPerSampleFromFormats(formats) <= t.device.limits.maxColorAttachmentBytesPerSample;
+
+ t.tryRenderPass(success, { colorAttachments });
+ });
+
+g.test('attachments,same_size')
+ .desc(
+ `
+ Test that attachments have the same size. Otherwise, a validation error should be generated.
+ - Succeed if all attachments have the same size.
+ - Fail if one of the color attachments has a different size.
+ - Fail if the depth stencil attachment has a different size.
+ `
+ )
+ .fn(t => {
+ const colorTexture1x1A = t.createTexture({ width: 1, height: 1, format: 'rgba8unorm' });
+ const colorTexture1x1B = t.createTexture({ width: 1, height: 1, format: 'rgba8unorm' });
+ const colorTexture2x2 = t.createTexture({ width: 2, height: 2, format: 'rgba8unorm' });
+ const depthStencilTexture1x1 = t.createTexture({
+ width: 1,
+ height: 1,
+ format: 'depth24plus-stencil8',
+ });
+ const depthStencilTexture2x2 = t.createTexture({
+ width: 2,
+ height: 2,
+ format: 'depth24plus-stencil8',
+ });
+
+ {
+ // Control case: all the same size (1x1)
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture1x1A),
+ t.getColorAttachment(colorTexture1x1B),
+ ],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture1x1),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ {
+ // One of the color attachments has a different size
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture1x1A),
+ t.getColorAttachment(colorTexture2x2),
+ ],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ // The depth stencil attachment has a different size
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture1x1A),
+ t.getColorAttachment(colorTexture1x1B),
+ ],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture2x2),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ });
+
+g.test('attachments,color_depth_mismatch')
+ .desc(`Test that attachments match whether they are used for color or depth stencil.`)
+ .fn(t => {
+ const colorTexture = t.createTexture({ format: 'rgba8unorm' });
+ const depthStencilTexture = t.createTexture({ format: 'depth24plus-stencil8' });
+
+ {
+ // Using depth-stencil for color
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(depthStencilTexture)],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ // Using color for depth-stencil
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(colorTexture),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ });
+
+g.test('attachments,layer_count')
+ .desc(
+ `
+ Test the layer counts for color or depth stencil.
+ - Fail if using 2D array texture view with arrayLayerCount > 1.
+ - Succeed if using 2D array texture view that covers the first layer of the texture.
+ - Succeed if using 2D array texture view that covers the last layer for depth stencil.
+ `
+ )
+ .paramsSimple([
+ { arrayLayerCount: 5, baseArrayLayer: 0, _success: false },
+ { arrayLayerCount: 1, baseArrayLayer: 0, _success: true },
+ { arrayLayerCount: 1, baseArrayLayer: 9, _success: true },
+ ])
+ .fn(t => {
+ const { arrayLayerCount, baseArrayLayer, _success } = t.params;
+
+ const ARRAY_LAYER_COUNT = 10;
+ const MIP_LEVEL_COUNT = 1;
+ const COLOR_FORMAT = 'rgba8unorm';
+ const DEPTH_STENCIL_FORMAT = 'depth24plus-stencil8';
+
+ const colorTexture = t.createTexture({
+ format: COLOR_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+ const depthStencilTexture = t.createTexture({
+ format: DEPTH_STENCIL_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+
+ const baseTextureViewDescriptor: GPUTextureViewDescriptor = {
+ dimension: '2d-array',
+ baseArrayLayer,
+ arrayLayerCount,
+ baseMipLevel: 0,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ };
+
+ {
+ // Check 2D array texture view for color
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: COLOR_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture, textureViewDescriptor)],
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ {
+ // Check 2D array texture view for depth stencil
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: DEPTH_STENCIL_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(
+ depthStencilTexture,
+ textureViewDescriptor
+ ),
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ });
+
+g.test('attachments,mip_level_count')
+ .desc(
+ `
+ Test the mip level count for color or depth stencil.
+ - Fail if using 2D texture view with mipLevelCount > 1.
+ - Succeed if using 2D texture view that covers the first level of the texture.
+ - Succeed if using 2D texture view that covers the last level of the texture.
+ `
+ )
+ .paramsSimple([
+ { mipLevelCount: 2, baseMipLevel: 0, _success: false },
+ { mipLevelCount: 1, baseMipLevel: 0, _success: true },
+ { mipLevelCount: 1, baseMipLevel: 3, _success: true },
+ ])
+ .fn(t => {
+ const { mipLevelCount, baseMipLevel, _success } = t.params;
+
+ const ARRAY_LAYER_COUNT = 1;
+ const MIP_LEVEL_COUNT = 4;
+ const COLOR_FORMAT = 'rgba8unorm';
+ const DEPTH_STENCIL_FORMAT = 'depth24plus-stencil8';
+
+ const colorTexture = t.createTexture({
+ format: COLOR_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+ const depthStencilTexture = t.createTexture({
+ format: DEPTH_STENCIL_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+
+ const baseTextureViewDescriptor: GPUTextureViewDescriptor = {
+ dimension: '2d',
+ baseArrayLayer: 0,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ baseMipLevel,
+ mipLevelCount,
+ };
+
+ {
+ // Check 2D texture view for color
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: COLOR_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture, textureViewDescriptor)],
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ {
+ // Check 2D texture view for depth stencil
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: DEPTH_STENCIL_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(
+ depthStencilTexture,
+ textureViewDescriptor
+ ),
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ });
+
+g.test('color_attachments,non_multisampled')
+ .desc(
+ `
+ Test that setting a resolve target is invalid if the color attachments is non multisampled.
+ `
+ )
+ .fn(t => {
+ const colorTexture = t.createTexture({ sampleCount: 1 });
+ const resolveTargetTexture = t.createTexture({ sampleCount: 1 });
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ resolveTarget: resolveTargetTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('color_attachments,sample_count')
+ .desc(
+ `
+ Test the usages of multisampled textures for color attachments.
+ - Succeed if using a multisampled color attachment without setting a resolve target.
+ - Fail if using multiple color attachments with different sample counts.
+ `
+ )
+ .fn(t => {
+ const colorTexture = t.createTexture({ sampleCount: 1 });
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+
+ {
+ // It is allowed to use a multisampled color attachment without setting resolve target
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(multisampledColorTexture)],
+ };
+ t.tryRenderPass(true, descriptor);
+ }
+ {
+ // It is not allowed to use multiple color attachments with different sample counts
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture),
+ t.getColorAttachment(multisampledColorTexture),
+ ],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ });
+
+g.test('resolveTarget,sample_count')
+ .desc(
+ `
+ Test that using multisampled resolve target is invalid for color attachments.
+ `
+ )
+ .fn(t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const multisampledResolveTargetTexture = t.createTexture({ sampleCount: 4 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = multisampledResolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,array_layer_count')
+ .desc(
+ `
+ Test that using a resolve target with array layer count is greater than 1 is invalid for color
+ attachments.
+ `
+ )
+ .fn(t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ arrayLayerCount: 2 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView({ dimension: '2d-array' });
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,mipmap_level_count')
+ .desc(
+ `
+ Test that using a resolve target with that mipmap level count is greater than 1 is invalid for
+ color attachments.
+ `
+ )
+ .fn(t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ mipLevelCount: 2 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,usage')
+ .desc(
+ `
+ Test that using a resolve target whose usage is not RENDER_ATTACHMENT is invalid for color
+ attachments.
+ `
+ )
+ .paramsSimple([
+ { usage: GPUConst.TextureUsage.COPY_SRC | GPUConst.TextureUsage.COPY_DST },
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING | GPUConst.TextureUsage.TEXTURE_BINDING },
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING | GPUConst.TextureUsage.STORAGE },
+ { usage: GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.TEXTURE_BINDING },
+ ])
+ .fn(t => {
+ const { usage } = t.params;
+
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ usage });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ const isValid = usage & GPUConst.TextureUsage.RENDER_ATTACHMENT ? true : false;
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('resolveTarget,error_state')
+ .desc(`Test that a resolve target that has a error is invalid for color attachments.`)
+ .fn(t => {
+ const ARRAY_LAYER_COUNT = 1;
+
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ arrayLayerCount: ARRAY_LAYER_COUNT });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ t.expectValidationError(() => {
+ colorAttachment.resolveTarget = resolveTargetTexture.createView({
+ dimension: '2d',
+ format: 'rgba8unorm',
+ baseArrayLayer: ARRAY_LAYER_COUNT + 1,
+ });
+ });
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,single_sample_count')
+ .desc(
+ `
+ Test that a resolve target that has multi sample color attachment and a single resolve target is
+ valid.
+ `
+ )
+ .fn(t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ sampleCount: 1 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(true, descriptor);
+ });
+
+g.test('resolveTarget,different_format')
+ .desc(`Test that a resolve target that has a different format is invalid.`)
+ .fn(t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ format: 'bgra8unorm' });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,different_size')
+ .desc(
+ `
+ Test that a resolve target that has a different size with the color attachment is invalid.
+ `
+ )
+ .fn(t => {
+ const size = 16;
+ const multisampledColorTexture = t.createTexture({ width: size, height: size, sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({
+ width: size * 2,
+ height: size * 2,
+ mipLevelCount: 2,
+ });
+
+ {
+ const resolveTargetTextureView = resolveTargetTexture.createView({
+ baseMipLevel: 0,
+ mipLevelCount: 1,
+ });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTextureView;
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ const resolveTargetTextureView = resolveTargetTexture.createView({ baseMipLevel: 1 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTextureView;
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ });
+
+g.test('depth_stencil_attachment,sample_counts_mismatch')
+ .desc(
+ `
+ Test that the depth stencil attachment that has different number of samples with the color
+ attachment is invalid.
+ `
+ )
+ .fn(t => {
+ const multisampledDepthStencilTexture = t.createTexture({
+ sampleCount: 4,
+ format: 'depth24plus-stencil8',
+ });
+
+ {
+ // It is not allowed to use a depth stencil attachment whose sample count is different from
+ // the one of the color attachment.
+ const depthStencilTexture = t.createTexture({
+ sampleCount: 1,
+ format: 'depth24plus-stencil8',
+ });
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(multisampledColorTexture)],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ const colorTexture = t.createTexture({ sampleCount: 1 });
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ depthStencilAttachment: t.getDepthStencilAttachment(multisampledDepthStencilTexture),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ // It is allowed to use a multisampled depth stencil attachment whose sample count is equal to
+ // the one of the color attachment.
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(multisampledColorTexture)],
+ depthStencilAttachment: t.getDepthStencilAttachment(multisampledDepthStencilTexture),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ {
+ // It is allowed to use a multisampled depth stencil attachment with no color attachment.
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(multisampledDepthStencilTexture),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ });
+
+g.test('depth_stencil_attachment,loadOp_storeOp_match_depthReadOnly_stencilReadOnly')
+ .desc(
+ `
+ Test GPURenderPassDepthStencilAttachment Usage:
+ - if the format has a depth aspect:
+ - if depthReadOnly is true
+ - depthLoadOp and depthStoreOp must not be provided
+ - else:
+ - depthLoadOp and depthStoreOp must be provided
+ - if the format has a stencil aspect:
+ - if stencilReadOnly is true
+ - stencilLoadOp and stencilStoreOp must not be provided
+ - else:
+ - stencilLoadOp and stencilStoreOp must be provided
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .beginSubcases() // Note: It's easier to debug if you comment this line out as you can then run an individual case.
+ .combine('depthReadOnly', [undefined, true, false])
+ .combine('depthLoadOp', [undefined, 'clear', 'load'] as GPULoadOp[])
+ .combine('depthStoreOp', [undefined, 'discard', 'store'] as GPUStoreOp[])
+ .combine('stencilReadOnly', [undefined, true, false])
+ .combine('stencilLoadOp', [undefined, 'clear', 'load'] as GPULoadOp[])
+ .combine('stencilStoreOp', [undefined, 'discard', 'store'] as GPUStoreOp[])
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format as GPUTextureFormat];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(t => {
+ const {
+ format,
+ depthReadOnly,
+ depthLoadOp,
+ depthStoreOp,
+ stencilReadOnly,
+ stencilLoadOp,
+ stencilStoreOp,
+ } = t.params;
+
+ const depthAttachment = t.trackForCleanup(
+ t.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ );
+ const depthAttachmentView = depthAttachment.createView();
+
+ const encoder = t.device.createCommandEncoder();
+
+ // If depthLoadOp is "clear", depthClearValue must be provided and must be between 0.0 and 1.0,
+ // and it will be ignored if depthLoadOp is not "clear".
+ const depthClearValue = depthLoadOp === 'clear' ? 0 : undefined;
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: depthAttachmentView,
+ depthLoadOp,
+ depthStoreOp,
+ depthReadOnly,
+ stencilLoadOp,
+ stencilStoreOp,
+ stencilReadOnly,
+ depthClearValue,
+ },
+ };
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.end();
+
+ const info = kTextureFormatInfo[format];
+ const hasDepthSettings = !!depthLoadOp && !!depthStoreOp && !depthReadOnly;
+ const hasStencilSettings = !!stencilLoadOp && !!stencilStoreOp && !stencilReadOnly;
+ const hasDepth = info.depth;
+ const hasStencil = info.stencil;
+
+ const goodAspectCombo =
+ (hasDepth && hasStencil ? !depthReadOnly === !stencilReadOnly : true) &&
+ (hasDepthSettings ? hasDepth : true) &&
+ (hasStencilSettings ? hasStencil : true);
+
+ const hasBothDepthOps = !!depthLoadOp && !!depthStoreOp;
+ const hasBothStencilOps = !!stencilLoadOp && !!stencilStoreOp;
+ const hasNeitherDepthOps = !depthLoadOp && !depthStoreOp;
+ const hasNeitherStencilOps = !stencilLoadOp && !stencilStoreOp;
+
+ const goodDepthCombo = hasDepth && !depthReadOnly ? hasBothDepthOps : hasNeitherDepthOps;
+ const goodStencilCombo =
+ hasStencil && !stencilReadOnly ? hasBothStencilOps : hasNeitherStencilOps;
+
+ const shouldError = !goodAspectCombo || !goodDepthCombo || !goodStencilCombo;
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, shouldError);
+ });
+
+g.test('depth_stencil_attachment,depth_clear_value')
+ .desc(
+ `
+ Test that depthClearValue is invalid if the value is out of the range(0.0 and 1.0) only when
+ depthLoadOp is 'clear'.
+ `
+ )
+ .params(u =>
+ u
+ .combine('depthLoadOp', ['load', 'clear', undefined] as const)
+ .combine('depthClearValue', [undefined, -1.0, 0.0, 0.5, 1.0, 1.5] as const)
+ )
+ .fn(t => {
+ const { depthLoadOp, depthClearValue } = t.params;
+
+ const depthStencilTexture = t.createTexture({
+ format: depthLoadOp === undefined ? 'stencil8' : 'depth24plus-stencil8',
+ });
+ const depthStencilAttachment = t.getDepthStencilAttachment(depthStencilTexture);
+ depthStencilAttachment.depthClearValue = depthClearValue;
+ depthStencilAttachment.depthLoadOp = depthLoadOp;
+ if (depthLoadOp === undefined) {
+ depthStencilAttachment.depthStoreOp = undefined;
+ }
+
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(t.createTexture())],
+ depthStencilAttachment,
+ };
+
+ // We can not check for out of range because NaN is not out of range.
+ // So (v < 0.0 || v > 1.0) would return false when depthClearValue is undefined (NaN)
+ const isDepthValueInRange = depthClearValue! >= 0.0 && depthClearValue! <= 1.0;
+ const isInvalid = depthLoadOp === 'clear' && !isDepthValueInRange;
+
+ t.tryRenderPass(!isInvalid, descriptor);
+ });
+
+g.test('resolveTarget,format_supports_resolve')
+ .desc(
+ `
+ For all formats that support 'multisample', test that they can be used as a resolveTarget
+ if and only if they support 'resolve'.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .filter(t => kTextureFormatInfo[t.format].multisample)
+ )
+ .beforeAllSubcases(t => {
+ t.skipIfTextureFormatNotSupported(t.params.format);
+ })
+ .fn(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const multisampledColorTexture = t.createTexture({ format, sampleCount: 4 });
+ const resolveTarget = t.createTexture({ format });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTarget.createView();
+
+ t.tryRenderPass(!!info.colorRender?.resolve, {
+ colorAttachments: [colorAttachment],
+ });
+ });
+
+g.test('timestampWrites,query_set_type')
+ .desc(
+ `
+ Test that all entries of the timestampWrites must have type 'timestamp'. If all query types are
+ not 'timestamp', a validation error should be generated.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('queryType', kQueryTypes)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(t => {
+ const { queryType } = t.params;
+
+ const timestampWrites = {
+ querySet: t.device.createQuerySet({ type: queryType, count: 2 }),
+ beginningOfPassWriteIndex: 0,
+ endOfPassWriteIndex: 1,
+ };
+
+ const isValid = queryType === 'timestamp';
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ timestampWrites,
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('timestampWrite,query_index')
+ .desc(
+ `Test that querySet.count should be greater than timestampWrite.queryIndex, and that the
+ query indexes are unique.`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('beginningOfPassWriteIndex', [undefined, 0, 1, 2, 3] as const)
+ .combine('endOfPassWriteIndex', [undefined, 0, 1, 2, 3] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(t => {
+ const { beginningOfPassWriteIndex, endOfPassWriteIndex } = t.params;
+
+ const querySetCount = 2;
+
+ const timestampWrites = {
+ querySet: t.device.createQuerySet({ type: 'timestamp', count: querySetCount }),
+ beginningOfPassWriteIndex,
+ endOfPassWriteIndex,
+ };
+
+ const isValid =
+ beginningOfPassWriteIndex !== endOfPassWriteIndex &&
+ (beginningOfPassWriteIndex === undefined || beginningOfPassWriteIndex < querySetCount) &&
+ (endOfPassWriteIndex === undefined || endOfPassWriteIndex < querySetCount);
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ timestampWrites,
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('occlusionQuerySet,query_set_type')
+ .desc(`Test that occlusionQuerySet must have type 'occlusion'.`)
+ .params(u => u.combine('queryType', kQueryTypes))
+ .beforeAllSubcases(t => {
+ if (t.params.queryType === 'timestamp') {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ }
+ })
+ .fn(t => {
+ const { queryType } = t.params;
+
+ const querySet = t.device.createQuerySet({
+ type: queryType,
+ count: 1,
+ });
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ occlusionQuerySet: querySet,
+ };
+
+ const isValid = queryType === 'occlusion';
+ t.tryRenderPass(isValid, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts
new file mode 100644
index 0000000000..975cc8f23a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts
@@ -0,0 +1,192 @@
+export const description = `
+Validation tests for render pass resolve.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../constants.js';
+import { ValidationTest } from '../validation_test.js';
+
+const kNumColorAttachments = 4;
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('resolve_attachment')
+ .desc(
+ `
+Test various validation behaviors when a resolveTarget is provided.
+
+- base case (valid).
+- resolve source is not multisampled.
+- resolve target is not single sampled.
+- resolve target missing RENDER_ATTACHMENT usage.
+- resolve target must have exactly one subresource:
+ - base mip level {0, >0}, mip level count {1, >1}.
+ - base array layer {0, >0}, array layer count {1, >1}.
+- resolve target GPUTextureView is invalid
+- resolve source and target have different formats.
+ - rgba8unorm -> {bgra8unorm, rgba8unorm-srgb}
+ - {bgra8unorm, rgba8unorm-srgb} -> rgba8unorm
+ - test with other color attachments having a different format
+- resolve source and target have different sizes.
+`
+ )
+ .paramsSimple([
+ // control case should be valid
+ { _valid: true },
+ // a single sampled resolve source should cause a validation error.
+ { colorAttachmentSamples: 1, _valid: false },
+ // a multisampled resolve target should cause a validation error.
+ { resolveTargetSamples: 4, _valid: false },
+ // resolveTargetUsage without RENDER_ATTACHMENT usage should cause a validation error.
+ { resolveTargetUsage: GPUConst.TextureUsage.COPY_SRC, _valid: false },
+ // non-zero resolve target base mip level should be valid.
+ {
+ resolveTargetViewBaseMipLevel: 1,
+ resolveTargetHeight: 4,
+ resolveTargetWidth: 4,
+ _valid: true,
+ },
+ // a validation error should be created when resolveTarget is invalid.
+ { resolveTargetInvalid: true, _valid: false },
+ // a validation error should be created when mip count > 1
+ { resolveTargetViewMipCount: 2, _valid: false },
+ {
+ resolveTargetViewBaseMipLevel: 1,
+ resolveTargetViewMipCount: 2,
+ resolveTargetHeight: 4,
+ resolveTargetWidth: 4,
+ _valid: false,
+ },
+ // non-zero resolve target base array layer should be valid.
+ { resolveTargetViewBaseArrayLayer: 1, _valid: true },
+ // a validation error should be created when array layer count > 1
+ { resolveTargetViewArrayLayerCount: 2, _valid: false },
+ { resolveTargetViewBaseArrayLayer: 1, resolveTargetViewArrayLayerCount: 2, _valid: false },
+ // other color attachments resolving with a different format should be valid.
+ { otherAttachmentFormat: 'bgra8unorm', _valid: true },
+ // mismatched colorAttachment and resolveTarget formats should cause a validation error.
+ { colorAttachmentFormat: 'bgra8unorm', _valid: false },
+ { colorAttachmentFormat: 'rgba8unorm-srgb', _valid: false },
+ { resolveTargetFormat: 'bgra8unorm', _valid: false },
+ { resolveTargetFormat: 'rgba8unorm-srgb', _valid: false },
+ // mismatched colorAttachment and resolveTarget sizes should cause a validation error.
+ { colorAttachmentHeight: 4, _valid: false },
+ { colorAttachmentWidth: 4, _valid: false },
+ { resolveTargetHeight: 4, _valid: false },
+ { resolveTargetWidth: 4, _valid: false },
+ ] as const)
+ .fn(t => {
+ const {
+ colorAttachmentFormat = 'rgba8unorm',
+ resolveTargetFormat = 'rgba8unorm',
+ otherAttachmentFormat = 'rgba8unorm',
+ colorAttachmentSamples = 4,
+ resolveTargetSamples = 1,
+ resolveTargetUsage = GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ resolveTargetInvalid = false,
+ resolveTargetViewMipCount = 1,
+ resolveTargetViewBaseMipLevel = 0,
+ resolveTargetViewArrayLayerCount = 1,
+ resolveTargetViewBaseArrayLayer = 0,
+ colorAttachmentHeight = 2,
+ colorAttachmentWidth = 2,
+ resolveTargetHeight = 2,
+ resolveTargetWidth = 2,
+ _valid,
+ } = t.params;
+
+ // Run the test in a nested loop such that the configured color attachment with resolve target
+ // is tested while occupying each individual colorAttachment slot.
+ for (let resolveSlot = 0; resolveSlot < kNumColorAttachments; resolveSlot++) {
+ const renderPassColorAttachmentDescriptors: GPURenderPassColorAttachment[] = [];
+ for (
+ let colorAttachmentSlot = 0;
+ colorAttachmentSlot < kNumColorAttachments;
+ colorAttachmentSlot++
+ ) {
+ // resolveSlot === colorAttachmentSlot denotes the color attachment slot that contains the
+ // color attachment with resolve target.
+ if (resolveSlot === colorAttachmentSlot) {
+ // Create the color attachment with resolve target with the configurable parameters.
+ const resolveSourceColorAttachment = t.device.createTexture({
+ format: colorAttachmentFormat,
+ size: {
+ width: colorAttachmentWidth,
+ height: colorAttachmentHeight,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: colorAttachmentSamples,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const resolveTarget = t.device.createTexture({
+ format: resolveTargetFormat,
+ size: {
+ width: resolveTargetWidth,
+ height: resolveTargetHeight,
+ depthOrArrayLayers:
+ resolveTargetViewBaseArrayLayer + resolveTargetViewArrayLayerCount,
+ },
+ sampleCount: resolveTargetSamples,
+ mipLevelCount: resolveTargetViewBaseMipLevel + resolveTargetViewMipCount,
+ usage: resolveTargetUsage,
+ });
+
+ renderPassColorAttachmentDescriptors.push({
+ view: resolveSourceColorAttachment.createView(),
+ loadOp: 'load',
+ storeOp: 'discard',
+ resolveTarget: resolveTargetInvalid
+ ? t.getErrorTextureView()
+ : resolveTarget.createView({
+ dimension: resolveTargetViewArrayLayerCount === 1 ? '2d' : '2d-array',
+ mipLevelCount: resolveTargetViewMipCount,
+ arrayLayerCount: resolveTargetViewArrayLayerCount,
+ baseMipLevel: resolveTargetViewBaseMipLevel,
+ baseArrayLayer: resolveTargetViewBaseArrayLayer,
+ }),
+ });
+ } else {
+ // Create a basic texture to fill other color attachment slots. This texture's dimensions
+ // and sample count must match the resolve source color attachment to be valid.
+ const colorAttachment = t.device.createTexture({
+ format: otherAttachmentFormat,
+ size: {
+ width: colorAttachmentWidth,
+ height: colorAttachmentHeight,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: colorAttachmentSamples,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const resolveTarget = t.device.createTexture({
+ format: otherAttachmentFormat,
+ size: {
+ width: colorAttachmentWidth,
+ height: colorAttachmentHeight,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: 1,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ renderPassColorAttachmentDescriptors.push({
+ view: colorAttachment.createView(),
+ loadOp: 'load',
+ storeOp: 'discard',
+ resolveTarget: resolveTarget.createView(),
+ });
+ }
+ }
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: renderPassColorAttachmentDescriptors,
+ });
+ pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !_valid);
+ }
+ });