| export const description = `API Operation Tests for multisample resolve in render passes.`; |
| |
| import { makeTestGroup } from '../../../../common/framework/test_group.js'; |
| import { isTextureFormatPossiblyResolvable, kColorTextureFormats } from '../../../format_info.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; |
| import * as ttu from '../../../texture_test_utils.js'; |
| |
| const kSlotsToResolve = [ |
| [0, 2], |
| [1, 3], |
| [0, 1, 2, 3], |
| ]; |
| |
| const kSize = 4; |
| const kDepthStencilFormat: GPUTextureFormat = 'depth24plus-stencil8'; |
| |
| export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); |
| |
| g.test('render_pass_resolve') |
| .desc( |
| ` |
| Test basic render pass resolve behavior for combinations of: |
| - number of color attachments, some with and some without a resolveTarget |
| - {a single draw+resolve pass, one draw-store pass and one empty load-resolve pass} |
| (attempts to test known driver bugs with empty resolve passes) |
| - in the resolve pass, storeOp set to {'store', 'discard'} |
| - mip levels {0, 1} and array layers {0, 1} |
| TODO: cases where color attachment and resolve target don't have the same mip level |
| - resolveTarget {2d array layer, TODO: 3d slice} {0, >0} with {2d, TODO: 3d} resolveTarget |
| TODO: cases where color attachment and resolve target don't have the same z (slice or layer) |
| - TODO: test that any not-resolved attachments are rendered to correctly. |
| - TODO: test different loadOps |
| - TODO?: resolveTarget mip level {0, >0} (TODO?: different mip level from colorAttachment) |
| - TODO?: resolveTarget {2d array layer, TODO: 3d slice} {0, >0} with {2d, TODO: 3d} resolveTarget |
| (different z from colorAttachment) |
| ` |
| ) |
| .params(u => |
| u |
| .combine('colorFormat', kColorTextureFormats) |
| .filter(t => isTextureFormatPossiblyResolvable(t.colorFormat)) |
| .combine('separateResolvePass', [false, true]) |
| .combine('storeOperation', ['discard', 'store'] as const) |
| .beginSubcases() |
| .combine('transientColorAttachment', [false, true]) |
| // Exclude if transient AND (separate pass OR storing) |
| .unless( |
| t => t.transientColorAttachment && (t.separateResolvePass || t.storeOperation === 'store') |
| ) |
| .combine('numColorAttachments', [2, 4] as const) |
| .combine('slotsToResolve', kSlotsToResolve) |
| .combine('resolveTargetBaseMipLevel', [0, 1] as const) |
| .combine('resolveTargetBaseArrayLayer', [0, 1] as const) |
| .combine('transientDepthStencilAttachment', [false, true]) |
| .combine('depthStencilAttachment', [false, true]) |
| .unless(t => !t.depthStencilAttachment && t.transientDepthStencilAttachment) |
| ) |
| .fn(t => { |
| const { colorFormat } = t.params; |
| t.skipIfTextureFormatNotSupported(colorFormat); |
| t.skipIfTextureFormatNotResolvable(colorFormat); |
| // MAINTENANCE_TODO(#4509): Remove this when TRANSIENT_ATTACHMENT is added to the WebGPU spec. |
| if (t.params.transientColorAttachment || t.params.transientDepthStencilAttachment) { |
| t.skipIfTransientAttachmentNotSupported(); |
| } |
| |
| const targets: GPUColorTargetState[] = []; |
| for (let i = 0; i < t.params.numColorAttachments; i++) { |
| targets.push({ format: colorFormat }); |
| } |
| |
| let depthStencil: GPUDepthStencilState | undefined; |
| if (t.params.depthStencilAttachment) { |
| depthStencil = { |
| format: kDepthStencilFormat, |
| depthWriteEnabled: false, |
| }; |
| } |
| |
| // These shaders will draw a white triangle into a texture. After draw, the top left |
| // half of the texture will be white, and the bottom right half will be unchanged. When this |
| // texture is resolved, there will be two distinct colors in each portion of the texture, as |
| // well as a line between the portions that contain the midpoint color due to the multisample |
| // resolve. |
| const pipeline = t.device.createRenderPipeline({ |
| layout: 'auto', |
| vertex: { |
| module: t.device.createShaderModule({ |
| code: ` |
| @vertex fn main( |
| @builtin(vertex_index) VertexIndex : u32 |
| ) -> @builtin(position) vec4<f32> { |
| var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>( |
| vec2<f32>(-1.0, -1.0), |
| vec2<f32>(-1.0, 1.0), |
| vec2<f32>( 1.0, 1.0)); |
| return vec4<f32>(pos[VertexIndex], 0.0, 1.0); |
| }`, |
| }), |
| entryPoint: 'main', |
| }, |
| fragment: { |
| module: t.device.createShaderModule({ |
| code: ` |
| struct Output { |
| @location(0) fragColor0 : vec4<f32>, |
| @location(1) fragColor1 : vec4<f32>, |
| @location(2) fragColor2 : vec4<f32>, |
| @location(3) fragColor3 : vec4<f32>, |
| }; |
| |
| @fragment fn main() -> Output { |
| return Output( |
| vec4<f32>(1.0, 1.0, 1.0, 1.0), |
| vec4<f32>(1.0, 1.0, 1.0, 1.0), |
| vec4<f32>(1.0, 1.0, 1.0, 1.0), |
| vec4<f32>(1.0, 1.0, 1.0, 1.0) |
| ); |
| }`, |
| }), |
| entryPoint: 'main', |
| targets, |
| }, |
| depthStencil, |
| primitive: { topology: 'triangle-list' }, |
| multisample: { count: 4 }, |
| }); |
| |
| const resolveTargets: GPUTexture[] = []; |
| const drawPassAttachments: GPURenderPassColorAttachment[] = []; |
| const resolvePassAttachments: GPURenderPassColorAttachment[] = []; |
| |
| // The resolve target must be the same size as the color attachment. If we're resolving to mip |
| // level 1, the resolve target base mip level should be 2x the color attachment size. |
| const kResolveTargetSize = kSize << t.params.resolveTargetBaseMipLevel; |
| |
| for (let i = 0; i < t.params.numColorAttachments; i++) { |
| const colorAttachment = t |
| .createTextureTracked({ |
| format: colorFormat, |
| size: [kSize, kSize, 1], |
| sampleCount: 4, |
| mipLevelCount: 1, |
| usage: t.params.transientColorAttachment |
| ? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT |
| : GPUTextureUsage.RENDER_ATTACHMENT, |
| }) |
| .createView(); |
| |
| let resolveTarget: GPUTextureView | undefined; |
| if (t.params.slotsToResolve.includes(i)) { |
| const resolveTargetTexture = t.createTextureTracked({ |
| format: colorFormat, |
| size: [kResolveTargetSize, kResolveTargetSize, t.params.resolveTargetBaseArrayLayer + 1], |
| sampleCount: 1, |
| mipLevelCount: t.params.resolveTargetBaseMipLevel + 1, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| resolveTargets.push(resolveTargetTexture); |
| |
| resolveTarget = resolveTargetTexture.createView({ |
| baseMipLevel: t.params.resolveTargetBaseMipLevel, |
| baseArrayLayer: t.params.resolveTargetBaseArrayLayer, |
| }); |
| } |
| |
| // Clear to black for the load operation. After the draw, the top left half of the attachment |
| // will be white and the bottom right half will be black. |
| if (t.params.separateResolvePass) { |
| drawPassAttachments.push({ |
| view: colorAttachment, |
| clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }); |
| resolvePassAttachments.push({ |
| view: colorAttachment, |
| resolveTarget, |
| loadOp: 'load', |
| storeOp: t.params.storeOperation, |
| }); |
| } else { |
| drawPassAttachments.push({ |
| view: colorAttachment, |
| resolveTarget, |
| clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, |
| loadOp: 'clear', |
| storeOp: t.params.storeOperation, |
| }); |
| } |
| } |
| |
| let depthStencilAttachment: GPURenderPassDepthStencilAttachment | undefined; |
| if (t.params.depthStencilAttachment) { |
| const depthStencilTexture = t.createTextureTracked({ |
| format: kDepthStencilFormat, |
| size: [kSize, kSize, 1], |
| sampleCount: 4, |
| usage: t.params.transientDepthStencilAttachment |
| ? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT |
| : GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| depthStencilAttachment = { |
| view: depthStencilTexture.createView(), |
| depthClearValue: 1.0, |
| depthLoadOp: 'clear', |
| depthStoreOp: 'discard', |
| stencilLoadOp: 'clear', |
| stencilStoreOp: 'discard', |
| }; |
| } |
| |
| const encoder = t.device.createCommandEncoder(); |
| const pass = encoder.beginRenderPass({ |
| colorAttachments: drawPassAttachments, |
| depthStencilAttachment, |
| }); |
| pass.setPipeline(pipeline); |
| pass.draw(3); |
| pass.end(); |
| if (t.params.separateResolvePass) { |
| const pass = encoder.beginRenderPass({ |
| colorAttachments: resolvePassAttachments, |
| depthStencilAttachment, |
| }); |
| pass.end(); |
| } |
| t.device.queue.submit([encoder.finish()]); |
| |
| // Verify the resolve targets contain the correct values. Note that we use z to specify the |
| // array layer from which to pull the pixels for testing. |
| const z = t.params.resolveTargetBaseArrayLayer; |
| for (const resolveTarget of resolveTargets) { |
| ttu.expectSinglePixelComparisonsAreOkInTexture( |
| t, |
| { texture: resolveTarget, mipLevel: t.params.resolveTargetBaseMipLevel }, |
| // Note: Channels that do not exist in the actual texture are not compared. |
| [ |
| // Top left pixel should be {1.0, 1.0, 1.0, 1.0}. |
| { coord: { x: 0, y: 0, z }, exp: { R: 1.0, G: 1.0, B: 1.0, A: 1.0 } }, |
| // Bottom right pixel should be {0, 0, 0, 0}. |
| { coord: { x: kSize - 1, y: kSize - 1, z }, exp: { R: 0, G: 0, B: 0, A: 0 } }, |
| // Top right pixel should be {0.5, 0.5, 0.5, 0.5} due to the multisampled resolve. |
| { coord: { x: kSize - 1, y: 0, z }, exp: { R: 0.5, G: 0.5, B: 0.5, A: 0.5 } }, |
| ] |
| ); |
| } |
| }); |