diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass')
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); + } + }); |