Import Tint changes from Dawn

Changes:
  - 21d23ec2345ddf06502438ca1376b937059b8413 [ir][spirv-writer] Scalarize quantizeToF16 by James Price <jrprice@google.com>
  - 72643887aebc7688a846e12962cfa0689abd2bba [ir][spirv-writer] Emit NonReadable and NonWritable by James Price <jrprice@google.com>
  - 757dd563d2ad9b636cd6772f95bc948430c80bdc [ir][spirv-writer] Emit vertex point size builtin by James Price <jrprice@google.com>
  - fef99889810618448a4bc0d22b43a5129434998e [ir][spirv-writer] Support dual-source blending by James Price <jrprice@google.com>
  - 0c1f1c9c141a049fa585962c0c205a855bf14f72 [ir] Support subgroup builtin inputs by James Price <jrprice@google.com>
  - d69b6f39129c8591c32de12ae9241268aa7c6443 [ir] Strip interpolation attributes when invalid by James Price <jrprice@google.com>
  - 86f780dfb461a1aedc8e0b34d92c18c67f656d01 [ir][spirv-writer] Emit shader IO without structs by James Price <jrprice@google.com>
  - b27bce431140c1c01cb3690dfe24265e4a635300 D3D12: Replace deprecated IDxcCompiler with IDxcCompiler3 by Jiawei Shao <jiawei.shao@intel.com>
GitOrigin-RevId: 21d23ec2345ddf06502438ca1376b937059b8413
Change-Id: I4add5e60c99aeb68b297010d8e11d03eda85ef63
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/151687
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 5a9d29c..66ce989 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -480,6 +480,23 @@
                 out_ << " ";
                 EmitBindingPoint(v->BindingPoint().value());
             }
+            if (v->Attributes().invariant) {
+                out_ << " @invariant";
+            }
+            if (v->Attributes().location.has_value()) {
+                out_ << " @location(" << v->Attributes().location.value() << ")";
+            }
+            if (v->Attributes().interpolation.has_value()) {
+                auto& interp = v->Attributes().interpolation.value();
+                out_ << " @interpolate(" << interp.type;
+                if (interp.sampling != core::InterpolationSampling::kUndefined) {
+                    out_ << ", " << interp.sampling;
+                }
+                out_ << ")";
+            }
+            if (v->Attributes().builtin.has_value()) {
+                out_ << " @builtin(" << v->Attributes().builtin.value() << ")";
+            }
         },
         [&](Swizzle* s) {
             EmitValueWithType(s);
diff --git a/src/tint/lang/core/ir/function_param.cc b/src/tint/lang/core/ir/function_param.cc
index 1b76ff6..877364e 100644
--- a/src/tint/lang/core/ir/function_param.cc
+++ b/src/tint/lang/core/ir/function_param.cc
@@ -50,6 +50,10 @@
             return "sample_index";
         case FunctionParam::Builtin::kSampleMask:
             return "sample_mask";
+        case FunctionParam::Builtin::kSubgroupInvocationId:
+            return "subgroup_invocation_id";
+        case FunctionParam::Builtin::kSubgroupSize:
+            return "subgroup_size";
     }
     return "<unknown>";
 }
diff --git a/src/tint/lang/core/ir/function_param.h b/src/tint/lang/core/ir/function_param.h
index f8f83d4..d9bf75c 100644
--- a/src/tint/lang/core/ir/function_param.h
+++ b/src/tint/lang/core/ir/function_param.h
@@ -53,6 +53,10 @@
         kSampleIndex,
         /// Builtin Sample mask
         kSampleMask,
+        /// Builtin Subgroup invocation id
+        kSubgroupInvocationId,
+        /// Builtin Subgroup size
+        kSubgroupSize,
     };
 
     /// Constructor
diff --git a/src/tint/lang/core/ir/transform/shader_io.cc b/src/tint/lang/core/ir/transform/shader_io.cc
index f279b86..3edc00d 100644
--- a/src/tint/lang/core/ir/transform/shader_io.cc
+++ b/src/tint/lang/core/ir/transform/shader_io.cc
@@ -52,6 +52,10 @@
             return core::BuiltinValue::kSampleIndex;
         case FunctionParam::Builtin::kSampleMask:
             return core::BuiltinValue::kSampleMask;
+        case FunctionParam::Builtin::kSubgroupInvocationId:
+            return core::BuiltinValue::kSubgroupInvocationId;
+        case FunctionParam::Builtin::kSubgroupSize:
+            return core::BuiltinValue::kSubgroupSize;
     }
     return core::BuiltinValue::kUndefined;
 }
@@ -100,6 +104,15 @@
         // Process the parameters and return value to prepare for building a wrapper function.
         GatherInputs();
         GatherOutput();
+
+        // Add an output for the vertex point size if needed.
+        std::optional<uint32_t> vertex_point_size_index;
+        if (func->Stage() == Function::PipelineStage::kVertex && backend->NeedsVertexPointSize()) {
+            vertex_point_size_index =
+                backend->AddOutput(ir->symbols.New("vertex_point_size"), ty.f32(),
+                                   {{}, {}, {BuiltinValue::kPointSize}, {}, false});
+        }
+
         auto new_params = backend->FinalizeInputs();
         auto* new_ret_val = backend->FinalizeOutputs();
 
@@ -124,6 +137,9 @@
         auto inner_call_args = BuildInnerCallArgs(wrapper);
         auto* inner_result = wrapper.Call(func->ReturnType(), func, std::move(inner_call_args));
         SetOutputs(wrapper, inner_result->Result());
+        if (vertex_point_size_index) {
+            backend->SetOutput(wrapper, vertex_point_size_index.value(), b.Constant(1_f));
+        }
 
         // Return the new result.
         wrapper.Return(ep, new_ret_val);
@@ -135,8 +151,12 @@
             if (auto* str = param->Type()->As<core::type::Struct>()) {
                 for (auto* member : str->Members()) {
                     auto name = str->Name().Name() + "_" + member->Name().Name();
-                    backend->AddInput(ir->symbols.Register(name), member->Type(),
-                                      member->Attributes());
+                    auto attributes = member->Attributes();
+                    if (attributes.interpolation &&
+                        func->Stage() != Function::PipelineStage::kFragment) {
+                        attributes.interpolation = {};
+                    }
+                    backend->AddInput(ir->symbols.Register(name), member->Type(), attributes);
                     members_to_strip.Add(member);
                 }
             } else {
@@ -144,7 +164,7 @@
                 core::type::StructMemberAttributes attributes;
                 if (auto loc = param->Location()) {
                     attributes.location = loc->value;
-                    if (loc->interpolation) {
+                    if (loc->interpolation && func->Stage() == Function::PipelineStage::kFragment) {
                         attributes.interpolation = *loc->interpolation;
                     }
                     param->ClearLocation();
@@ -170,8 +190,11 @@
         if (auto* str = func->ReturnType()->As<core::type::Struct>()) {
             for (auto* member : str->Members()) {
                 auto name = str->Name().Name() + "_" + member->Name().Name();
-                backend->AddOutput(ir->symbols.Register(name), member->Type(),
-                                   member->Attributes());
+                auto attributes = member->Attributes();
+                if (attributes.interpolation && func->Stage() != Function::PipelineStage::kVertex) {
+                    attributes.interpolation = {};
+                }
+                backend->AddOutput(ir->symbols.Register(name), member->Type(), attributes);
                 members_to_strip.Add(member);
             }
         } else {
@@ -179,6 +202,9 @@
             core::type::StructMemberAttributes attributes;
             if (auto loc = func->ReturnLocation()) {
                 attributes.location = loc->value;
+                if (loc->interpolation && func->Stage() == Function::PipelineStage::kVertex) {
+                    attributes.interpolation = *loc->interpolation;
+                }
                 func->ClearReturnLocation();
             } else if (auto builtin = func->ReturnBuiltin()) {
                 attributes.builtin = ReturnBuiltin(*builtin);
diff --git a/src/tint/lang/core/ir/transform/shader_io.h b/src/tint/lang/core/ir/transform/shader_io.h
index e3e7194..22210c0 100644
--- a/src/tint/lang/core/ir/transform/shader_io.h
+++ b/src/tint/lang/core/ir/transform/shader_io.h
@@ -37,20 +37,24 @@
     /// @param name the name of the input
     /// @param type the type of the input
     /// @param attributes the IO attributes
-    virtual void AddInput(Symbol name,
-                          const core::type::Type* type,
-                          core::type::StructMemberAttributes attributes) {
+    /// @returns the index of the input
+    virtual uint32_t AddInput(Symbol name,
+                              const core::type::Type* type,
+                              core::type::StructMemberAttributes attributes) {
         inputs.Push({name, type, std::move(attributes)});
+        return uint32_t(inputs.Length() - 1);
     }
 
     /// Add an output.
     /// @param name the name of the output
     /// @param type the type of the output
     /// @param attributes the IO attributes
-    virtual void AddOutput(Symbol name,
-                           const core::type::Type* type,
-                           core::type::StructMemberAttributes attributes) {
+    /// @returns the index of the output
+    virtual uint32_t AddOutput(Symbol name,
+                               const core::type::Type* type,
+                               core::type::StructMemberAttributes attributes) {
         outputs.Push({name, type, std::move(attributes)});
+        return uint32_t(outputs.Length() - 1);
     }
 
     /// Finalize the shader inputs and create any state needed for the new entry point function.
@@ -73,6 +77,9 @@
     /// @param value the value to set
     virtual void SetOutput(Builder& builder, uint32_t idx, Value* value) = 0;
 
+    /// @returns true if a vertex point size builtin should be added
+    virtual bool NeedsVertexPointSize() const { return false; }
+
   protected:
     /// The IR module.
     Module* ir = nullptr;
diff --git a/src/tint/lang/core/ir/var.cc b/src/tint/lang/core/ir/var.cc
index 1042786..501d6af 100644
--- a/src/tint/lang/core/ir/var.cc
+++ b/src/tint/lang/core/ir/var.cc
@@ -15,6 +15,7 @@
 #include "src/tint/lang/core/ir/var.h"
 
 #include "src/tint/lang/core/ir/store.h"
+#include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Var);
diff --git a/src/tint/lang/core/ir/var.h b/src/tint/lang/core/ir/var.h
index c10adf3..384f7b1 100644
--- a/src/tint/lang/core/ir/var.h
+++ b/src/tint/lang/core/ir/var.h
@@ -18,15 +18,27 @@
 #include <string>
 
 #include "src/tint/api/common/binding_point.h"
-#include "src/tint/lang/core/access.h"
-#include "src/tint/lang/core/address_space.h"
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/core/interpolation.h"
 #include "src/tint/lang/core/ir/operand_instruction.h"
-#include "src/tint/lang/core/type/pointer.h"
-#include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/rtti/castable.h"
 
 namespace tint::core::ir {
 
+/// Attributes that can be applied to a variable that will be used for shader IO.
+struct IOAttributes {
+    /// The value of a `@location` attribute.
+    std::optional<uint32_t> location;
+    /// The value of a `@index` attribute.
+    std::optional<uint32_t> index;
+    /// The value of a `@builtin` attribute.
+    std::optional<core::BuiltinValue> builtin;
+    /// The values of a `@interpolate` attribute.
+    std::optional<core::Interpolation> interpolation;
+    /// True if the variable is annotated with `@invariant`.
+    bool invariant = false;
+};
+
 /// A var instruction in the IR.
 class Var : public Castable<Var, OperandInstruction<1, 1>> {
   public:
@@ -51,6 +63,12 @@
     /// @returns the binding points if `Attributes` contains `kBindingPoint`
     std::optional<struct BindingPoint> BindingPoint() { return binding_point_; }
 
+    /// Sets the IO attributes
+    /// @param attrs the attributes
+    void SetAttributes(const IOAttributes& attrs) { attributes_ = attrs; }
+    /// @returns the IO attributes
+    const IOAttributes& Attributes() { return attributes_; }
+
     /// Destroys this instruction along with any assignment instructions, if the var is never read.
     void DestroyIfOnlyAssigned();
 
@@ -59,6 +77,7 @@
 
   private:
     std::optional<struct BindingPoint> binding_point_;
+    IOAttributes attributes_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/hlsl/validate/hlsl.cc b/src/tint/lang/hlsl/validate/hlsl.cc
index af19505..6b5a759 100644
--- a/src/tint/lang/hlsl/validate/hlsl.cc
+++ b/src/tint/lang/hlsl/validate/hlsl.cc
@@ -71,7 +71,7 @@
 
         // Match Dawn's compile flags
         // See dawn\src\dawn_native\d3d12\RenderPipelineD3D12.cpp
-        // and dawn_native\d3d12\ShaderModuleD3D12.cpp (GetDXCArguments)
+        // and dawn_native\d3d\ShaderUtils.cpp (GetDXCArguments)
         auto res = dxc(
             "-T " + std::string(stage_prefix) + "_" + std::string(shader_model_version),  // Profile
             "-HV 2018",                                        // Use HLSL 2018
diff --git a/src/tint/lang/spirv/writer/discard_test.cc b/src/tint/lang/spirv/writer/discard_test.cc
index 5fe3f7c..cf190b9 100644
--- a/src/tint/lang/spirv/writer/discard_test.cc
+++ b/src/tint/lang/spirv/writer/discard_test.cc
@@ -43,30 +43,30 @@
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-   %ep_inner = OpFunction %float None %18
+   %ep_inner = OpFunction %float None %16
 %front_facing = OpFunctionParameter %bool
+         %17 = OpLabel
+               OpSelectionMerge %18 None
+               OpBranchConditional %front_facing %19 %18
          %19 = OpLabel
-               OpSelectionMerge %20 None
-               OpBranchConditional %front_facing %21 %20
-         %21 = OpLabel
                OpStore %continue_execution %false
-               OpBranch %20
-         %20 = OpLabel
-         %23 = OpAccessChain %_ptr_StorageBuffer_int %1 %uint_0
-         %27 = OpLoad %bool %continue_execution
-               OpSelectionMerge %28 None
-               OpBranchConditional %27 %29 %28
-         %29 = OpLabel
-               OpStore %23 %int_42
-               OpBranch %28
-         %28 = OpLabel
-         %31 = OpLoad %bool %continue_execution
-         %32 = OpLogicalEqual %bool %31 %false
-               OpSelectionMerge %33 None
-               OpBranchConditional %32 %34 %33
-         %34 = OpLabel
+               OpBranch %18
+         %18 = OpLabel
+         %21 = OpAccessChain %_ptr_StorageBuffer_int %1 %uint_0
+         %25 = OpLoad %bool %continue_execution
+               OpSelectionMerge %26 None
+               OpBranchConditional %25 %27 %26
+         %27 = OpLabel
+               OpStore %21 %int_42
+               OpBranch %26
+         %26 = OpLabel
+         %29 = OpLoad %bool %continue_execution
+         %30 = OpLogicalEqual %bool %29 %false
+               OpSelectionMerge %31 None
+               OpBranchConditional %30 %32 %31
+         %32 = OpLabel
                OpKill
-         %33 = OpLabel
+         %31 = OpLabel
                OpReturnValue %float_0_5
                OpFunctionEnd
 )");
diff --git a/src/tint/lang/spirv/writer/function_test.cc b/src/tint/lang/spirv/writer/function_test.cc
index 0f41ed6..dcbb23c 100644
--- a/src/tint/lang/spirv/writer/function_test.cc
+++ b/src/tint/lang/spirv/writer/function_test.cc
@@ -300,5 +300,75 @@
     EXPECT_INST("%result = OpFunctionCall %void %foo");
 }
 
+TEST_F(SpirvWriterTest, Function_ShaderIO_VertexPointSize) {
+    auto* func = b.Function("main", ty.vec4<f32>(), core::ir::Function::PipelineStage::kVertex);
+    func->SetReturnBuiltin(core::ir::Function::ReturnBuiltin::kPosition);
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Construct(ty.vec4<f32>(), 0.5_f));
+    });
+
+    Options options;
+    options.emit_vertex_point_size = true;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST(
+        R"(OpEntryPoint Vertex %main "main" %main_position_Output %main___point_size_Output)");
+    EXPECT_INST(R"(
+               OpDecorate %main_position_Output BuiltIn Position
+               OpDecorate %main___point_size_Output BuiltIn PointSize
+)");
+    EXPECT_INST(R"(
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+%main_position_Output = OpVariable %_ptr_Output_v4float Output
+%_ptr_Output_float = OpTypePointer Output %float
+%main___point_size_Output = OpVariable %_ptr_Output_float Output
+)");
+    EXPECT_INST(R"(
+       %main = OpFunction %void None %14
+         %15 = OpLabel
+         %16 = OpFunctionCall %v4float %main_inner
+               OpStore %main_position_Output %16
+               OpStore %main___point_size_Output %float_1
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Function_ShaderIO_DualSourceBlend) {
+    auto* outputs = ty.Struct(mod.symbols.New("Outputs"),
+                              {
+                                  {mod.symbols.Register("a"), ty.f32(), {0u, 0u, {}, {}, false}},
+                                  {mod.symbols.Register("b"), ty.f32(), {0u, 1u, {}, {}, false}},
+                              });
+
+    auto* func = b.Function("main", outputs, core::ir::Function::PipelineStage::kFragment);
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Construct(outputs, 0.5_f, 0.6_f));
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(OpEntryPoint Fragment %main "main" %main_loc0_Output %main_loc0_Output_0)");
+    EXPECT_INST(R"(
+               OpDecorate %main_loc0_Output Location 0
+               OpDecorate %main_loc0_Output Index 0
+               OpDecorate %main_loc0_Output_0 Location 0
+               OpDecorate %main_loc0_Output_0 Index 1
+    )");
+    EXPECT_INST(R"(
+%main_loc0_Output = OpVariable %_ptr_Output_float Output
+%main_loc0_Output_0 = OpVariable %_ptr_Output_float Output
+    )");
+    EXPECT_INST(R"(
+       %main = OpFunction %void None %14
+         %15 = OpLabel
+         %16 = OpFunctionCall %Outputs %main_inner
+         %17 = OpCompositeExtract %float %16 0
+               OpStore %main_loc0_Output %17
+         %18 = OpCompositeExtract %float %16 1
+               OpStore %main_loc0_Output_0 %18
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index b098a86..d3a4b6e 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -324,8 +324,7 @@
     });
 }
 
-uint32_t Printer::Type(const core::type::Type* ty,
-                       core::AddressSpace addrspace /* = kUndefined */) {
+uint32_t Printer::Type(const core::type::Type* ty) {
     ty = DedupType(ty, ir_->Types());
     return types_.GetOrCreate(ty, [&] {
         auto id = module_.NextId();
@@ -369,11 +368,11 @@
                                   {id, U32Operand(SpvDecorationArrayStride), arr->Stride()});
             },
             [&](const core::type::Pointer* ptr) {
-                module_.PushType(spv::Op::OpTypePointer,
-                                 {id, U32Operand(StorageClass(ptr->AddressSpace())),
-                                  Type(ptr->StoreType(), ptr->AddressSpace())});
+                module_.PushType(
+                    spv::Op::OpTypePointer,
+                    {id, U32Operand(StorageClass(ptr->AddressSpace())), Type(ptr->StoreType())});
             },
-            [&](const core::type::Struct* str) { EmitStructType(id, str, addrspace); },
+            [&](const core::type::Struct* str) { EmitStructType(id, str); },
             [&](const core::type::Texture* tex) { EmitTextureType(id, tex); },
             [&](const core::type::Sampler*) { module_.PushType(spv::Op::OpTypeSampler, {id}); },
             [&](const raise::SampledImage* s) {
@@ -401,9 +400,7 @@
     return block_labels_.GetOrCreate(block, [&] { return module_.NextId(); });
 }
 
-void Printer::EmitStructType(uint32_t id,
-                             const core::type::Struct* str,
-                             core::AddressSpace addrspace /* = kUndefined */) {
+void Printer::EmitStructType(uint32_t id, const core::type::Struct* str) {
     // Helper to return `type` or a potentially nested array element type within `type` as a matrix
     // type, or nullptr if no such matrix type is present.
     auto get_nested_matrix_type = [&](const core::type::Type* type) {
@@ -422,56 +419,6 @@
             spv::Op::OpMemberDecorate,
             {operands[0], member->Index(), U32Operand(SpvDecorationOffset), member->Offset()});
 
-        // Generate shader IO decorations.
-        const auto& attrs = member->Attributes();
-        if (attrs.location) {
-            module_.PushAnnot(
-                spv::Op::OpMemberDecorate,
-                {operands[0], member->Index(), U32Operand(SpvDecorationLocation), *attrs.location});
-            if (attrs.interpolation) {
-                switch (attrs.interpolation->type) {
-                    case core::InterpolationType::kLinear:
-                        module_.PushAnnot(
-                            spv::Op::OpMemberDecorate,
-                            {operands[0], member->Index(), U32Operand(SpvDecorationNoPerspective)});
-                        break;
-                    case core::InterpolationType::kFlat:
-                        module_.PushAnnot(
-                            spv::Op::OpMemberDecorate,
-                            {operands[0], member->Index(), U32Operand(SpvDecorationFlat)});
-                        break;
-                    case core::InterpolationType::kPerspective:
-                    case core::InterpolationType::kUndefined:
-                        break;
-                }
-                switch (attrs.interpolation->sampling) {
-                    case core::InterpolationSampling::kCentroid:
-                        module_.PushAnnot(
-                            spv::Op::OpMemberDecorate,
-                            {operands[0], member->Index(), U32Operand(SpvDecorationCentroid)});
-                        break;
-                    case core::InterpolationSampling::kSample:
-                        module_.PushCapability(SpvCapabilitySampleRateShading);
-                        module_.PushAnnot(
-                            spv::Op::OpMemberDecorate,
-                            {operands[0], member->Index(), U32Operand(SpvDecorationSample)});
-                        break;
-                    case core::InterpolationSampling::kCenter:
-                    case core::InterpolationSampling::kUndefined:
-                        break;
-                }
-            }
-        }
-        if (attrs.builtin) {
-            module_.PushAnnot(spv::Op::OpMemberDecorate,
-                              {operands[0], member->Index(), U32Operand(SpvDecorationBuiltIn),
-                               Builtin(*attrs.builtin, addrspace)});
-        }
-        if (attrs.invariant) {
-            module_.PushAnnot(spv::Op::OpMemberDecorate,
-                              {operands[0], member->Index(), U32Operand(SpvDecorationInvariant)});
-        }
-
         // Emit matrix layout decorations if necessary.
         if (auto* matrix_type = get_nested_matrix_type(member->Type())) {
             const uint32_t effective_row_count = (matrix_type->rows() == 2) ? 2 : 4;
@@ -691,13 +638,9 @@
             operands.push_back(Value(var));
 
             // Add the `DepthReplacing` execution mode if `frag_depth` is used.
-            if (auto* str = ptr->StoreType()->As<core::type::Struct>()) {
-                for (auto* member : str->Members()) {
-                    if (member->Attributes().builtin == core::BuiltinValue::kFragDepth) {
-                        module_.PushExecutionMode(spv::Op::OpExecutionMode,
-                                                  {id, U32Operand(SpvExecutionModeDepthReplacing)});
-                    }
-                }
+            if (var->Attributes().builtin == core::BuiltinValue::kFragDepth) {
+                module_.PushExecutionMode(spv::Op::OpExecutionMode,
+                                          {id, U32Operand(SpvExecutionModeDepthReplacing)});
             }
         }
     }
@@ -1835,9 +1778,55 @@
     current_function_.push_inst(spv::Op::OpFunctionCall, operands);
 }
 
+void Printer::EmitIOAttributes(uint32_t id,
+                               const core::ir::IOAttributes& attrs,
+                               core::AddressSpace addrspace) {
+    if (attrs.location) {
+        module_.PushAnnot(spv::Op::OpDecorate,
+                          {id, U32Operand(SpvDecorationLocation), *attrs.location});
+    }
+    if (attrs.index) {
+        module_.PushAnnot(spv::Op::OpDecorate, {id, U32Operand(SpvDecorationIndex), *attrs.index});
+    }
+    if (attrs.interpolation) {
+        switch (attrs.interpolation->type) {
+            case core::InterpolationType::kLinear:
+                module_.PushAnnot(spv::Op::OpDecorate,
+                                  {id, U32Operand(SpvDecorationNoPerspective)});
+                break;
+            case core::InterpolationType::kFlat:
+                module_.PushAnnot(spv::Op::OpDecorate, {id, U32Operand(SpvDecorationFlat)});
+                break;
+            case core::InterpolationType::kPerspective:
+            case core::InterpolationType::kUndefined:
+                break;
+        }
+        switch (attrs.interpolation->sampling) {
+            case core::InterpolationSampling::kCentroid:
+                module_.PushAnnot(spv::Op::OpDecorate, {id, U32Operand(SpvDecorationCentroid)});
+                break;
+            case core::InterpolationSampling::kSample:
+                module_.PushCapability(SpvCapabilitySampleRateShading);
+                module_.PushAnnot(spv::Op::OpDecorate, {id, U32Operand(SpvDecorationSample)});
+                break;
+            case core::InterpolationSampling::kCenter:
+            case core::InterpolationSampling::kUndefined:
+                break;
+        }
+    }
+    if (attrs.builtin) {
+        module_.PushAnnot(spv::Op::OpDecorate, {id, U32Operand(SpvDecorationBuiltIn),
+                                                Builtin(*attrs.builtin, addrspace)});
+    }
+    if (attrs.invariant) {
+        module_.PushAnnot(spv::Op::OpDecorate, {id, U32Operand(SpvDecorationInvariant)});
+    }
+}
+
 void Printer::EmitVar(core::ir::Var* var) {
     auto id = Value(var);
     auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+    auto* store_ty = ptr->StoreType();
     auto ty = Type(ptr);
 
     switch (ptr->AddressSpace()) {
@@ -1848,13 +1837,14 @@
                 current_function_.push_inst(spv::Op::OpStore, {id, Value(var->Initializer())});
             } else {
                 current_function_.push_var(
-                    {ty, id, U32Operand(SpvStorageClassFunction), ConstantNull(ptr->StoreType())});
+                    {ty, id, U32Operand(SpvStorageClassFunction), ConstantNull(store_ty)});
             }
             break;
         }
         case core::AddressSpace::kIn: {
             TINT_ASSERT(!current_function_);
             module_.PushType(spv::Op::OpVariable, {ty, id, U32Operand(SpvStorageClassInput)});
+            EmitIOAttributes(id, var->Attributes(), core::AddressSpace::kIn);
             break;
         }
         case core::AddressSpace::kPrivate: {
@@ -1864,7 +1854,7 @@
                 TINT_ASSERT(var->Initializer()->Is<core::ir::Constant>());
                 operands.push_back(Value(var->Initializer()));
             } else {
-                operands.push_back(ConstantNull(ptr->StoreType()));
+                operands.push_back(ConstantNull(store_ty));
             }
             module_.PushType(spv::Op::OpVariable, operands);
             break;
@@ -1878,6 +1868,7 @@
         case core::AddressSpace::kOut: {
             TINT_ASSERT(!current_function_);
             module_.PushType(spv::Op::OpVariable, {ty, id, U32Operand(SpvStorageClassOutput)});
+            EmitIOAttributes(id, var->Attributes(), core::AddressSpace::kOut);
             break;
         }
         case core::AddressSpace::kHandle:
@@ -1891,6 +1882,19 @@
                               {id, U32Operand(SpvDecorationDescriptorSet), bp.group});
             module_.PushAnnot(spv::Op::OpDecorate,
                               {id, U32Operand(SpvDecorationBinding), bp.binding});
+
+            // Add NonReadable and NonWritable decorations to storage textures and buffers.
+            auto* st = store_ty->As<core::type::StorageTexture>();
+            if (st || store_ty->Is<core::type::Struct>()) {
+                auto access = st ? st->access() : ptr->Access();
+                if (access == core::Access::kRead) {
+                    module_.PushAnnot(spv::Op::OpDecorate,
+                                      {id, U32Operand(SpvDecorationNonWritable)});
+                } else if (access == core::Access::kWrite) {
+                    module_.PushAnnot(spv::Op::OpDecorate,
+                                      {id, U32Operand(SpvDecorationNonReadable)});
+                }
+            }
             break;
         }
         case core::AddressSpace::kWorkgroup: {
@@ -1899,7 +1903,7 @@
             if (zero_init_workgroup_memory_) {
                 // If requested, use the VK_KHR_zero_initialize_workgroup_memory to zero-initialize
                 // the workgroup variable using an null constant initializer.
-                operands.push_back(ConstantNull(ptr->StoreType()));
+                operands.push_back(ConstantNull(store_ty));
             }
             module_.PushType(spv::Op::OpVariable, operands);
             break;
diff --git a/src/tint/lang/spirv/writer/printer/printer.h b/src/tint/lang/spirv/writer/printer/printer.h
index 2f0d53a..4071ab0 100644
--- a/src/tint/lang/spirv/writer/printer/printer.h
+++ b/src/tint/lang/spirv/writer/printer/printer.h
@@ -98,10 +98,8 @@
 
     /// Get the result ID of the type `ty`, emitting a type declaration instruction if necessary.
     /// @param ty the type to get the ID for
-    /// @param addrspace the optional address space that this type is being used for
     /// @returns the result ID of the type
-    uint32_t Type(const core::type::Type* ty,
-                  core::AddressSpace addrspace = core::AddressSpace::kUndefined);
+    uint32_t Type(const core::type::Type* ty);
 
   private:
     /// Convert a builtin to the corresponding SPIR-V enum value, taking into account the target
@@ -148,11 +146,8 @@
 
     /// Emit a struct type.
     /// @param id the result ID to use
-    /// @param addrspace the optional address space that this type is being used for
     /// @param str the struct type to emit
-    void EmitStructType(uint32_t id,
-                        const core::type::Struct* str,
-                        core::AddressSpace addrspace = core::AddressSpace::kUndefined);
+    void EmitStructType(uint32_t id, const core::type::Struct* str);
 
     /// Emit a texture type.
     /// @param id the result ID to use
@@ -220,6 +215,14 @@
     /// @param call the intrinsic call instruction to emit
     void EmitIntrinsicCall(spirv::ir::IntrinsicCall* call);
 
+    /// Emit IO attributes.
+    /// @param id the ID of the variable to decorate
+    /// @param attrs the shader IO attrs
+    /// @param addrspace the address of the variable
+    void EmitIOAttributes(uint32_t id,
+                          const core::ir::IOAttributes& attrs,
+                          core::AddressSpace addrspace);
+
     /// Emit a load instruction.
     /// @param load the load instruction to emit
     void EmitLoad(core::ir::Load* load);
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
index 7a3bd38..fea3909 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
@@ -91,6 +91,11 @@
                     case core::Function::kTextureStore:
                         worklist.Push(builtin);
                         break;
+                    case core::Function::kQuantizeToF16:
+                        if (builtin->Result()->Type()->Is<core::type::Vector>()) {
+                            worklist.Push(builtin);
+                        }
+                        break;
                     default:
                         break;
                 }
@@ -147,6 +152,9 @@
                 case core::Function::kTextureStore:
                     replacement = TextureStore(builtin);
                     break;
+                case core::Function::kQuantizeToF16:
+                    replacement = QuantizeToF16Vec(builtin);
+                    break;
                 default:
                     break;
             }
@@ -818,6 +826,29 @@
         extract->InsertBefore(builtin);
         return extract->Result();
     }
+
+    /// Scalarize the vector form of a `quantizeToF16()` builtin.
+    /// See crbug.com/tint/1741.
+    /// @param builtin the builtin call instruction
+    /// @returns the replacement value
+    core::ir::Value* QuantizeToF16Vec(core::ir::CoreBuiltinCall* builtin) {
+        auto* arg = builtin->Args()[0];
+        auto* vec = arg->Type()->As<core::type::Vector>();
+        TINT_ASSERT(vec);
+
+        // Replace the builtin call with a call to the spirv.dot intrinsic.
+        Vector<core::ir::Value*, 4> args;
+        for (uint32_t i = 0; i < vec->Width(); i++) {
+            auto* el = b.Access(ty.f32(), arg, u32(i));
+            auto* scalar_call = b.Call(ty.f32(), core::Function::kQuantizeToF16, el);
+            args.Push(scalar_call->Result());
+            el->InsertBefore(builtin);
+            scalar_call->InsertBefore(builtin);
+        }
+        auto* construct = b.Construct(vec, std::move(args));
+        construct->InsertBefore(builtin);
+        return construct->Result();
+    }
 };
 
 }  // namespace
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc
index 0fa6eaf..d20393f 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc
@@ -2804,5 +2804,74 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_BuiltinPolyfillTest, QuantizeToF16_Scalar) {
+    auto* arg = b.FunctionParam("arg", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({arg});
+
+    b.Append(func->Block(), [&] {
+        auto* result = b.Call(ty.f32(), core::Function::kQuantizeToF16, arg);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%arg:f32):f32 -> %b1 {
+  %b1 = block {
+    %3:f32 = quantizeToF16 %arg
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(BuiltinPolyfill);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_BuiltinPolyfillTest, QuantizeToF16_Vector) {
+    auto* arg = b.FunctionParam("arg", ty.vec4<f32>());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({arg});
+
+    b.Append(func->Block(), [&] {
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kQuantizeToF16, arg);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%arg:vec4<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %3:vec4<f32> = quantizeToF16 %arg
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%arg:vec4<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %3:f32 = access %arg, 0u
+    %4:f32 = quantizeToF16 %3
+    %5:f32 = access %arg, 1u
+    %6:f32 = quantizeToF16 %5
+    %7:f32 = access %arg, 2u
+    %8:f32 = quantizeToF16 %7
+    %9:f32 = access %arg, 3u
+    %10:f32 = quantizeToF16 %9
+    %11:vec4<f32> = construct %4, %6, %8, %10
+    ret %11
+  }
+}
+)";
+
+    Run(BuiltinPolyfill);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 71edb73..c4be7ea 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -78,7 +78,8 @@
     RUN_TRANSFORM(ExpandImplicitSplats, module);
     RUN_TRANSFORM(HandleMatrixArithmetic, module);
     RUN_TRANSFORM(MergeReturn, module);
-    RUN_TRANSFORM(ShaderIO, module, ShaderIOConfig{options.clamp_frag_depth});
+    RUN_TRANSFORM(ShaderIO, module,
+                  ShaderIOConfig{options.clamp_frag_depth, options.emit_vertex_point_size});
     RUN_TRANSFORM(core::ir::transform::Std140, module);
     RUN_TRANSFORM(VarForDynamicIndex, module);
 
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.cc b/src/tint/lang/spirv/writer/raise/shader_io.cc
index 2716b67..0c7a3b2 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -22,7 +22,6 @@
 #include "src/tint/lang/core/ir/transform/shader_io.h"
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/type/array.h"
-#include "src/tint/lang/core/type/struct.h"
 
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
@@ -32,23 +31,14 @@
 namespace {
 
 /// PIMPL state for the parts of the shader IO transform specific to SPIR-V.
-/// For SPIR-V, we split builtins and locations into two separate structures each for input and
-/// output, and declare global variables for them. The wrapper entry point then loads from and
-/// stores to these variables.
-/// We also modify the type of the SampleMask builtin to be an array, as required by Vulkan.
+/// For SPIR-V, we declare a global variable for each input and output. The wrapper entry point then
+/// loads from and stores to these variables. We also modify the type of the SampleMask builtin to
+/// be an array, as required by Vulkan.
 struct StateImpl : core::ir::transform::ShaderIOBackendState {
-    /// The global variable for input builtins.
-    core::ir::Var* builtin_input_var = nullptr;
-    /// The global variable for input locations.
-    core::ir::Var* location_input_var = nullptr;
-    /// The global variable for output builtins.
-    core::ir::Var* builtin_output_var = nullptr;
-    /// The global variable for output locations.
-    core::ir::Var* location_output_var = nullptr;
-    /// The member indices for inputs.
-    Vector<uint32_t, 4> input_indices;
-    /// The member indices for outputs.
-    Vector<uint32_t, 4> output_indices;
+    /// The input variables.
+    Vector<core::ir::Var*, 4> input_vars;
+    /// The output variables.
+    Vector<core::ir::Var*, 4> output_vars;
 
     /// The configuration options.
     const ShaderIOConfig& config;
@@ -63,69 +53,63 @@
     /// Destructor
     ~StateImpl() override {}
 
-    /// Split the members listed in @p entries into two separate structures for builtins and
-    /// locations, and make global variables for them. Record the new member indices in @p indices.
-    /// @param builtin_var the generated global variable for builtins
-    /// @param location_var the generated global variable for locations
-    /// @param indices the new member indices
-    /// @param entries the entries to split
+    /// Declare a global variable for each IO entry listed in @p entries.
+    /// @param vars the list of variables
+    /// @param entries the entries to emit
     /// @param addrspace the address to use for the global variables
     /// @param access the access mode to use for the global variables
     /// @param name_suffix the suffix to add to struct and variable names
-    void MakeStructs(core::ir::Var*& builtin_var,
-                     core::ir::Var*& location_var,
-                     Vector<uint32_t, 4>* indices,
-                     Vector<core::type::Manager::StructMemberDesc, 4>& entries,
-                     core::AddressSpace addrspace,
-                     core::Access access,
-                     const char* name_suffix) {
-        // Build separate lists of builtin and location entries and record their new indices.
-        uint32_t next_builtin_idx = 0;
-        uint32_t next_location_idx = 0;
-        Vector<core::type::Manager::StructMemberDesc, 4> builtin_members;
-        Vector<core::type::Manager::StructMemberDesc, 4> location_members;
+    void MakeVars(Vector<core::ir::Var*, 4>& vars,
+                  Vector<core::type::Manager::StructMemberDesc, 4>& entries,
+                  core::AddressSpace addrspace,
+                  core::Access access,
+                  const char* name_suffix) {
         for (auto io : entries) {
+            StringStream name;
+            name << ir->NameOf(func).Name();
+
             if (io.attributes.builtin) {
                 // SampleMask must be an array for Vulkan.
                 if (io.attributes.builtin.value() == core::BuiltinValue::kSampleMask) {
                     io.type = ty.array<u32, 1>();
                 }
-                builtin_members.Push(io);
-                indices->Push(next_builtin_idx++);
-            } else {
-                location_members.Push(io);
-                indices->Push(next_location_idx++);
-            }
-        }
+                name << "_" << io.attributes.builtin.value();
 
-        // Declare the structs and variables if needed.
-        auto make_struct = [&](auto& members, const char* iotype) {
-            auto name = ir->NameOf(func).Name() + iotype + name_suffix;
-            auto* str = ty.Struct(ir->symbols.New(name + "Struct"), std::move(members));
-            auto* var = b.Var(name, ty.ptr(addrspace, str, access));
-            str->SetStructFlag(core::type::kBlock);
+                // Vulkan requires that fragment integer builtin inputs be Flat decorated.
+                if (func->Stage() == core::ir::Function::PipelineStage::kFragment &&
+                    addrspace == core::AddressSpace::kIn &&
+                    io.type->is_integer_scalar_or_vector()) {
+                    io.attributes.interpolation = {core::InterpolationType::kFlat};
+                }
+            } else {
+                name << "_loc" << io.attributes.location.value();
+            }
+            name << name_suffix;
+
+            // Create an IO variable and add it to the root block.
+            auto* ptr = ty.ptr(addrspace, io.type, access);
+            auto* var = b.Var(name.str(), ptr);
+            var->SetAttributes(core::ir::IOAttributes{
+                io.attributes.location,
+                io.attributes.index,
+                io.attributes.builtin,
+                io.attributes.interpolation,
+                io.attributes.invariant,
+            });
             b.RootBlock()->Append(var);
-            return var;
-        };
-        if (!builtin_members.IsEmpty()) {
-            builtin_var = make_struct(builtin_members, "_Builtin");
-        }
-        if (!location_members.IsEmpty()) {
-            location_var = make_struct(location_members, "_Location");
+            vars.Push(var);
         }
     }
 
     /// @copydoc ShaderIO::BackendState::FinalizeInputs
     Vector<core::ir::FunctionParam*, 4> FinalizeInputs() override {
-        MakeStructs(builtin_input_var, location_input_var, &input_indices, inputs,
-                    core::AddressSpace::kIn, core::Access::kRead, "Inputs");
+        MakeVars(input_vars, inputs, core::AddressSpace::kIn, core::Access::kRead, "_Input");
         return tint::Empty;
     }
 
     /// @copydoc ShaderIO::BackendState::FinalizeOutputs
     core::ir::Value* FinalizeOutputs() override {
-        MakeStructs(builtin_output_var, location_output_var, &output_indices, outputs,
-                    core::AddressSpace::kOut, core::Access::kWrite, "Outputs");
+        MakeVars(output_vars, outputs, core::AddressSpace::kOut, core::Access::kWrite, "_Output");
         return nullptr;
     }
 
@@ -133,16 +117,12 @@
     core::ir::Value* GetInput(core::ir::Builder& builder, uint32_t idx) override {
         // Load the input from the global variable declared earlier.
         auto* ptr = ty.ptr(core::AddressSpace::kIn, inputs[idx].type, core::Access::kRead);
-        core::ir::Access* from = nullptr;
+        auto* from = input_vars[idx]->Result();
         if (inputs[idx].attributes.builtin) {
             if (inputs[idx].attributes.builtin.value() == core::BuiltinValue::kSampleMask) {
                 // SampleMask becomes an array for SPIR-V, so load from the first element.
-                from = builder.Access(ptr, builtin_input_var, u32(input_indices[idx]), 0_u);
-            } else {
-                from = builder.Access(ptr, builtin_input_var, u32(input_indices[idx]));
+                from = builder.Access(ptr, input_vars[idx], 0_u)->Result();
             }
-        } else {
-            from = builder.Access(ptr, location_input_var, u32(input_indices[idx]));
         }
         return builder.Load(from)->Result();
     }
@@ -151,21 +131,17 @@
     void SetOutput(core::ir::Builder& builder, uint32_t idx, core::ir::Value* value) override {
         // Store the output to the global variable declared earlier.
         auto* ptr = ty.ptr(core::AddressSpace::kOut, outputs[idx].type, core::Access::kWrite);
-        core::ir::Access* to = nullptr;
+        auto* to = output_vars[idx]->Result();
         if (outputs[idx].attributes.builtin) {
             if (outputs[idx].attributes.builtin.value() == core::BuiltinValue::kSampleMask) {
                 // SampleMask becomes an array for SPIR-V, so store to the first element.
-                to = builder.Access(ptr, builtin_output_var, u32(output_indices[idx]), 0_u);
-            } else {
-                to = builder.Access(ptr, builtin_output_var, u32(output_indices[idx]));
+                to = builder.Access(ptr, to, 0_u)->Result();
             }
 
             // Clamp frag_depth values if necessary.
             if (outputs[idx].attributes.builtin.value() == core::BuiltinValue::kFragDepth) {
                 value = ClampFragDepth(builder, value);
             }
-        } else {
-            to = builder.Access(ptr, location_output_var, u32(output_indices[idx]));
         }
         builder.Store(to, value);
     }
@@ -213,6 +189,9 @@
             .Call(ty.f32(), core::Function::kClamp, frag_depth, frag_depth_min, frag_depth_max)
             ->Result();
     }
+
+    /// @copydoc ShaderIO::BackendState::NeedsVertexPointSize
+    bool NeedsVertexPointSize() const override { return config.emit_vertex_point_size; }
 };
 }  // namespace
 
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.h b/src/tint/lang/spirv/writer/raise/shader_io.h
index 5924c12..41a2c64 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.h
+++ b/src/tint/lang/spirv/writer/raise/shader_io.h
@@ -30,10 +30,12 @@
 struct ShaderIOConfig {
     /// true if frag_depth builtin outputs should be clamped
     bool clamp_frag_depth = false;
+    /// true if a vertex point size builtin output should be added
+    bool emit_vertex_point_size = false;
 };
 
-/// ShaderIO is a transform that modifies each entry point function's parameters and return
-/// value to prepare them for SPIR-V codegen.
+/// ShaderIO is a transform that moves each entry point function's parameters and return value to
+/// global variables to prepare them for SPIR-V codegen.
 /// @param module the module to transform
 /// @param config the configuration
 /// @returns an error string on failure
diff --git a/src/tint/lang/spirv/writer/raise/shader_io_test.cc b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
index 0638d50..e4edbf9 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -94,27 +94,19 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-foo_BuiltinInputsStruct = struct @align(16), @block {
-  front_facing:bool @offset(0), @builtin(front_facing)
-  position:vec4<f32> @offset(16), @invariant, @builtin(position)
-}
-
-foo_LocationInputsStruct = struct @align(4), @block {
-  color1:f32 @offset(0), @location(0)
-  color2:f32 @offset(4), @location(1), @interpolate(linear, sample)
-}
-
 %b1 = block {  # root
-  %foo_BuiltinInputs:ptr<__in, foo_BuiltinInputsStruct, read> = var
-  %foo_LocationInputs:ptr<__in, foo_LocationInputsStruct, read> = var
+  %foo_front_facing_Input:ptr<__in, bool, read> = var @builtin(front_facing)
+  %foo_position_Input:ptr<__in, vec4<f32>, read> = var @invariant @builtin(position)
+  %foo_loc0_Input:ptr<__in, f32, read> = var @location(0)
+  %foo_loc1_Input:ptr<__in, f32, read> = var @location(1) @interpolate(linear, sample)
 }
 
 %foo_inner = func(%front_facing:bool, %position:vec4<f32>, %color1:f32, %color2:f32):void -> %b2 {
   %b2 = block {
     if %front_facing [t: %b3] {  # if_1
       %b3 = block {  # true
-        %8:f32 = add %color1, %color2
-        %9:vec4<f32> = mul %position, %8
+        %10:f32 = add %color1, %color2
+        %11:vec4<f32> = mul %position, %10
         exit_if  # if_1
       }
     }
@@ -123,15 +115,11 @@
 }
 %foo = @fragment func():void -> %b4 {
   %b4 = block {
-    %11:ptr<__in, bool, read> = access %foo_BuiltinInputs, 0u
-    %12:bool = load %11
-    %13:ptr<__in, vec4<f32>, read> = access %foo_BuiltinInputs, 1u
-    %14:vec4<f32> = load %13
-    %15:ptr<__in, f32, read> = access %foo_LocationInputs, 0u
-    %16:f32 = load %15
-    %17:ptr<__in, f32, read> = access %foo_LocationInputs, 1u
-    %18:f32 = load %17
-    %19:void = call %foo_inner, %12, %14, %16, %18
+    %13:bool = load %foo_front_facing_Input
+    %14:vec4<f32> = load %foo_position_Input
+    %15:f32 = load %foo_loc0_Input
+    %16:f32 = load %foo_loc1_Input
+    %17:void = call %foo_inner, %13, %14, %15, %16
     ret
   }
 }
@@ -226,31 +214,23 @@
   color2:f32 @offset(36)
 }
 
-foo_BuiltinInputsStruct = struct @align(16), @block {
-  Inputs_front_facing:bool @offset(0), @builtin(front_facing)
-  Inputs_position:vec4<f32> @offset(16), @invariant, @builtin(position)
-}
-
-foo_LocationInputsStruct = struct @align(4), @block {
-  Inputs_color1:f32 @offset(0), @location(0)
-  Inputs_color2:f32 @offset(4), @location(1), @interpolate(linear, sample)
-}
-
 %b1 = block {  # root
-  %foo_BuiltinInputs:ptr<__in, foo_BuiltinInputsStruct, read> = var
-  %foo_LocationInputs:ptr<__in, foo_LocationInputsStruct, read> = var
+  %foo_front_facing_Input:ptr<__in, bool, read> = var @builtin(front_facing)
+  %foo_position_Input:ptr<__in, vec4<f32>, read> = var @invariant @builtin(position)
+  %foo_loc0_Input:ptr<__in, f32, read> = var @location(0)
+  %foo_loc1_Input:ptr<__in, f32, read> = var @location(1) @interpolate(linear, sample)
 }
 
 %foo_inner = func(%inputs:Inputs):void -> %b2 {
   %b2 = block {
-    %5:bool = access %inputs, 0i
-    if %5 [t: %b3] {  # if_1
+    %7:bool = access %inputs, 0i
+    if %7 [t: %b3] {  # if_1
       %b3 = block {  # true
-        %6:vec4<f32> = access %inputs, 1i
-        %7:f32 = access %inputs, 2i
-        %8:f32 = access %inputs, 3i
-        %9:f32 = add %7, %8
-        %10:vec4<f32> = mul %6, %9
+        %8:vec4<f32> = access %inputs, 1i
+        %9:f32 = access %inputs, 2i
+        %10:f32 = access %inputs, 3i
+        %11:f32 = add %9, %10
+        %12:vec4<f32> = mul %8, %11
         exit_if  # if_1
       }
     }
@@ -259,16 +239,12 @@
 }
 %foo = @fragment func():void -> %b4 {
   %b4 = block {
-    %12:ptr<__in, bool, read> = access %foo_BuiltinInputs, 0u
-    %13:bool = load %12
-    %14:ptr<__in, vec4<f32>, read> = access %foo_BuiltinInputs, 1u
-    %15:vec4<f32> = load %14
-    %16:ptr<__in, f32, read> = access %foo_LocationInputs, 0u
-    %17:f32 = load %16
-    %18:ptr<__in, f32, read> = access %foo_LocationInputs, 1u
-    %19:f32 = load %18
-    %20:Inputs = construct %13, %15, %17, %19
-    %21:void = call %foo_inner, %20
+    %14:bool = load %foo_front_facing_Input
+    %15:vec4<f32> = load %foo_position_Input
+    %16:f32 = load %foo_loc0_Input
+    %17:f32 = load %foo_loc1_Input
+    %18:Inputs = construct %14, %15, %16, %17
+    %19:void = call %foo_inner, %18
     ret
   }
 }
@@ -347,29 +323,21 @@
   color1:f32 @offset(16)
 }
 
-foo_BuiltinInputsStruct = struct @align(16), @block {
-  front_facing:bool @offset(0), @builtin(front_facing)
-  Inputs_position:vec4<f32> @offset(16), @invariant, @builtin(position)
-}
-
-foo_LocationInputsStruct = struct @align(4), @block {
-  Inputs_color1:f32 @offset(0), @location(0)
-  color2:f32 @offset(4), @location(1), @interpolate(linear, sample)
-}
-
 %b1 = block {  # root
-  %foo_BuiltinInputs:ptr<__in, foo_BuiltinInputsStruct, read> = var
-  %foo_LocationInputs:ptr<__in, foo_LocationInputsStruct, read> = var
+  %foo_front_facing_Input:ptr<__in, bool, read> = var @builtin(front_facing)
+  %foo_position_Input:ptr<__in, vec4<f32>, read> = var @invariant @builtin(position)
+  %foo_loc0_Input:ptr<__in, f32, read> = var @location(0)
+  %foo_loc1_Input:ptr<__in, f32, read> = var @location(1) @interpolate(linear, sample)
 }
 
 %foo_inner = func(%front_facing:bool, %inputs:Inputs, %color2:f32):void -> %b2 {
   %b2 = block {
     if %front_facing [t: %b3] {  # if_1
       %b3 = block {  # true
-        %7:vec4<f32> = access %inputs, 0i
-        %8:f32 = access %inputs, 1i
-        %9:f32 = add %8, %color2
-        %10:vec4<f32> = mul %7, %9
+        %9:vec4<f32> = access %inputs, 0i
+        %10:f32 = access %inputs, 1i
+        %11:f32 = add %10, %color2
+        %12:vec4<f32> = mul %9, %11
         exit_if  # if_1
       }
     }
@@ -378,16 +346,12 @@
 }
 %foo = @fragment func():void -> %b4 {
   %b4 = block {
-    %12:ptr<__in, bool, read> = access %foo_BuiltinInputs, 0u
-    %13:bool = load %12
-    %14:ptr<__in, vec4<f32>, read> = access %foo_BuiltinInputs, 1u
-    %15:vec4<f32> = load %14
-    %16:ptr<__in, f32, read> = access %foo_LocationInputs, 0u
-    %17:f32 = load %16
-    %18:Inputs = construct %15, %17
-    %19:ptr<__in, f32, read> = access %foo_LocationInputs, 1u
-    %20:f32 = load %19
-    %21:void = call %foo_inner, %13, %18, %20
+    %14:bool = load %foo_front_facing_Input
+    %15:vec4<f32> = load %foo_position_Input
+    %16:f32 = load %foo_loc0_Input
+    %17:Inputs = construct %15, %16
+    %18:f32 = load %foo_loc1_Input
+    %19:void = call %foo_inner, %14, %17, %18
     ret
   }
 }
@@ -421,12 +385,8 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-foo_BuiltinOutputsStruct = struct @align(16), @block {
-  tint_symbol:vec4<f32> @offset(0), @invariant, @builtin(position)
-}
-
 %b1 = block {  # root
-  %foo_BuiltinOutputs:ptr<__out, foo_BuiltinOutputsStruct, write> = var
+  %foo_position_Output:ptr<__out, vec4<f32>, write> = var @invariant @builtin(position)
 }
 
 %foo_inner = func():vec4<f32> -> %b2 {
@@ -438,8 +398,7 @@
 %foo = @vertex func():void -> %b3 {
   %b3 = block {
     %5:vec4<f32> = call %foo_inner
-    %6:ptr<__out, vec4<f32>, write> = access %foo_BuiltinOutputs, 0u
-    store %6, %5
+    store %foo_position_Output, %5
     ret
   }
 }
@@ -472,12 +431,8 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-foo_LocationOutputsStruct = struct @align(16), @block {
-  tint_symbol:vec4<f32> @offset(0), @location(1)
-}
-
 %b1 = block {  # root
-  %foo_LocationOutputs:ptr<__out, foo_LocationOutputsStruct, write> = var
+  %foo_loc1_Output:ptr<__out, vec4<f32>, write> = var @location(1)
 }
 
 %foo_inner = func():vec4<f32> -> %b2 {
@@ -489,8 +444,7 @@
 %foo = @fragment func():void -> %b3 {
   %b3 = block {
     %5:vec4<f32> = call %foo_inner
-    %6:ptr<__out, vec4<f32>, write> = access %foo_LocationOutputs, 0u
-    store %6, %5
+    store %foo_loc1_Output, %5
     ret
   }
 }
@@ -559,39 +513,28 @@
   color2:f32 @offset(20)
 }
 
-foo_BuiltinOutputsStruct = struct @align(16), @block {
-  Outputs_position:vec4<f32> @offset(0), @invariant, @builtin(position)
-}
-
-foo_LocationOutputsStruct = struct @align(4), @block {
-  Outputs_color1:f32 @offset(0), @location(0)
-  Outputs_color2:f32 @offset(4), @location(1), @interpolate(linear, sample)
-}
-
 %b1 = block {  # root
-  %foo_BuiltinOutputs:ptr<__out, foo_BuiltinOutputsStruct, write> = var
-  %foo_LocationOutputs:ptr<__out, foo_LocationOutputsStruct, write> = var
+  %foo_position_Output:ptr<__out, vec4<f32>, write> = var @invariant @builtin(position)
+  %foo_loc0_Output:ptr<__out, f32, write> = var @location(0)
+  %foo_loc1_Output:ptr<__out, f32, write> = var @location(1) @interpolate(linear, sample)
 }
 
 %foo_inner = func():Outputs -> %b2 {
   %b2 = block {
-    %4:vec4<f32> = construct 0.0f
-    %5:Outputs = construct %4, 0.25f, 0.75f
-    ret %5
+    %5:vec4<f32> = construct 0.0f
+    %6:Outputs = construct %5, 0.25f, 0.75f
+    ret %6
   }
 }
 %foo = @vertex func():void -> %b3 {
   %b3 = block {
-    %7:Outputs = call %foo_inner
-    %8:vec4<f32> = access %7, 0u
-    %9:ptr<__out, vec4<f32>, write> = access %foo_BuiltinOutputs, 0u
-    store %9, %8
-    %10:f32 = access %7, 1u
-    %11:ptr<__out, f32, write> = access %foo_LocationOutputs, 0u
-    store %11, %10
-    %12:f32 = access %7, 2u
-    %13:ptr<__out, f32, write> = access %foo_LocationOutputs, 1u
-    store %13, %12
+    %8:Outputs = call %foo_inner
+    %9:vec4<f32> = access %8, 0u
+    store %foo_position_Output, %9
+    %10:f32 = access %8, 1u
+    store %foo_loc0_Output, %10
+    %11:f32 = access %8, 2u
+    store %foo_loc1_Output, %11
     ret
   }
 }
@@ -638,6 +581,7 @@
         auto* inputs = b.FunctionParam("inputs", str_ty);
         ep->SetStage(core::ir::Function::PipelineStage::kFragment);
         ep->SetParams({inputs});
+        ep->SetReturnLocation(0u, {});
 
         b.Append(ep->Block(), [&] {  //
             auto* position = b.Access(vec4f, inputs, 0_u);
@@ -660,7 +604,7 @@
     ret %4
   }
 }
-%frag = @fragment func(%inputs:Interface):vec4<f32> -> %b2 {
+%frag = @fragment func(%inputs:Interface):vec4<f32> [@location(0)] -> %b2 {
   %b2 = block {
     %7:vec4<f32> = access %inputs, 0u
     %8:vec4<f32> = access %inputs, 1u
@@ -677,32 +621,12 @@
   color:vec4<f32> @offset(16)
 }
 
-vert_BuiltinOutputsStruct = struct @align(16), @block {
-  Interface_position:vec4<f32> @offset(0), @builtin(position)
-}
-
-vert_LocationOutputsStruct = struct @align(16), @block {
-  Interface_color:vec4<f32> @offset(0), @location(0)
-}
-
-frag_BuiltinInputsStruct = struct @align(16), @block {
-  Interface_position:vec4<f32> @offset(0), @builtin(position)
-}
-
-frag_LocationInputsStruct = struct @align(16), @block {
-  Interface_color:vec4<f32> @offset(0), @location(0)
-}
-
-frag_LocationOutputsStruct = struct @align(16), @block {
-  tint_symbol:vec4<f32> @offset(0)
-}
-
 %b1 = block {  # root
-  %vert_BuiltinOutputs:ptr<__out, vert_BuiltinOutputsStruct, write> = var
-  %vert_LocationOutputs:ptr<__out, vert_LocationOutputsStruct, write> = var
-  %frag_BuiltinInputs:ptr<__in, frag_BuiltinInputsStruct, read> = var
-  %frag_LocationInputs:ptr<__in, frag_LocationInputsStruct, read> = var
-  %frag_LocationOutputs:ptr<__out, frag_LocationOutputsStruct, write> = var
+  %vert_position_Output:ptr<__out, vec4<f32>, write> = var @builtin(position)
+  %vert_loc0_Output:ptr<__out, vec4<f32>, write> = var @location(0)
+  %frag_position_Input:ptr<__in, vec4<f32>, read> = var @builtin(position)
+  %frag_loc0_Input:ptr<__in, vec4<f32>, read> = var @location(0)
+  %frag_loc0_Output:ptr<__out, vec4<f32>, write> = var @location(0)
 }
 
 %vert_inner = func():Interface -> %b2 {
@@ -725,24 +649,19 @@
   %b4 = block {
     %16:Interface = call %vert_inner
     %17:vec4<f32> = access %16, 0u
-    %18:ptr<__out, vec4<f32>, write> = access %vert_BuiltinOutputs, 0u
-    store %18, %17
-    %19:vec4<f32> = access %16, 1u
-    %20:ptr<__out, vec4<f32>, write> = access %vert_LocationOutputs, 0u
-    store %20, %19
+    store %vert_position_Output, %17
+    %18:vec4<f32> = access %16, 1u
+    store %vert_loc0_Output, %18
     ret
   }
 }
 %frag = @fragment func():void -> %b5 {
   %b5 = block {
-    %22:ptr<__in, vec4<f32>, read> = access %frag_BuiltinInputs, 0u
-    %23:vec4<f32> = load %22
-    %24:ptr<__in, vec4<f32>, read> = access %frag_LocationInputs, 0u
-    %25:vec4<f32> = load %24
-    %26:Interface = construct %23, %25
-    %27:vec4<f32> = call %frag_inner, %26
-    %28:ptr<__out, vec4<f32>, write> = access %frag_LocationOutputs, 0u
-    store %28, %27
+    %20:vec4<f32> = load %frag_position_Input
+    %21:vec4<f32> = load %frag_loc0_Input
+    %22:Interface = construct %20, %21
+    %23:vec4<f32> = call %frag_inner, %22
+    store %frag_loc0_Output, %23
     ret
   }
 }
@@ -805,18 +724,10 @@
   color:vec4<f32> @offset(16)
 }
 
-vert_BuiltinOutputsStruct = struct @align(16), @block {
-  Outputs_position:vec4<f32> @offset(0), @builtin(position)
-}
-
-vert_LocationOutputsStruct = struct @align(16), @block {
-  Outputs_color:vec4<f32> @offset(0), @location(0)
-}
-
 %b1 = block {  # root
   %1:ptr<storage, Outputs, read> = var
-  %vert_BuiltinOutputs:ptr<__out, vert_BuiltinOutputsStruct, write> = var
-  %vert_LocationOutputs:ptr<__out, vert_LocationOutputsStruct, write> = var
+  %vert_position_Output:ptr<__out, vec4<f32>, write> = var @builtin(position)
+  %vert_loc0_Output:ptr<__out, vec4<f32>, write> = var @location(0)
 }
 
 %vert_inner = func():Outputs -> %b2 {
@@ -829,11 +740,9 @@
   %b3 = block {
     %7:Outputs = call %vert_inner
     %8:vec4<f32> = access %7, 0u
-    %9:ptr<__out, vec4<f32>, write> = access %vert_BuiltinOutputs, 0u
-    store %9, %8
-    %10:vec4<f32> = access %7, 1u
-    %11:ptr<__out, vec4<f32>, write> = access %vert_LocationOutputs, 0u
-    store %11, %10
+    store %vert_position_Output, %8
+    %9:vec4<f32> = access %7, 1u
+    store %vert_loc0_Output, %9
     ret
   }
 }
@@ -894,22 +803,10 @@
   mask:u32 @offset(4)
 }
 
-foo_BuiltinInputsStruct = struct @align(4), @block {
-  mask_in:array<u32, 1> @offset(0), @builtin(sample_mask)
-}
-
-foo_BuiltinOutputsStruct = struct @align(4), @block {
-  Outputs_mask:array<u32, 1> @offset(0), @builtin(sample_mask)
-}
-
-foo_LocationOutputsStruct = struct @align(4), @block {
-  Outputs_color:f32 @offset(0), @location(0)
-}
-
 %b1 = block {  # root
-  %foo_BuiltinInputs:ptr<__in, foo_BuiltinInputsStruct, read> = var
-  %foo_BuiltinOutputs:ptr<__out, foo_BuiltinOutputsStruct, write> = var
-  %foo_LocationOutputs:ptr<__out, foo_LocationOutputsStruct, write> = var
+  %foo_sample_mask_Input:ptr<__in, array<u32, 1>, read> = var @builtin(sample_mask)
+  %foo_loc0_Output:ptr<__out, f32, write> = var @location(0)
+  %foo_sample_mask_Output:ptr<__out, array<u32, 1>, write> = var @builtin(sample_mask)
 }
 
 %foo_inner = func(%mask_in:u32):Outputs -> %b2 {
@@ -920,15 +817,157 @@
 }
 %foo = @fragment func():void -> %b3 {
   %b3 = block {
-    %8:ptr<__in, u32, read> = access %foo_BuiltinInputs, 0u, 0u
+    %8:ptr<__in, u32, read> = access %foo_sample_mask_Input, 0u
     %9:u32 = load %8
     %10:Outputs = call %foo_inner, %9
     %11:f32 = access %10, 0u
-    %12:ptr<__out, f32, write> = access %foo_LocationOutputs, 0u
-    store %12, %11
-    %13:u32 = access %10, 1u
-    %14:ptr<__out, u32, write> = access %foo_BuiltinOutputs, 0u, 0u
-    store %14, %13
+    store %foo_loc0_Output, %11
+    %12:u32 = access %10, 1u
+    %13:ptr<__out, u32, write> = access %foo_sample_mask_Output, 0u
+    store %13, %12
+    ret
+  }
+}
+)";
+
+    ShaderIOConfig config;
+    config.clamp_frag_depth = false;
+    Run(ShaderIO, config);
+
+    EXPECT_EQ(expect, str());
+}
+
+// Test that interpolation attributes are stripped from vertex inputs and fragment outputs.
+TEST_F(SpirvWriter_ShaderIOTest, InterpolationOnVertexInputOrFragmentOutput) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"),
+                             {
+                                 {
+                                     mod.symbols.New("color"),
+                                     ty.f32(),
+                                     {1u,
+                                      {},
+                                      {},
+                                      core::Interpolation{core::InterpolationType::kLinear,
+                                                          core::InterpolationSampling::kSample},
+                                      false},
+                                 },
+                             });
+
+    // Vertex shader.
+    {
+        auto* ep = b.Function("vert", ty.vec4<f32>());
+        ep->SetReturnBuiltin(core::ir::Function::ReturnBuiltin::kPosition);
+        ep->SetReturnInvariant(true);
+        ep->SetStage(core::ir::Function::PipelineStage::kVertex);
+
+        auto* str_param = b.FunctionParam("input", str_ty);
+        auto* ival = b.FunctionParam("ival", ty.i32());
+        ival->SetLocation(1, core::Interpolation{core::InterpolationType::kFlat});
+        ep->SetParams({str_param, ival});
+
+        b.Append(ep->Block(), [&] {  //
+            b.Return(ep, b.Construct(ty.vec4<f32>(), 0.5_f));
+        });
+    }
+
+    // Fragment shader with struct output.
+    {
+        auto* ep = b.Function("frag1", str_ty);
+        ep->SetStage(core::ir::Function::PipelineStage::kFragment);
+
+        b.Append(ep->Block(), [&] {  //
+            b.Return(ep, b.Construct(str_ty, 0.5_f));
+        });
+    }
+
+    // Fragment shader with non-struct output.
+    {
+        auto* ep = b.Function("frag2", ty.i32());
+        ep->SetStage(core::ir::Function::PipelineStage::kFragment);
+        ep->SetReturnLocation(0, core::Interpolation{core::InterpolationType::kFlat});
+
+        b.Append(ep->Block(), [&] {  //
+            b.Return(ep, b.Constant(42_i));
+        });
+    }
+
+    auto* src = R"(
+MyStruct = struct @align(4) {
+  color:f32 @offset(0), @location(1), @interpolate(linear, sample)
+}
+
+%vert = @vertex func(%input:MyStruct, %ival:i32 [@location(1), @interpolate(flat)]):vec4<f32> [@invariant, @position] -> %b1 {
+  %b1 = block {
+    %4:vec4<f32> = construct 0.5f
+    ret %4
+  }
+}
+%frag1 = @fragment func():MyStruct -> %b2 {
+  %b2 = block {
+    %6:MyStruct = construct 0.5f
+    ret %6
+  }
+}
+%frag2 = @fragment func():i32 [@location(0), @interpolate(flat)] -> %b3 {
+  %b3 = block {
+    ret 42i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(4) {
+  color:f32 @offset(0)
+}
+
+%b1 = block {  # root
+  %vert_loc1_Input:ptr<__in, f32, read> = var @location(1)
+  %vert_loc1_Input_1:ptr<__in, i32, read> = var @location(1)  # %vert_loc1_Input_1: 'vert_loc1_Input'
+  %vert_position_Output:ptr<__out, vec4<f32>, write> = var @invariant @builtin(position)
+  %frag1_loc1_Output:ptr<__out, f32, write> = var @location(1)
+  %frag2_loc0_Output:ptr<__out, i32, write> = var @location(0)
+}
+
+%vert_inner = func(%input:MyStruct, %ival:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %9:vec4<f32> = construct 0.5f
+    ret %9
+  }
+}
+%frag1_inner = func():MyStruct -> %b3 {
+  %b3 = block {
+    %11:MyStruct = construct 0.5f
+    ret %11
+  }
+}
+%frag2_inner = func():i32 -> %b4 {
+  %b4 = block {
+    ret 42i
+  }
+}
+%vert = @vertex func():void -> %b5 {
+  %b5 = block {
+    %14:f32 = load %vert_loc1_Input
+    %15:MyStruct = construct %14
+    %16:i32 = load %vert_loc1_Input_1
+    %17:vec4<f32> = call %vert_inner, %15, %16
+    store %vert_position_Output, %17
+    ret
+  }
+}
+%frag1 = @fragment func():void -> %b6 {
+  %b6 = block {
+    %19:MyStruct = call %frag1_inner
+    %20:f32 = access %19, 0u
+    store %frag1_loc1_Output, %20
+    ret
+  }
+}
+%frag2 = @fragment func():void -> %b7 {
+  %b7 = block {
+    %22:i32 = call %frag2_inner
+    store %frag2_loc0_Output, %22
     ret
   }
 }
@@ -984,22 +1023,14 @@
   depth:f32 @offset(4)
 }
 
-foo_BuiltinOutputsStruct = struct @align(4), @block {
-  Outputs_depth:f32 @offset(0), @builtin(frag_depth)
-}
-
-foo_LocationOutputsStruct = struct @align(4), @block {
-  Outputs_color:f32 @offset(0), @location(0)
-}
-
 FragDepthClampArgs = struct @align(4), @block {
   min:f32 @offset(0)
   max:f32 @offset(4)
 }
 
 %b1 = block {  # root
-  %foo_BuiltinOutputs:ptr<__out, foo_BuiltinOutputsStruct, write> = var
-  %foo_LocationOutputs:ptr<__out, foo_LocationOutputsStruct, write> = var
+  %foo_loc0_Output:ptr<__out, f32, write> = var @location(0)
+  %foo_frag_depth_Output:ptr<__out, f32, write> = var @builtin(frag_depth)
   %tint_frag_depth_clamp_args:ptr<push_constant, FragDepthClampArgs, read_write> = var
 }
 
@@ -1013,15 +1044,13 @@
   %b3 = block {
     %7:Outputs = call %foo_inner
     %8:f32 = access %7, 0u
-    %9:ptr<__out, f32, write> = access %foo_LocationOutputs, 0u
-    store %9, %8
-    %10:f32 = access %7, 1u
-    %11:ptr<__out, f32, write> = access %foo_BuiltinOutputs, 0u
-    %12:FragDepthClampArgs = load %tint_frag_depth_clamp_args
-    %13:f32 = access %12, 0u
-    %14:f32 = access %12, 1u
-    %15:f32 = clamp %10, %13, %14
-    store %11, %15
+    store %foo_loc0_Output, %8
+    %9:f32 = access %7, 1u
+    %10:FragDepthClampArgs = load %tint_frag_depth_clamp_args
+    %11:f32 = access %10, 0u
+    %12:f32 = access %10, 1u
+    %13:f32 = clamp %9, %11, %12
+    store %foo_frag_depth_Output, %13
     ret
   }
 }
@@ -1034,5 +1063,53 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_ShaderIOTest, EmitVertexPointSize) {
+    auto* ep = b.Function("foo", ty.vec4<f32>());
+    ep->SetStage(core::ir::Function::PipelineStage::kVertex);
+    ep->SetReturnBuiltin(core::ir::Function::ReturnBuiltin::kPosition);
+
+    b.Append(ep->Block(), [&] {  //
+        b.Return(ep, b.Construct(ty.vec4<f32>(), 0.5_f));
+    });
+
+    auto* src = R"(
+%foo = @vertex func():vec4<f32> [@position] -> %b1 {
+  %b1 = block {
+    %2:vec4<f32> = construct 0.5f
+    ret %2
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %foo_position_Output:ptr<__out, vec4<f32>, write> = var @builtin(position)
+  %foo___point_size_Output:ptr<__out, f32, write> = var @builtin(__point_size)
+}
+
+%foo_inner = func():vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:vec4<f32> = construct 0.5f
+    ret %4
+  }
+}
+%foo = @vertex func():void -> %b3 {
+  %b3 = block {
+    %6:vec4<f32> = call %foo_inner
+    store %foo_position_Output, %6
+    store %foo___point_size_Output, 1.0f
+    ret
+  }
+}
+)";
+
+    ShaderIOConfig config;
+    config.emit_vertex_point_size = true;
+    Run(ShaderIO, config);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/var_test.cc b/src/tint/lang/spirv/writer/var_test.cc
index c73fa13..2e37175 100644
--- a/src/tint/lang/spirv/writer/var_test.cc
+++ b/src/tint/lang/spirv/writer/var_test.cc
@@ -175,8 +175,8 @@
     EXPECT_INST("%v = OpVariable %_ptr_Workgroup_int Workgroup %4");
 }
 
-TEST_F(SpirvWriterTest, StorageVar) {
-    auto* v = b.Var("v", ty.ptr<storage, i32>());
+TEST_F(SpirvWriterTest, StorageVar_ReadOnly) {
+    auto* v = b.Var("v", ty.ptr<storage, i32, read>());
     v->SetBindingPoint(0, 0);
     b.RootBlock()->Append(v);
 
@@ -185,6 +185,7 @@
                OpDecorate %tint_symbol_1 Block
                OpDecorate %1 DescriptorSet 0
                OpDecorate %1 Binding 0
+               OpDecorate %1 NonWritable
 )");
     EXPECT_INST(R"(
 %tint_symbol_1 = OpTypeStruct %int
@@ -194,7 +195,7 @@
 }
 
 TEST_F(SpirvWriterTest, StorageVar_LoadAndStore) {
-    auto* v = b.Var("v", ty.ptr<storage, i32>());
+    auto* v = b.Var("v", ty.ptr<storage, i32, read_write>());
     v->SetBindingPoint(0, 0);
     b.RootBlock()->Append(v);
 
@@ -219,6 +220,31 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, StorageVar_WriteOnly) {
+    auto* v = b.Var("v", ty.ptr<storage, i32, write>());
+    v->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(v);
+
+    auto* func = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kCompute,
+                            std::array{1u, 1u, 1u});
+    b.Append(func->Block(), [&] {
+        b.Store(v, 42_i);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               OpDecorate %tint_symbol_1 Block
+               OpDecorate %1 DescriptorSet 0
+               OpDecorate %1 Binding 0
+               OpDecorate %1 NonReadable
+)");
+    EXPECT_INST(R"(
+          %9 = OpAccessChain %_ptr_StorageBuffer_int %1 %uint_0
+               OpStore %9 %int_42
+)");
+}
+
 TEST_F(SpirvWriterTest, UniformVar) {
     auto* v = b.Var("v", ty.ptr<uniform, i32>());
     v->SetBindingPoint(0, 0);
@@ -363,5 +389,73 @@
     EXPECT_INST("%load = OpLoad %3 %v");
 }
 
+TEST_F(SpirvWriterTest, ReadOnlyStorageTextureVar) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* v = b.Var("v", ty.ptr(core::AddressSpace::kHandle,
+                                ty.Get<core::type::StorageTexture>(
+                                    core::type::TextureDimension::k2d, format, read,
+                                    core::type::StorageTexture::SubtypeFor(format, ty)),
+                                core::Access::kRead));
+    v->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(v);
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               OpDecorate %v DescriptorSet 0
+               OpDecorate %v Binding 0
+               OpDecorate %v NonWritable
+)");
+    EXPECT_INST(R"(
+          %3 = OpTypeImage %float 2D 0 0 0 2 Rgba8
+%_ptr_UniformConstant_3 = OpTypePointer UniformConstant %3
+          %v = OpVariable %_ptr_UniformConstant_3 UniformConstant
+)");
+}
+
+TEST_F(SpirvWriterTest, ReadWriteStorageTextureVar) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* v = b.Var("v", ty.ptr(core::AddressSpace::kHandle,
+                                ty.Get<core::type::StorageTexture>(
+                                    core::type::TextureDimension::k2d, format, read_write,
+                                    core::type::StorageTexture::SubtypeFor(format, ty)),
+                                core::Access::kRead));
+    v->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(v);
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               OpDecorate %v DescriptorSet 0
+               OpDecorate %v Binding 0
+)");
+    EXPECT_INST(R"(
+          %3 = OpTypeImage %float 2D 0 0 0 2 Rgba8
+%_ptr_UniformConstant_3 = OpTypePointer UniformConstant %3
+          %v = OpVariable %_ptr_UniformConstant_3 UniformConstant
+)");
+}
+
+TEST_F(SpirvWriterTest, WriteOnlyStorageTextureVar) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* v = b.Var("v", ty.ptr(core::AddressSpace::kHandle,
+                                ty.Get<core::type::StorageTexture>(
+                                    core::type::TextureDimension::k2d, format, write,
+                                    core::type::StorageTexture::SubtypeFor(format, ty)),
+                                core::Access::kRead));
+    v->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(v);
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               OpDecorate %v DescriptorSet 0
+               OpDecorate %v Binding 0
+               OpDecorate %v NonReadable
+)");
+    EXPECT_INST(R"(
+          %3 = OpTypeImage %float 2D 0 0 0 2 Rgba8
+%_ptr_UniformConstant_3 = OpTypePointer UniformConstant %3
+          %v = OpVariable %_ptr_UniformConstant_3 UniformConstant
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index 1464464..4850db3 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -421,6 +421,14 @@
                                     param->SetBuiltin(
                                         core::ir::FunctionParam::Builtin::kSampleMask);
                                     break;
+                                case core::BuiltinValue::kSubgroupInvocationId:
+                                    param->SetBuiltin(
+                                        core::ir::FunctionParam::Builtin::kSubgroupInvocationId);
+                                    break;
+                                case core::BuiltinValue::kSubgroupSize:
+                                    param->SetBuiltin(
+                                        core::ir::FunctionParam::Builtin::kSubgroupSize);
+                                    break;
                                 default:
                                     TINT_ICE() << "Unknown builtin value in parameter attributes "
                                                << ident_sem->Value();
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
index 0d61b8a..7ef8cf8 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
@@ -25,6 +25,7 @@
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/ir/var.h"
 #include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/core/type/scalar.h"
 #include "src/tint/lang/core/type/struct.h"
 #include "src/tint/lang/core/type/vector.h"