Add depth-clamping support for Metal

This CL adds depth clamping support to Metal by invoking
MTLRenderCommandEncoder::setDepthClipMode. I only implemented the
feature for the new-style of RenderPipelineDescriptor since the
old one seems to be deprecated.

Bug: dawn:716
Change-Id: Icd63c72294546042ae452360863a7f9c16b40f95
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/45640
Commit-Queue: Brian Ho <hob@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
diff --git a/dawn.json b/dawn.json
index ea011fd..a2aa130 100644
--- a/dawn.json
+++ b/dawn.json
@@ -832,7 +832,8 @@
             {"name": "shader float16", "type": "bool", "default": "false"},
             {"name": "pipeline statistics query", "type": "bool", "default": "false"},
             {"name": "timestamp query", "type": "bool", "default": "false"},
-            {"name": "multi planar formats", "type": "bool", "default": "false"}
+            {"name": "multi planar formats", "type": "bool", "default": "false"},
+            {"name": "depth clamping", "type": "bool", "default": "false"}
         ]
     },
     "depth stencil state descriptor": {
@@ -1598,6 +1599,14 @@
         ]
     },
 
+    "primitive depth clamping state": {
+        "category": "structure",
+        "chained": true,
+        "members": [
+            {"name": "clamp depth", "type": "bool", "default": "false"}
+        ]
+    },
+
     "depth stencil state": {
         "category": "structure",
         "extensible": true,
@@ -1866,7 +1875,8 @@
             {"value": 5, "name": "shader module SPIRV descriptor"},
             {"value": 6, "name": "shader module WGSL descriptor"},
             {"value": 7, "name": "sampler descriptor dummy anisotropic filtering"},
-            {"value": 8, "name": "render pipeline descriptor dummy extension"}
+            {"value": 8, "name": "render pipeline descriptor dummy extension"},
+            {"value": 9, "name": "primitive depth clamping state"}
         ]
     },
     "texture": {
diff --git a/src/dawn_native/Extensions.cpp b/src/dawn_native/Extensions.cpp
index 98d4a61..47b2481 100644
--- a/src/dawn_native/Extensions.cpp
+++ b/src/dawn_native/Extensions.cpp
@@ -52,7 +52,11 @@
               {"multiplanar_formats",
                "Import and use multi-planar texture formats with per plane views",
                "https://bugs.chromium.org/p/dawn/issues/detail?id=551"},
-              &WGPUDeviceProperties::multiPlanarFormats}}};
+              &WGPUDeviceProperties::multiPlanarFormats},
+             {Extension::DepthClamping,
+              {"depth_clamping", "Clamp depth to [0, 1] in NDC space instead of clipping",
+               "https://bugs.chromium.org/p/dawn/issues/detail?id=716"},
+              &WGPUDeviceProperties::depthClamping}}};
 
     }  // anonymous namespace
 
diff --git a/src/dawn_native/Extensions.h b/src/dawn_native/Extensions.h
index 08689ca..00dd639 100644
--- a/src/dawn_native/Extensions.h
+++ b/src/dawn_native/Extensions.h
@@ -29,6 +29,7 @@
         PipelineStatisticsQuery,
         TimestampQuery,
         MultiPlanarFormats,
+        DepthClamping,
 
         EnumCount,
         InvalidEnum = EnumCount,
diff --git a/src/dawn_native/RenderPipeline.cpp b/src/dawn_native/RenderPipeline.cpp
index 15e8e7e..eb30623 100644
--- a/src/dawn_native/RenderPipeline.cpp
+++ b/src/dawn_native/RenderPipeline.cpp
@@ -131,9 +131,16 @@
             return {};
         }
 
-        MaybeError ValidatePrimitiveState(const PrimitiveState* descriptor) {
-            if (descriptor->nextInChain != nullptr) {
-                return DAWN_VALIDATION_ERROR("nextInChain must be nullptr");
+        MaybeError ValidatePrimitiveState(const DeviceBase* device,
+                                          const PrimitiveState* descriptor) {
+            const ChainedStruct* chained = descriptor->nextInChain;
+            if (chained != nullptr) {
+                if (chained->sType != wgpu::SType::PrimitiveDepthClampingState) {
+                    return DAWN_VALIDATION_ERROR("Unsupported sType");
+                }
+                if (!device->IsExtensionEnabled(Extension::DepthClamping)) {
+                    return DAWN_VALIDATION_ERROR("The depth clamping feature is not supported");
+                }
             }
 
             DAWN_TRY(ValidatePrimitiveTopology(descriptor->topology));
@@ -269,7 +276,6 @@
 
             return {};
         }
-
     }  // anonymous namespace
 
     // Helper functions
@@ -306,7 +312,7 @@
 
         DAWN_TRY(ValidateVertexState(device, &descriptor->vertex, descriptor->layout));
 
-        DAWN_TRY(ValidatePrimitiveState(&descriptor->primitive));
+        DAWN_TRY(ValidatePrimitiveState(device, &descriptor->primitive));
 
         if (descriptor->depthStencil) {
             DAWN_TRY(ValidateDepthStencilState(device, descriptor->depthStencil));
@@ -393,6 +399,12 @@
         }
 
         mPrimitive = descriptor->primitive;
+        const ChainedStruct* chained = mPrimitive.nextInChain;
+        if (chained != nullptr) {
+            ASSERT(chained->sType == wgpu::SType::PrimitiveDepthClampingState);
+            const auto* clampState = static_cast<const PrimitiveDepthClampingState*>(chained);
+            mClampDepth = clampState->clampDepth;
+        }
         mMultisample = descriptor->multisample;
 
         if (mAttachmentState->HasDepthStencilAttachment()) {
@@ -532,6 +544,11 @@
         return mDepthStencil.depthBiasClamp;
     }
 
+    bool RenderPipelineBase::ShouldClampDepth() const {
+        ASSERT(!IsError());
+        return mClampDepth;
+    }
+
     ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments>
     RenderPipelineBase::GetColorAttachmentsMask() const {
         ASSERT(!IsError());
@@ -624,7 +641,7 @@
 
         // Record primitive state
         recorder.Record(mPrimitive.topology, mPrimitive.stripIndexFormat, mPrimitive.frontFace,
-                        mPrimitive.cullMode);
+                        mPrimitive.cullMode, mClampDepth);
 
         // Record multisample state
         // Sample count hashed as part of the attachment state
@@ -738,7 +755,8 @@
             const PrimitiveState& stateB = b->mPrimitive;
             if (stateA.topology != stateB.topology ||
                 stateA.stripIndexFormat != stateB.stripIndexFormat ||
-                stateA.frontFace != stateB.frontFace || stateA.cullMode != stateB.cullMode) {
+                stateA.frontFace != stateB.frontFace || stateA.cullMode != stateB.cullMode ||
+                a->mClampDepth != b->mClampDepth) {
                 return false;
             }
         }
diff --git a/src/dawn_native/RenderPipeline.h b/src/dawn_native/RenderPipeline.h
index 0ee90b5..bf36af5 100644
--- a/src/dawn_native/RenderPipeline.h
+++ b/src/dawn_native/RenderPipeline.h
@@ -78,6 +78,7 @@
         int32_t GetDepthBias() const;
         float GetDepthBiasSlopeScale() const;
         float GetDepthBiasClamp() const;
+        bool ShouldClampDepth() const;
 
         ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments> GetColorAttachmentsMask() const;
         bool HasDepthStencilAttachment() const;
@@ -116,6 +117,7 @@
         PrimitiveState mPrimitive;
         DepthStencilState mDepthStencil;
         MultisampleState mMultisample;
+        bool mClampDepth = false;
     };
 
 }  // namespace dawn_native
diff --git a/src/dawn_native/metal/BackendMTL.mm b/src/dawn_native/metal/BackendMTL.mm
index 0637514..dcf01f9 100644
--- a/src/dawn_native/metal/BackendMTL.mm
+++ b/src/dawn_native/metal/BackendMTL.mm
@@ -227,6 +227,9 @@
                     // See https://github.com/gpuweb/gpuweb/issues/1325
                 }
             }
+            if (@available(macOS 10.11, iOS 11.0, *)) {
+                mSupportedExtensions.EnableExtension(Extension::DepthClamping);
+            }
 
             mSupportedExtensions.EnableExtension(Extension::ShaderFloat16);
         }
diff --git a/src/dawn_native/metal/CommandBufferMTL.mm b/src/dawn_native/metal/CommandBufferMTL.mm
index ad6c59a..71f0d0f 100644
--- a/src/dawn_native/metal/CommandBufferMTL.mm
+++ b/src/dawn_native/metal/CommandBufferMTL.mm
@@ -1193,6 +1193,11 @@
                     [encoder setDepthBias:newPipeline->GetDepthBias()
                                slopeScale:newPipeline->GetDepthBiasSlopeScale()
                                     clamp:newPipeline->GetDepthBiasClamp()];
+                    if (@available(macOS 10.11, iOS 11.0, *)) {
+                        MTLDepthClipMode clipMode = newPipeline->ShouldClampDepth() ?
+                            MTLDepthClipModeClamp : MTLDepthClipModeClip;
+                        [encoder setDepthClipMode:clipMode];
+                    }
                     newPipeline->Encode(encoder);
 
                     lastPipeline = newPipeline;
diff --git a/src/tests/BUILD.gn b/src/tests/BUILD.gn
index bce93a7..18d2fb5 100644
--- a/src/tests/BUILD.gn
+++ b/src/tests/BUILD.gn
@@ -321,6 +321,7 @@
     "end2end/ObjectCachingTests.cpp",
     "end2end/OpArrayLengthTests.cpp",
     "end2end/PipelineLayoutTests.cpp",
+    "end2end/PrimitiveStateTests.cpp",
     "end2end/PrimitiveTopologyTests.cpp",
     "end2end/QueryTests.cpp",
     "end2end/QueueTests.cpp",
diff --git a/src/tests/end2end/PrimitiveStateTests.cpp b/src/tests/end2end/PrimitiveStateTests.cpp
new file mode 100644
index 0000000..3cee443
--- /dev/null
+++ b/src/tests/end2end/PrimitiveStateTests.cpp
@@ -0,0 +1,300 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/DawnTest.h"
+
+#include "common/Assert.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/WGPUHelpers.h"
+
+constexpr static unsigned int kRTSize = 1;
+
+class DepthClampingTest : public DawnTest {
+  protected:
+    void SetUp() override {
+        DawnTest::SetUp();
+        DAWN_SKIP_TEST_IF(!SupportsExtensions({"depth_clamping"}));
+
+        wgpu::TextureDescriptor renderTargetDescriptor;
+        renderTargetDescriptor.size = {kRTSize, kRTSize};
+        renderTargetDescriptor.format = wgpu::TextureFormat::RGBA8Unorm;
+        renderTargetDescriptor.usage =
+            wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+        renderTarget = device.CreateTexture(&renderTargetDescriptor);
+
+        renderTargetView = renderTarget.CreateView();
+
+        wgpu::TextureDescriptor depthDescriptor;
+        depthDescriptor.dimension = wgpu::TextureDimension::e2D;
+        depthDescriptor.size = {kRTSize, kRTSize};
+        depthDescriptor.format = wgpu::TextureFormat::Depth24PlusStencil8;
+        depthDescriptor.usage = wgpu::TextureUsage::RenderAttachment;
+        depthTexture = device.CreateTexture(&depthDescriptor);
+
+        depthTextureView = depthTexture.CreateView();
+
+        vsModule = utils::CreateShaderModule(device, R"(
+            [[block]] struct UBO {
+                color : vec3<f32>;
+                depth : f32;
+            };
+            [[group(0), binding(0)]] var<uniform> ubo : UBO;
+            [[builtin(position)]] var<out> Position : vec4<f32>;
+
+            [[stage(vertex)]] fn main() -> void {
+                Position = vec4<f32>(0.0, 0.0, ubo.depth, 1.0);
+            })");
+
+        fsModule = utils::CreateShaderModule(device, R"(
+            [[block]] struct UBO {
+                color : vec3<f32>;
+                depth : f32;
+            };
+            [[group(0), binding(0)]] var<uniform> ubo : UBO;
+
+            [[location(0)]] var<out> fragColor : vec4<f32>;
+
+            [[stage(fragment)]] fn main() -> void {
+                fragColor = vec4<f32>(ubo.color, 1.0);
+            })");
+    }
+
+    std::vector<const char*> GetRequiredExtensions() override {
+        std::vector<const char*> requiredExtensions = {};
+        if (SupportsExtensions({"depth_clamping"})) {
+            requiredExtensions.push_back("depth_clamping");
+        }
+        return requiredExtensions;
+    }
+
+    struct TestSpec {
+        wgpu::PrimitiveDepthClampingState* depthClampingState;
+        RGBA8 color;
+        float depth;
+        wgpu::CompareFunction depthCompareFunction;
+    };
+
+    // Each test param represents a pair of triangles with a color, depth, stencil value, and
+    // depthStencil state, one frontfacing, one backfacing Draw the triangles in order and check the
+    // expected colors for the frontfaces and backfaces
+    void DoTest(const std::vector<TestSpec>& testParams, const RGBA8& expected) {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        struct TriangleData {
+            float color[3];
+            float depth;
+        };
+
+        utils::ComboRenderPassDescriptor renderPass({renderTargetView}, depthTextureView);
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+
+        for (size_t i = 0; i < testParams.size(); ++i) {
+            const TestSpec& test = testParams[i];
+
+            TriangleData data = {
+                {static_cast<float>(test.color.r) / 255.f, static_cast<float>(test.color.g) / 255.f,
+                 static_cast<float>(test.color.b) / 255.f},
+                test.depth,
+            };
+            // Upload a buffer for each triangle's depth and color data
+            wgpu::Buffer buffer = utils::CreateBufferFromData(device, &data, sizeof(TriangleData),
+                                                              wgpu::BufferUsage::Uniform);
+
+            // Create a pipeline for the triangles with the test spec's params.
+            utils::ComboRenderPipelineDescriptor2 descriptor;
+            descriptor.primitive.nextInChain = test.depthClampingState;
+            descriptor.primitive.topology = wgpu::PrimitiveTopology::PointList;
+            descriptor.vertex.module = vsModule;
+            descriptor.cFragment.module = fsModule;
+            wgpu::DepthStencilState* depthStencil = descriptor.EnableDepthStencil();
+            depthStencil->depthWriteEnabled = true;
+            depthStencil->depthCompare = test.depthCompareFunction;
+            depthStencil->format = wgpu::TextureFormat::Depth24PlusStencil8;
+
+            wgpu::RenderPipeline pipeline = device.CreateRenderPipeline2(&descriptor);
+
+            // Create a bind group for the data
+            wgpu::BindGroup bindGroup = utils::MakeBindGroup(
+                device, pipeline.GetBindGroupLayout(0), {{0, buffer}});
+
+            pass.SetPipeline(pipeline);
+            pass.SetBindGroup(0, bindGroup);
+            pass.Draw(1);
+        }
+        pass.EndPass();
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+
+        EXPECT_PIXEL_RGBA8_EQ(expected, renderTarget,  0, 0) << "Pixel check failed";
+    }
+
+    wgpu::Texture renderTarget;
+    wgpu::Texture depthTexture;
+    wgpu::TextureView renderTargetView;
+    wgpu::TextureView depthTextureView;
+    wgpu::ShaderModule vsModule;
+    wgpu::ShaderModule fsModule;
+};
+
+// Test that fragments beyond the far plane are clamped to 1.0 if depth clamping is enabled.
+TEST_P(DepthClampingTest, ClampOnBeyondFarPlane) {
+    wgpu::PrimitiveDepthClampingState clampingState;
+    clampingState.clampDepth = true;
+
+    DoTest(
+        {
+            // Draw a red triangle at depth 1.
+            {
+                nullptr, /* depthClampingState */
+                RGBA8(255, 0, 0, 255), /* color */
+                1.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+            // Draw a green triangle at depth 2 which should get clamped to 1.
+            {
+                &clampingState,
+                RGBA8(0, 255, 0, 255), /* color */
+                2.f, /* depth */
+                wgpu::CompareFunction::Equal,
+            },
+        },
+        // Since we draw the green triangle with an "equal" depth compare function, the resulting
+        // fragment should be green.
+        RGBA8(0, 255, 0, 255));
+}
+
+// Test that fragments beyond the near plane are clamped to 0.0 if depth clamping is enabled.
+TEST_P(DepthClampingTest, ClampOnBeyondNearPlane) {
+    wgpu::PrimitiveDepthClampingState clampingState;
+    clampingState.clampDepth = true;
+
+    DoTest(
+        {
+            // Draw a red triangle at depth 0.
+            {
+                nullptr, /* depthClampingState */
+                RGBA8(255, 0, 0, 255), /* color */
+                0.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+            // Draw a green triangle at depth -1 which should get clamped to 0.
+            {
+                &clampingState,
+                RGBA8(0, 255, 0, 255), /* color */
+                -1.f, /* depth */
+                wgpu::CompareFunction::Equal,
+            },
+        },
+        // Since we draw the green triangle with an "equal" depth compare function, the resulting
+        // fragment should be green.
+        RGBA8(0, 255, 0, 255));
+}
+
+// Test that fragments inside the view frustum are unaffected by depth clamping.
+TEST_P(DepthClampingTest, ClampOnInsideViewFrustum) {
+    wgpu::PrimitiveDepthClampingState clampingState;
+    clampingState.clampDepth = true;
+
+    DoTest(
+        {
+            {
+                &clampingState,
+                RGBA8(0, 255, 0, 255), /* color */
+                0.5f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+        },
+        RGBA8(0, 255, 0, 255));
+}
+
+
+// Test that fragments outside the view frustum are clipped if depth clamping is disabled.
+TEST_P(DepthClampingTest, ClampOffOutsideViewFrustum) {
+    wgpu::PrimitiveDepthClampingState clampingState;
+    clampingState.clampDepth = false;
+
+    DoTest(
+        {
+            {
+                &clampingState,
+                RGBA8(0, 255, 0, 255), /* color */
+                2.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+            {
+                &clampingState,
+                RGBA8(0, 255, 0, 255), /* color */
+                -1.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+        },
+        RGBA8(0, 0, 0, 0));
+}
+
+// Test that fragments outside the view frustum are clipped if clampDepth is left unspecified.
+TEST_P(DepthClampingTest, ClampUnspecifiedOutsideViewFrustum) {
+    DoTest(
+        {
+            {
+                nullptr, /* depthClampingState */
+                RGBA8(0, 255, 0, 255), /* color */
+                -1.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+            {
+                nullptr, /* depthClampingState */
+                RGBA8(0, 255, 0, 255), /* color */
+                2.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+        },
+        RGBA8(0, 0, 0, 0));
+}
+
+// Test that fragments are properly clipped or clamped if multiple render pipelines are used
+// within the same render pass with differing clampDepth values.
+TEST_P(DepthClampingTest, MultipleRenderPipelines) {
+    wgpu::PrimitiveDepthClampingState clampingState;
+    clampingState.clampDepth = true;
+
+    wgpu::PrimitiveDepthClampingState clippingState;
+    clippingState.clampDepth = false;
+
+    DoTest(
+        {
+            // Draw green with clamping
+            {
+                &clampingState,
+                RGBA8(0, 255, 0, 255), /* color */
+                2.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+            // Draw red with clipping
+            {
+                &clippingState,
+                RGBA8(255, 0, 0, 255), /* color */
+                2.f, /* depth */
+                wgpu::CompareFunction::Always,
+            },
+        },
+        RGBA8(0, 255, 0, 255)); // Result should be green
+}
+
+DAWN_INSTANTIATE_TEST(DepthClampingTest,
+                      D3D12Backend(),
+                      MetalBackend(),
+                      OpenGLBackend(),
+                      OpenGLESBackend(),
+                      VulkanBackend());
diff --git a/src/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
index 0dd9bc0..4dea0c7 100644
--- a/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -550,6 +550,28 @@
     }
 }
 
+// Test that specifying a clampDepth value results in an error if the feature is not enabled.
+TEST_F(RenderPipelineValidationTest, ClampDepthWithoutExtension) {
+    {
+        utils::ComboRenderPipelineDescriptor2 descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        wgpu::PrimitiveDepthClampingState clampingState;
+        clampingState.clampDepth = true;
+        descriptor.primitive.nextInChain = &clampingState;
+        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline2(&descriptor));
+    }
+    {
+        utils::ComboRenderPipelineDescriptor2 descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        wgpu::PrimitiveDepthClampingState clampingState;
+        clampingState.clampDepth = false;
+        descriptor.primitive.nextInChain = &clampingState;
+        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline2(&descriptor));
+    }
+}
+
 // Test that the entryPoint names must be present for the correct stage in the shader module.
 TEST_F(RenderPipelineValidationTest, EntryPointNameValidation) {
     wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@@ -739,3 +761,34 @@
     descriptor.layout = layout1;
     ASSERT_DEVICE_ERROR(device.CreateRenderPipeline2(&descriptor));
 }
+
+class DepthClampingValidationTest : public RenderPipelineValidationTest {
+  protected:
+    WGPUDevice CreateTestDevice() override {
+        dawn_native::DeviceDescriptor descriptor;
+        descriptor.requiredExtensions = {"depth_clamping"};
+        return adapter.CreateDevice(&descriptor);
+    }
+};
+
+// Tests that specifying a clampDepth value succeeds if the extension is enabled.
+TEST_F(DepthClampingValidationTest, Success) {
+    {
+        utils::ComboRenderPipelineDescriptor2 descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        wgpu::PrimitiveDepthClampingState clampingState;
+        clampingState.clampDepth = true;
+        descriptor.primitive.nextInChain = &clampingState;
+        device.CreateRenderPipeline2(&descriptor);
+    }
+    {
+        utils::ComboRenderPipelineDescriptor2 descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        wgpu::PrimitiveDepthClampingState clampingState;
+        clampingState.clampDepth = false;
+        descriptor.primitive.nextInChain = &clampingState;
+        device.CreateRenderPipeline2(&descriptor);
+    }
+}