Add tests about max inter-stage shader variables and components

This patch adds tests about maxInterStageShaderVariables and
maxInterStageShaderComponents to verify that we can create such
render pipelines with the values of maximum inter-stage shader
variables and components queried from the adapter.

Bug: dawn:685, dawn:1448
Test: dawn_end2end_tests
Change-Id: I2799c75857f1e04775ac509429f7cb3bc0db8e50
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/155361
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Austin Eng <enga@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/src/dawn/native/Limits.cpp b/src/dawn/native/Limits.cpp
index bbf60cd..7f0e402 100644
--- a/src/dawn/native/Limits.cpp
+++ b/src/dawn/native/Limits.cpp
@@ -305,6 +305,8 @@
     limits->maxVertexBuffers = std::min(limits->maxVertexBuffers, uint32_t(kMaxVertexBuffers));
     limits->maxInterStageShaderComponents =
         std::min(limits->maxInterStageShaderComponents, kMaxInterStageShaderComponents);
+    limits->maxInterStageShaderVariables =
+        std::min(limits->maxInterStageShaderVariables, kMaxInterStageShaderVariables);
     limits->maxSampledTexturesPerShaderStage =
         std::min(limits->maxSampledTexturesPerShaderStage, kMaxSampledTexturesPerShaderStage);
     limits->maxSamplersPerShaderStage =
diff --git a/src/dawn/tests/end2end/MaxLimitTests.cpp b/src/dawn/tests/end2end/MaxLimitTests.cpp
index 43e4327..97d9e97 100644
--- a/src/dawn/tests/end2end/MaxLimitTests.cpp
+++ b/src/dawn/tests/end2end/MaxLimitTests.cpp
@@ -719,5 +719,319 @@
                       OpenGLESBackend(),
                       VulkanBackend());
 
+// Verifies the limits maxInterStageShaderVariables and maxInterStageShaderComponents work correctly
+class MaxInterStageLimitTests : public MaxLimitTests {
+  public:
+    struct MaxInterStageLimitTestsSpec {
+        bool renderPointLists;
+        bool hasSampleMask;
+        bool hasSampleIndex;
+        bool hasFrontFacing;
+    };
+
+    void DoTest(const MaxInterStageLimitTestsSpec& spec) {
+        wgpu::RenderPipeline pipeline = CreateRenderPipeline(spec);
+        EXPECT_NE(nullptr, pipeline.Get());
+    }
+
+  private:
+    struct InterStageVariableAllocation {
+        uint32_t variableSize;
+        uint32_t variableCount;
+    };
+    using InterStageVariableAllocations = std::array<InterStageVariableAllocation, 2>;
+
+    // Allocate the inter-stage shader variables that consume all inter-stage shader variables and
+    // components
+    InterStageVariableAllocations AllocateInterStageVariables(
+        const MaxInterStageLimitTestsSpec& spec) {
+        wgpu::Limits baseLimits = GetAdapterLimits().limits;
+
+        uint32_t interStageVariableCount = baseLimits.maxInterStageShaderVariables;
+
+        uint32_t builtinCount = static_cast<uint32_t>(spec.renderPointLists) +
+                                static_cast<uint32_t>(spec.hasFrontFacing) +
+                                static_cast<uint32_t>(spec.hasSampleIndex) +
+                                static_cast<uint32_t>(spec.hasSampleMask);
+        uint32_t userDefinedInterStageComponents =
+            baseLimits.maxInterStageShaderComponents - builtinCount;
+
+        InterStageVariableAllocations allocation;
+
+        // Allocate `userDefinedInterStageComponents` inter-stage shader components to fill
+        // `interStageVariableCount` inter-stage shader variables.
+        // For example, assuming we are neither rendering point lists and nor using built-ins other
+        // than @builtin(position):
+        // 1. on D3D12, maxInterStageShaderVariables == 30 and maxInterStageShaderComponents == 120,
+        //    so allocation[1].variableCount = 0 (allocation[1].variableSize == 5 is not used) and
+        //    allocation[0].variableCount = 30.
+        // 2. on Vulkan. maxInterStageShaderVariables can be 16 and maxInterStageShaderComponents
+        //    can be 60, so the inter-stage variables are 12 vec4fs and 4 vec3fs.
+        allocation[0].variableSize = userDefinedInterStageComponents / interStageVariableCount;
+        allocation[1].variableSize = allocation[0].variableSize + 1;
+        allocation[1].variableCount =
+            userDefinedInterStageComponents - interStageVariableCount * allocation[0].variableSize;
+        allocation[0].variableCount = interStageVariableCount - allocation[1].variableCount;
+        DAWN_ASSERT(userDefinedInterStageComponents ==
+                    allocation[0].variableSize * allocation[0].variableCount +
+                        allocation[1].variableSize * allocation[1].variableCount);
+
+        return allocation;
+    }
+
+    std::string GetWGSLTypeFromVariableSize(uint32_t variableSize) {
+        switch (variableSize) {
+            case 1:
+                return "f32";
+            case 2:
+                return "vec2f";
+            case 3:
+                return "vec3f";
+            case 4:
+                return "vec4f";
+            // It's OK to return an empty string for variableSize == 5 as when variableSizes[i] ==
+            // 0, variableSizesCount[i] must be 5, thus the returned empty string won't be used.
+            case 5:
+                return "";
+            default:
+                DAWN_UNREACHABLE();
+                return "";
+        }
+    }
+
+    std::string GetInterStageVariableDeclarations(const InterStageVariableAllocations& allocation) {
+        std::stringstream stream;
+
+        stream << "struct VertexOut {" << std::endl;
+
+        uint32_t interStageVariableCount =
+            allocation[0].variableCount + allocation[1].variableCount;
+        for (uint32_t location = 0; location < interStageVariableCount; ++location) {
+            uint32_t variableIndexInAllocation = (location < allocation[0].variableCount) ? 0 : 1;
+            std::string wgslType =
+                GetWGSLTypeFromVariableSize(allocation[variableIndexInAllocation].variableSize);
+            stream << "@location(" << location << ") color" << location << " : " << wgslType << ", "
+                   << std::endl;
+        }
+
+        stream << "@builtin(position) pos : vec4f" << std::endl << "}" << std::endl;
+
+        return stream.str();
+    }
+
+    wgpu::ShaderModule GetShaderModuleForTest(const MaxInterStageLimitTestsSpec& spec) {
+        std::stringstream stream;
+
+        InterStageVariableAllocations allocation = AllocateInterStageVariables(spec);
+        stream << GetInterStageVariableDeclarations(allocation) << std::endl
+               << GetVertexShaderForTest(allocation) << std::endl
+               << GetFragmentShaderForTest(allocation, spec) << std::endl;
+        return utils::CreateShaderModule(device, stream.str().c_str());
+    }
+
+    std::string GetVertexShaderForTest(const InterStageVariableAllocations& allocation) {
+        std::stringstream stream;
+        stream << R"(
+        @vertex
+        fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOut {
+            var pos = array<vec2f, 3>(
+                vec2f(-1.0, -1.0),
+                vec2f( 2.0,  0.0),
+                vec2f( 0.0,  2.0));
+            var output : VertexOut;
+            output.pos = vec4f(pos[vertexIndex], 0.0, 1.0);
+            var vertexIndexFloat  = f32(vertexIndex);
+)";
+        // Ensure every inter-stage shader variable is used instead of being optimized out.
+        uint32_t interStageVariableCount =
+            allocation[0].variableCount + allocation[1].variableCount;
+        for (uint32_t location = 0; location < interStageVariableCount; ++location) {
+            uint32_t variableIndexInAllocation = (location < allocation[0].variableCount) ? 0 : 1;
+            std::string wgslType =
+                GetWGSLTypeFromVariableSize(allocation[variableIndexInAllocation].variableSize);
+            stream << "output.color" << location << " = " << wgslType << "(";
+            for (uint32_t index = 0; index < allocation[variableIndexInAllocation].variableSize;
+                 ++index) {
+                stream << "vertexIndexFloat / "
+                       << location * allocation[variableIndexInAllocation].variableCount + index +
+                              1;
+                if (index != allocation[variableIndexInAllocation].variableSize - 1) {
+                    stream << ", ";
+                }
+            }
+            stream << ");" << std::endl;
+        }
+        stream << "return output;" << std::endl << "}" << std::endl;
+        return stream.str();
+    }
+
+    std::string GetInterStageVariableToVec4f(uint32_t location, uint32_t variableSize) {
+        std::stringstream stream;
+
+        switch (variableSize) {
+            case 1:
+                stream << "vec4f(input.color" << location << ", 0, 0, 1)";
+                break;
+            case 2:
+                stream << "vec4f(input.color" << location << ", 0, 1)";
+                break;
+            case 3:
+                stream << "vec4f(input.color" << location << ", 1)";
+                break;
+            case 4:
+                stream << "input.color" << location;
+                break;
+            default:
+                DAWN_UNREACHABLE();
+                break;
+        }
+
+        return stream.str();
+    }
+
+    std::string GetFragmentShaderForTest(const InterStageVariableAllocations& allocation,
+                                         const MaxInterStageLimitTestsSpec& spec) {
+        std::stringstream stream;
+
+        stream << "@fragment fn fs_main(input: VertexOut";
+        if (spec.hasFrontFacing) {
+            stream << ", @builtin(front_facing) isFront : bool";
+        }
+        if (spec.hasSampleIndex) {
+            stream << ", @builtin(sample_index) sampleIndex : u32";
+        }
+        if (spec.hasSampleMask) {
+            stream << ", @builtin(sample_mask) sampleMask : u32";
+        }
+        // Ensure every inter-stage shader variable and built-in variable is used instead of being
+        // optimized out.
+        stream << ") -> @location(0) vec4f {" << std::endl << "return input.pos";
+        if (spec.hasFrontFacing) {
+            stream << " + vec4f(f32(isFront), 0, 0, 1)";
+        }
+        if (spec.hasSampleIndex) {
+            stream << " + vec4f(f32(sampleIndex), 0, 0, 1)";
+        }
+        if (spec.hasSampleMask) {
+            stream << " + vec4f(f32(sampleMask), 0, 0, 1)";
+        }
+        uint32_t interStageVariableCount =
+            allocation[0].variableCount + allocation[1].variableCount;
+        for (uint32_t location = 0; location < interStageVariableCount; ++location) {
+            uint32_t variableIndexInAllocation = (location < allocation[0].variableCount) ? 0 : 1;
+            stream << " + "
+                   << GetInterStageVariableToVec4f(
+                          location, allocation[variableIndexInAllocation].variableSize);
+        }
+        stream << ";}";
+        return stream.str();
+    }
+
+    wgpu::RenderPipeline CreateRenderPipeline(const MaxInterStageLimitTestsSpec& spec) {
+        wgpu::ShaderModule shaderModule = GetShaderModuleForTest(spec);
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = shaderModule;
+        descriptor.vertex.entryPoint = "vs_main";
+        descriptor.cFragment.module = shaderModule;
+        descriptor.cFragment.entryPoint = "fs_main";
+        descriptor.vertex.bufferCount = 0;
+        descriptor.cBuffers[0].attributeCount = 0;
+        descriptor.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
+        descriptor.primitive.topology = spec.renderPointLists
+                                            ? wgpu::PrimitiveTopology::PointList
+                                            : wgpu::PrimitiveTopology::TriangleList;
+        if (spec.hasSampleIndex || spec.hasSampleMask) {
+            descriptor.multisample.count = 4;
+        }
+        return device.CreateRenderPipeline(&descriptor);
+    }
+};
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with no built-in variables.
+TEST_P(MaxInterStageLimitTests, NoBuiltins) {
+    MaxInterStageLimitTestsSpec spec = {};
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with @builtin(sample_mask). On D3D SV_Coverage doesn't consume an independent float4
+// register.
+TEST_P(MaxInterStageLimitTests, SampleMask) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.hasSampleMask = true;
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with @builtin(sample_index). On D3D SV_SampleIndex consumes an independent float4
+// register.
+TEST_P(MaxInterStageLimitTests, SampleIndex) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.hasSampleIndex = true;
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with @builtin(front_facing). On D3D SV_IsFrontFace consumes an independent float4
+// register.
+TEST_P(MaxInterStageLimitTests, FrontFacing) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.hasFrontFacing = true;
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with @builtin(front_facing). On D3D SV_IsFrontFace and SV_SampleIndex consume one
+// independent float4 register.
+TEST_P(MaxInterStageLimitTests, SampleIndex_FrontFacing) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.hasSampleIndex = true;
+    spec.hasFrontFacing = true;
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with @builtin(sample_mask),
+// @builtin(sample_index) and @builtin(front_facing).
+TEST_P(MaxInterStageLimitTests, SampleMask_SampleIndex_FrontFacing) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.hasSampleMask = true;
+    spec.hasSampleIndex = true;
+    spec.hasFrontFacing = true;
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with PointList primitive topology. On Vulkan when the primitive topology is PointList,
+// the SPIR-V builtin PointSize must be declared in vertex shader, which will consume 1 inter-stage
+// shader component.
+TEST_P(MaxInterStageLimitTests, RenderPointList) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.renderPointLists = true;
+    DoTest(spec);
+}
+
+// Tests that both maxInterStageShaderComponents and maxInterStageShaderVariables work for a render
+// pipeline with PointList primitive topology, @builtin(sample_mask),
+// @builtin(sample_index) and @builtin(front_facing).
+TEST_P(MaxInterStageLimitTests, RenderPointList_SampleMask_SampleIndex_FrontFacing) {
+    MaxInterStageLimitTestsSpec spec = {};
+    spec.renderPointLists = true;
+    spec.hasSampleMask = true;
+    spec.hasSampleIndex = true;
+    spec.hasFrontFacing = true;
+    DoTest(spec);
+}
+
+DAWN_INSTANTIATE_TEST(MaxInterStageLimitTests,
+                      D3D11Backend(),
+                      D3D12Backend({}, {"use_dxc"}),
+                      D3D12Backend({"use_dxc"}),
+                      MetalBackend(),
+                      OpenGLBackend(),
+                      OpenGLESBackend(),
+                      VulkanBackend());
+
 }  // anonymous namespace
 }  // namespace dawn