Import Tint changes from Dawn

Changes:
  - 328ffe7fbbc04ae41312347d1a9ab6d7a8bfe2dc Revert "Add deprecated proxy methods." by dan sinclair <dsinclair@chromium.org>
  - e25ac2f0fc3a36b1210dfc5788ef61948fb3d749 [tint][utils] Add AlignedStorage by Ben Clayton <bclayton@google.com>
  - 95cada18965836843ef1ee92cffeed6623adb2d5 [tint][exe] Add --use-storage-input-output-16 flag by James Price <jrprice@google.com>
  - 2f85bc743dda22b69f280b2dd5c034f3cde4919d [spirv-writer][ast] Polyfill f16 shader IO by James Price <jrprice@google.com>
  - 7c14dbaaf61a7cf95f852c07570dcd275385b6ff [spirv-writer][ast] Only use StorageInputOutput16 if needed by James Price <jrprice@google.com>
  - e90702ebf9fe7a9edf2c1e5e824ede95ae729e03 [spirv-writer] Polyfill f16 shader IO by James Price <jrprice@google.com>
  - 2c1743ab159399f0f0154088805a3db841836926 [spirv-writer] Only use StorageInputOutput16 if needed by James Price <jrprice@google.com>
  - 9ae3ff6eee865192b7ce95da7a46474a638c0855 GLSL: run OFI if any stage uses firstInstance. by Stephen White <senorblanco@chromium.org>
  - ca9f38634fcf3f782eb59db1894046aa117e857a GLSL: set integers to precision highp. by Stephen White <senorblanco@chromium.org>
GitOrigin-RevId: 328ffe7fbbc04ae41312347d1a9ab6d7a8bfe2dc
Change-Id: Ie94905aba99133a90ef23b32579b7007c5e8fae5
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/174060
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Commit-Queue: Copybara Prod <copybara-worker@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 81a9e5b..4d18bb9 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -178,6 +178,10 @@
 
     tint::Vector<std::string, 4> transforms;
 
+#if TINT_BUILD_SPV_WRITER
+    bool use_storage_input_output_16 = true;
+#endif  // TINT_BULD_SPV_WRITER
+
 #if TINT_BUILD_HLSL_WRITER
     std::string fxc_path;
     std::string dxc_path;
@@ -345,6 +349,13 @@
     });
 #endif
 
+#if TINT_BUILD_SPV_WRITER
+    auto& use_storage_input_output_16 =
+        options.Add<BoolOption>("use-storage-input-output-16",
+                                "Use the StorageInputOutput16 SPIR-V capability", Default{true});
+    TINT_DEFER(opts->use_storage_input_output_16 = *use_storage_input_output_16.value);
+#endif
+
     auto& disable_wg_init = options.Add<BoolOption>(
         "disable-workgroup-init", "Disable workgroup memory zero initialization", Default{false});
     TINT_DEFER(opts->disable_workgroup_init = *disable_wg_init.value);
@@ -687,6 +698,7 @@
     tint::spirv::writer::Options gen_options;
     gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
+    gen_options.use_storage_input_output_16 = options.use_storage_input_output_16;
     gen_options.bindings = tint::spirv::writer::GenerateBindings(program);
 
     tint::Result<tint::spirv::writer::Output> result;
@@ -1067,9 +1079,13 @@
 
         gen_options.texture_builtins_from_uniform = std::move(textureBuiltinsFromUniform);
 
-        // Place the first_instance push constant member after user-defined push constants (if any).
-        gen_options.first_instance_offset =
-            inspector.GetEntryPoint(entry_point_name).push_constant_size;
+        auto entry_point = inspector.GetEntryPoint(entry_point_name);
+
+        if (entry_point.instance_index_used) {
+            // Place the first_instance push constant member after user-defined push constants (if
+            // any).
+            gen_options.first_instance_offset = entry_point.push_constant_size;
+        }
 
         auto result = tint::glsl::writer::Generate(prg, gen_options, entry_point_name);
         if (result != tint::Success) {
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index a9c8c31..326c1a2 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -347,6 +347,7 @@
 
     if (version_.IsES() && requires_default_precision_qualifier_) {
         current_buffer_->Insert("precision highp float;", helpers_insertion_point++, indent);
+        current_buffer_->Insert("precision highp int;", helpers_insertion_point++, indent);
     }
 
     if (!helpers_.lines.empty()) {
diff --git a/src/tint/lang/glsl/writer/ast_printer/function_test.cc b/src/tint/lang/glsl/writer/ast_printer/function_test.cc
index 32a8b0f..a04b329 100644
--- a/src/tint/lang/glsl/writer/ast_printer/function_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/function_test.cc
@@ -113,6 +113,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 void func() {
   return;
@@ -160,6 +161,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 layout(location = 0) in float foo_1;
 layout(location = 1) out float value;
@@ -200,6 +202,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 float frag_main(vec4 coord) {
   return coord.x;
@@ -252,6 +255,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 layout(location = 1) out float col1_1;
 layout(location = 2) out float col2_1;
@@ -391,6 +395,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct UBO {
   vec4 coord;
@@ -432,6 +437,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Uniforms {
   vec4 coord;
@@ -473,6 +479,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   int a;
@@ -521,6 +528,7 @@
     EXPECT_EQ(gen.Result(),
               R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   int a;
@@ -566,6 +574,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   int a;
@@ -611,6 +620,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   int a;
@@ -658,6 +668,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct S {
   float x;
@@ -705,6 +716,7 @@
     EXPECT_EQ(gen.Result(),
               R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct S {
   float x;
@@ -741,6 +753,7 @@
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.Result(), R"(#version 310 es
 precision highp float;
+precision highp int;
 
 void tint_symbol() {
 }
diff --git a/src/tint/lang/glsl/writer/ast_printer/member_accessor_test.cc b/src/tint/lang/glsl/writer/ast_printer/member_accessor_test.cc
index ef7e638..302d899 100644
--- a/src/tint/lang/glsl/writer/ast_printer/member_accessor_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/member_accessor_test.cc
@@ -298,6 +298,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   int a;
@@ -351,6 +352,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   float z;
@@ -399,6 +401,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   float z;
@@ -447,6 +450,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   float z;
@@ -494,6 +498,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Data {
   float z;
@@ -547,6 +552,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Inner {
   vec3 a;
@@ -608,6 +614,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Inner {
   vec3 a;
@@ -670,6 +677,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Inner {
   vec3 a;
@@ -731,6 +739,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Inner {
   vec3 a;
@@ -791,6 +800,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Inner {
   vec3 a;
@@ -852,6 +862,7 @@
     auto* expected =
         R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct Inner {
   ivec3 a;
diff --git a/src/tint/lang/glsl/writer/ast_printer/sanitizer_test.cc b/src/tint/lang/glsl/writer/ast_printer/sanitizer_test.cc
index 83a653c..0cd03ab 100644
--- a/src/tint/lang/glsl/writer/ast_printer/sanitizer_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/sanitizer_test.cc
@@ -60,6 +60,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 layout(binding = 1, std430) buffer my_struct_ssbo {
   float a[];
@@ -100,6 +101,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 layout(binding = 1, std430) buffer my_struct_ssbo {
   float z;
@@ -144,6 +146,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 layout(binding = 1, std430) buffer my_struct_ssbo {
   float a[];
@@ -181,6 +184,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 void tint_symbol() {
   int idx = 3;
@@ -223,6 +227,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 struct S {
   int a;
@@ -269,6 +274,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 void tint_symbol() {
   int v = 0;
@@ -314,6 +320,7 @@
     auto got = gen.Result();
     auto* expect = R"(#version 310 es
 precision highp float;
+precision highp int;
 
 void tint_symbol() {
   mat4 a[4] = mat4[4](mat4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), mat4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), mat4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), mat4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f));
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc
index d9eee77..9c58491 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc
@@ -1722,7 +1722,6 @@
 OpCapability Float16
 OpCapability UniformAndStorageBuffer16BitAccess
 OpCapability StorageBuffer16BitAccess
-OpCapability StorageInputOutput16
 %15 = OpExtInstImport "GLSL.std.450"
 OpMemoryModel Logical GLSL450
 OpEntryPoint Fragment %3 "a_func"
@@ -1822,7 +1821,6 @@
 OpCapability Float16
 OpCapability UniformAndStorageBuffer16BitAccess
 OpCapability StorageBuffer16BitAccess
-OpCapability StorageInputOutput16
 OpMemoryModel Logical GLSL450
 OpEntryPoint Fragment %3 "a_func"
 OpExecutionMode %3 OriginUpperLeft
@@ -1928,7 +1926,6 @@
 OpCapability Float16
 OpCapability UniformAndStorageBuffer16BitAccess
 OpCapability StorageBuffer16BitAccess
-OpCapability StorageInputOutput16
 %17 = OpExtInstImport "GLSL.std.450"
 OpMemoryModel Logical GLSL450
 OpEntryPoint Fragment %3 "a_func"
@@ -2031,7 +2028,6 @@
 OpCapability Float16
 OpCapability UniformAndStorageBuffer16BitAccess
 OpCapability StorageBuffer16BitAccess
-OpCapability StorageInputOutput16
 OpMemoryModel Logical GLSL450
 OpEntryPoint Fragment %3 "a_func"
 OpExecutionMode %3 OriginUpperLeft
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index d76a591..4272172 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -188,7 +188,7 @@
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
         ast::transform::CanonicalizeEntryPointIO::Config(
             ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kSpirv, 0xFFFFFFFF,
-            options.emit_vertex_point_size));
+            options.emit_vertex_point_size, !options.use_storage_input_output_16));
 
     SanitizedResult result;
     ast::transform::DataMap outputs;
diff --git a/src/tint/lang/spirv/writer/ast_printer/binary_expression_test.cc b/src/tint/lang/spirv/writer/ast_printer/binary_expression_test.cc
index 15e4fb2..570448e 100644
--- a/src/tint/lang/spirv/writer/ast_printer/binary_expression_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/binary_expression_test.cc
@@ -1344,8 +1344,7 @@
             return R"(OpCapability Shader
 OpCapability Float16
 OpCapability UniformAndStorageBuffer16BitAccess
-OpCapability StorageBuffer16BitAccess
-OpCapability StorageInputOutput16)";
+OpCapability StorageBuffer16BitAccess)";
     }
     return {};
 }
@@ -1639,8 +1638,7 @@
             return R"(OpCapability Shader
 OpCapability Float16
 OpCapability UniformAndStorageBuffer16BitAccess
-OpCapability StorageBuffer16BitAccess
-OpCapability StorageInputOutput16)";
+OpCapability StorageBuffer16BitAccess)";
     }
     return {};
 }
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index eb393ca..67d829d 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -350,7 +350,6 @@
             module_.PushCapability(SpvCapabilityFloat16);
             module_.PushCapability(SpvCapabilityUniformAndStorageBuffer16BitAccess);
             module_.PushCapability(SpvCapabilityStorageBuffer16BitAccess);
-            module_.PushCapability(SpvCapabilityStorageInputOutput16);
             break;
         default:
             return false;
@@ -739,6 +738,13 @@
         return false;
     }
 
+    // Emit the StorageInputOutput16 capability if needed.
+    if (sc == core::AddressSpace::kIn || sc == core::AddressSpace::kOut) {
+        if (type->DeepestElement()->Is<core::type::F16>()) {
+            module_.PushCapability(SpvCapabilityStorageInputOutput16);
+        }
+    }
+
     module_.PushDebug(spv::Op::OpName, {Operand(var_id), Operand(v->name->symbol.Name())});
 
     OperandList ops = {Operand(type_id), result, U32Operand(ConvertAddressSpace(sc))};
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index e16b267..f7fdee7 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -141,6 +141,9 @@
     /// VK_KHR_zero_initialize_workgroup_memory is enabled.
     bool use_zero_initialize_workgroup_memory_extension = false;
 
+    /// Set to `true` to use the StorageInputOutput16 capability for shader IO that uses f16 types.
+    bool use_storage_input_output_16 = true;
+
     /// Set to `true` to generate a PointSize builtin and have it set to 1.0
     /// from all vertex shaders in the module.
     bool emit_vertex_point_size = true;
@@ -172,6 +175,7 @@
                  disable_runtime_sized_array_index_clamping,
                  disable_workgroup_init,
                  use_zero_initialize_workgroup_memory_extension,
+                 use_storage_input_output_16,
                  emit_vertex_point_size,
                  clamp_frag_depth,
                  pass_matrix_by_pointer,
diff --git a/src/tint/lang/spirv/writer/function_test.cc b/src/tint/lang/spirv/writer/function_test.cc
index 2658f3f..bb68130 100644
--- a/src/tint/lang/spirv/writer/function_test.cc
+++ b/src/tint/lang/spirv/writer/function_test.cc
@@ -346,6 +346,118 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, Function_ShaderIO_F16_Input_WithCapability) {
+    auto* input = b.FunctionParam("input", ty.vec4<f16>());
+    input->SetLocation(1, std::nullopt);
+    auto* func = b.Function("main", ty.vec4<f32>(), core::ir::Function::PipelineStage::kFragment);
+    func->SetReturnLocation(2, std::nullopt);
+    func->SetParams({input});
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Convert(ty.vec4<f32>(), input));
+    });
+
+    Options options;
+    options.use_storage_input_output_16 = true;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpCapability StorageInputOutput16");
+    EXPECT_INST(R"(OpEntryPoint Fragment %main "main" %main_loc1_Input %main_loc2_Output)");
+    EXPECT_INST("%main_loc1_Input = OpVariable %_ptr_Input_v4half Input");
+    EXPECT_INST("%main_loc2_Output = OpVariable %_ptr_Output_v4float Output");
+    EXPECT_INST(R"(
+       %main = OpFunction %void None %16
+         %17 = OpLabel
+         %18 = OpLoad %v4half %main_loc1_Input
+         %19 = OpFunctionCall %v4float %main_inner %18
+               OpStore %main_loc2_Output %19
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Function_ShaderIO_F16_Input_WithoutCapability) {
+    auto* input = b.FunctionParam("input", ty.vec4<f16>());
+    input->SetLocation(1, std::nullopt);
+    auto* func = b.Function("main", ty.vec4<f32>(), core::ir::Function::PipelineStage::kFragment);
+    func->SetReturnLocation(2, std::nullopt);
+    func->SetParams({input});
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Convert(ty.vec4<f32>(), input));
+    });
+
+    Options options;
+    options.use_storage_input_output_16 = false;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST(R"(OpEntryPoint Fragment %main "main" %main_loc1_Input %main_loc2_Output)");
+    EXPECT_INST("%main_loc1_Input = OpVariable %_ptr_Input_v4float Input");
+    EXPECT_INST("%main_loc2_Output = OpVariable %_ptr_Output_v4float Output");
+    EXPECT_INST(R"(
+       %main = OpFunction %void None %16
+         %17 = OpLabel
+         %18 = OpLoad %v4float %main_loc1_Input
+         %19 = OpFConvert %v4half %18
+         %20 = OpFunctionCall %v4float %main_inner %19
+               OpStore %main_loc2_Output %20
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Function_ShaderIO_F16_Output_WithCapability) {
+    auto* input = b.FunctionParam("input", ty.vec4<f32>());
+    input->SetLocation(1, std::nullopt);
+    auto* func = b.Function("main", ty.vec4<f16>(), core::ir::Function::PipelineStage::kFragment);
+    func->SetReturnLocation(2, std::nullopt);
+    func->SetParams({input});
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Convert(ty.vec4<f16>(), input));
+    });
+
+    Options options;
+    options.use_storage_input_output_16 = true;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpCapability StorageInputOutput16");
+    EXPECT_INST(R"(OpEntryPoint Fragment %main "main" %main_loc1_Input %main_loc2_Output)");
+    EXPECT_INST("%main_loc1_Input = OpVariable %_ptr_Input_v4float Input");
+    EXPECT_INST("%main_loc2_Output = OpVariable %_ptr_Output_v4half Output");
+    EXPECT_INST(R"(
+       %main = OpFunction %void None %16
+         %17 = OpLabel
+         %18 = OpLoad %v4float %main_loc1_Input
+         %19 = OpFunctionCall %v4half %main_inner %18
+               OpStore %main_loc2_Output %19
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Function_ShaderIO_F16_Output_WithoutCapability) {
+    auto* input = b.FunctionParam("input", ty.vec4<f32>());
+    input->SetLocation(1, std::nullopt);
+    auto* func = b.Function("main", ty.vec4<f16>(), core::ir::Function::PipelineStage::kFragment);
+    func->SetReturnLocation(2, std::nullopt);
+    func->SetParams({input});
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Convert(ty.vec4<f16>(), input));
+    });
+
+    Options options;
+    options.use_storage_input_output_16 = false;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST(R"(OpEntryPoint Fragment %main "main" %main_loc1_Input %main_loc2_Output)");
+    EXPECT_INST("%main_loc1_Input = OpVariable %_ptr_Input_v4float Input");
+    EXPECT_INST("%main_loc2_Output = OpVariable %_ptr_Output_v4float Output");
+    EXPECT_INST(R"(
+       %main = OpFunction %void None %16
+         %17 = OpLabel
+         %18 = OpLoad %v4float %main_loc1_Input
+         %19 = OpFunctionCall %v4half %main_inner %18
+         %20 = OpFConvert %v4float %19
+               OpStore %main_loc2_Output %20
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
 TEST_F(SpirvWriterTest, Function_ShaderIO_DualSourceBlend) {
     auto* outputs =
         ty.Struct(mod.symbols.New("Outputs"), {
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index ef41faf..9fc98fd 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -493,7 +493,6 @@
                     module_.PushCapability(SpvCapabilityFloat16);
                     module_.PushCapability(SpvCapabilityUniformAndStorageBuffer16BitAccess);
                     module_.PushCapability(SpvCapabilityStorageBuffer16BitAccess);
-                    module_.PushCapability(SpvCapabilityStorageInputOutput16);
                     module_.PushType(spv::Op::OpTypeFloat, {id, 16u});
                 },
                 [&](const core::type::Vector* vec) {
@@ -2065,6 +2064,9 @@
             }
             case core::AddressSpace::kIn: {
                 TINT_ASSERT(!current_function_);
+                if (store_ty->DeepestElement()->Is<core::type::F16>()) {
+                    module_.PushCapability(SpvCapabilityStorageInputOutput16);
+                }
                 module_.PushType(spv::Op::OpVariable, {ty, id, U32Operand(SpvStorageClassInput)});
                 EmitIOAttributes(id, var->Attributes(), core::AddressSpace::kIn);
                 break;
@@ -2089,6 +2091,9 @@
             }
             case core::AddressSpace::kOut: {
                 TINT_ASSERT(!current_function_);
+                if (store_ty->DeepestElement()->Is<core::type::F16>()) {
+                    module_.PushCapability(SpvCapabilityStorageInputOutput16);
+                }
                 module_.PushType(spv::Op::OpVariable, {ty, id, U32Operand(SpvStorageClassOutput)});
                 EmitIOAttributes(id, var->Attributes(), core::AddressSpace::kOut);
                 break;
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 70d4669..bbaeb1f 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -144,7 +144,8 @@
     RUN_TRANSFORM(raise::HandleMatrixArithmetic, module);
     RUN_TRANSFORM(raise::MergeReturn, module);
     RUN_TRANSFORM(raise::ShaderIO, module,
-                  raise::ShaderIOConfig{options.clamp_frag_depth, options.emit_vertex_point_size});
+                  raise::ShaderIOConfig{options.clamp_frag_depth, options.emit_vertex_point_size,
+                                        !options.use_storage_input_output_16});
     RUN_TRANSFORM(core::ir::transform::Std140, module);
     RUN_TRANSFORM(raise::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 8266f23..ddb311b 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -112,8 +112,19 @@
             }
             name << name_suffix;
 
+            // Replace f16 types with f32 types if necessary.
+            auto* store_type = io.type;
+            if (config.polyfill_f16_io) {
+                if (store_type->DeepestElement()->Is<core::type::F16>()) {
+                    store_type = ty.f32();
+                    if (auto* vec = io.type->As<core::type::Vector>()) {
+                        store_type = ty.vec(store_type, vec->Width());
+                    }
+                }
+            }
+
             // Create an IO variable and add it to the root block.
-            auto* ptr = ty.ptr(addrspace, io.type, access);
+            auto* ptr = ty.ptr(addrspace, store_type, access);
             auto* var = b.Var(name.str(), ptr);
             var->SetAttributes(core::ir::IOAttributes{
                 io.attributes.location,
@@ -150,7 +161,15 @@
                 from = builder.Access(ptr, input_vars[idx], 0_u)->Result(0);
             }
         }
-        return builder.Load(from)->Result(0);
+
+        auto* value = builder.Load(from)->Result(0);
+
+        // Convert f32 values to f16 values if needed.
+        if (config.polyfill_f16_io && inputs[idx].type->DeepestElement()->Is<core::type::F16>()) {
+            value = builder.Convert(inputs[idx].type, value)->Result(0);
+        }
+
+        return value;
     }
 
     /// @copydoc ShaderIO::BackendState::SetOutput
@@ -169,6 +188,12 @@
                 value = ClampFragDepth(builder, value);
             }
         }
+
+        // Convert f16 values to f32 values if needed.
+        if (config.polyfill_f16_io && value->Type()->DeepestElement()->Is<core::type::F16>()) {
+            value = builder.Convert(to->Type()->UnwrapPtr(), value)->Result(0);
+        }
+
         builder.Store(to, value);
     }
 
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.h b/src/tint/lang/spirv/writer/raise/shader_io.h
index 3ec521c..38cd9ad 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.h
+++ b/src/tint/lang/spirv/writer/raise/shader_io.h
@@ -46,6 +46,8 @@
     bool clamp_frag_depth = false;
     /// true if a vertex point size builtin output should be added
     bool emit_vertex_point_size = false;
+    /// true if f16 IO types should be replaced with f32 types and converted
+    bool polyfill_f16_io = false;
 };
 
 /// ShaderIO is a transform that moves each entry point function's parameters and return value to
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 5738a20..61d3ef7 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -1491,5 +1491,197 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_ShaderIOTest, F16_IO_WithoutPolyfill) {
+    auto* outputs =
+        ty.Struct(mod.symbols.New("Outputs"), {
+                                                  {
+                                                      mod.symbols.New("out1"),
+                                                      ty.f16(),
+                                                      core::type::StructMemberAttributes{
+                                                          /* location */ 1u,
+                                                          /* index */ std::nullopt,
+                                                          /* color */ std::nullopt,
+                                                          /* builtin */ std::nullopt,
+                                                          /* interpolation */ std::nullopt,
+                                                          /* invariant */ false,
+                                                      },
+                                                  },
+                                                  {
+                                                      mod.symbols.New("out2"),
+                                                      ty.vec4<f16>(),
+                                                      core::type::StructMemberAttributes{
+                                                          /* location */ 2u,
+                                                          /* index */ std::nullopt,
+                                                          /* color */ std::nullopt,
+                                                          /* builtin */ std::nullopt,
+                                                          /* interpolation */ std::nullopt,
+                                                          /* invariant */ false,
+                                                      },
+                                                  },
+                                              });
+
+    auto* in1 = b.FunctionParam("in1", ty.f16());
+    auto* in2 = b.FunctionParam("in2", ty.vec4<f16>());
+    in1->SetLocation(1, std::nullopt);
+    in1->SetLocation(2, std::nullopt);
+    auto* func = b.Function("main", outputs, core::ir::Function::PipelineStage::kFragment);
+    func->SetParams({in1, in2});
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Construct(outputs, in1, in2));
+    });
+
+    auto* src = R"(
+Outputs = struct @align(8) {
+  out1:f16 @offset(0), @location(1)
+  out2:vec4<f16> @offset(8), @location(2)
+}
+
+%main = @fragment func(%in1:f16 [@location(2)], %in2:vec4<f16>):Outputs -> %b1 {
+  %b1 = block {
+    %4:Outputs = construct %in1, %in2
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Outputs = struct @align(8) {
+  out1:f16 @offset(0)
+  out2:vec4<f16> @offset(8)
+}
+
+%b1 = block {  # root
+  %main_loc2_Input:ptr<__in, f16, read> = var @location(2)
+  %main_Input:ptr<__in, vec4<f16>, read> = var
+  %main_loc1_Output:ptr<__out, f16, write> = var @location(1)
+  %main_loc2_Output:ptr<__out, vec4<f16>, write> = var @location(2)
+}
+
+%main_inner = func(%in1:f16, %in2:vec4<f16>):Outputs -> %b2 {
+  %b2 = block {
+    %8:Outputs = construct %in1, %in2
+    ret %8
+  }
+}
+%main = @fragment func():void -> %b3 {
+  %b3 = block {
+    %10:f16 = load %main_loc2_Input
+    %11:vec4<f16> = load %main_Input
+    %12:Outputs = call %main_inner, %10, %11
+    %13:f16 = access %12, 0u
+    store %main_loc1_Output, %13
+    %14:vec4<f16> = access %12, 1u
+    store %main_loc2_Output, %14
+    ret
+  }
+}
+)";
+
+    ShaderIOConfig config;
+    config.polyfill_f16_io = false;
+    Run(ShaderIO, config);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_ShaderIOTest, F16_IO_WithPolyfill) {
+    auto* outputs =
+        ty.Struct(mod.symbols.New("Outputs"), {
+                                                  {
+                                                      mod.symbols.New("out1"),
+                                                      ty.f16(),
+                                                      core::type::StructMemberAttributes{
+                                                          /* location */ 1u,
+                                                          /* index */ std::nullopt,
+                                                          /* color */ std::nullopt,
+                                                          /* builtin */ std::nullopt,
+                                                          /* interpolation */ std::nullopt,
+                                                          /* invariant */ false,
+                                                      },
+                                                  },
+                                                  {
+                                                      mod.symbols.New("out2"),
+                                                      ty.vec4<f16>(),
+                                                      core::type::StructMemberAttributes{
+                                                          /* location */ 2u,
+                                                          /* index */ std::nullopt,
+                                                          /* color */ std::nullopt,
+                                                          /* builtin */ std::nullopt,
+                                                          /* interpolation */ std::nullopt,
+                                                          /* invariant */ false,
+                                                      },
+                                                  },
+                                              });
+
+    auto* in1 = b.FunctionParam("in1", ty.f16());
+    auto* in2 = b.FunctionParam("in2", ty.vec4<f16>());
+    in1->SetLocation(1, std::nullopt);
+    in1->SetLocation(2, std::nullopt);
+    auto* func = b.Function("main", outputs, core::ir::Function::PipelineStage::kFragment);
+    func->SetParams({in1, in2});
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Construct(outputs, in1, in2));
+    });
+
+    auto* src = R"(
+Outputs = struct @align(8) {
+  out1:f16 @offset(0), @location(1)
+  out2:vec4<f16> @offset(8), @location(2)
+}
+
+%main = @fragment func(%in1:f16 [@location(2)], %in2:vec4<f16>):Outputs -> %b1 {
+  %b1 = block {
+    %4:Outputs = construct %in1, %in2
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Outputs = struct @align(8) {
+  out1:f16 @offset(0)
+  out2:vec4<f16> @offset(8)
+}
+
+%b1 = block {  # root
+  %main_loc2_Input:ptr<__in, f32, read> = var @location(2)
+  %main_Input:ptr<__in, vec4<f32>, read> = var
+  %main_loc1_Output:ptr<__out, f32, write> = var @location(1)
+  %main_loc2_Output:ptr<__out, vec4<f32>, write> = var @location(2)
+}
+
+%main_inner = func(%in1:f16, %in2:vec4<f16>):Outputs -> %b2 {
+  %b2 = block {
+    %8:Outputs = construct %in1, %in2
+    ret %8
+  }
+}
+%main = @fragment func():void -> %b3 {
+  %b3 = block {
+    %10:f32 = load %main_loc2_Input
+    %11:f16 = convert %10
+    %12:vec4<f32> = load %main_Input
+    %13:vec4<f16> = convert %12
+    %14:Outputs = call %main_inner, %11, %13
+    %15:f16 = access %14, 0u
+    %16:f32 = convert %15
+    store %main_loc1_Output, %16
+    %17:vec4<f16> = access %14, 1u
+    %18:vec4<f32> = convert %17
+    store %main_loc2_Output, %18
+    ret
+  }
+}
+)";
+
+    ShaderIOConfig config;
+    config.polyfill_f16_io = true;
+    Run(ShaderIO, config);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/type_test.cc b/src/tint/lang/spirv/writer/type_test.cc
index 08fd086..27fdc1b 100644
--- a/src/tint/lang/spirv/writer/type_test.cc
+++ b/src/tint/lang/spirv/writer/type_test.cc
@@ -98,7 +98,6 @@
     EXPECT_INST("OpCapability Float16");
     EXPECT_INST("OpCapability UniformAndStorageBuffer16BitAccess");
     EXPECT_INST("OpCapability StorageBuffer16BitAccess");
-    EXPECT_INST("OpCapability StorageInputOutput16");
     EXPECT_INST("%half = OpTypeFloat 16");
 }
 
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index 842e41b..db9a682 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -339,6 +339,17 @@
                     value = b.IndexAccessor(value, 0_i);
                 }
             }
+
+            // Replace f16 types with f32 types if necessary.
+            if (cfg.polyfill_f16_io && type->DeepestElement()->Is<core::type::F16>()) {
+                value = b.Call(ast_type, value);
+
+                ast_type = b.ty.f32();
+                if (auto* vec = type->As<core::type::Vector>()) {
+                    ast_type = b.ty.vec(ast_type, vec->Width());
+                }
+            }
+
             b.GlobalVar(symbol, ast_type, core::AddressSpace::kIn, std::move(attrs));
             return value;
         } else if (cfg.shader_style == ShaderStyle::kMsl &&
@@ -406,9 +417,27 @@
             }
         }
 
+        ast::Type ast_type;
+
+        // Replace f16 types with f32 types if necessary.
+        if (cfg.shader_style == ShaderStyle::kSpirv && cfg.polyfill_f16_io &&
+            type->DeepestElement()->Is<core::type::F16>()) {
+            auto make_ast_type = [&] {
+                auto ty = b.ty.f32();
+                if (auto* vec = type->As<core::type::Vector>()) {
+                    ty = b.ty.vec(ty, vec->Width());
+                }
+                return ty;
+            };
+            ast_type = make_ast_type();
+            value = b.Call(make_ast_type(), value);
+        } else {
+            ast_type = CreateASTTypeFor(ctx, type);
+        }
+
         OutputValue output;
         output.name = name;
-        output.type = CreateASTTypeFor(ctx, type);
+        output.type = ast_type;
         output.attributes = std::move(attrs);
         output.value = value;
         output.location = location;
@@ -984,10 +1013,12 @@
 
 CanonicalizeEntryPointIO::Config::Config(ShaderStyle style,
                                          uint32_t sample_mask,
-                                         bool emit_point_size)
+                                         bool emit_point_size,
+                                         bool polyfill_f16)
     : shader_style(style),
       fixed_sample_mask(sample_mask),
-      emit_vertex_point_size(emit_point_size) {}
+      emit_vertex_point_size(emit_point_size),
+      polyfill_f16_io(polyfill_f16) {}
 
 CanonicalizeEntryPointIO::Config::Config(const Config&) = default;
 CanonicalizeEntryPointIO::Config::~Config() = default;
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h
index d63d479..ddf761d 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h
@@ -118,9 +118,11 @@
         /// @param style the approach to use for emitting shader IO.
         /// @param sample_mask an optional sample mask to combine with shader masks
         /// @param emit_vertex_point_size `true` to generate a pointsize builtin
+        /// @param polyfill_f16_io `true` to replace f16 types with f32 types
         explicit Config(ShaderStyle style,
                         uint32_t sample_mask = 0xFFFFFFFF,
-                        bool emit_vertex_point_size = false);
+                        bool emit_vertex_point_size = false,
+                        bool polyfill_f16_io = false);
 
         /// Copy constructor
         Config(const Config&);
@@ -137,6 +139,9 @@
         /// Set to `true` to generate a pointsize builtin and have it set to 1.0
         /// from all vertex shaders in the module.
         const bool emit_vertex_point_size;
+
+        /// Set to `true` to replace f16 IO types with f32 types and convert them.
+        const bool polyfill_f16_io = false;
     };
 
     /// HLSLWaveIntrinsic is an InternalAttribute that is used to decorate a stub function so that
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
index 7c33b29..ec83e9e 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
@@ -4420,5 +4420,60 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(CanonicalizeEntryPointIOTest, F16_Polyfill_Spirv) {
+    auto* src = R"(
+enable f16;
+
+struct Outputs {
+  @location(1) a : f16,
+  @location(2) b : vec4<f16>,
+}
+
+@fragment
+fn frag_main(@location(1) loc1 : f16,
+             @location(2) loc2 : vec4<f16>) -> Outputs {
+  return Outputs(loc1 * 2, loc2 * 3);
+}
+)";
+
+    auto* expect = R"(
+enable f16;
+
+@location(1) @internal(disable_validation__ignore_address_space) var<__in> loc1_1 : f32;
+
+@location(2) @internal(disable_validation__ignore_address_space) var<__in> loc2_1 : vec4<f32>;
+
+@location(1) @internal(disable_validation__ignore_address_space) var<__out> a_1 : f32;
+
+@location(2) @internal(disable_validation__ignore_address_space) var<__out> b_1 : vec4<f32>;
+
+struct Outputs {
+  a : f16,
+  b : vec4<f16>,
+}
+
+fn frag_main_inner(loc1 : f16, loc2 : vec4<f16>) -> Outputs {
+  return Outputs((loc1 * 2), (loc2 * 3));
+}
+
+@fragment
+fn frag_main() {
+  let inner_result = frag_main_inner(f16(loc1_1), vec4<f16>(loc2_1));
+  a_1 = f32(inner_result.a);
+  b_1 = vec4<f32>(inner_result.b);
+}
+)";
+
+    DataMap data;
+
+    data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv,
+                                               /* fixed_sample_mask */ 0xFFFFFFFF,
+                                               /* emit_vertex_point_size */ false,
+                                               /* polyfill_f16_io */ true);
+    auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index.cc b/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
index 04bdd9a..143b43f 100644
--- a/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
@@ -53,15 +53,6 @@
 constexpr char kFirstVertexName[] = "first_vertex";
 constexpr char kFirstInstanceName[] = "first_instance";
 
-bool ShouldRun(const Program& program) {
-    for (auto* fn : program.AST().Functions()) {
-        if (fn->PipelineStage() == PipelineStage::kVertex) {
-            return true;
-        }
-    }
-    return false;
-}
-
 }  // namespace
 
 OffsetFirstIndex::OffsetFirstIndex() = default;
@@ -70,14 +61,13 @@
 Transform::ApplyResult OffsetFirstIndex::Apply(const Program& src,
                                                const DataMap& inputs,
                                                DataMap&) const {
-    if (!ShouldRun(src)) {
-        return SkipTransform;
-    }
-
     const Config* cfg = inputs.Get<Config>();
     if (!cfg) {
         return SkipTransform;
     }
+    if (!cfg->first_vertex_offset.has_value() && !cfg->first_instance_offset.has_value()) {
+        return SkipTransform;
+    }
 
     ProgramBuilder b;
     program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
@@ -86,9 +76,6 @@
     std::unordered_map<const sem::Variable*, const char*> builtin_vars;
     std::unordered_map<const core::type::StructMember*, const char*> builtin_members;
 
-    bool has_vertex_index = false;
-    bool has_instance_index = false;
-
     // Traverse the AST scanning for builtin accesses via variables (includes
     // parameters) or structure member accesses.
     for (auto* node : ctx.src->ASTNodes().Objects()) {
@@ -100,13 +87,11 @@
                         cfg->first_vertex_offset.has_value()) {
                         auto* sem_var = ctx.src->Sem().Get(var);
                         builtin_vars.emplace(sem_var, kFirstVertexName);
-                        has_vertex_index = true;
                     }
                     if (builtin == core::BuiltinValue::kInstanceIndex && cfg &&
                         cfg->first_instance_offset.has_value()) {
                         auto* sem_var = ctx.src->Sem().Get(var);
                         builtin_vars.emplace(sem_var, kFirstInstanceName);
-                        has_instance_index = true;
                     }
                 }
             }
@@ -119,23 +104,17 @@
                         cfg->first_vertex_offset.has_value()) {
                         auto* sem_mem = ctx.src->Sem().Get(member);
                         builtin_members.emplace(sem_mem, kFirstVertexName);
-                        has_vertex_index = true;
                     }
                     if (builtin == core::BuiltinValue::kInstanceIndex && cfg &&
                         cfg->first_instance_offset.has_value()) {
                         auto* sem_mem = ctx.src->Sem().Get(member);
                         builtin_members.emplace(sem_mem, kFirstInstanceName);
-                        has_instance_index = true;
                     }
                 }
             }
         }
     }
 
-    if (!has_vertex_index && !has_instance_index) {
-        return SkipTransform;
-    }
-
     Vector<const ast::StructMember*, 8> members;
 
     const ast::Variable* push_constants_var = nullptr;
@@ -159,11 +138,11 @@
     }
 
     // Add push constant members and calculate byte offsets
-    if (has_vertex_index) {
+    if (cfg->first_vertex_offset.has_value()) {
         members.Push(b.Member(kFirstVertexName, b.ty.u32(),
                               Vector{b.MemberOffset(AInt(*cfg->first_vertex_offset))}));
     }
-    if (has_instance_index) {
+    if (cfg->first_instance_offset.has_value()) {
         members.Push(b.Member(kFirstInstanceName, b.ty.u32(),
                               Vector{b.MemberOffset(AInt(*cfg->first_instance_offset))}));
     }
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc b/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc
index 7d026ed..09dd505 100644
--- a/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc
@@ -41,12 +41,49 @@
 TEST_F(OffsetFirstIndexTest, ShouldRunEmptyModule) {
     auto* src = R"()";
 
-    DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
-    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
+    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src));
 }
 
-TEST_F(OffsetFirstIndexTest, ShouldRunFragmentStage) {
+TEST_F(OffsetFirstIndexTest, ShouldRunNoVertexIndexNoInstanceIndex) {
+    auto* src = R"(
+@fragment
+fn entry() {
+  return;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, std::nullopt);
+    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, std::move(config)));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunNoVertexIndex) {
+    auto* src = R"(
+@fragment
+fn entry() {
+  return;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, 0);
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, std::move(config)));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunNoInstanceIndex) {
+    auto* src = R"(
+@fragment
+fn entry() {
+  return;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, std::move(config)));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRun) {
     auto* src = R"(
 @fragment
 fn entry() {
@@ -56,20 +93,7 @@
 
     DataMap config;
     config.Add<OffsetFirstIndex::Config>(0, 4);
-    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
-}
-
-TEST_F(OffsetFirstIndexTest, ShouldRunVertexStage) {
-    auto* src = R"(
-@vertex
-fn entry() -> @builtin(position) vec4<f32> {
-  return vec4<f32>();
-}
-)";
-
-    DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
-    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, std::move(config)));
 }
 
 TEST_F(OffsetFirstIndexTest, ShouldRunVertexStageWithVertexIndex) {
@@ -82,7 +106,7 @@
 
     DataMap config;
     config.Add<OffsetFirstIndex::Config>(0, 4);
-    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, config));
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, std::move(config)));
 }
 
 TEST_F(OffsetFirstIndexTest, ShouldRunVertexStageWithInstanceIndex) {
@@ -95,16 +119,14 @@
 
     DataMap config;
     config.Add<OffsetFirstIndex::Config>(0, 4);
-    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, config));
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, std::move(config)));
 }
 
 TEST_F(OffsetFirstIndexTest, EmptyModule) {
     auto* src = "";
     auto* expect = "";
 
-    DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
-    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+    auto got = Run<OffsetFirstIndex>(src);
 
     EXPECT_EQ(expect, str(got));
 }
@@ -118,9 +140,7 @@
 )";
     auto* expect = src;
 
-    DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
-    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+    auto got = Run<OffsetFirstIndex>(src);
 
     EXPECT_EQ(expect, str(got));
 }
@@ -160,7 +180,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -201,7 +221,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -244,7 +264,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, 4);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -287,7 +307,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, 4);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -577,7 +597,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -614,7 +634,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -663,7 +683,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
@@ -712,7 +732,7 @@
 )";
 
     DataMap config;
-    config.Add<OffsetFirstIndex::Config>(0, 4);
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
     auto got = Run<OffsetFirstIndex>(src, std::move(config));
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/utils/containers/hashmap_base.h b/src/tint/utils/containers/hashmap_base.h
index 2a86e3a..ba78fd0 100644
--- a/src/tint/utils/containers/hashmap_base.h
+++ b/src/tint/utils/containers/hashmap_base.h
@@ -38,6 +38,7 @@
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/math/hash.h"
 #include "src/tint/utils/math/math.h"
+#include "src/tint/utils/memory/aligned_storage.h"
 #include "src/tint/utils/traits/traits.h"
 
 namespace tint {
@@ -418,21 +419,14 @@
   protected:
     /// Node holds an Entry in a linked list.
     struct Node {
-        /// A structure that has the same size and alignment as Entry.
-        /// Replacement for std::aligned_storage as this is broken on earlier versions of MSVC.
-        struct alignas(alignof(ENTRY)) Storage {
-            /// Byte array of length sizeof(ENTRY)
-            uint8_t data[sizeof(ENTRY)];
-        };
-
         /// Destructs the entry.
         void Destroy() { Entry().~ENTRY(); }
 
         /// @returns the storage reinterpreted as an `Entry&`
-        ENTRY& Entry() { return *Bitcast<ENTRY*>(&storage.data[0]); }
+        ENTRY& Entry() { return storage.Get(); }
 
         /// @returns the storage reinterpreted as a `const Entry&`
-        const ENTRY& Entry() const { return *Bitcast<const ENTRY*>(&storage.data[0]); }
+        const ENTRY& Entry() const { return storage.Get(); }
 
         /// @returns a reference to the Entry's HashmapKey
         const HashmapBase::Key& Key() const { return HashmapBase::Entry::KeyOf(Entry()); }
@@ -450,7 +444,7 @@
         /// storage is a buffer that has the same size and alignment as Entry.
         /// The storage holds a constructed Entry when linked in the slots, and is destructed when
         /// removed from slots.
-        Storage storage;
+        AlignedStorage<ENTRY> storage;
 
         /// next is the next Node in the slot, or in the free list.
         Node* next;
diff --git a/src/tint/utils/containers/vector.h b/src/tint/utils/containers/vector.h
index c7edd0e..cd806c1 100644
--- a/src/tint/utils/containers/vector.h
+++ b/src/tint/utils/containers/vector.h
@@ -41,6 +41,7 @@
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/math/hash.h"
+#include "src/tint/utils/memory/aligned_storage.h"
 #include "src/tint/utils/memory/bitcast.h"
 
 #ifndef TINT_VECTOR_MUTATION_CHECKS_ENABLED
@@ -912,26 +913,18 @@
     constexpr static bool HasSmallArray = N > 0;
 
     /// A structure that has the same size and alignment as T.
-    /// Replacement for std::aligned_storage as this is broken on earlier versions of MSVC.
-    struct alignas(alignof(T)) TStorage {
-        /// @returns the storage reinterpreted as a T*
-        T* Get() { return Bitcast<T*>(&data[0]); }
-        /// @returns the storage reinterpreted as a T*
-        const T* Get() const { return Bitcast<const T*>(&data[0]); }
-        /// Byte array of length sizeof(T)
-        uint8_t data[sizeof(T)];
-    };
+    using TStorage = AlignedStorage<T>;
 
     /// The internal structure for the vector with a small array.
     struct ImplWithSmallArray {
         TStorage small_arr[N];
-        tint::Slice<T> slice = {small_arr[0].Get(), 0, N};
+        tint::Slice<T> slice = {&small_arr[0].Get(), 0, N};
 
         /// Allocates a new vector of `T` either from #small_arr, or from the heap, then assigns the
         /// pointer it to #slice.data, and updates #slice.cap.
         void Allocate(size_t new_cap) {
             if (new_cap < N) {
-                slice.data = small_arr[0].Get();
+                slice.data = &small_arr[0].Get();
                 slice.cap = N;
             } else {
                 slice.data = Bitcast<T*>(new TStorage[new_cap]);
@@ -941,14 +934,14 @@
 
         /// Frees `data`, if isn't a pointer to #small_arr
         void Free(T* data) const {
-            if (data != small_arr[0].Get()) {
+            if (data != &small_arr[0].Get()) {
                 delete[] Bitcast<TStorage*>(data);
             }
         }
 
         /// Indicates whether the slice structure can be std::move()d.
         /// @returns true if #slice.data does not point to #small_arr
-        bool CanMove() const { return slice.data != small_arr[0].Get(); }
+        bool CanMove() const { return slice.data != &small_arr[0].Get(); }
     };
 
     /// The internal structure for the vector without a small array.
diff --git a/src/tint/utils/diagnostic/diagnostic.h b/src/tint/utils/diagnostic/diagnostic.h
index fc4438d..ac5c42a 100644
--- a/src/tint/utils/diagnostic/diagnostic.h
+++ b/src/tint/utils/diagnostic/diagnostic.h
@@ -252,8 +252,6 @@
     /// @returns true iff the diagnostic list contains errors diagnostics (or of
     /// higher severity).
     bool ContainsErrors() const { return error_count_ > 0; }
-    /// deprecated, use `ContainsErrors`
-    bool contains_errors() const { return ContainsErrors(); }
     /// @returns the number of error diagnostics (or of higher severity).
     size_t NumErrors() const { return error_count_; }
     /// @returns the number of entries in the list.
diff --git a/src/tint/utils/diagnostic/formatter.h b/src/tint/utils/diagnostic/formatter.h
index 1841fe0..63da5e2 100644
--- a/src/tint/utils/diagnostic/formatter.h
+++ b/src/tint/utils/diagnostic/formatter.h
@@ -69,8 +69,6 @@
     /// @return the list of diagnostics `list` formatted to a string.
     /// @param list the list of diagnostic messages to format
     std::string Format(const List& list) const;
-    /// deprecated, use `Format`
-    std::string format(const List& list) const { return Format(list); }
 
   private:
     struct State;
diff --git a/src/tint/utils/memory/BUILD.bazel b/src/tint/utils/memory/BUILD.bazel
index 0bd13c9..6cb1b67 100644
--- a/src/tint/utils/memory/BUILD.bazel
+++ b/src/tint/utils/memory/BUILD.bazel
@@ -42,6 +42,7 @@
     "memory.cc",
   ],
   hdrs = [
+    "aligned_storage.h",
     "bitcast.h",
     "block_allocator.h",
     "bump_allocator.h",
diff --git a/src/tint/utils/memory/BUILD.cmake b/src/tint/utils/memory/BUILD.cmake
index 3151d3f..1d69077 100644
--- a/src/tint/utils/memory/BUILD.cmake
+++ b/src/tint/utils/memory/BUILD.cmake
@@ -39,6 +39,7 @@
 # Kind:      lib
 ################################################################################
 tint_add_target(tint_utils_memory lib
+  utils/memory/aligned_storage.h
   utils/memory/bitcast.h
   utils/memory/block_allocator.h
   utils/memory/bump_allocator.h
diff --git a/src/tint/utils/memory/BUILD.gn b/src/tint/utils/memory/BUILD.gn
index e5b44f2..0ab12e1 100644
--- a/src/tint/utils/memory/BUILD.gn
+++ b/src/tint/utils/memory/BUILD.gn
@@ -44,6 +44,7 @@
 
 libtint_source_set("memory") {
   sources = [
+    "aligned_storage.h",
     "bitcast.h",
     "block_allocator.h",
     "bump_allocator.h",
diff --git a/src/tint/utils/memory/aligned_storage.h b/src/tint/utils/memory/aligned_storage.h
new file mode 100644
index 0000000..c532c4f
--- /dev/null
+++ b/src/tint/utils/memory/aligned_storage.h
@@ -0,0 +1,53 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_MEMORY_ALIGNED_STORAGE_H_
+#define SRC_TINT_UTILS_MEMORY_ALIGNED_STORAGE_H_
+
+#include <cstddef>
+
+#include "src/tint/utils/memory/bitcast.h"
+
+namespace tint {
+
+/// A structure that has the same size and alignment as Entry.
+/// Replacement for std::aligned_storage as this is broken on earlier versions of MSVC.
+template <typename T>
+struct alignas(alignof(T)) AlignedStorage {
+    /// Byte array of length sizeof(T)
+    std::byte data[sizeof(T)];
+
+    /// @returns a pointer to aligned storage, reinterpreted as T&
+    T& Get() { return *Bitcast<T*>(&data[0]); }
+
+    /// @returns a pointer to aligned storage, reinterpreted as T&
+    const T& Get() const { return *Bitcast<const T*>(&data[0]); }
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_MEMORY_ALIGNED_STORAGE_H_
diff --git a/src/tint/utils/rtti/switch.h b/src/tint/utils/rtti/switch.h
index 24173d0..2436f5f 100644
--- a/src/tint/utils/rtti/switch.h
+++ b/src/tint/utils/rtti/switch.h
@@ -33,7 +33,7 @@
 
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/macros/defer.h"
-#include "src/tint/utils/memory/bitcast.h"
+#include "src/tint/utils/memory/aligned_storage.h"
 #include "src/tint/utils/rtti/castable.h"
 #include "src/tint/utils/rtti/ignore.h"
 
@@ -300,13 +300,8 @@
         }
     }
 
-    // Replacement for std::aligned_storage as this is broken on earlier versions of MSVC.
-    using ReturnTypeOrU8 = std::conditional_t<kHasReturnType, ReturnType, uint8_t>;
-    struct alignas(alignof(ReturnTypeOrU8)) ReturnStorage {
-        uint8_t data[sizeof(ReturnTypeOrU8)];
-    };
-    ReturnStorage return_storage;
-    auto* result = tint::Bitcast<ReturnTypeOrU8*>(&return_storage);
+    AlignedStorage<std::conditional_t<kHasReturnType, ReturnType, uint8_t>> return_storage;
+    auto* result = &return_storage.Get();
 
     const tint::TypeInfo& type_info = object->TypeInfo();