Expose PrimitiveID feature in Dawn

Adds the necessary plumbing to expose @builtin(primitive_id)
in Tint with a feature in Dawn, currently chromium-experimental-primitive-id

Bug: 342172182
Change-Id: I5c6d32afc504042bac0f76adbd0a714e265f5b6b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/254394
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
diff --git a/src/dawn/dawn.json b/src/dawn/dawn.json
index d1134ac..9f37766 100644
--- a/src/dawn/dawn.json
+++ b/src/dawn/dawn.json
@@ -2343,7 +2343,8 @@
             {"value": 55, "name": "chromium experimental subgroup matrix", "tags": ["dawn"]},
             {"value": 56, "name": "shared fence EGL sync", "tags": ["dawn", "native"]},
             {"value": 57, "name": "dawn device allocator control", "tags": ["dawn"]},
-            {"value": 58, "name": "texture component swizzle", "tags": ["dawn"]}
+            {"value": 58, "name": "texture component swizzle", "tags": ["dawn"]},
+            {"value": 59, "name": "chromium experimental primitive id", "tags": ["dawn"]}
         ]
     },
     "filter mode": {
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index f603992..f6d97d5 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -1720,6 +1720,10 @@
         mWGSLAllowedFeatures.extensions.insert(
             tint::wgsl::Extension::kChromiumExperimentalSubgroupMatrix);
     }
+    if (mEnabledFeatures.IsEnabled(Feature::ChromiumExperimentalPrimitiveId)) {
+        mWGSLAllowedFeatures.extensions.insert(
+            tint::wgsl::Extension::kChromiumExperimentalPrimitiveId);
+    }
 
     // Language features are enabled instance-wide.
     const auto& allowedFeatures = GetInstance()->GetAllowedWGSLLanguageFeatures();
diff --git a/src/dawn/native/Features.cpp b/src/dawn/native/Features.cpp
index 4116433..be3baad 100644
--- a/src/dawn/native/Features.cpp
+++ b/src/dawn/native/Features.cpp
@@ -428,6 +428,11 @@
      {"Supports configuring device allocator via DawnDeviceAllocatorControl",
       "https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/dawn/features/"
       "dawn_device_allocator_control.md",
+      FeatureInfo::FeatureState::Experimental}},
+    {Feature::ChromiumExperimentalPrimitiveId,
+     {"Supports the \"enable chromium_experimental_primitive_id;\" directive in WGSL",
+      "https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/tint/extensions/"
+      "chromium_experimental_primitive_id.md",
       FeatureInfo::FeatureState::Experimental}}};
 
 }  // anonymous namespace
diff --git a/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp b/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp
index 7f0201b..2801402 100644
--- a/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp
+++ b/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp
@@ -162,6 +162,8 @@
     EnableFeature(Feature::DawnPartialLoadResolveTexture);
     EnableFeature(Feature::RG11B10UfloatRenderable);
     EnableFeature(Feature::TextureFormatsTier1);
+    EnableFeature(Feature::ChromiumExperimentalPrimitiveId);
+
     if (mDeviceInfo.isUMA && mDeviceInfo.supportsMapNoOverwriteDynamicBuffers) {
         // With UMA we should allow mapping usages on more type of buffers.
         EnableFeature(Feature::BufferMapExtendedUsages);
diff --git a/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp b/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
index cad0945..8910b14 100644
--- a/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
+++ b/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
@@ -174,6 +174,7 @@
     EnableFeature(Feature::FlexibleTextureViews);
     EnableFeature(Feature::TextureFormatsTier1);
     EnableFeature(Feature::TextureComponentSwizzle);
+    EnableFeature(Feature::ChromiumExperimentalPrimitiveId);
 
     if (AreTimestampQueriesSupported()) {
         EnableFeature(Feature::TimestampQuery);
diff --git a/src/dawn/native/metal/PhysicalDeviceMTL.mm b/src/dawn/native/metal/PhysicalDeviceMTL.mm
index 9ad5f4f..b60836c 100644
--- a/src/dawn/native/metal/PhysicalDeviceMTL.mm
+++ b/src/dawn/native/metal/PhysicalDeviceMTL.mm
@@ -691,6 +691,9 @@
 
     if ([*mDevice supportsFamily:MTLGPUFamilyApple7]) {
         EnableFeature(Feature::ChromiumExperimentalSubgroupMatrix);
+        // TODO(342172182): This may be available in more places?
+        // (mwyrzykowski says "Apple7 and all Macs")
+        EnableFeature(Feature::ChromiumExperimentalPrimitiveId);
     }
 
     EnableFeature(Feature::SharedTextureMemoryIOSurface);
diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp
index ca1b540..08a4070 100644
--- a/src/dawn/native/vulkan/DeviceVk.cpp
+++ b/src/dawn/native/vulkan/DeviceVk.cpp
@@ -516,6 +516,11 @@
         usedKnobs.features.depthClamp = VK_TRUE;
     }
 
+    if (HasFeature(Feature::ChromiumExperimentalPrimitiveId)) {
+        DAWN_ASSERT(mDeviceInfo.features.geometryShader == VK_TRUE);
+        usedKnobs.features.geometryShader = VK_TRUE;
+    }
+
     bool shaderFloat16Int8FeaturesAdded = false;
     if (HasFeature(Feature::ShaderF16)) {
         DAWN_ASSERT(usedKnobs.HasExt(DeviceExt::ShaderFloat16Int8) &&
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index f3c3af8..bfbdfde 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -333,6 +333,12 @@
         EnableFeature(Feature::ClipDistances);
     }
 
+    // primitive_id is currently exposed if we support geometry shaders.
+    // TODO(342172182): We could also potentially use tessellation or mesh shaders.
+    if (mDeviceInfo.features.geometryShader == VK_TRUE) {
+        EnableFeature(Feature::ChromiumExperimentalPrimitiveId);
+    }
+
     bool shaderF16Enabled = false;
     if (mDeviceInfo.HasExt(DeviceExt::ShaderFloat16Int8) &&
         mDeviceInfo.HasExt(DeviceExt::_16BitStorage) &&
diff --git a/src/dawn/node/binding/Converter.cpp b/src/dawn/node/binding/Converter.cpp
index e274b45..59abac4 100644
--- a/src/dawn/node/binding/Converter.cpp
+++ b/src/dawn/node/binding/Converter.cpp
@@ -1627,6 +1627,9 @@
         case interop::GPUFeatureName::kTextureComponentSwizzle:
             out = wgpu::FeatureName::TextureComponentSwizzle;
             return true;
+        case interop::GPUFeatureName::kChromiumExperimentalPrimitiveId:
+            out = wgpu::FeatureName::ChromiumExperimentalPrimitiveId;
+            return true;
     }
     return false;
 }
@@ -1661,6 +1664,7 @@
         CASE(TextureFormatsTier1, kTextureFormatsTier1);
         CASE(TextureFormatsTier2, kTextureFormatsTier2);
         CASE(TextureComponentSwizzle, kTextureComponentSwizzle);
+        CASE(ChromiumExperimentalPrimitiveId, kChromiumExperimentalPrimitiveId);
 
 #undef CASE
 
diff --git a/src/dawn/node/interop/DawnExtensions.idl b/src/dawn/node/interop/DawnExtensions.idl
index 22b2c1a..3e69eea 100644
--- a/src/dawn/node/interop/DawnExtensions.idl
+++ b/src/dawn/node/interop/DawnExtensions.idl
@@ -32,6 +32,7 @@
     "multi-draw-indirect",
     "chromium-experimental-subgroup-matrix",
     "texture-component-swizzle",
+    "chromium-experimental-primitive-id",
 };
 
 enum GPUSubgroupMatrixComponentType {
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index a9a9e25..6437389 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -657,6 +657,7 @@
     "end2end/PipelineLayoutTests.cpp",
     "end2end/PixelLocalStorageTests.cpp",
     "end2end/PolyfillBuiltinSimpleTests.cpp",
+    "end2end/PrimitiveIdTests.cpp",
     "end2end/PrimitiveStateTests.cpp",
     "end2end/PrimitiveTopologyTests.cpp",
     "end2end/QueryTests.cpp",
diff --git a/src/dawn/tests/end2end/PrimitiveIdTests.cpp b/src/dawn/tests/end2end/PrimitiveIdTests.cpp
new file mode 100644
index 0000000..904c7ca
--- /dev/null
+++ b/src/dawn/tests/end2end/PrimitiveIdTests.cpp
@@ -0,0 +1,177 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <vector>
+
+#include "dawn/tests/DawnTest.h"
+#include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+constexpr uint32_t kRTSize = 16;
+constexpr wgpu::TextureFormat kFormat = wgpu::TextureFormat::RGBA8Unorm;
+
+constexpr wgpu::FeatureName kPrimitiveIdFeature =
+    wgpu::FeatureName::ChromiumExperimentalPrimitiveId;
+
+using RequirePrimitiveIdFeature = bool;
+DAWN_TEST_PARAM_STRUCT(PrimitiveIdTestsParams, RequirePrimitiveIdFeature);
+
+class PrimitiveIdTests : public DawnTestWithParams<PrimitiveIdTestsParams> {
+  public:
+    wgpu::Texture CreateDefault2DTexture() {
+        wgpu::TextureDescriptor descriptor;
+        descriptor.dimension = wgpu::TextureDimension::e2D;
+        descriptor.size.width = kRTSize;
+        descriptor.size.height = kRTSize;
+        descriptor.size.depthOrArrayLayers = 1;
+        descriptor.sampleCount = 1;
+        descriptor.format = kFormat;
+        descriptor.mipLevelCount = 1;
+        descriptor.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+        return device.CreateTexture(&descriptor);
+    }
+
+  protected:
+    std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+        mIsPrimitiveIdSupportedOnAdapter = SupportsFeatures({kPrimitiveIdFeature});
+        if (!mIsPrimitiveIdSupportedOnAdapter) {
+            return {};
+        }
+
+        if (GetParam().mRequirePrimitiveIdFeature) {
+            return {kPrimitiveIdFeature};
+        }
+
+        return {};
+    }
+
+    bool IsPrimitiveIdSupportedOnAdapter() const { return mIsPrimitiveIdSupportedOnAdapter; }
+
+  private:
+    bool mIsPrimitiveIdSupportedOnAdapter = false;
+};
+
+// Test simple primitive ID within shader with enable directive. The result should be as expected if
+// the device enables the extension, otherwise a shader creation error should be caught.
+TEST_P(PrimitiveIdTests, BasicPrimitiveIdFeaturesTest) {
+    // Skip if device doesn't support the extension.
+    DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(kPrimitiveIdFeature));
+
+    const char* shader = R"(
+enable chromium_experimental_primitive_id;
+
+@vertex
+fn VSMain(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f {
+    var pos = array(
+        vec2f(-1, -1), vec2f(1, -1), vec2f(-1, 1),
+        vec2f(1, 1), vec2f(1, -1), vec2f(-1, 1));
+
+    return vec4f(pos[VertexIndex % 6], 0.0, 1.0);
+}
+
+var<private> colorId : array<vec3f, 5> = array<vec3f, 5>(
+    vec3f(1, 0, 0),
+    vec3f(0, 1, 0),
+    vec3f(0, 0, 1),
+    vec3f(1, 1, 0),
+    vec3f(1, 1, 1)
+);
+
+@fragment
+fn FSMain(@builtin(primitive_id) pid : u32) -> @location(0) vec4f {
+    // Select a color based on the primitive ID
+    return vec4f(colorId[pid%5], 1.0);
+})";
+
+    wgpu::ShaderModule shaderModule = utils::CreateShaderModule(device, shader);
+
+    // Create render pipeline.
+    wgpu::RenderPipeline pipeline;
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+
+        descriptor.vertex.module = shaderModule;
+
+        descriptor.cFragment.module = shaderModule;
+        descriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList;
+        descriptor.cTargets[0].format = kFormat;
+
+        pipeline = device.CreateRenderPipeline(&descriptor);
+    }
+
+    wgpu::Texture renderTarget = CreateDefault2DTexture();
+
+    auto drawPrimitives = [&](uint32_t count) {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        {
+            // In the render pass we clear renderTarget to black, then draw a colored triangles
+            // based on the primitive ID to the top left and bottom right of the rnederTarget.
+            utils::ComboRenderPassDescriptor renderPass({renderTarget.CreateView()});
+            renderPass.cColorAttachments[0].clearValue = {0.0f, 0.0f, 0.0f, 1.0f};
+
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+            pass.SetPipeline(pipeline);
+            pass.Draw(count * 3);
+            pass.End();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    };
+
+    // Draw N primitives and ensure that the color written corresponds to the given primitive ID.
+    drawPrimitives(2);
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kRed, renderTarget, 1, kRTSize - 1);
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kGreen, renderTarget, kRTSize - 1, 1);
+
+    drawPrimitives(4);
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kBlue, renderTarget, 1, kRTSize - 1);
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kYellow, renderTarget, kRTSize - 1, 1);
+
+    drawPrimitives(6);
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kWhite, renderTarget, 1, kRTSize - 1);
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kRed, renderTarget, kRTSize - 1, 1);
+}
+
+// DawnTestBase::CreateDeviceImpl always enables allow_unsafe_apis toggle.
+DAWN_INSTANTIATE_TEST_P(PrimitiveIdTests,
+                        {
+                            D3D11Backend(),
+                            D3D12Backend(),
+                            VulkanBackend(),
+                            MetalBackend(),
+                            OpenGLBackend(),
+                            OpenGLESBackend(),
+                        },
+                        {true, false});
+
+}  // anonymous namespace
+}  // namespace dawn
diff --git a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
index ee7bc9d..b6458d4 100644
--- a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
@@ -882,6 +882,7 @@
     {"chromium_internal_graphite", true, {}, {}},
     {"chromium_experimental_framebuffer_fetch", true, {"framebuffer-fetch"}, {}},
     {"chromium_experimental_subgroup_matrix", true, {"chromium-experimental-subgroup-matrix"}, {}},
+    {"chromium_experimental_primitive_id", true, {"chromium-experimental-primitive-id"}, {}},
 
     // Currently the following WGSL extensions are not enabled under any situation.
     /*
diff --git a/src/dawn/wire/SupportedFeatures.cpp b/src/dawn/wire/SupportedFeatures.cpp
index de716ab..04e50aa 100644
--- a/src/dawn/wire/SupportedFeatures.cpp
+++ b/src/dawn/wire/SupportedFeatures.cpp
@@ -121,6 +121,7 @@
         case WGPUFeatureName_TextureFormatsTier1:
         case WGPUFeatureName_TextureFormatsTier2:
         case WGPUFeatureName_TextureComponentSwizzle:
+        case WGPUFeatureName_ChromiumExperimentalPrimitiveId:
             return true;
     }