diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts new file mode 100644 index 0000000000..4799e2a572 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts @@ -0,0 +1,519 @@ +export const description = ` +Tests that the final sample mask is the logical AND of all the relevant masks. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { assert } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { TypeF32, TypeU32 } from '../../../util/conversion.js'; +import { makeTextureWithContents } from '../../../util/texture.js'; +import { TexelView } from '../../../util/texture/texel_view.js'; + +const kColors = [ + // Red + new Uint8Array([0xff, 0, 0, 0xff]), + // Green + new Uint8Array([0, 0xff, 0, 0xff]), + // Blue + new Uint8Array([0, 0, 0xff, 0xff]), + // Yellow + new Uint8Array([0xff, 0xff, 0, 0xff]), +]; + +const kDepthClearValue = 1.0; +const kDepthWriteValue = 0.0; +const kStencilClearValue = 0; +const kStencilReferenceValue = 0xff; + +// Format of the render target and resolve target +const format = 'rgba8unorm'; + +// Format of depth stencil attachment +const depthStencilFormat = 'depth24plus-stencil8'; + +const kRenderTargetSize = 1; + +function hasSample( + rasterizationMask: number, + sampleMask: number, + fragmentShaderOutputMask: number, + sampleIndex: number = 0 +): boolean { + return (rasterizationMask & sampleMask & fragmentShaderOutputMask & (1 << sampleIndex)) > 0; +} + +class F extends GPUTest { + async GetTargetTexture( + sampleCount: number, + rasterizationMask: number, + sampleMask: number, + fragmentShaderOutputMask: number + ): Promise<{ color: GPUTexture; depthStencil: GPUTexture }> { + // Create a 2x2 color texture to sample from + // texel 0 - Red + // texel 1 - Green + // texel 2 - Blue + // texel 3 - Yellow + const kSampleTextureSize = 2; + const sampleTexture = makeTextureWithContents( + this.device, + TexelView.fromTexelsAsBytes(format, coord => { + const id = coord.x + coord.y * kSampleTextureSize; + return kColors[id]; + }), + { + size: [kSampleTextureSize, kSampleTextureSize, 1], + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + } + ); + + const sampler = this.device.createSampler({ + magFilter: 'nearest', + minFilter: 'nearest', + }); + + const fragmentMaskUniformBuffer = this.device.createBuffer({ + size: 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + }); + this.trackForCleanup(fragmentMaskUniformBuffer); + this.device.queue.writeBuffer( + fragmentMaskUniformBuffer, + 0, + new Uint32Array([fragmentShaderOutputMask]) + ); + + const pipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: ` + struct VertexOutput { + @builtin(position) Position : vec4<f32>, + @location(0) @interpolate(perspective, sample) fragUV : vec2<f32>, + } + + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + var pos = array<vec2<f32>, 30>( + // center quad + // only covers pixel center which is sample point when sampleCount === 1 + // small enough to avoid covering any multi sample points + vec2<f32>( 0.2, 0.2), + vec2<f32>( 0.2, -0.2), + vec2<f32>(-0.2, -0.2), + vec2<f32>( 0.2, 0.2), + vec2<f32>(-0.2, -0.2), + vec2<f32>(-0.2, 0.2), + + // Sub quads are representing rasterization mask and + // are slightly scaled to avoid covering the pixel center + + // top-left quad + vec2<f32>( -0.01, 1.0), + vec2<f32>( -0.01, 0.01), + vec2<f32>(-1.0, 0.01), + vec2<f32>( -0.01, 1.0), + vec2<f32>(-1.0, 0.01), + vec2<f32>(-1.0, 1.0), + + // top-right quad + vec2<f32>(1.0, 1.0), + vec2<f32>(1.0, 0.01), + vec2<f32>(0.01, 0.01), + vec2<f32>(1.0, 1.0), + vec2<f32>(0.01, 0.01), + vec2<f32>(0.01, 1.0), + + // bottom-left quad + vec2<f32>( -0.01, -0.01), + vec2<f32>( -0.01, -1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>( -0.01, -0.01), + vec2<f32>(-1.0, -1.0), + vec2<f32>(-1.0, -0.01), + + // bottom-right quad + vec2<f32>(1.0, -0.01), + vec2<f32>(1.0, -1.0), + vec2<f32>(0.01, -1.0), + vec2<f32>(1.0, -0.01), + vec2<f32>(0.01, -1.0), + vec2<f32>(0.01, -0.01) + ); + + var uv = array<vec2<f32>, 30>( + // center quad + vec2<f32>(1.0, 0.0), + vec2<f32>(1.0, 1.0), + vec2<f32>(0.0, 1.0), + vec2<f32>(1.0, 0.0), + vec2<f32>(0.0, 1.0), + vec2<f32>(0.0, 0.0), + + // top-left quad (texel 0) + vec2<f32>(0.5, 0.0), + vec2<f32>(0.5, 0.5), + vec2<f32>(0.0, 0.5), + vec2<f32>(0.5, 0.0), + vec2<f32>(0.0, 0.5), + vec2<f32>(0.0, 0.0), + + // top-right quad (texel 1) + vec2<f32>(1.0, 0.0), + vec2<f32>(1.0, 0.5), + vec2<f32>(0.5, 0.5), + vec2<f32>(1.0, 0.0), + vec2<f32>(0.5, 0.5), + vec2<f32>(0.5, 0.0), + + // bottom-left quad (texel 2) + vec2<f32>(0.5, 0.5), + vec2<f32>(0.5, 1.0), + vec2<f32>(0.0, 1.0), + vec2<f32>(0.5, 0.5), + vec2<f32>(0.0, 1.0), + vec2<f32>(0.0, 0.5), + + // bottom-right quad (texel 3) + vec2<f32>(1.0, 0.5), + vec2<f32>(1.0, 1.0), + vec2<f32>(0.5, 1.0), + vec2<f32>(1.0, 0.5), + vec2<f32>(0.5, 1.0), + vec2<f32>(0.5, 0.5) + ); + + var output : VertexOutput; + output.Position = vec4<f32>(pos[VertexIndex], ${kDepthWriteValue}, 1.0); + output.fragUV = uv[VertexIndex]; + return output; + }`, + }), + entryPoint: 'main', + }, + fragment: { + module: this.device.createShaderModule({ + code: ` + @group(0) @binding(0) var mySampler: sampler; + @group(0) @binding(1) var myTexture: texture_2d<f32>; + @group(0) @binding(2) var<uniform> fragMask: u32; + + struct FragmentOutput { + @builtin(sample_mask) mask : u32, + @location(0) color : vec4<f32>, + } + + @fragment + fn main(@location(0) @interpolate(perspective, sample) fragUV: vec2<f32>) -> FragmentOutput { + return FragmentOutput(fragMask, textureSample(myTexture, mySampler, fragUV)); + }`, + }), + entryPoint: 'main', + targets: [{ format }], + }, + primitive: { topology: 'triangle-list' }, + multisample: { + count: sampleCount, + mask: sampleMask, + alphaToCoverageEnabled: false, + }, + depthStencil: { + format: depthStencilFormat, + depthWriteEnabled: true, + depthCompare: 'always', + + stencilFront: { + compare: 'always', + passOp: 'replace', + }, + stencilBack: { + compare: 'always', + passOp: 'replace', + }, + }, + }); + + const uniformBindGroup = this.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: sampler, + }, + { + binding: 1, + resource: sampleTexture.createView(), + }, + { + binding: 2, + resource: { + buffer: fragmentMaskUniformBuffer, + }, + }, + ], + }); + + const renderTargetTexture = this.device.createTexture({ + format, + size: { + width: kRenderTargetSize, + height: kRenderTargetSize, + depthOrArrayLayers: 1, + }, + sampleCount, + mipLevelCount: 1, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + }); + const resolveTargetTexture = + sampleCount === 1 + ? null + : this.device.createTexture({ + format, + size: { + width: kRenderTargetSize, + height: kRenderTargetSize, + depthOrArrayLayers: 1, + }, + sampleCount: 1, + mipLevelCount: 1, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const depthStencilTexture = this.device.createTexture({ + size: { + width: kRenderTargetSize, + height: kRenderTargetSize, + }, + format: depthStencilFormat, + sampleCount, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + }); + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: renderTargetTexture.createView(), + resolveTarget: resolveTargetTexture?.createView(), + + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthStencilTexture.createView(), + depthClearValue: kDepthClearValue, + depthLoadOp: 'clear', + depthStoreOp: 'store', + stencilClearValue: kStencilClearValue, + stencilLoadOp: 'clear', + stencilStoreOp: 'store', + }, + }; + const commandEncoder = this.device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, uniformBindGroup); + passEncoder.setStencilReference(kStencilReferenceValue); + + if (sampleCount === 1) { + if ((rasterizationMask & 1) !== 0) { + // draw center quad + passEncoder.draw(6); + } + } else { + assert(sampleCount === 4); + if ((rasterizationMask & 1) !== 0) { + // draw top-left quad + passEncoder.draw(6, 1, 6); + } + if ((rasterizationMask & 2) !== 0) { + // draw top-right quad + passEncoder.draw(6, 1, 12); + } + if ((rasterizationMask & 4) !== 0) { + // draw bottom-left quad + passEncoder.draw(6, 1, 18); + } + if ((rasterizationMask & 8) !== 0) { + // draw bottom-right quad + passEncoder.draw(6, 1, 24); + } + } + passEncoder.end(); + this.device.queue.submit([commandEncoder.finish()]); + + return { + color: renderTargetTexture, + depthStencil: depthStencilTexture, + }; + } + + CheckColorAttachmentResult( + texture: GPUTexture, + sampleCount: number, + rasterizationMask: number, + sampleMask: number, + fragmentShaderOutputMask: number + ) { + const buffer = this.copySinglePixelTextureToBufferUsingComputePass( + TypeF32, // correspond to 'rgba8unorm' format + 4, + texture.createView(), + sampleCount + ); + + const expectedDstData = new Float32Array(sampleCount * 4); + if (sampleCount === 1) { + if (hasSample(rasterizationMask, sampleMask, fragmentShaderOutputMask)) { + // Texel 3 is sampled at the pixel center + expectedDstData[0] = kColors[3][0] / 0xff; + expectedDstData[1] = kColors[3][1] / 0xff; + expectedDstData[2] = kColors[3][2] / 0xff; + expectedDstData[3] = kColors[3][3] / 0xff; + } + } else { + for (let i = 0; i < sampleCount; i++) { + if (hasSample(rasterizationMask, sampleMask, fragmentShaderOutputMask, i)) { + const o = i * 4; + expectedDstData[o + 0] = kColors[i][0] / 0xff; + expectedDstData[o + 1] = kColors[i][1] / 0xff; + expectedDstData[o + 2] = kColors[i][2] / 0xff; + expectedDstData[o + 3] = kColors[i][3] / 0xff; + } + } + } + + this.expectGPUBufferValuesEqual(buffer, expectedDstData); + } + + CheckDepthStencilResult( + aspect: 'depth-only' | 'stencil-only', + depthStencilTexture: GPUTexture, + sampleCount: number, + rasterizationMask: number, + sampleMask: number, + fragmentShaderOutputMask: number + ) { + const buffer = this.copySinglePixelTextureToBufferUsingComputePass( + // Use f32 as the scalar type for depth (depth24plus, depth32float) + // Use u32 as the scalar type for stencil (stencil8) + aspect === 'depth-only' ? TypeF32 : TypeU32, + 1, + depthStencilTexture.createView({ aspect }), + sampleCount + ); + + const expectedDstData = + aspect === 'depth-only' ? new Float32Array(sampleCount) : new Uint32Array(sampleCount); + for (let i = 0; i < sampleCount; i++) { + const s = hasSample(rasterizationMask, sampleMask, fragmentShaderOutputMask, i); + if (aspect === 'depth-only') { + expectedDstData[i] = s ? kDepthWriteValue : kDepthClearValue; + } else { + expectedDstData[i] = s ? kStencilReferenceValue : kStencilClearValue; + } + } + this.expectGPUBufferValuesEqual(buffer, expectedDstData); + } +} + +export const g = makeTestGroup(F); + +g.test('final_output') + .desc( + ` +Tests that the final sample mask is the logical AND of all the relevant masks -- meaning that the samples +not included in the final mask are discarded on any attachments including +- color outputs +- depth tests +- stencil operations + +The test draws 0/1/1+ textured quads of which each sample in the standard 4-sample pattern results in a different color: +- Sample 0, Texel 0, top-left: Red +- Sample 1, Texel 1, top-left: Green +- Sample 2, Texel 2, top-left: Blue +- Sample 3, Texel 3, top-left: Yellow + +The test checks each sample value of the render target texture and depth stencil texture using a compute pass to +textureLoad each sample index from the texture and write to a storage buffer to compare with expected values. + +- for sampleCount = { 1, 4 } and various combinations of: + - rasterization mask = { 0, ..., 2 ** sampleCount - 1 } + - sample mask = { 0, 0b0001, 0b0010, 0b0111, 0b1011, 0b1101, 0b1110, 0b1111, 0b11110 } + - fragment shader output @builtin(sample_mask) = { 0, 0b0001, 0b0010, 0b0111, 0b1011, 0b1101, 0b1110, 0b1111, 0b11110 } +- [choosing 0b11110 because the 5th bit should be ignored] +` + ) + .params(u => + u + .combine('sampleCount', [1, 4] as const) + .expand('rasterizationMask', function* (p) { + for (let i = 0, len = 2 ** p.sampleCount - 1; i <= len; i++) { + yield i; + } + }) + .beginSubcases() + .combine('sampleMask', [ + 0, + 0b0001, + 0b0010, + 0b0111, + 0b1011, + 0b1101, + 0b1110, + 0b1111, + 0b11110, + ] as const) + .combine('fragmentShaderOutputMask', [ + 0, + 0b0001, + 0b0010, + 0b0111, + 0b1011, + 0b1101, + 0b1110, + 0b1111, + 0b11110, + ] as const) + ) + .fn(async t => { + const { sampleCount, rasterizationMask, sampleMask, fragmentShaderOutputMask } = t.params; + + const { color, depthStencil } = await t.GetTargetTexture( + sampleCount, + rasterizationMask, + sampleMask, + fragmentShaderOutputMask + ); + + t.CheckColorAttachmentResult( + color, + sampleCount, + rasterizationMask, + sampleMask, + fragmentShaderOutputMask + ); + + t.CheckDepthStencilResult( + 'depth-only', + depthStencil, + sampleCount, + rasterizationMask, + sampleMask, + fragmentShaderOutputMask + ); + + t.CheckDepthStencilResult( + 'stencil-only', + depthStencil, + sampleCount, + rasterizationMask, + sampleMask, + fragmentShaderOutputMask + ); + }); |