From 47d19f5e63bbdf00138b677bfc2d19627cb0c4ad Mon Sep 17 00:00:00 2001
From: Gyuyoung Kim <gyuyoung@igalia.com>
Date: Fri, 16 Dec 2022 17:06:24 +0900
Subject: [PATCH] op: Introduce depth_bias.spec.ts file with a 'depth_bias'
 test (#2075)

This PR introduces a new test to check if depth bias works as expected
by drawing a square with different depth bias values like 'positive',
'negative', 'infinity', 'slope', 'clamp', etc.

Issue: #2023
---
 .../api/operation/rendering/depth.spec.ts     |   6 -
 .../operation/rendering/depth_bias.spec.ts    | 263 ++++++++++++++++++
 2 files changed, 263 insertions(+), 6 deletions(-)
 create mode 100644 src/webgpu/api/operation/rendering/depth_bias.spec.ts

diff --git a/src/webgpu/api/operation/rendering/depth.spec.ts b/src/webgpu/api/operation/rendering/depth.spec.ts
index 705014b7ba1f..853de3ee6afd 100644
--- a/src/webgpu/api/operation/rendering/depth.spec.ts
+++ b/src/webgpu/api/operation/rendering/depth.spec.ts
@@ -148,12 +148,6 @@ class DepthTest extends GPUTest {
 
 export const g = makeTestGroup(DepthTest);
 
-g.test('depth_bias')
-  .desc(
-    `Tests render results with different depth bias values: positive, negative, infinity, slope, clamp, etc.`
-  )
-  .unimplemented();
-
 g.test('depth_disabled')
   .desc('Tests render results with depth test disabled.')
   .fn(async t => {
diff --git a/src/webgpu/api/operation/rendering/depth_bias.spec.ts b/src/webgpu/api/operation/rendering/depth_bias.spec.ts
new file mode 100644
index 000000000000..08fdb85df687
--- /dev/null
+++ b/src/webgpu/api/operation/rendering/depth_bias.spec.ts
@@ -0,0 +1,263 @@
+export const description = `
+Tests render results with different depth bias values like 'positive', 'negative', 'infinity',
+'slope', 'clamp', etc.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { unreachable } from '../../../../common/util/util.js';
+import { DepthStencilFormat, EncodableTextureFormat } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { kValue } from '../../../util/constants.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+enum QuadAngle {
+  Flat,
+  TiltedX,
+}
+
+// Floating point depth buffers use the following formula to calculate bias
+// bias = depthBias * 2 ** (exponent(max z of primitive) - number of bits in mantissa) +
+//        slopeScale * maxSlope
+// https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias
+// https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdSetDepthBias.html
+// https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1516269-setdepthbias
+//
+// To get a final bias of 0.25 for primitives with z = 0.25, we can use
+// depthBias = 0.25 / (2 ** (-2 - 23)) = 8388608.
+const kPointTwoFiveBiasForPointTwoFiveZOnFloat = 8388608;
+
+class DepthBiasTest extends GPUTest {
+  runDepthBiasTest(
+    depthFormat: EncodableTextureFormat & DepthStencilFormat,
+    {
+      quadAngle,
+      bias,
+      biasSlopeScale,
+      biasClamp,
+      expectedDepth,
+    }: {
+      quadAngle: QuadAngle;
+      bias: number;
+      biasSlopeScale: number;
+      biasClamp: number;
+      expectedDepth: number;
+    }
+  ) {
+    const renderTargetFormat = 'rgba8unorm';
+    let vertexShaderCode: string;
+    switch (quadAngle) {
+      case QuadAngle.Flat:
+        // Draw a square at z = 0.25.
+        vertexShaderCode = `
+          @vertex
+          fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+            var pos = array<vec2<f32>, 6>(
+            vec2<f32>(-1.0, -1.0),
+            vec2<f32>( 1.0, -1.0),
+            vec2<f32>(-1.0,  1.0),
+            vec2<f32>(-1.0,  1.0),
+            vec2<f32>( 1.0, -1.0),
+            vec2<f32>( 1.0,  1.0));
+            return vec4<f32>(pos[VertexIndex], 0.25, 1.0);
+          }
+          `;
+        break;
+      case QuadAngle.TiltedX:
+        // Draw a square ranging from 0 to 0.5, bottom to top.
+        vertexShaderCode = `
+          @vertex
+          fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+            var pos = array<vec3<f32>, 6>(
+            vec3<f32>(-1.0, -1.0, 0.0),
+            vec3<f32>( 1.0, -1.0, 0.0),
+            vec3<f32>(-1.0,  1.0, 0.5),
+            vec3<f32>(-1.0,  1.0, 0.5),
+            vec3<f32>( 1.0, -1.0, 0.0),
+            vec3<f32>( 1.0,  1.0, 0.5));
+            return vec4<f32>(pos[VertexIndex], 1.0);
+          }
+          `;
+        break;
+      default:
+        unreachable();
+    }
+
+    const renderTarget = this.device.createTexture({
+      format: renderTargetFormat,
+      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+    });
+
+    const depthTexture = this.device.createTexture({
+      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+      format: depthFormat,
+      sampleCount: 1,
+      mipLevelCount: 1,
+      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+    });
+
+    const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+      view: depthTexture.createView(),
+      depthLoadOp: 'load',
+      depthStoreOp: 'store',
+      stencilLoadOp: 'load',
+      stencilStoreOp: 'store',
+    };
+
+    const encoder = this.device.createCommandEncoder();
+    const pass = encoder.beginRenderPass({
+      colorAttachments: [
+        {
+          view: renderTarget.createView(),
+          storeOp: 'store',
+          loadOp: 'load',
+        },
+      ],
+      depthStencilAttachment,
+    });
+
+    let depthCompare: GPUCompareFunction = 'always';
+    if (depthFormat !== 'depth32float') {
+      depthCompare = 'greater';
+    }
+
+    const testState = {
+      format: depthFormat,
+      depthCompare,
+      depthWriteEnabled: true,
+      depthBias: bias,
+      depthBiasSlopeScale: biasSlopeScale,
+      depthBiasClamp: biasClamp,
+    } as const;
+
+    // Draw a square with the given depth state and bias values.
+    const testPipeline = this.createRenderPipelineForTest(vertexShaderCode, testState);
+    pass.setPipeline(testPipeline);
+    pass.draw(6);
+    pass.end();
+    this.device.queue.submit([encoder.finish()]);
+
+    const expColor = { Depth: expectedDepth };
+    const expTexelView = TexelView.fromTexelsAsColors(depthFormat, coords => expColor);
+
+    const result = textureContentIsOKByT2B(
+      this,
+      { texture: depthTexture },
+      [1, 1],
+      { expTexelView },
+      { maxDiffULPsForFloatFormat: 1 }
+    );
+    this.eventualExpectOK(result);
+    this.trackForCleanup(renderTarget);
+  }
+
+  createRenderPipelineForTest(
+    vertex: string,
+    depthStencil: GPUDepthStencilState
+  ): GPURenderPipeline {
+    return this.device.createRenderPipeline({
+      layout: 'auto',
+      vertex: {
+        module: this.device.createShaderModule({
+          code: vertex,
+        }),
+        entryPoint: 'main',
+      },
+      fragment: {
+        targets: [{ format: 'rgba8unorm' }],
+        module: this.device.createShaderModule({
+          code: `
+            @fragment fn main() -> @location(0) vec4<f32> {
+              return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+            }`,
+        }),
+        entryPoint: 'main',
+      },
+      depthStencil,
+    });
+  }
+}
+
+export const g = makeTestGroup(DepthBiasTest);
+
+g.test('depth_bias')
+  .desc(
+    `
+  Tests that a square with different depth bias values like 'positive', 'negative', 'infinity',
+  'slope', 'clamp', etc. is drawn as expected.
+
+  TODO: Need to test 'depth24plus-stencil8' format?
+  `
+  )
+  .params(u =>
+    u //
+      .combineWithParams([
+        {
+          quadAngle: QuadAngle.Flat,
+          bias: kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+          biasSlopeScale: 0,
+          biasClamp: 0,
+          expectedDepth: 0.5,
+        },
+        {
+          quadAngle: QuadAngle.Flat,
+          bias: kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+          biasSlopeScale: 0,
+          biasClamp: 0.125,
+          expectedDepth: 0.375,
+        },
+        {
+          quadAngle: QuadAngle.Flat,
+          bias: -kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+          biasSlopeScale: 0,
+          biasClamp: 0.125,
+          expectedDepth: 0,
+        },
+        {
+          quadAngle: QuadAngle.Flat,
+          bias: -kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+          biasSlopeScale: 0,
+          biasClamp: -0.125,
+          expectedDepth: 0.125,
+        },
+        {
+          quadAngle: QuadAngle.TiltedX,
+          bias: 0,
+          biasSlopeScale: 0,
+          biasClamp: 0,
+          expectedDepth: 0.25,
+        },
+        {
+          quadAngle: QuadAngle.TiltedX,
+          bias: 0,
+          biasSlopeScale: 1,
+          biasClamp: 0,
+          expectedDepth: 0.75,
+        },
+        {
+          quadAngle: QuadAngle.TiltedX,
+          bias: 0,
+          biasSlopeScale: -0.5,
+          biasClamp: 0,
+          expectedDepth: 0,
+        },
+        {
+          quadAngle: QuadAngle.TiltedX,
+          bias: 0,
+          biasSlopeScale: kValue.f32.infinity.positive,
+          biasClamp: 0,
+          expectedDepth: 1,
+        },
+        {
+          quadAngle: QuadAngle.TiltedX,
+          bias: 0,
+          biasSlopeScale: kValue.f32.infinity.negative,
+          biasClamp: 0,
+          expectedDepth: 0,
+        },
+      ] as const)
+  )
+  .fn(async t => {
+    t.runDepthBiasTest('depth32float', t.params);
+  });