[tint] Resolve subgroup_size as varying in fragment

Unlike compute the builtin "subgroup_size" needs to be varying in
fragment shader.

Bug:361593660
Change-Id: Icd4693347a182679ef9749630d77e73a028cd862
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/204498
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Peter McNeeley <petermcneeley@google.com>
diff --git a/src/dawn/tests/end2end/SubgroupsTests.cpp b/src/dawn/tests/end2end/SubgroupsTests.cpp
index bb83ddf..39577f0 100644
--- a/src/dawn/tests/end2end/SubgroupsTests.cpp
+++ b/src/dawn/tests/end2end/SubgroupsTests.cpp
@@ -25,12 +25,14 @@
 // 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 <cstdint>
 #include <string>
 #include <vector>
 
 #include "dawn/common/GPUInfo.h"
 #include "dawn/common/Math.h"
 #include "dawn/tests/DawnTest.h"
+#include "dawn/utils/ComboRenderPipelineDescriptor.h"
 #include "dawn/utils/WGPUHelpers.h"
 
 namespace dawn {
@@ -245,6 +247,118 @@
                         {false, true}  // UseChromiumExperimentalSubgroups
 );
 
+class SubgroupsShaderTestsFragment : public SubgroupsTestsBase<SubgroupsShaderTestsParams> {
+  protected:
+    // Testing reading subgroup_size in fragment shader. There is no workgroup size here and
+    // subgroup_size is varying.
+    void FragmentSubgroupSizeTest() {
+        wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
+            @vertex
+            fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f {
+                var pos = array(
+                    vec2f(-1.0, -1.0),
+                    vec2f(-1.0,  1.0),
+                    vec2f( 1.0, -1.0),
+                    vec2f( 1.0,  1.0),
+                    vec2f(-1.0,  1.0),
+                    vec2f( 1.0, -1.0));
+                return vec4f(pos[VertexIndex], 0.5, 1.0);
+            })");
+
+        std::stringstream fsCode;
+        {
+            EnableExtensions(fsCode);
+            fsCode << R"(
+            @group(0) @binding(0) var<storage, read_write> output : array<u32>;
+            @fragment fn main(@builtin(subgroup_size) sg_size : u32) -> @location(0) vec4f {
+                output[0] = sg_size;
+                return vec4f(0.0, 1.0, 0.0, 1.0);
+            })";
+        }
+
+        wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, fsCode.str().c_str());
+
+        utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, 100, 100);
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        descriptor.cTargets[0].format = renderPass.colorFormat;
+
+        auto pipeline = device.CreateRenderPipeline(&descriptor);
+
+        constexpr uint32_t kArrayNumElements = 1u;
+        uint32_t outputBufferSizeInBytes = sizeof(uint32_t) * kArrayNumElements;
+        wgpu::BufferDescriptor outputBufferDesc;
+        outputBufferDesc.size = outputBufferSizeInBytes;
+        outputBufferDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc;
+        wgpu::Buffer outputBuffer = device.CreateBuffer(&outputBufferDesc);
+        wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                         {
+                                                             {0, outputBuffer},
+                                                         });
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        {
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+            pass.SetPipeline(pipeline);
+            pass.SetBindGroup(0, bindGroup);
+            pass.Draw(6);
+            pass.End();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+
+        // Check that the fragment shader ran.
+        EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kGreen, renderPass.color, 0, 0);
+        EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kGreen, renderPass.color, 0, 99);
+        EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kGreen, renderPass.color, 99, 0);
+        EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kGreen, renderPass.color, 99, 99);
+
+        EXPECT_BUFFER(outputBuffer, 0, outputBufferSizeInBytes, new ExpectReadSubgroupSizeOutput());
+    }
+
+  private:
+    class ExpectReadSubgroupSizeOutput : public dawn::detail::Expectation {
+      public:
+        ExpectReadSubgroupSizeOutput() {}
+        testing::AssertionResult Check(const void* data, size_t size) override {
+            DAWN_ASSERT(size == sizeof(int32_t));
+            const uint32_t* actual = static_cast<const uint32_t*>(data);
+            const uint32_t& outputSubgroupSizeAt0 = actual[0];
+            // Subgroup size can vary across fragment invocation (unlike compute) but we could still
+            // improve this check by using the min and max subgroup size for the device.
+            if (!(
+                    // subgroup_size should be at least 1
+                    (1 <= outputSubgroupSizeAt0) &&
+                    // subgroup_size should be no larger than 128
+                    (outputSubgroupSizeAt0 <= 128) &&
+                    // subgroup_size should be a power of 2
+                    (IsPowerOfTwo(outputSubgroupSizeAt0)))) {
+                testing::AssertionResult result = testing::AssertionFailure()
+                                                  << "Got invalid subgroup_size output: "
+                                                  << outputSubgroupSizeAt0;
+                return result;
+            }
+            return testing::AssertionSuccess();
+        }
+    };
+};
+
+// Test that subgroup_size builtin attribute read by each invocation is valid and identical for any
+// workgroup size between 1 and 256.
+TEST_P(SubgroupsShaderTestsFragment, ReadSubgroupSize) {
+    DAWN_TEST_UNSUPPORTED_IF(!IsSubgroupsEnabledInWGSL());
+    FragmentSubgroupSizeTest();
+}
+
+// DawnTestBase::CreateDeviceImpl always enables allow_unsafe_apis toggle.
+DAWN_INSTANTIATE_TEST_P(SubgroupsShaderTestsFragment,
+                        {D3D12Backend(), D3D12Backend({}, {"use_dxc"}), MetalBackend(),
+                         VulkanBackend()},
+                        {false, true}  // UseChromiumExperimentalSubgroups
+);
+
 enum class BroadcastType {
     I32,
     U32,
diff --git a/src/tint/lang/wgsl/resolver/uniformity.cc b/src/tint/lang/wgsl/resolver/uniformity.cc
index c790b2c..caeecb4 100644
--- a/src/tint/lang/wgsl/resolver/uniformity.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity.cc
@@ -1198,10 +1198,15 @@
                     return false;
                 }
                 if (builtin == core::BuiltinValue::kSubgroupSize) {
-                    // Currently Tint only allow using subgroup_size builtin as a compute shader
-                    // input.
-                    TINT_ASSERT(entry_point->PipelineStage() == ast::PipelineStage::kCompute);
-                    return false;
+                    if (entry_point->PipelineStage() == ast::PipelineStage::kCompute) {
+                        // Subgroup size is uniform in compute.
+                        return false;
+                    } else {
+                        // Currently the only other allowed usage for subgroup_size is in fragment.
+                        TINT_ASSERT(entry_point->PipelineStage() == ast::PipelineStage::kFragment);
+                        // Subgroup size is considered to be varying for fragment.
+                        return true;
+                    }
                 }
             }
             return true;
diff --git a/src/tint/lang/wgsl/resolver/uniformity_test.cc b/src/tint/lang/wgsl/resolver/uniformity_test.cc
index 022998c..7431b5b 100644
--- a/src/tint/lang/wgsl/resolver/uniformity_test.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity_test.cc
@@ -635,7 +635,12 @@
 class FragmentBuiltin : public UniformityAnalysisTestBase,
                         public ::testing::TestWithParam<BuiltinEntry> {};
 TEST_P(FragmentBuiltin, AsParam) {
-    std::string src = R"(
+    std::string src = std::string((GetParam().name == "subgroup_size")
+                                      ? R"(enable chromium_experimental_subgroups;
+)"
+                                      : R"(
+                                      )") +
+                      R"(
 @fragment
 fn main(@builtin()" + GetParam().name +
                       R"() b : )" + GetParam().type + R"() {
@@ -649,15 +654,15 @@
     RunTest(src, should_pass);
     if (!should_pass) {
         EXPECT_EQ(error_,
-                  R"(test:5:9 error: 'dpdx' must only be called from uniform control flow
+                  R"(test:6:9 error: 'dpdx' must only be called from uniform control flow
     _ = dpdx(0.5);
         ^^^^^^^^^
 
-test:4:3 note: control flow depends on possibly non-uniform value
+test:5:3 note: control flow depends on possibly non-uniform value
   if (u32(vec4(b).x) == 0u) {
   ^^
 
-test:4:16 note: builtin 'b' of 'main' may be non-uniform
+test:5:16 note: builtin 'b' of 'main' may be non-uniform
   if (u32(vec4(b).x) == 0u) {
                ^
 )");
@@ -665,7 +670,12 @@
 }
 
 TEST_P(FragmentBuiltin, InStruct) {
-    std::string src = R"(
+    std::string src = std::string((GetParam().name == "subgroup_size")
+                                      ? R"(enable chromium_experimental_subgroups;
+)"
+                                      : R"(
+                                      )") +
+                      R"(
 struct S {
   @builtin()" + GetParam().name +
                       R"() b : )" + GetParam().type + R"(
@@ -683,15 +693,15 @@
     RunTest(src, should_pass);
     if (!should_pass) {
         EXPECT_EQ(error_,
-                  R"(test:9:9 error: 'dpdx' must only be called from uniform control flow
+                  R"(test:10:9 error: 'dpdx' must only be called from uniform control flow
     _ = dpdx(0.5);
         ^^^^^^^^^
 
-test:8:3 note: control flow depends on possibly non-uniform value
+test:9:3 note: control flow depends on possibly non-uniform value
   if (u32(vec4(s.b).x) == 0u) {
   ^^
 
-test:8:16 note: parameter 's' of 'main' may be non-uniform
+test:9:16 note: parameter 's' of 'main' may be non-uniform
   if (u32(vec4(s.b).x) == 0u) {
                ^
 )");
@@ -703,7 +713,8 @@
                          ::testing::Values(BuiltinEntry{"position", "vec4<f32>", false},
                                            BuiltinEntry{"front_facing", "bool", false},
                                            BuiltinEntry{"sample_index", "u32", false},
-                                           BuiltinEntry{"sample_mask", "u32", false}),
+                                           BuiltinEntry{"sample_mask", "u32", false},
+                                           BuiltinEntry{"subgroup_size", "u32", false}),
                          [](const ::testing::TestParamInfo<FragmentBuiltin::ParamType>& p) {
                              return p.param.name;
                          });
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
index 2162ce8..ce7de68 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
@@ -307,13 +307,16 @@
         MakeBuiltinParam(b, ty.bool_(), core::BuiltinValue::kFrontFacing),
         MakeBuiltinParam(b, ty.u32(), core::BuiltinValue::kSampleIndex),
         MakeBuiltinParam(b, ty.u32(), core::BuiltinValue::kSampleMask),
+        MakeBuiltinParam(b, ty.u32(), core::BuiltinValue::kSubgroupSize),
     });
 
     fn->Block()->Append(b.Return(fn));
 
     EXPECT_WGSL(R"(
+enable chromium_experimental_subgroups;
+
 @fragment
-fn f(@builtin(front_facing) v : bool, @builtin(sample_index) v_1 : u32, @builtin(sample_mask) v_2 : u32) {
+fn f(@builtin(front_facing) v : bool, @builtin(sample_index) v_1 : u32, @builtin(sample_mask) v_2 : u32, @builtin(subgroup_size) v_3 : u32) {
 }
 )");
 }
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl
new file mode 100644
index 0000000..372412a
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl
@@ -0,0 +1,12 @@
+enable chromium_experimental_subgroups;
+
+@group(0) @binding(0)
+var<storage, read_write> output: array<u32>;
+
+@fragment
+fn main(
+  @builtin(subgroup_invocation_id) subgroup_invocation_id : u32,
+  @builtin(subgroup_size) subgroup_size : u32,
+) {
+  output[subgroup_invocation_id] = subgroup_size;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.dxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.dxc.hlsl
new file mode 100644
index 0000000..cf9946b
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.dxc.hlsl
@@ -0,0 +1,10 @@
+RWByteAddressBuffer output : register(u0);
+
+void main_inner(uint subgroup_invocation_id, uint subgroup_size) {
+  output.Store((4u * subgroup_invocation_id), asuint(subgroup_size));
+}
+
+void main() {
+  main_inner(WaveGetLaneIndex(), WaveGetLaneCount());
+  return;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.fxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.fxc.hlsl
new file mode 100644
index 0000000..cf9946b
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.fxc.hlsl
@@ -0,0 +1,10 @@
+RWByteAddressBuffer output : register(u0);
+
+void main_inner(uint subgroup_invocation_id, uint subgroup_size) {
+  output.Store((4u * subgroup_invocation_id), asuint(subgroup_size));
+}
+
+void main() {
+  main_inner(WaveGetLaneIndex(), WaveGetLaneCount());
+  return;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.glsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.glsl
new file mode 100644
index 0000000..bd86a34
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.glsl
@@ -0,0 +1,18 @@
+SKIP: FAILED
+
+
+enable chromium_experimental_subgroups;
+
+@group(0) @binding(0) var<storage, read_write> tint_symbol : array<u32>;
+
+@fragment
+fn tint_symbol_1(@builtin(subgroup_invocation_id) subgroup_invocation_id : u32, @builtin(subgroup_size) subgroup_size : u32) {
+  tint_symbol[subgroup_invocation_id] = subgroup_size;
+}
+
+Failed to generate: <dawn>/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl:1:8 error: GLSL backend does not support extension 'chromium_experimental_subgroups'
+enable chromium_experimental_subgroups;
+       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+tint executable returned error: exit status 1
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.dxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.dxc.hlsl
new file mode 100644
index 0000000..a8c5780
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.dxc.hlsl
@@ -0,0 +1,11 @@
+SKIP: FAILED
+
+../../src/tint/lang/hlsl/writer/raise/shader_io.cc:101 internal compiler error: TINT_UNREACHABLE 
+********************************************************************
+*  The tint shader compiler has encountered an unexpected error.   *
+*                                                                  *
+*  Please help us fix this issue by submitting a bug report at     *
+*  crbug.com/tint with the source program that triggered the bug.  *
+********************************************************************
+
+tint executable returned error: signal: illegal instruction
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.fxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.fxc.hlsl
new file mode 100644
index 0000000..a8c5780
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.fxc.hlsl
@@ -0,0 +1,11 @@
+SKIP: FAILED
+
+../../src/tint/lang/hlsl/writer/raise/shader_io.cc:101 internal compiler error: TINT_UNREACHABLE 
+********************************************************************
+*  The tint shader compiler has encountered an unexpected error.   *
+*                                                                  *
+*  Please help us fix this issue by submitting a bug report at     *
+*  crbug.com/tint with the source program that triggered the bug.  *
+********************************************************************
+
+tint executable returned error: signal: illegal instruction
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.glsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.glsl
new file mode 100644
index 0000000..0da5df3
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.glsl
@@ -0,0 +1,3 @@
+SKIP: FAILED
+
+signal: segmentation fault
\ No newline at end of file
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.msl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.msl
new file mode 100644
index 0000000..354275f
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.ir.msl
@@ -0,0 +1,27 @@
+#include <metal_stdlib>
+using namespace metal;
+
+template<typename T, size_t N>
+struct tint_array {
+  const constant T& operator[](size_t i) const constant { return elements[i]; }
+  device T& operator[](size_t i) device { return elements[i]; }
+  const device T& operator[](size_t i) const device { return elements[i]; }
+  thread T& operator[](size_t i) thread { return elements[i]; }
+  const thread T& operator[](size_t i) const thread { return elements[i]; }
+  threadgroup T& operator[](size_t i) threadgroup { return elements[i]; }
+  const threadgroup T& operator[](size_t i) const threadgroup { return elements[i]; }
+  T elements[N];
+};
+
+struct tint_module_vars_struct {
+  device tint_array<uint, 1>* output;
+};
+
+void tint_symbol_inner(uint subgroup_invocation_id, uint subgroup_size, tint_module_vars_struct tint_module_vars) {
+  (*tint_module_vars.output)[subgroup_invocation_id] = subgroup_size;
+}
+
+fragment void tint_symbol(uint subgroup_invocation_id [[thread_index_in_simdgroup]], uint subgroup_size [[threads_per_simdgroup]], device tint_array<uint, 1>* output [[buffer(0)]]) {
+  tint_module_vars_struct const tint_module_vars = tint_module_vars_struct{.output=output};
+  tint_symbol_inner(subgroup_invocation_id, subgroup_size, tint_module_vars);
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.msl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.msl
new file mode 100644
index 0000000..e4773aa
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.msl
@@ -0,0 +1,29 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+template<typename T, size_t N>
+struct tint_array {
+    const constant T& operator[](size_t i) const constant { return elements[i]; }
+    device T& operator[](size_t i) device { return elements[i]; }
+    const device T& operator[](size_t i) const device { return elements[i]; }
+    thread T& operator[](size_t i) thread { return elements[i]; }
+    const thread T& operator[](size_t i) const thread { return elements[i]; }
+    threadgroup T& operator[](size_t i) threadgroup { return elements[i]; }
+    const threadgroup T& operator[](size_t i) const threadgroup { return elements[i]; }
+    T elements[N];
+};
+
+struct tint_symbol_3 {
+  /* 0x0000 */ tint_array<uint, 1> arr;
+};
+
+void tint_symbol_inner(uint subgroup_invocation_id, uint subgroup_size, device tint_array<uint, 1>* const tint_symbol_1) {
+  (*(tint_symbol_1))[subgroup_invocation_id] = subgroup_size;
+}
+
+fragment void tint_symbol(device tint_symbol_3* tint_symbol_2 [[buffer(0)]], uint subgroup_invocation_id [[thread_index_in_simdgroup]], uint subgroup_size [[threads_per_simdgroup]]) {
+  tint_symbol_inner(subgroup_invocation_id, subgroup_size, &((*(tint_symbol_2)).arr));
+  return;
+}
+
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.spvasm b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.spvasm
new file mode 100644
index 0000000..cb875b0
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.spvasm
@@ -0,0 +1,56 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 1
+; Bound: 24
+; Schema: 0
+               OpCapability Shader
+               OpCapability GroupNonUniform
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %main_subgroup_invocation_id_Input %main_subgroup_size_Input
+               OpExecutionMode %main OriginUpperLeft
+               OpMemberName %tint_symbol_1 0 "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %main_subgroup_invocation_id_Input "main_subgroup_invocation_id_Input"
+               OpName %main_subgroup_size_Input "main_subgroup_size_Input"
+               OpName %main_inner "main_inner"
+               OpName %subgroup_invocation_id "subgroup_invocation_id"
+               OpName %subgroup_size "subgroup_size"
+               OpName %main "main"
+               OpDecorate %_runtimearr_uint ArrayStride 4
+               OpMemberDecorate %tint_symbol_1 0 Offset 0
+               OpDecorate %tint_symbol_1 Block
+               OpDecorate %1 DescriptorSet 0
+               OpDecorate %1 Binding 0
+               OpDecorate %1 Coherent
+               OpDecorate %main_subgroup_invocation_id_Input Flat
+               OpDecorate %main_subgroup_invocation_id_Input BuiltIn SubgroupLocalInvocationId
+               OpDecorate %main_subgroup_size_Input Flat
+               OpDecorate %main_subgroup_size_Input BuiltIn SubgroupSize
+       %uint = OpTypeInt 32 0
+%_runtimearr_uint = OpTypeRuntimeArray %uint
+%tint_symbol_1 = OpTypeStruct %_runtimearr_uint
+%_ptr_StorageBuffer_tint_symbol_1 = OpTypePointer StorageBuffer %tint_symbol_1
+          %1 = OpVariable %_ptr_StorageBuffer_tint_symbol_1 StorageBuffer
+%_ptr_Input_uint = OpTypePointer Input %uint
+%main_subgroup_invocation_id_Input = OpVariable %_ptr_Input_uint Input
+%main_subgroup_size_Input = OpVariable %_ptr_Input_uint Input
+       %void = OpTypeVoid
+         %13 = OpTypeFunction %void %uint %uint
+%_ptr_StorageBuffer_uint = OpTypePointer StorageBuffer %uint
+     %uint_0 = OpConstant %uint 0
+         %19 = OpTypeFunction %void
+ %main_inner = OpFunction %void None %13
+%subgroup_invocation_id = OpFunctionParameter %uint
+%subgroup_size = OpFunctionParameter %uint
+         %14 = OpLabel
+         %15 = OpAccessChain %_ptr_StorageBuffer_uint %1 %uint_0 %subgroup_invocation_id
+               OpStore %15 %subgroup_size None
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %19
+         %20 = OpLabel
+         %21 = OpLoad %uint %main_subgroup_invocation_id_Input None
+         %22 = OpLoad %uint %main_subgroup_size_Input None
+         %23 = OpFunctionCall %void %main_inner %21 %22
+               OpReturn
+               OpFunctionEnd
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.wgsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.wgsl
new file mode 100644
index 0000000..0a06e13
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins.wgsl.expected.wgsl
@@ -0,0 +1,8 @@
+enable chromium_experimental_subgroups;
+
+@group(0) @binding(0) var<storage, read_write> output : array<u32>;
+
+@fragment
+fn main(@builtin(subgroup_invocation_id) subgroup_invocation_id : u32, @builtin(subgroup_size) subgroup_size : u32) {
+  output[subgroup_invocation_id] = subgroup_size;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl
new file mode 100644
index 0000000..4b10630
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl
@@ -0,0 +1,14 @@
+enable chromium_experimental_subgroups;
+
+@group(0) @binding(0)
+var<storage, read_write> output: array<u32>;
+
+struct FragmentInputs {
+  @builtin(subgroup_invocation_id) subgroup_invocation_id : u32,
+  @builtin(subgroup_size) subgroup_size : u32,
+};
+
+@fragment
+fn main(inputs : FragmentInputs) {
+  output[inputs.subgroup_invocation_id] = inputs.subgroup_size;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.dxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.dxc.hlsl
new file mode 100644
index 0000000..f32544d
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.dxc.hlsl
@@ -0,0 +1,16 @@
+RWByteAddressBuffer output : register(u0);
+
+struct FragmentInputs {
+  uint subgroup_invocation_id;
+  uint subgroup_size;
+};
+
+void main_inner(FragmentInputs inputs) {
+  output.Store((4u * inputs.subgroup_invocation_id), asuint(inputs.subgroup_size));
+}
+
+void main() {
+  FragmentInputs tint_symbol = {WaveGetLaneIndex(), WaveGetLaneCount()};
+  main_inner(tint_symbol);
+  return;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.fxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.fxc.hlsl
new file mode 100644
index 0000000..f32544d
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.fxc.hlsl
@@ -0,0 +1,16 @@
+RWByteAddressBuffer output : register(u0);
+
+struct FragmentInputs {
+  uint subgroup_invocation_id;
+  uint subgroup_size;
+};
+
+void main_inner(FragmentInputs inputs) {
+  output.Store((4u * inputs.subgroup_invocation_id), asuint(inputs.subgroup_size));
+}
+
+void main() {
+  FragmentInputs tint_symbol = {WaveGetLaneIndex(), WaveGetLaneCount()};
+  main_inner(tint_symbol);
+  return;
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.glsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.glsl
new file mode 100644
index 0000000..aca086f
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.glsl
@@ -0,0 +1,25 @@
+SKIP: FAILED
+
+
+enable chromium_experimental_subgroups;
+
+@group(0) @binding(0) var<storage, read_write> tint_symbol : array<u32>;
+
+struct FragmentInputs {
+  @builtin(subgroup_invocation_id)
+  subgroup_invocation_id : u32,
+  @builtin(subgroup_size)
+  subgroup_size : u32,
+}
+
+@fragment
+fn tint_symbol_1(inputs : FragmentInputs) {
+  tint_symbol[inputs.subgroup_invocation_id] = inputs.subgroup_size;
+}
+
+Failed to generate: <dawn>/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl:1:8 error: GLSL backend does not support extension 'chromium_experimental_subgroups'
+enable chromium_experimental_subgroups;
+       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+tint executable returned error: exit status 1
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.dxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.dxc.hlsl
new file mode 100644
index 0000000..a8c5780
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.dxc.hlsl
@@ -0,0 +1,11 @@
+SKIP: FAILED
+
+../../src/tint/lang/hlsl/writer/raise/shader_io.cc:101 internal compiler error: TINT_UNREACHABLE 
+********************************************************************
+*  The tint shader compiler has encountered an unexpected error.   *
+*                                                                  *
+*  Please help us fix this issue by submitting a bug report at     *
+*  crbug.com/tint with the source program that triggered the bug.  *
+********************************************************************
+
+tint executable returned error: signal: illegal instruction
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.fxc.hlsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.fxc.hlsl
new file mode 100644
index 0000000..a8c5780
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.fxc.hlsl
@@ -0,0 +1,11 @@
+SKIP: FAILED
+
+../../src/tint/lang/hlsl/writer/raise/shader_io.cc:101 internal compiler error: TINT_UNREACHABLE 
+********************************************************************
+*  The tint shader compiler has encountered an unexpected error.   *
+*                                                                  *
+*  Please help us fix this issue by submitting a bug report at     *
+*  crbug.com/tint with the source program that triggered the bug.  *
+********************************************************************
+
+tint executable returned error: signal: illegal instruction
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.glsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.glsl
new file mode 100644
index 0000000..0da5df3
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.glsl
@@ -0,0 +1,3 @@
+SKIP: FAILED
+
+signal: segmentation fault
\ No newline at end of file
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.msl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.msl
new file mode 100644
index 0000000..e52db92
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.ir.msl
@@ -0,0 +1,32 @@
+#include <metal_stdlib>
+using namespace metal;
+
+struct FragmentInputs {
+  uint subgroup_invocation_id;
+  uint subgroup_size;
+};
+
+template<typename T, size_t N>
+struct tint_array {
+  const constant T& operator[](size_t i) const constant { return elements[i]; }
+  device T& operator[](size_t i) device { return elements[i]; }
+  const device T& operator[](size_t i) const device { return elements[i]; }
+  thread T& operator[](size_t i) thread { return elements[i]; }
+  const thread T& operator[](size_t i) const thread { return elements[i]; }
+  threadgroup T& operator[](size_t i) threadgroup { return elements[i]; }
+  const threadgroup T& operator[](size_t i) const threadgroup { return elements[i]; }
+  T elements[N];
+};
+
+struct tint_module_vars_struct {
+  device tint_array<uint, 1>* output;
+};
+
+void tint_symbol_inner(FragmentInputs inputs, tint_module_vars_struct tint_module_vars) {
+  (*tint_module_vars.output)[inputs.subgroup_invocation_id] = inputs.subgroup_size;
+}
+
+fragment void tint_symbol(uint FragmentInputs_subgroup_invocation_id [[thread_index_in_simdgroup]], uint FragmentInputs_subgroup_size [[threads_per_simdgroup]], device tint_array<uint, 1>* output [[buffer(0)]]) {
+  tint_module_vars_struct const tint_module_vars = tint_module_vars_struct{.output=output};
+  tint_symbol_inner(FragmentInputs{.subgroup_invocation_id=FragmentInputs_subgroup_invocation_id, .subgroup_size=FragmentInputs_subgroup_size}, tint_module_vars);
+}
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.msl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.msl
new file mode 100644
index 0000000..d5a469a1
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.msl
@@ -0,0 +1,35 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+template<typename T, size_t N>
+struct tint_array {
+    const constant T& operator[](size_t i) const constant { return elements[i]; }
+    device T& operator[](size_t i) device { return elements[i]; }
+    const device T& operator[](size_t i) const device { return elements[i]; }
+    thread T& operator[](size_t i) thread { return elements[i]; }
+    const thread T& operator[](size_t i) const thread { return elements[i]; }
+    threadgroup T& operator[](size_t i) threadgroup { return elements[i]; }
+    const threadgroup T& operator[](size_t i) const threadgroup { return elements[i]; }
+    T elements[N];
+};
+
+struct tint_symbol_4 {
+  /* 0x0000 */ tint_array<uint, 1> arr;
+};
+
+struct FragmentInputs {
+  uint subgroup_invocation_id;
+  uint subgroup_size;
+};
+
+void tint_symbol_inner(FragmentInputs inputs, device tint_array<uint, 1>* const tint_symbol_2) {
+  (*(tint_symbol_2))[inputs.subgroup_invocation_id] = inputs.subgroup_size;
+}
+
+fragment void tint_symbol(device tint_symbol_4* tint_symbol_3 [[buffer(0)]], uint subgroup_invocation_id [[thread_index_in_simdgroup]], uint subgroup_size [[threads_per_simdgroup]]) {
+  FragmentInputs const tint_symbol_1 = {.subgroup_invocation_id=subgroup_invocation_id, .subgroup_size=subgroup_size};
+  tint_symbol_inner(tint_symbol_1, &((*(tint_symbol_3)).arr));
+  return;
+}
+
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.spvasm b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..eeddbaf
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.spvasm
@@ -0,0 +1,63 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 1
+; Bound: 27
+; Schema: 0
+               OpCapability Shader
+               OpCapability GroupNonUniform
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %main_subgroup_invocation_id_Input %main_subgroup_size_Input
+               OpExecutionMode %main OriginUpperLeft
+               OpMemberName %tint_symbol_1 0 "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %main_subgroup_invocation_id_Input "main_subgroup_invocation_id_Input"
+               OpName %main_subgroup_size_Input "main_subgroup_size_Input"
+               OpName %main_inner "main_inner"
+               OpMemberName %FragmentInputs 0 "subgroup_invocation_id"
+               OpMemberName %FragmentInputs 1 "subgroup_size"
+               OpName %FragmentInputs "FragmentInputs"
+               OpName %inputs "inputs"
+               OpName %main "main"
+               OpDecorate %_runtimearr_uint ArrayStride 4
+               OpMemberDecorate %tint_symbol_1 0 Offset 0
+               OpDecorate %tint_symbol_1 Block
+               OpDecorate %1 DescriptorSet 0
+               OpDecorate %1 Binding 0
+               OpDecorate %1 Coherent
+               OpDecorate %main_subgroup_invocation_id_Input Flat
+               OpDecorate %main_subgroup_invocation_id_Input BuiltIn SubgroupLocalInvocationId
+               OpDecorate %main_subgroup_size_Input Flat
+               OpDecorate %main_subgroup_size_Input BuiltIn SubgroupSize
+               OpMemberDecorate %FragmentInputs 0 Offset 0
+               OpMemberDecorate %FragmentInputs 1 Offset 4
+       %uint = OpTypeInt 32 0
+%_runtimearr_uint = OpTypeRuntimeArray %uint
+%tint_symbol_1 = OpTypeStruct %_runtimearr_uint
+%_ptr_StorageBuffer_tint_symbol_1 = OpTypePointer StorageBuffer %tint_symbol_1
+          %1 = OpVariable %_ptr_StorageBuffer_tint_symbol_1 StorageBuffer
+%_ptr_Input_uint = OpTypePointer Input %uint
+%main_subgroup_invocation_id_Input = OpVariable %_ptr_Input_uint Input
+%main_subgroup_size_Input = OpVariable %_ptr_Input_uint Input
+       %void = OpTypeVoid
+%FragmentInputs = OpTypeStruct %uint %uint
+         %13 = OpTypeFunction %void %FragmentInputs
+%_ptr_StorageBuffer_uint = OpTypePointer StorageBuffer %uint
+     %uint_0 = OpConstant %uint 0
+         %21 = OpTypeFunction %void
+ %main_inner = OpFunction %void None %13
+     %inputs = OpFunctionParameter %FragmentInputs
+         %14 = OpLabel
+         %15 = OpCompositeExtract %uint %inputs 0
+         %16 = OpAccessChain %_ptr_StorageBuffer_uint %1 %uint_0 %15
+         %19 = OpCompositeExtract %uint %inputs 1
+               OpStore %16 %19 None
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %21
+         %22 = OpLabel
+         %23 = OpLoad %uint %main_subgroup_invocation_id_Input None
+         %24 = OpLoad %uint %main_subgroup_size_Input None
+         %25 = OpCompositeConstruct %FragmentInputs %23 %24
+         %26 = OpFunctionCall %void %main_inner %25
+               OpReturn
+               OpFunctionEnd
diff --git a/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.wgsl b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..b7e71f3
--- /dev/null
+++ b/test/tint/types/functions/shader_io/fragment_subgroup_builtins_struct.wgsl.expected.wgsl
@@ -0,0 +1,15 @@
+enable chromium_experimental_subgroups;
+
+@group(0) @binding(0) var<storage, read_write> output : array<u32>;
+
+struct FragmentInputs {
+  @builtin(subgroup_invocation_id)
+  subgroup_invocation_id : u32,
+  @builtin(subgroup_size)
+  subgroup_size : u32,
+}
+
+@fragment
+fn main(inputs : FragmentInputs) {
+  output[inputs.subgroup_invocation_id] = inputs.subgroup_size;
+}