[glsl] Add OffsetFirstIndex transform

Use the push constant layout to load the offset values and add them to
the builtins.

Bug: 42251044
Change-Id: Ie46d6cc462960254dbc31467e27edabcaa841c3f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/209855
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/tint/lang/glsl/writer/raise/BUILD.bazel b/src/tint/lang/glsl/writer/raise/BUILD.bazel
index 8122943..172c47c 100644
--- a/src/tint/lang/glsl/writer/raise/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/raise/BUILD.bazel
@@ -42,6 +42,7 @@
     "binary_polyfill.cc",
     "bitcast_polyfill.cc",
     "builtin_polyfill.cc",
+    "offset_first_index.cc",
     "raise.cc",
     "shader_io.cc",
     "texture_builtins_from_uniform.cc",
@@ -51,6 +52,7 @@
     "binary_polyfill.h",
     "bitcast_polyfill.h",
     "builtin_polyfill.h",
+    "offset_first_index.h",
     "raise.h",
     "shader_io.h",
     "texture_builtins_from_uniform.h",
@@ -103,6 +105,7 @@
     "binary_polyfill_test.cc",
     "bitcast_polyfill_test.cc",
     "builtin_polyfill_test.cc",
+    "offset_first_index_test.cc",
     "shader_io_test.cc",
     "texture_builtins_from_uniform_test.cc",
     "texture_polyfill_test.cc",
diff --git a/src/tint/lang/glsl/writer/raise/BUILD.cmake b/src/tint/lang/glsl/writer/raise/BUILD.cmake
index 0893974..16fc29d 100644
--- a/src/tint/lang/glsl/writer/raise/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/raise/BUILD.cmake
@@ -47,6 +47,8 @@
   lang/glsl/writer/raise/bitcast_polyfill.h
   lang/glsl/writer/raise/builtin_polyfill.cc
   lang/glsl/writer/raise/builtin_polyfill.h
+  lang/glsl/writer/raise/offset_first_index.cc
+  lang/glsl/writer/raise/offset_first_index.h
   lang/glsl/writer/raise/raise.cc
   lang/glsl/writer/raise/raise.h
   lang/glsl/writer/raise/shader_io.cc
@@ -110,6 +112,7 @@
   lang/glsl/writer/raise/binary_polyfill_test.cc
   lang/glsl/writer/raise/bitcast_polyfill_test.cc
   lang/glsl/writer/raise/builtin_polyfill_test.cc
+  lang/glsl/writer/raise/offset_first_index_test.cc
   lang/glsl/writer/raise/shader_io_test.cc
   lang/glsl/writer/raise/texture_builtins_from_uniform_test.cc
   lang/glsl/writer/raise/texture_polyfill_test.cc
diff --git a/src/tint/lang/glsl/writer/raise/BUILD.gn b/src/tint/lang/glsl/writer/raise/BUILD.gn
index 3886a8a..208d910 100644
--- a/src/tint/lang/glsl/writer/raise/BUILD.gn
+++ b/src/tint/lang/glsl/writer/raise/BUILD.gn
@@ -51,6 +51,8 @@
       "bitcast_polyfill.h",
       "builtin_polyfill.cc",
       "builtin_polyfill.h",
+      "offset_first_index.cc",
+      "offset_first_index.h",
       "raise.cc",
       "raise.h",
       "shader_io.cc",
@@ -105,6 +107,7 @@
         "binary_polyfill_test.cc",
         "bitcast_polyfill_test.cc",
         "builtin_polyfill_test.cc",
+        "offset_first_index_test.cc",
         "shader_io_test.cc",
         "texture_builtins_from_uniform_test.cc",
         "texture_polyfill_test.cc",
diff --git a/src/tint/lang/glsl/writer/raise/offset_first_index.cc b/src/tint/lang/glsl/writer/raise/offset_first_index.cc
new file mode 100644
index 0000000..dd431ad
--- /dev/null
+++ b/src/tint/lang/glsl/writer/raise/offset_first_index.cc
@@ -0,0 +1,126 @@
+// 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.
+
+#include "src/tint/lang/glsl/writer/raise/offset_first_index.h"
+
+#include "src/tint/lang/core/fluent_types.h"  // IWYU pragma: export
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::glsl::writer::raise {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+/// PIMPL state for the transform.
+struct State {
+    /// The configuration options.
+    const OffsetFirstIndexConfig& config;
+
+    /// The IR module.
+    core::ir::Module& ir;
+
+    /// The IR builder.
+    core::ir::Builder b{ir};
+
+    /// Process the module.
+    void Process() {
+        // Find module-scope `in` variables that have `instance_index` or `vertex_index` builtins.
+        for (auto* global : *ir.root_block) {
+            auto* var = global->As<core::ir::Var>();
+            if (!var) {
+                continue;
+            }
+
+            auto builtin = var->Attributes().builtin;
+            if (builtin == core::BuiltinValue::kInstanceIndex) {
+                if (config.first_instance_offset) {
+                    AddOffset(var, *config.first_instance_offset);
+                }
+            }
+            if (builtin == core::BuiltinValue::kVertexIndex) {
+                if (config.first_vertex_offset) {
+                    AddOffset(var, *config.first_vertex_offset);
+                }
+            }
+        }
+    }
+
+    /// Add an offset to the value loaded from @p var.
+    /// @param var the variable that contains the builtin value
+    /// @param push_constant_offset the offset in the push constants where the offset is stored
+    void AddOffset(core::ir::Var* var, uint32_t push_constant_offset) {
+        // ShaderIO transforms these input builtins such that they are loaded a single time and then
+        // converted to u32. We add the offset to the result of the conversion.
+        auto* load = GetSingularUse<core::ir::Load>(var);
+        auto* index = GetSingularUse<core::ir::Convert>(load);
+
+        // Replace users of the original load with the result of the offset calculation.
+        auto* offset_index = b.InstructionResult<u32>();
+        index->Result(0)->ReplaceAllUsesWith(offset_index);
+
+        // Load the offset from the push constant structure and add it to the index.
+        b.InsertAfter(index, [&] {
+            auto* push_constants = config.push_constant_layout.var;
+            auto idx = u32(config.push_constant_layout.IndexOf(push_constant_offset));
+            auto* offset = b.Load(b.Access<ptr<push_constant, u32>>(push_constants, idx));
+            b.AddWithResult(offset_index, index, offset);
+        });
+    }
+
+    /// Assert that @p inst has a single use and that it is of type @p T, and return that use.
+    /// @param inst the instruction to get the use for
+    /// @returns the use
+    template <typename T>
+    core::ir::Instruction* GetSingularUse(core::ir::Instruction* inst) {
+        auto& usages = inst->Result(0)->UsagesUnsorted();
+        TINT_ASSERT(usages.Count() == 1);
+        auto* index = usages.begin()->instruction->As<T>();
+        TINT_ASSERT(index);
+        return index;
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> OffsetFirstIndex(core::ir::Module& ir, const OffsetFirstIndexConfig& config) {
+    auto result = ValidateAndDumpIfNeeded(ir, "glsl.OffsetFirstIndex transform",
+                                          core::ir::Capabilities{
+                                              core::ir::Capability::kAllowHandleVarsWithoutBindings,
+                                          });
+    if (result != Success) {
+        return result.Failure();
+    }
+
+    State{config, ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::glsl::writer::raise
diff --git a/src/tint/lang/glsl/writer/raise/offset_first_index.h b/src/tint/lang/glsl/writer/raise/offset_first_index.h
new file mode 100644
index 0000000..d1a8433
--- /dev/null
+++ b/src/tint/lang/glsl/writer/raise/offset_first_index.h
@@ -0,0 +1,63 @@
+// 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_LANG_GLSL_WRITER_RAISE_OFFSET_FIRST_INDEX_H_
+#define SRC_TINT_LANG_GLSL_WRITER_RAISE_OFFSET_FIRST_INDEX_H_
+
+#include "src/tint/lang/core/ir/transform/prepare_push_constants.h"
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}  // namespace tint::core::ir
+
+namespace tint::glsl::writer::raise {
+
+/// OffsetFirstIndexConfig describes the configuration options for the OffsetFirstIndex transform.
+struct OffsetFirstIndexConfig {
+    /// Push constant layout information.
+    const core::ir::transform::PushConstantLayout& push_constant_layout;
+
+    /// Offset of the firstVertex push constant.
+    std::optional<uint32_t> first_vertex_offset{};
+
+    /// Offset of the firstInstance push constant.
+    std::optional<uint32_t> first_instance_offset{};
+};
+
+/// OffsetFirstIndex is a transform that adds an offset to the `vertex_index` and `instance_index`
+/// builtin values.
+///
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> OffsetFirstIndex(core::ir::Module& module,
+                                     const OffsetFirstIndexConfig& config);
+
+}  // namespace tint::glsl::writer::raise
+
+#endif  // SRC_TINT_LANG_GLSL_WRITER_RAISE_OFFSET_FIRST_INDEX_H_
diff --git a/src/tint/lang/glsl/writer/raise/offset_first_index_test.cc b/src/tint/lang/glsl/writer/raise/offset_first_index_test.cc
new file mode 100644
index 0000000..f59cb1f
--- /dev/null
+++ b/src/tint/lang/glsl/writer/raise/offset_first_index_test.cc
@@ -0,0 +1,376 @@
+// 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.
+
+#include "src/tint/lang/glsl/writer/raise/offset_first_index.h"
+
+#include "gtest/gtest.h"
+#include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+#include "src/tint/lang/core/number.h"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::glsl::writer::raise {
+namespace {
+
+using GlslWriter_OffsetFirstIndexTest = core::ir::transform::TransformTest;
+
+TEST_F(GlslWriter_OffsetFirstIndexTest, NoModify_NoBuiltins) {
+    auto* func = b.Function("foo", ty.vec4<f32>(), core::ir::Function::PipelineStage::kVertex);
+    func->SetReturnBuiltin(core::BuiltinValue::kPosition);
+    b.Append(func->Block(), [&] {  //
+        b.Return(func, b.Zero(ty.vec4<f32>()));
+    });
+
+    auto* src = R"(
+%foo = @vertex func():vec4<f32> [@position] {
+  $B1: {
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    core::ir::transform::PreparePushConstantsConfig push_constants_config;
+    auto push_constants = PreparePushConstants(mod, push_constants_config);
+    EXPECT_EQ(push_constants, Success);
+
+    OffsetFirstIndexConfig config{push_constants.Get()};
+    config.first_vertex_offset = 4;
+    config.first_instance_offset = 8;
+    Run(OffsetFirstIndex, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(GlslWriter_OffsetFirstIndexTest, NoModify_BuiltinsWithNoOffsets) {
+    core::IOAttributes attributes;
+
+    auto* vertex_idx = b.Var("vertex_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kVertexIndex;
+    vertex_idx->SetAttributes(attributes);
+    mod.root_block->Append(vertex_idx);
+
+    auto* instance_idx = b.Var("instance_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kInstanceIndex;
+    instance_idx->SetAttributes(attributes);
+    mod.root_block->Append(instance_idx);
+
+    auto* func = b.Function("foo", ty.vec4<f32>(), core::ir::Function::PipelineStage::kVertex);
+    func->SetReturnBuiltin(core::BuiltinValue::kPosition);
+
+    b.Append(func->Block(), [&] {
+        auto* vertex = b.Convert<u32>(b.Load(vertex_idx));
+        auto* instance = b.Convert<u32>(b.Load(instance_idx));
+        b.Let("add", b.Add<u32>(vertex, instance));
+        b.Return(func, b.Zero(ty.vec4<f32>()));
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %4:u32 = load %vertex_index
+    %5:u32 = convert %4
+    %6:u32 = load %instance_index
+    %7:u32 = convert %6
+    %8:u32 = add %5, %7
+    %add:u32 = let %8
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    core::ir::transform::PreparePushConstantsConfig push_constants_config;
+    auto push_constants = PreparePushConstants(mod, push_constants_config);
+    EXPECT_EQ(push_constants, Success);
+
+    OffsetFirstIndexConfig config{push_constants.Get()};
+    Run(OffsetFirstIndex, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(GlslWriter_OffsetFirstIndexTest, VertexOffset) {
+    core::IOAttributes attributes;
+
+    auto* vertex_idx = b.Var("vertex_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kVertexIndex;
+    vertex_idx->SetAttributes(attributes);
+    mod.root_block->Append(vertex_idx);
+
+    auto* instance_idx = b.Var("instance_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kInstanceIndex;
+    instance_idx->SetAttributes(attributes);
+    mod.root_block->Append(instance_idx);
+
+    auto* func = b.Function("foo", ty.vec4<f32>(), core::ir::Function::PipelineStage::kVertex);
+    func->SetReturnBuiltin(core::BuiltinValue::kPosition);
+
+    b.Append(func->Block(), [&] {
+        auto* vertex = b.Convert<u32>(b.Load(vertex_idx));
+        auto* instance = b.Convert<u32>(b.Load(instance_idx));
+        b.Let("add", b.Add<u32>(vertex, instance));
+        b.Return(func, b.Zero(ty.vec4<f32>()));
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %4:u32 = load %vertex_index
+    %5:u32 = convert %4
+    %6:u32 = load %instance_index
+    %7:u32 = convert %6
+    %8:u32 = add %5, %7
+    %add:u32 = let %8
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+tint_push_constant_struct = struct @align(4), @block {
+  first_vertex:u32 @offset(4)
+}
+
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+  %tint_push_constants:ptr<push_constant, tint_push_constant_struct, read> = var
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %5:u32 = load %vertex_index
+    %6:u32 = convert %5
+    %7:ptr<push_constant, u32, read> = access %tint_push_constants, 0u
+    %8:u32 = load %7
+    %9:u32 = add %6, %8
+    %10:u32 = load %instance_index
+    %11:u32 = convert %10
+    %12:u32 = add %9, %11
+    %add:u32 = let %12
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+
+    core::ir::transform::PreparePushConstantsConfig push_constants_config;
+    push_constants_config.AddInternalConstant(4, mod.symbols.New("first_vertex"), ty.u32());
+    auto push_constants = PreparePushConstants(mod, push_constants_config);
+    EXPECT_EQ(push_constants, Success);
+
+    OffsetFirstIndexConfig config{push_constants.Get()};
+    config.first_vertex_offset = 4;
+    Run(OffsetFirstIndex, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(GlslWriter_OffsetFirstIndexTest, InstanceOffset) {
+    core::IOAttributes attributes;
+
+    auto* vertex_idx = b.Var("vertex_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kVertexIndex;
+    vertex_idx->SetAttributes(attributes);
+    mod.root_block->Append(vertex_idx);
+
+    auto* instance_idx = b.Var("instance_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kInstanceIndex;
+    instance_idx->SetAttributes(attributes);
+    mod.root_block->Append(instance_idx);
+
+    auto* func = b.Function("foo", ty.vec4<f32>(), core::ir::Function::PipelineStage::kVertex);
+    func->SetReturnBuiltin(core::BuiltinValue::kPosition);
+
+    b.Append(func->Block(), [&] {
+        auto* vertex = b.Convert<u32>(b.Load(vertex_idx));
+        auto* instance = b.Convert<u32>(b.Load(instance_idx));
+        b.Let("add", b.Add<u32>(vertex, instance));
+        b.Return(func, b.Zero(ty.vec4<f32>()));
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %4:u32 = load %vertex_index
+    %5:u32 = convert %4
+    %6:u32 = load %instance_index
+    %7:u32 = convert %6
+    %8:u32 = add %5, %7
+    %add:u32 = let %8
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+tint_push_constant_struct = struct @align(4), @block {
+  first_instance:u32 @offset(4)
+}
+
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+  %tint_push_constants:ptr<push_constant, tint_push_constant_struct, read> = var
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %5:u32 = load %vertex_index
+    %6:u32 = convert %5
+    %7:u32 = load %instance_index
+    %8:u32 = convert %7
+    %9:ptr<push_constant, u32, read> = access %tint_push_constants, 0u
+    %10:u32 = load %9
+    %11:u32 = add %8, %10
+    %12:u32 = add %6, %11
+    %add:u32 = let %12
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+
+    core::ir::transform::PreparePushConstantsConfig push_constants_config;
+    push_constants_config.AddInternalConstant(4, mod.symbols.New("first_instance"), ty.u32());
+    auto push_constants = PreparePushConstants(mod, push_constants_config);
+    EXPECT_EQ(push_constants, Success);
+
+    OffsetFirstIndexConfig config{push_constants.Get()};
+    config.first_instance_offset = 4;
+    Run(OffsetFirstIndex, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(GlslWriter_OffsetFirstIndexTest, VertexAndInstanceOffset) {
+    core::IOAttributes attributes;
+
+    auto* vertex_idx = b.Var("vertex_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kVertexIndex;
+    vertex_idx->SetAttributes(attributes);
+    mod.root_block->Append(vertex_idx);
+
+    auto* instance_idx = b.Var("instance_index", ty.ptr(core::AddressSpace::kIn, ty.u32()));
+    attributes.builtin = core::BuiltinValue::kInstanceIndex;
+    instance_idx->SetAttributes(attributes);
+    mod.root_block->Append(instance_idx);
+
+    auto* func = b.Function("foo", ty.vec4<f32>(), core::ir::Function::PipelineStage::kVertex);
+    func->SetReturnBuiltin(core::BuiltinValue::kPosition);
+
+    b.Append(func->Block(), [&] {
+        auto* vertex = b.Convert<u32>(b.Load(vertex_idx));
+        auto* instance = b.Convert<u32>(b.Load(instance_idx));
+        b.Let("add", b.Add<u32>(vertex, instance));
+        b.Return(func, b.Zero(ty.vec4<f32>()));
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %4:u32 = load %vertex_index
+    %5:u32 = convert %4
+    %6:u32 = load %instance_index
+    %7:u32 = convert %6
+    %8:u32 = add %5, %7
+    %add:u32 = let %8
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+tint_push_constant_struct = struct @align(4), @block {
+  first_vertex:u32 @offset(4)
+  first_instance:u32 @offset(8)
+}
+
+$B1: {  # root
+  %vertex_index:ptr<__in, u32, read> = var @builtin(vertex_index)
+  %instance_index:ptr<__in, u32, read> = var @builtin(instance_index)
+  %tint_push_constants:ptr<push_constant, tint_push_constant_struct, read> = var
+}
+
+%foo = @vertex func():vec4<f32> [@position] {
+  $B2: {
+    %5:u32 = load %vertex_index
+    %6:u32 = convert %5
+    %7:ptr<push_constant, u32, read> = access %tint_push_constants, 0u
+    %8:u32 = load %7
+    %9:u32 = add %6, %8
+    %10:u32 = load %instance_index
+    %11:u32 = convert %10
+    %12:ptr<push_constant, u32, read> = access %tint_push_constants, 1u
+    %13:u32 = load %12
+    %14:u32 = add %11, %13
+    %15:u32 = add %9, %14
+    %add:u32 = let %15
+    ret vec4<f32>(0.0f)
+  }
+}
+)";
+
+    core::ir::transform::PreparePushConstantsConfig push_constants_config;
+    push_constants_config.AddInternalConstant(4, mod.symbols.New("first_vertex"), ty.u32());
+    push_constants_config.AddInternalConstant(8, mod.symbols.New("first_instance"), ty.u32());
+    auto push_constants = PreparePushConstants(mod, push_constants_config);
+    EXPECT_EQ(push_constants, Success);
+
+    OffsetFirstIndexConfig config{push_constants.Get()};
+    config.first_vertex_offset = 4;
+    config.first_instance_offset = 8;
+    Run(OffsetFirstIndex, config);
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::glsl::writer::raise
diff --git a/src/tint/lang/glsl/writer/raise/raise.cc b/src/tint/lang/glsl/writer/raise/raise.cc
index b2e66e8..cf69fde 100644
--- a/src/tint/lang/glsl/writer/raise/raise.cc
+++ b/src/tint/lang/glsl/writer/raise/raise.cc
@@ -55,6 +55,7 @@
 #include "src/tint/lang/glsl/writer/raise/binary_polyfill.h"
 #include "src/tint/lang/glsl/writer/raise/bitcast_polyfill.h"
 #include "src/tint/lang/glsl/writer/raise/builtin_polyfill.h"
+#include "src/tint/lang/glsl/writer/raise/offset_first_index.h"
 #include "src/tint/lang/glsl/writer/raise/shader_io.h"
 #include "src/tint/lang/glsl/writer/raise/texture_builtins_from_uniform.h"
 #include "src/tint/lang/glsl/writer/raise/texture_polyfill.h"
@@ -149,7 +150,6 @@
 
     RUN_TRANSFORM(core::ir::transform::BlockDecoratedStructs, module);
 
-    // TODO(dsinclair): OffsetFirstIndex
     // TODO(dsinclair): CombineSamplers
     // TODO(dsinclair): PadStructs
 
@@ -191,6 +191,12 @@
     RUN_TRANSFORM(raise::ShaderIO, module,
                   raise::ShaderIOConfig{push_constant_layout.Get(), options.depth_range_offsets});
 
+    // Must come after ShaderIO as it operates on module-scope `in` variables.
+    RUN_TRANSFORM(
+        raise::OffsetFirstIndex, module,
+        raise::OffsetFirstIndexConfig{push_constant_layout.Get(), options.first_vertex_offset,
+                                      options.first_instance_offset});
+
     RUN_TRANSFORM(core::ir::transform::Std140, module);
 
     // These transforms need to be run last as various transforms introduce terminator arguments,
diff --git a/test/tint/bug/chromium/1251009.wgsl.expected.ir.glsl b/test/tint/bug/chromium/1251009.wgsl.expected.ir.glsl
index bd82bea..afbbb22 100644
--- a/test/tint/bug/chromium/1251009.wgsl.expected.ir.glsl
+++ b/test/tint/bug/chromium/1251009.wgsl.expected.ir.glsl
@@ -29,7 +29,8 @@
   VertexInputs0 v_1 = VertexInputs0(v, tint_symbol_loc0_Input);
   uint v_2 = tint_symbol_loc1_Input;
   uint v_3 = uint(gl_InstanceID);
-  gl_Position = tint_symbol_inner(v_1, v_2, v_3, VertexInputs1(tint_symbol_loc2_Input, tint_symbol_loc3_Input));
+  uint v_4 = (v_3 + tint_push_constants.tint_first_instance);
+  gl_Position = tint_symbol_inner(v_1, v_2, v_4, VertexInputs1(tint_symbol_loc2_Input, tint_symbol_loc3_Input));
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;
diff --git a/test/tint/bug/tint/824.wgsl.expected.ir.glsl b/test/tint/bug/tint/824.wgsl.expected.ir.glsl
index 998732c..d63adac 100644
--- a/test/tint/bug/tint/824.wgsl.expected.ir.glsl
+++ b/test/tint/bug/tint/824.wgsl.expected.ir.glsl
@@ -23,10 +23,11 @@
 }
 void main() {
   uint v = uint(gl_VertexID);
-  Output v_1 = tint_symbol_inner(v, uint(gl_InstanceID));
-  gl_Position = v_1.Position;
+  uint v_1 = uint(gl_InstanceID);
+  Output v_2 = tint_symbol_inner(v, (v_1 + tint_push_constants.tint_first_instance));
+  gl_Position = v_2.Position;
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
-  tint_symbol_loc0_Output = v_1.color;
+  tint_symbol_loc0_Output = v_2.color;
   gl_PointSize = 1.0f;
 }
diff --git a/test/tint/types/functions/shader_io/vertex_input_builtins.wgsl.expected.ir.glsl b/test/tint/types/functions/shader_io/vertex_input_builtins.wgsl.expected.ir.glsl
index 69964dc..9e343bf 100644
--- a/test/tint/types/functions/shader_io/vertex_input_builtins.wgsl.expected.ir.glsl
+++ b/test/tint/types/functions/shader_io/vertex_input_builtins.wgsl.expected.ir.glsl
@@ -12,7 +12,8 @@
 }
 void main() {
   uint v = uint(gl_VertexID);
-  gl_Position = tint_symbol_inner(v, uint(gl_InstanceID));
+  uint v_1 = uint(gl_InstanceID);
+  gl_Position = tint_symbol_inner(v, (v_1 + tint_push_constants.tint_first_instance));
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;
diff --git a/test/tint/types/functions/shader_io/vertex_input_builtins_struct.wgsl.expected.ir.glsl b/test/tint/types/functions/shader_io/vertex_input_builtins_struct.wgsl.expected.ir.glsl
index 11b1fee..586cbfe 100644
--- a/test/tint/types/functions/shader_io/vertex_input_builtins_struct.wgsl.expected.ir.glsl
+++ b/test/tint/types/functions/shader_io/vertex_input_builtins_struct.wgsl.expected.ir.glsl
@@ -17,7 +17,8 @@
 }
 void main() {
   uint v = uint(gl_VertexID);
-  gl_Position = tint_symbol_inner(VertexInputs(v, uint(gl_InstanceID)));
+  uint v_1 = uint(gl_InstanceID);
+  gl_Position = tint_symbol_inner(VertexInputs(v, (v_1 + tint_push_constants.tint_first_instance)));
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;
diff --git a/test/tint/types/functions/shader_io/vertex_input_mixed.wgsl.expected.ir.glsl b/test/tint/types/functions/shader_io/vertex_input_mixed.wgsl.expected.ir.glsl
index 19fc8ed..ed3116c 100644
--- a/test/tint/types/functions/shader_io/vertex_input_mixed.wgsl.expected.ir.glsl
+++ b/test/tint/types/functions/shader_io/vertex_input_mixed.wgsl.expected.ir.glsl
@@ -33,7 +33,8 @@
   VertexInputs0 v_2 = VertexInputs0(v_1, tint_symbol_loc0_Input);
   uint v_3 = tint_symbol_loc1_Input;
   uint v_4 = uint(gl_InstanceID);
-  gl_Position = tint_symbol_inner(v_2, v_3, v_4, VertexInputs1(tint_symbol_loc2_Input, tint_symbol_loc3_Input));
+  uint v_5 = (v_4 + tint_push_constants.tint_first_instance);
+  gl_Position = tint_symbol_inner(v_2, v_3, v_5, VertexInputs1(tint_symbol_loc2_Input, tint_symbol_loc3_Input));
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;
diff --git a/test/tint/types/functions/shader_io/vertex_input_mixed_f16.wgsl.expected.ir.glsl b/test/tint/types/functions/shader_io/vertex_input_mixed_f16.wgsl.expected.ir.glsl
index c66c061..a082a5d 100644
--- a/test/tint/types/functions/shader_io/vertex_input_mixed_f16.wgsl.expected.ir.glsl
+++ b/test/tint/types/functions/shader_io/vertex_input_mixed_f16.wgsl.expected.ir.glsl
@@ -39,8 +39,9 @@
   VertexInputs0 v_2 = VertexInputs0(v_1, tint_symbol_loc0_Input);
   uint v_3 = tint_symbol_loc1_Input;
   uint v_4 = uint(gl_InstanceID);
-  VertexInputs1 v_5 = VertexInputs1(tint_symbol_loc2_Input, tint_symbol_loc3_Input, tint_symbol_loc5_Input);
-  gl_Position = tint_symbol_inner(v_2, v_3, v_4, v_5, tint_symbol_loc4_Input);
+  uint v_5 = (v_4 + tint_push_constants.tint_first_instance);
+  VertexInputs1 v_6 = VertexInputs1(tint_symbol_loc2_Input, tint_symbol_loc3_Input, tint_symbol_loc5_Input);
+  gl_Position = tint_symbol_inner(v_2, v_3, v_5, v_6, tint_symbol_loc4_Input);
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;
diff --git a/test/tint/var/uses/instance_index.wgsl.expected.ir.glsl b/test/tint/var/uses/instance_index.wgsl.expected.ir.glsl
index fe37517..d0ce286 100644
--- a/test/tint/var/uses/instance_index.wgsl.expected.ir.glsl
+++ b/test/tint/var/uses/instance_index.wgsl.expected.ir.glsl
@@ -10,7 +10,8 @@
   return vec4(float(b));
 }
 void main() {
-  gl_Position = tint_symbol_inner(uint(gl_InstanceID));
+  uint v = uint(gl_InstanceID);
+  gl_Position = tint_symbol_inner((v + tint_push_constants.tint_first_instance));
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;
diff --git a/test/tint/var/uses/push_constant_and_instance_index.wgsl.expected.ir.glsl b/test/tint/var/uses/push_constant_and_instance_index.wgsl.expected.ir.glsl
index e91048e..67dd105 100644
--- a/test/tint/var/uses/push_constant_and_instance_index.wgsl.expected.ir.glsl
+++ b/test/tint/var/uses/push_constant_and_instance_index.wgsl.expected.ir.glsl
@@ -12,7 +12,8 @@
   return vec4((v + float(b)));
 }
 void main() {
-  gl_Position = tint_symbol_inner(uint(gl_InstanceID));
+  uint v_1 = uint(gl_InstanceID);
+  gl_Position = tint_symbol_inner((v_1 + tint_push_constants.tint_first_instance));
   gl_Position[1u] = -(gl_Position.y);
   gl_Position[2u] = ((2.0f * gl_Position.z) - gl_Position.w);
   gl_PointSize = 1.0f;