From fd777c3d00dce755d04cbe5da9a407a2f06ff008 Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Mon, 27 Mar 2023 22:09:57 -0700 Subject: [PATCH] Slightly expand sample_mask tests (#2466) * Slightly expand sample_mask tests - Tighten the rasterized primitives around the standard sample locations as a test that they're in the exact places we expect. - Parameterize by whether UV is interpolated or not. The non-interpolated fragment_output_mask tests now pass on Mali, but alpha_to_coverage tests still all fail, but we don't know why. * address comments --- .../render_pipeline/sample_mask.spec.ts | 291 +++++++++--------- 1 file changed, 144 insertions(+), 147 deletions(-) diff --git a/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts b/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts index 09352762e76d..d415c639e163 100644 --- a/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts +++ b/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts @@ -1,6 +1,10 @@ export const description = ` Tests that the final sample mask is the logical AND of all the relevant masks, including -the rasterization mask, sample mask, fragment output mask, and alpha to coverage mask (when alphaToCoverageEnabled === true) +the rasterization mask, sample mask, fragment output mask, and alpha to coverage mask (when alphaToCoverageEnabled === true). + +Also tested: +- The positions of samples in the standard sample patterns. +- Per-sample interpolation sampling: @interpolate(perspective, sample). TODO: add a test without a 0th color attachment (sparse color attachment), with different color attachments and alpha value output. The cross-platform behavior is unknown. could be any of: @@ -8,7 +12,6 @@ The cross-platform behavior is unknown. could be any of: - coverage is always 0% - it uses the first non-null attachment - it's an error - Details could be found at: https://github.com/gpuweb/cts/issues/2201 `; @@ -121,108 +124,142 @@ function getExpectedStencilData( return expectedData; } -const kSampleMaskTestVertexShader = ` -struct VertexOutput { +const kSampleMaskTestShader = ` +struct Varyings { @builtin(position) Position : vec4, - @location(0) @interpolate(perspective, sample) fragUV : vec2, + @location(0) @interpolate(flat) uvFlat : vec2, + @location(1) @interpolate(perspective, sample) uvInterpolated : vec2, } +// +// Vertex shader +// + @vertex -fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { - var pos = array, 30>( - // center quad - // only covers pixel center which is sample point when sampleCount === 1 - // small enough to avoid covering any multi sample points - vec2( 0.2, 0.2), - vec2( 0.2, -0.2), - vec2(-0.2, -0.2), - vec2( 0.2, 0.2), - vec2(-0.2, -0.2), - vec2(-0.2, 0.2), - - // Sub quads are representing rasterization mask and - // are slightly scaled to avoid covering the pixel center - - // top-left quad - vec2( -0.01, 1.0), - vec2( -0.01, 0.01), - vec2(-1.0, 0.01), - vec2( -0.01, 1.0), - vec2(-1.0, 0.01), - vec2(-1.0, 1.0), - - // top-right quad - vec2(1.0, 1.0), - vec2(1.0, 0.01), - vec2(0.01, 0.01), - vec2(1.0, 1.0), - vec2(0.01, 0.01), - vec2(0.01, 1.0), - - // bottom-left quad - vec2( -0.01, -0.01), - vec2( -0.01, -1.0), - vec2(-1.0, -1.0), - vec2( -0.01, -0.01), - vec2(-1.0, -1.0), - vec2(-1.0, -0.01), - - // bottom-right quad - vec2(1.0, -0.01), - vec2(1.0, -1.0), - vec2(0.01, -1.0), - vec2(1.0, -0.01), - vec2(0.01, -1.0), - vec2(0.01, -0.01) +fn vmain(@builtin(vertex_index) VertexIndex : u32, + @builtin(instance_index) InstanceIndex : u32) -> Varyings { + // Standard sample locations within a pixel, where the pixel ranges from (-1,-1) to (1,1), and is + // centered at (0,0) (NDC - the test uses a 1x1 render target). + // https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels + var sampleCenters = array( + // sampleCount = 1 + vec2f(0, 0), + // sampleCount = 4 + vec2f(-2, 6) / 8, + vec2f( 6, 2) / 8, + vec2f(-6, -2) / 8, + vec2f( 2, -6) / 8, + ); + // A tiny quad to draw around the sample center to ensure we hit only the expected point. + let kTinyQuadRadius = 1.0 / 32; + var tinyQuad = array( + vec2f( kTinyQuadRadius, kTinyQuadRadius), + vec2f( kTinyQuadRadius, -kTinyQuadRadius), + vec2f(-kTinyQuadRadius, -kTinyQuadRadius), + vec2f( kTinyQuadRadius, kTinyQuadRadius), + vec2f(-kTinyQuadRadius, -kTinyQuadRadius), + vec2f(-kTinyQuadRadius, kTinyQuadRadius), ); - var uv = array, 30>( + var uvsFlat = array( + // sampleCount = 1 + // Note: avoids hitting the point between the 4 texels. + vec2f(0.51, 0.51), + // sampleCount = 4 + vec2f(0.25, 0.25), + vec2f(0.75, 0.25), + vec2f(0.25, 0.75), + vec2f(0.75, 0.75), + ); + var uvsInterpolated = array( // center quad - vec2(1.0, 0.0), - vec2(1.0, 1.0), - vec2(0.0, 1.0), - vec2(1.0, 0.0), - vec2(0.0, 1.0), - vec2(0.0, 0.0), + // Note: the interpolated point will be exactly in the middle of the 4 texels. + // The test expects to get texel 1,1 (the 3rd texel) in this case. + vec2f(1.0, 0.0), + vec2f(1.0, 1.0), + vec2f(0.0, 1.0), + vec2f(1.0, 0.0), + vec2f(0.0, 1.0), + vec2f(0.0, 0.0), // top-left quad (texel 0) - vec2(0.5, 0.0), - vec2(0.5, 0.5), - vec2(0.0, 0.5), - vec2(0.5, 0.0), - vec2(0.0, 0.5), - vec2(0.0, 0.0), + vec2f(0.5, 0.0), + vec2f(0.5, 0.5), + vec2f(0.0, 0.5), + vec2f(0.5, 0.0), + vec2f(0.0, 0.5), + vec2f(0.0, 0.0), // top-right quad (texel 1) - vec2(1.0, 0.0), - vec2(1.0, 0.5), - vec2(0.5, 0.5), - vec2(1.0, 0.0), - vec2(0.5, 0.5), - vec2(0.5, 0.0), + vec2f(1.0, 0.0), + vec2f(1.0, 0.5), + vec2f(0.5, 0.5), + vec2f(1.0, 0.0), + vec2f(0.5, 0.5), + vec2f(0.5, 0.0), // bottom-left quad (texel 2) - vec2(0.5, 0.5), - vec2(0.5, 1.0), - vec2(0.0, 1.0), - vec2(0.5, 0.5), - vec2(0.0, 1.0), - vec2(0.0, 0.5), + vec2f(0.5, 0.5), + vec2f(0.5, 1.0), + vec2f(0.0, 1.0), + vec2f(0.5, 0.5), + vec2f(0.0, 1.0), + vec2f(0.0, 0.5), // bottom-right quad (texel 3) - vec2(1.0, 0.5), - vec2(1.0, 1.0), - vec2(0.5, 1.0), - vec2(1.0, 0.5), - vec2(0.5, 1.0), - vec2(0.5, 0.5) + vec2f(1.0, 0.5), + vec2f(1.0, 1.0), + vec2f(0.5, 1.0), + vec2f(1.0, 0.5), + vec2f(0.5, 1.0), + vec2f(0.5, 0.5) ); - var output : VertexOutput; - output.Position = vec4(pos[VertexIndex], ${kDepthWriteValue}, 1.0); - output.fragUV = uv[VertexIndex]; + var output : Varyings; + let pos = sampleCenters[InstanceIndex] + tinyQuad[VertexIndex]; + output.Position = vec4(pos, ${kDepthWriteValue}, 1.0); + output.uvFlat = uvsFlat[InstanceIndex]; + output.uvInterpolated = uvsInterpolated[InstanceIndex * 6 + VertexIndex]; return output; -}`; +} + +// +// Fragment shaders +// + +@group(0) @binding(0) var mySampler: sampler; +@group(0) @binding(1) var myTexture: texture_2d; + +// For test named 'fragment_output_mask' + +@group(0) @binding(2) var fragMask: u32; +struct FragmentOutput1 { + @builtin(sample_mask) mask : u32, + @location(0) color : vec4, +} +@fragment fn fmain__fragment_output_mask__flat(varyings: Varyings) -> FragmentOutput1 { + return FragmentOutput1(fragMask, textureSample(myTexture, mySampler, varyings.uvFlat)); +} +@fragment fn fmain__fragment_output_mask__interp(varyings: Varyings) -> FragmentOutput1 { + return FragmentOutput1(fragMask, textureSample(myTexture, mySampler, varyings.uvInterpolated)); +} + +// For test named 'alpha_to_coverage_mask' + +struct FragmentOutput2 { + @location(0) color0 : vec4, + @location(1) color1 : vec4, +} +@group(0) @binding(2) var alpha: vec2; +@fragment fn fmain__alpha_to_coverage_mask__flat(varyings: Varyings) -> FragmentOutput2 { + var c = textureSample(myTexture, mySampler, varyings.uvFlat); + return FragmentOutput2(vec4(c.xyz, alpha[0]), vec4(c.xyz, alpha[1])); +} +@fragment fn fmain__alpha_to_coverage_mask__interp(varyings: Varyings) -> FragmentOutput2 { + var c = textureSample(myTexture, mySampler, varyings.uvInterpolated); + return FragmentOutput2(vec4(c.xyz, alpha[0]), vec4(c.xyz, alpha[1])); +} +`; class F extends TextureTestMixin(GPUTest) { private sampleTexture: GPUTexture | undefined; @@ -358,25 +395,25 @@ class F extends TextureTestMixin(GPUTest) { if (sampleCount === 1) { if ((rasterizationMask & 1) !== 0) { // draw center quad - passEncoder.draw(6); + passEncoder.draw(6, 1, 0, 0); } } else { assert(sampleCount === 4); if ((rasterizationMask & 1) !== 0) { // draw top-left quad - passEncoder.draw(6, 1, 6); + passEncoder.draw(6, 1, 0, 1); } if ((rasterizationMask & 2) !== 0) { // draw top-right quad - passEncoder.draw(6, 1, 12); + passEncoder.draw(6, 1, 0, 2); } if ((rasterizationMask & 4) !== 0) { // draw bottom-left quad - passEncoder.draw(6, 1, 18); + passEncoder.draw(6, 1, 0, 3); } if ((rasterizationMask & 8) !== 0) { // draw bottom-right quad - passEncoder.draw(6, 1, 24); + passEncoder.draw(6, 1, 0, 4); } } passEncoder.end(); @@ -470,9 +507,11 @@ textureLoad each sample index from the texture and write to a storage buffer to ) .params(u => u + .combine('interpolated', [false, true]) .combine('sampleCount', [1, 4] as const) .expand('rasterizationMask', function* (p) { - for (let i = 0, len = 2 ** p.sampleCount - 1; i <= len; i++) { + const maxMask = 2 ** p.sampleCount - 1; + for (let i = 0; i <= maxMask; i++) { yield i; } }) @@ -514,32 +553,13 @@ textureLoad each sample index from the texture and write to a storage buffer to new Uint32Array([fragmentShaderOutputMask]) ); + const module = t.device.createShaderModule({ code: kSampleMaskTestShader }); const pipeline = t.device.createRenderPipeline({ layout: 'auto', - vertex: { - module: t.device.createShaderModule({ - code: kSampleMaskTestVertexShader, - }), - entryPoint: 'main', - }, + vertex: { module, entryPoint: 'vmain' }, fragment: { - module: t.device.createShaderModule({ - code: ` - @group(0) @binding(0) var mySampler: sampler; - @group(0) @binding(1) var myTexture: texture_2d; - @group(0) @binding(2) var fragMask: u32; - - struct FragmentOutput { - @builtin(sample_mask) mask : u32, - @location(0) color : vec4, - } - - @fragment - fn main(@location(0) @interpolate(perspective, sample) fragUV: vec2) -> FragmentOutput { - return FragmentOutput(fragMask, textureSample(myTexture, mySampler, fragUV)); - }`, - }), - entryPoint: 'main', + module, + entryPoint: `fmain__fragment_output_mask__${t.params.interpolated ? 'interp' : 'flat'}`, targets: [{ format }], }, primitive: { topology: 'triangle-list' }, @@ -622,8 +642,11 @@ color' <= color. ) .params(u => u + .combine('interpolated', [false, true]) + .combine('sampleCount', [4] as const) .expand('rasterizationMask', function* (p) { - for (let i = 0, len = 0xf; i <= len; i++) { + const maxMask = 2 ** p.sampleCount - 1; + for (let i = 0; i <= maxMask; i++) { yield i; } }) @@ -631,9 +654,8 @@ color' <= color. .combine('alpha1', [0.0, 0.5, 1.0] as const) ) .fn(async t => { - const sampleCount = 4; + const { sampleCount, rasterizationMask, alpha1 } = t.params; const sampleMask = 0xffffffff; - const { rasterizationMask, alpha1 } = t.params; const alphaValues = new Float32Array(4); // [alpha0, alpha1, 0, 0] const alphaValueUniformBuffer = t.device.createBuffer({ @@ -642,38 +664,13 @@ color' <= color. }); t.trackForCleanup(alphaValueUniformBuffer); + const module = t.device.createShaderModule({ code: kSampleMaskTestShader }); const pipeline = t.device.createRenderPipeline({ layout: 'auto', - vertex: { - module: t.device.createShaderModule({ - code: kSampleMaskTestVertexShader, - }), - entryPoint: 'main', - }, + vertex: { module, entryPoint: 'vmain' }, fragment: { - module: t.device.createShaderModule({ - code: ` - @group(0) @binding(0) var mySampler: sampler; - @group(0) @binding(1) var myTexture: texture_2d; - @group(0) @binding(2) var alpha: vec4; - - struct FragmentOutput { - @location(0) color0 : vec4, - @location(1) color1 : vec4, - } - - @fragment - fn main(@location(0) @interpolate(perspective, sample) fragUV: vec2) -> FragmentOutput { - var c = textureSample(myTexture, mySampler, fragUV); - var output: FragmentOutput; - output.color0 = c; - output.color0.a = alpha[0]; - output.color1 = c; - output.color1.a = alpha[1]; - return output; - }`, - }), - entryPoint: 'main', + module, + entryPoint: `fmain__alpha_to_coverage_mask__${t.params.interpolated ? 'interp' : 'flat'}`, targets: [{ format }, { format }], }, primitive: { topology: 'triangle-list' },