Implement push_constant-based firstIndex transform.

This transform offsets the vertex_index and/or instance_index builtin
variables by a user-supplied first_vertex and first_instance offset.
This is required for shading languages that use zero-based first_*
variables such as GLSL and HLSL.

Also, GLSL requires push constants to be wrapped in a struct, and the
struct to be emitted, but not emitted as an interface block. Thus,
change the "skip_push_constants" flag in the AddBlockAttribute
to "push_constants_wrap_only", which causes push constants to be
wrapped in a struct, but to not have the BlockAttribute added.

Bug: tint:2140
Change-Id: I7ce7bb67b58efa110e91fc38d840e674adaf9d0b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/169461
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Stephen White <senorblanco@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/dawn/native/RenderPipeline.cpp b/src/dawn/native/RenderPipeline.cpp
index 666f6bc..eaecd18 100644
--- a/src/dawn/native/RenderPipeline.cpp
+++ b/src/dawn/native/RenderPipeline.cpp
@@ -974,6 +974,11 @@
         mUsesFragDepth = GetStage(SingleShaderStage::Fragment).metadata->usesFragDepth;
     }
 
+    if (HasStage(SingleShaderStage::Vertex)) {
+        mUsesVertexIndex = GetStage(SingleShaderStage::Vertex).metadata->usesVertexIndex;
+        mUsesInstanceIndex = GetStage(SingleShaderStage::Vertex).metadata->usesInstanceIndex;
+    }
+
     SetContentHash(ComputeContentHash());
     GetObjectTrackingList()->Track(this);
 
@@ -1168,6 +1173,16 @@
     return mUsesFragDepth;
 }
 
+bool RenderPipelineBase::UsesVertexIndex() const {
+    DAWN_ASSERT(!IsError());
+    return mUsesVertexIndex;
+}
+
+bool RenderPipelineBase::UsesInstanceIndex() const {
+    DAWN_ASSERT(!IsError());
+    return mUsesInstanceIndex;
+}
+
 size_t RenderPipelineBase::ComputeContentHash() {
     ObjectContentHasher recorder;
 
diff --git a/src/dawn/native/RenderPipeline.h b/src/dawn/native/RenderPipeline.h
index ee16c51..987eae5 100644
--- a/src/dawn/native/RenderPipeline.h
+++ b/src/dawn/native/RenderPipeline.h
@@ -128,6 +128,8 @@
     bool WritesDepth() const;
     bool WritesStencil() const;
     bool UsesFragDepth() const;
+    bool UsesVertexIndex() const;
+    bool UsesInstanceIndex() const;
 
     const AttachmentState* GetAttachmentState() const;
 
@@ -168,6 +170,8 @@
     bool mWritesDepth = false;
     bool mWritesStencil = false;
     bool mUsesFragDepth = false;
+    bool mUsesVertexIndex = false;
+    bool mUsesInstanceIndex = false;
 };
 
 }  // namespace dawn::native
diff --git a/src/dawn/native/opengl/CommandBufferGL.cpp b/src/dawn/native/opengl/CommandBufferGL.cpp
index 745e5c2..578c13b 100644
--- a/src/dawn/native/opengl/CommandBufferGL.cpp
+++ b/src/dawn/native/opengl/CommandBufferGL.cpp
@@ -1123,6 +1123,10 @@
                 vertexStateBufferBindingTracker.Apply(gl);
                 bindGroupTracker.Apply(gl);
 
+                if (lastPipeline->UsesInstanceIndex()) {
+                    gl.Uniform1ui(PipelineLayout::PushConstantLocation::FirstInstance,
+                                  draw->firstInstance);
+                }
                 if (gl.DrawArraysInstancedBaseInstanceANGLE) {
                     gl.DrawArraysInstancedBaseInstanceANGLE(
                         lastPipeline->GetGLPrimitiveTopology(), draw->firstVertex,
@@ -1145,6 +1149,10 @@
                 vertexStateBufferBindingTracker.Apply(gl);
                 bindGroupTracker.Apply(gl);
 
+                if (lastPipeline->UsesInstanceIndex()) {
+                    gl.Uniform1ui(PipelineLayout::PushConstantLocation::FirstInstance,
+                                  draw->firstInstance);
+                }
                 if (gl.DrawElementsInstancedBaseVertexBaseInstanceANGLE) {
                     gl.DrawElementsInstancedBaseVertexBaseInstanceANGLE(
                         lastPipeline->GetGLPrimitiveTopology(), draw->indexCount, indexBufferFormat,
@@ -1181,6 +1189,9 @@
 
             case Command::DrawIndirect: {
                 DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
+                if (lastPipeline->UsesInstanceIndex()) {
+                    gl.Uniform1ui(PipelineLayout::PushConstantLocation::FirstInstance, 0);
+                }
                 vertexStateBufferBindingTracker.Apply(gl);
                 bindGroupTracker.Apply(gl);
 
@@ -1198,6 +1209,9 @@
             case Command::DrawIndexedIndirect: {
                 DrawIndexedIndirectCmd* draw = iter->NextCommand<DrawIndexedIndirectCmd>();
 
+                if (lastPipeline->UsesInstanceIndex()) {
+                    gl.Uniform1ui(PipelineLayout::PushConstantLocation::FirstInstance, 0);
+                }
                 vertexStateBufferBindingTracker.Apply(gl);
                 bindGroupTracker.Apply(gl);
 
diff --git a/src/dawn/native/opengl/PipelineLayoutGL.h b/src/dawn/native/opengl/PipelineLayoutGL.h
index eb4863b..e2ab802 100644
--- a/src/dawn/native/opengl/PipelineLayoutGL.h
+++ b/src/dawn/native/opengl/PipelineLayoutGL.h
@@ -55,6 +55,10 @@
 
     GLuint GetInternalUniformBinding() const;
 
+    enum PushConstantLocation {
+        FirstInstance = 0,
+    };
+
   private:
     ~PipelineLayout() override = default;
     BindingIndexInfo mIndexInfo;
diff --git a/src/dawn/native/opengl/ShaderModuleGL.cpp b/src/dawn/native/opengl/ShaderModuleGL.cpp
index 0ce3792..821a350 100644
--- a/src/dawn/native/opengl/ShaderModuleGL.cpp
+++ b/src/dawn/native/opengl/ShaderModuleGL.cpp
@@ -418,6 +418,9 @@
                                        /* fullSubgroups */ {}));
             }
 
+            r.tintOptions.first_instance_offset =
+                4 * PipelineLayout::PushConstantLocation::FirstInstance;
+
             auto result = tint::glsl::writer::Generate(program, r.tintOptions, remappedEntryPoint);
             DAWN_INVALID_IF(result != tint::Success, "An error occurred while generating GLSL:\n%s",
                             result.Failure().reason.str());
diff --git a/src/dawn/tests/end2end/DepthStencilCopyTests.cpp b/src/dawn/tests/end2end/DepthStencilCopyTests.cpp
index 162defa..dd0ca02 100644
--- a/src/dawn/tests/end2end/DepthStencilCopyTests.cpp
+++ b/src/dawn/tests/end2end/DepthStencilCopyTests.cpp
@@ -1024,8 +1024,9 @@
         // glDrawArraysInstancedBaseInstance.
         DAWN_TEST_UNSUPPORTED_IF(IsOpenGLES() && !IsANGLE());
 
-        // TODO(crbug.com/dawn/2202): Failing on ANGLE/D3D11 for unknown reasons.
-        DAWN_TEST_UNSUPPORTED_IF(IsANGLED3D11());
+        // TODO(crbug.com/dawn/2202): Failing on ANGLE/SwiftShader due to undiagnosed ANGLE bug.
+        // Passes on ANGLE/D3D11.
+        DAWN_TEST_UNSUPPORTED_IF(IsANGLESwiftShader());
 
         // Create a stencil texture
         constexpr uint32_t kWidth = 4;
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 f1831e9..1a66806 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -67,6 +67,7 @@
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
+#include "src/tint/lang/wgsl/ast/transform/offset_first_index.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
@@ -152,8 +153,6 @@
         data.Add<ast::transform::SingleEntryPoint::Config>(entry_point);
     }
 
-    data.Add<ast::transform::AddBlockAttribute::Config>(true);
-
     manager.Add<ast::transform::PreservePadding>();  // Must come before DirectVariableAccess
 
     manager.Add<ast::transform::Unshadow>();  // Must come before DirectVariableAccess
@@ -203,6 +202,8 @@
         manager.Add<ast::transform::ZeroInitWorkgroupMemory>();
     }
 
+    manager.Add<ast::transform::OffsetFirstIndex>();
+
     // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<ast::transform::CanonicalizeEntryPointIO>();
 
@@ -245,6 +246,8 @@
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
         ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kGlsl);
 
+    data.Add<ast::transform::OffsetFirstIndex::Config>(std::nullopt, options.first_instance_offset);
+
     SanitizedResult result;
     ast::transform::DataMap outputs;
     result.program = manager.Run(in, data, outputs);
@@ -297,7 +300,8 @@
                 }
                 bool is_block =
                     ast::HasAttribute<ast::transform::AddBlockAttribute::BlockAttribute>(
-                        str->attributes);
+                        str->attributes) &&
+                    !sem->UsedAs(core::AddressSpace::kPushConstant);
                 if (!has_rt_arr && !is_block) {
                     EmitStructType(current_buffer_, sem);
                 }
@@ -2093,6 +2097,7 @@
 
     auto name = decl->name->symbol.Name();
     auto* type = var->Type()->UnwrapRef();
+    out << "layout(location=0) ";
     EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name);
     out << ";";
 }
diff --git a/src/tint/lang/glsl/writer/common/options.h b/src/tint/lang/glsl/writer/common/options.h
index c97026e..1f5298f 100644
--- a/src/tint/lang/glsl/writer/common/options.h
+++ b/src/tint/lang/glsl/writer/common/options.h
@@ -28,6 +28,7 @@
 #ifndef SRC_TINT_LANG_GLSL_WRITER_COMMON_OPTIONS_H_
 #define SRC_TINT_LANG_GLSL_WRITER_COMMON_OPTIONS_H_
 
+#include <optional>
 #include <string>
 #include <unordered_map>
 
@@ -78,6 +79,9 @@
     /// Options used in the binding mappings for external textures
     ExternalTextureOptions external_texture_options = {};
 
+    /// Offset of the firstInstance push constant.
+    std::optional<int32_t> first_instance_offset;
+
     /// Options used to map WGSL textureNumLevels/textureNumSamples builtins to internal uniform
     /// buffer values. If not specified, emits corresponding GLSL builtins
     /// textureQueryLevels/textureSamples directly.
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.bazel b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
index 4b1340a..6229a94 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
@@ -56,6 +56,7 @@
     "hoist_to_decl_before.cc",
     "manager.cc",
     "multiplanar_external_texture.cc",
+    "offset_first_index.cc",
     "preserve_padding.cc",
     "promote_initializers_to_let.cc",
     "promote_side_effects_to_decl.cc",
@@ -91,6 +92,7 @@
     "hoist_to_decl_before.h",
     "manager.h",
     "multiplanar_external_texture.h",
+    "offset_first_index.h",
     "preserve_padding.h",
     "promote_initializers_to_let.h",
     "promote_side_effects_to_decl.h",
@@ -159,6 +161,7 @@
     "hoist_to_decl_before_test.cc",
     "manager_test.cc",
     "multiplanar_external_texture_test.cc",
+    "offset_first_index_test.cc",
     "preserve_padding_test.cc",
     "promote_initializers_to_let_test.cc",
     "promote_side_effects_to_decl_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index 11ddfaa..068369c 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -73,6 +73,8 @@
   lang/wgsl/ast/transform/manager.h
   lang/wgsl/ast/transform/multiplanar_external_texture.cc
   lang/wgsl/ast/transform/multiplanar_external_texture.h
+  lang/wgsl/ast/transform/offset_first_index.cc
+  lang/wgsl/ast/transform/offset_first_index.h
   lang/wgsl/ast/transform/preserve_padding.cc
   lang/wgsl/ast/transform/preserve_padding.h
   lang/wgsl/ast/transform/promote_initializers_to_let.cc
@@ -159,6 +161,7 @@
   lang/wgsl/ast/transform/hoist_to_decl_before_test.cc
   lang/wgsl/ast/transform/manager_test.cc
   lang/wgsl/ast/transform/multiplanar_external_texture_test.cc
+  lang/wgsl/ast/transform/offset_first_index_test.cc
   lang/wgsl/ast/transform/preserve_padding_test.cc
   lang/wgsl/ast/transform/promote_initializers_to_let_test.cc
   lang/wgsl/ast/transform/promote_side_effects_to_decl_test.cc
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 4632240..b94227e 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -78,6 +78,8 @@
     "manager.h",
     "multiplanar_external_texture.cc",
     "multiplanar_external_texture.h",
+    "offset_first_index.cc",
+    "offset_first_index.h",
     "preserve_padding.cc",
     "preserve_padding.h",
     "promote_initializers_to_let.cc",
@@ -160,6 +162,7 @@
         "hoist_to_decl_before_test.cc",
         "manager_test.cc",
         "multiplanar_external_texture_test.cc",
+        "offset_first_index_test.cc",
         "preserve_padding_test.cc",
         "promote_initializers_to_let_test.cc",
         "promote_side_effects_to_decl_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
index 607bf54..82e0716 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
@@ -39,7 +39,6 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddBlockAttribute);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddBlockAttribute::BlockAttribute);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddBlockAttribute::Config);
 
 namespace tint::ast::transform {
 
@@ -48,13 +47,12 @@
 AddBlockAttribute::~AddBlockAttribute() = default;
 
 Transform::ApplyResult AddBlockAttribute::Apply(const Program& src,
-                                                const DataMap& inputs,
+                                                const DataMap&,
                                                 DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     auto& sem = src.Sem();
-    auto* cfg = inputs.Get<Config>();
 
     // A map from a type in the source program to a block-decorated wrapper that contains it in the
     // destination program.
@@ -82,11 +80,6 @@
         bool needs_wrapping = !str ||                    // Type is not a structure
                               str->HasFixedFootprint();  // Struct has a fixed footprint
 
-        if (cfg && cfg->skip_push_constants &&
-            var->AddressSpace() == core::AddressSpace::kPushConstant) {
-            continue;
-        }
-
         if (needs_wrapping) {
             const char* kMemberName = "inner";
 
@@ -136,8 +129,4 @@
                                                                          ctx.dst->AllocateNodeID());
 }
 
-AddBlockAttribute::Config::Config(bool skip_push_consts) : skip_push_constants(skip_push_consts) {}
-
-AddBlockAttribute::Config::~Config() = default;
-
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
index 61a884d..544e1ee 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
@@ -66,19 +66,6 @@
     /// Destructor
     ~AddBlockAttribute() override;
 
-    /// Transform configuration options
-    struct Config final : public Castable<Config, ast::transform::Data> {
-        /// Constructor
-        /// @param skip_push_const whether to skip push constants
-        explicit Config(bool skip_push_const);
-
-        /// Destructor
-        ~Config() override;
-
-        /// Whether to skip push constants
-        bool skip_push_constants;
-    };
-
     /// @copydoc Transform::Apply
     ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index.cc b/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
new file mode 100644
index 0000000..eb64f60
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
@@ -0,0 +1,201 @@
+// 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/wgsl/ast/transform/offset_first_index.h"
+
+#include <memory>
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::OffsetFirstIndex);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::OffsetFirstIndex::Config);
+
+namespace tint::ast::transform {
+namespace {
+
+// Push constant names
+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;
+OffsetFirstIndex::~OffsetFirstIndex() = default;
+
+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;
+    }
+
+    ProgramBuilder b;
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
+
+    // Map of builtin usages
+    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()) {
+        if (auto* var = node->As<Variable>()) {
+            for (auto* attr : var->attributes) {
+                if (auto* builtin_attr = attr->As<BuiltinAttribute>()) {
+                    core::BuiltinValue builtin = src.Sem().Get(builtin_attr)->Value();
+                    if (builtin == core::BuiltinValue::kVertexIndex && cfg &&
+                        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;
+                    }
+                }
+            }
+        }
+        if (auto* member = node->As<StructMember>()) {
+            for (auto* attr : member->attributes) {
+                if (auto* builtin_attr = attr->As<BuiltinAttribute>()) {
+                    core::BuiltinValue builtin = src.Sem().Get(builtin_attr)->Value();
+                    if (builtin == core::BuiltinValue::kVertexIndex && cfg &&
+                        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;
+    }
+
+    // Abort on any use of push constants in the module.
+    for (auto* global : src.AST().GlobalVariables()) {
+        if (auto* var = global->As<ast::Var>()) {
+            auto* v = src.Sem().Get(var);
+            if (TINT_UNLIKELY(v->AddressSpace() == core::AddressSpace::kPushConstant)) {
+                TINT_ICE()
+                    << "OffsetFirstIndex doesn't know how to handle module that already use push "
+                       "constants (yet)";
+                return resolver::Resolve(b);
+            }
+        }
+    }
+
+    b.Enable(wgsl::Extension::kChromiumExperimentalPushConstant);
+
+    // Add push constant members and calculate byte offsets
+    tint::Vector<const StructMember*, 8> members;
+    if (has_vertex_index) {
+        members.Push(b.Member(kFirstVertexName, b.ty.u32(),
+                              Vector{b.MemberOffset(AInt(*cfg->first_vertex_offset))}));
+    }
+    if (has_instance_index) {
+        members.Push(b.Member(kFirstInstanceName, b.ty.u32(),
+                              Vector{b.MemberOffset(AInt(*cfg->first_instance_offset))}));
+    }
+    auto struct_ = b.Structure(b.Symbols().New("PushConstants"), std::move(members));
+    // Create a global to hold the uniform buffer
+    Symbol buffer_name = b.Symbols().New("push_constants");
+    b.GlobalVar(buffer_name, b.ty.Of(struct_), core::AddressSpace::kPushConstant);
+
+    // Fix up all references to the builtins with the offsets
+    ctx.ReplaceAll([&](const Expression* expr) -> const Expression* {
+        if (auto* sem = ctx.src->Sem().GetVal(expr)) {
+            if (auto* user = sem->UnwrapLoad()->As<sem::VariableUser>()) {
+                auto it = builtin_vars.find(user->Variable());
+                if (it != builtin_vars.end()) {
+                    return ctx.dst->Add(ctx.CloneWithoutTransform(expr),
+                                        ctx.dst->MemberAccessor(buffer_name, it->second));
+                }
+            }
+            if (auto* access = sem->As<sem::StructMemberAccess>()) {
+                auto it = builtin_members.find(access->Member());
+                if (it != builtin_members.end()) {
+                    return ctx.dst->Add(ctx.CloneWithoutTransform(expr),
+                                        ctx.dst->MemberAccessor(buffer_name, it->second));
+                }
+            }
+        }
+        // Not interested in this expression. Just clone.
+        return nullptr;
+    });
+
+    ctx.Clone();
+    return resolver::Resolve(b);
+}
+
+OffsetFirstIndex::Config::Config(std::optional<int32_t> first_vertex_off,
+                                 std::optional<int32_t> first_instance_off)
+    : first_vertex_offset(first_vertex_off), first_instance_offset(first_instance_off) {}
+
+OffsetFirstIndex::Config::~Config() = default;
+
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index.h b/src/tint/lang/wgsl/ast/transform/offset_first_index.h
new file mode 100644
index 0000000..aa37559
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index.h
@@ -0,0 +1,111 @@
+// 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_WGSL_AST_TRANSFORM_OFFSET_FIRST_INDEX_H_
+#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_OFFSET_FIRST_INDEX_H_
+
+#include "src/tint/lang/wgsl/ast/transform/transform.h"
+
+namespace tint::ast::transform {
+
+/// Adds firstVertex/Instance (injected via push constants) to
+/// vertex/instance index builtins.
+///
+/// This transform assumes that Name transform has been run before.
+///
+/// Some shading languages start vertex and instance numbering at 0,
+/// regardless of the firstVertex/firstInstance value specified. This transform
+/// adds the value of firstVertex/firstInstance to each builtin. This action is
+/// performed by adding a new push constant equal to original builtin +
+/// firstVertex/firstInstance to each function that references one of
+/// these builtins.
+///
+/// For D3D, this affects both firstVertex and firstInstance. For OpenGL,
+/// it applies to only firstInstance. For this reason, the first_vertex_offset
+/// and first_instance_offset are optional, where std::nullopt indicates that
+/// no subsitution is to be performed.
+///
+/// Before:
+/// ```
+///   @builtin(vertex_index) var<in> vert_idx : u32;
+///   @builtin(instance_index) var<in> inst_idx : u32;
+///   fn func() -> u32 {
+///     return vert_idx * inst_idx;
+///   }
+/// ```
+///
+/// After:
+/// ```
+/// struct PushConstants {
+///   first_vertex : u32,
+///   first_instance : u32,
+/// }
+///
+/// var<push_constant> push_constants : PushConstants;
+///
+///   @builtin(vertex_index) var<in> vert_idx : u32;
+///   @builtin(instance_index) var<in> inst_idx : u32;
+///   fn func() -> u32 {
+///     return (vert_idx + push_constants.first_vertex) * (inst_idx +
+///     push_constants.first_instance);
+///   }
+/// ```
+///
+class OffsetFirstIndex final : public Castable<OffsetFirstIndex, Transform> {
+  public:
+    /// Transform configuration options
+    struct Config final : public Castable<Config, ast::transform::Data> {
+        /// Constructor
+        /// @param first_vertex_off Offset of the firstVertex push constant
+        /// @param first_instance_off Location of the firstInstance push constant
+        Config(std::optional<int32_t> first_vertex_off, std::optional<int32_t> first_instance_off);
+
+        /// Destructor
+        ~Config() override;
+
+        /// Offset of the firstVertex push constant
+        const std::optional<uint32_t> first_vertex_offset;
+
+        /// Offset of the firstInstance push constant
+        const std::optional<uint32_t> first_instance_offset;
+    };
+
+    /// Constructor
+    OffsetFirstIndex();
+
+    /// Destructor
+    ~OffsetFirstIndex() override;
+
+    /// @copydoc Transform::Apply
+    ApplyResult Apply(const Program& program,
+                      const DataMap& inputs,
+                      DataMap& outputs) const override;
+};
+
+}  // namespace tint::ast::transform
+
+#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_OFFSET_FIRST_INDEX_H_
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
new file mode 100644
index 0000000..37a79a9
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc
@@ -0,0 +1,856 @@
+// 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/wgsl/ast/transform/offset_first_index.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/tint/lang/wgsl/ast/transform/helper_test.h"
+
+namespace tint::ast::transform {
+namespace {
+
+using OffsetFirstIndexTest = TransformTest;
+
+TEST_F(OffsetFirstIndexTest, ShouldRunEmptyModule) {
+    auto* src = R"()";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunFragmentStage) {
+    auto* src = R"(
+@fragment
+fn entry() {
+  return;
+}
+)";
+
+    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));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunVertexStageWithVertexIndex) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunVertexStageWithInstanceIndex) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, 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));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicVertexShader) {
+    auto* src = R"(
+@vertex
+fn entry() -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+    auto* expect = src;
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleVertexIndex) {
+    auto* src = R"(
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test(vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleVertexIndex_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test(vert_idx);
+  return vec4<f32>();
+}
+
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleInstanceIndex) {
+    auto* src = R"(
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test(inst_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  @size(4)
+  padding_0 : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleInstanceIndex_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test(inst_idx);
+  return vec4<f32>();
+}
+
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  @size(4)
+  padding_0 : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexes) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test((inputs.instance_idx + push_constants.first_instance), (inputs.vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexes_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test((inputs.instance_idx + push_constants.first_instance), (inputs.vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexesVertexDisabled) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test((inputs.instance_idx + push_constants.first_instance), inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, 0);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexesInstanceDisabled) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, (inputs.vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexesBothDisabled) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = src;
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, std::nullopt);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+// Test a shader with a user-declared struct called PushConstants, to force
+// renaming of the Tint-provided PushConstants struct.
+TEST_F(OffsetFirstIndexTest, ForceRenamingPushConstantsStruct) {
+    auto* src = R"(
+struct PushConstants {
+  f : f32,
+}
+
+@group(0) @binding(0) var<uniform> p : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>(f32(vert_idx) + p.f);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants_1 {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants_1;
+
+struct PushConstants {
+  f : f32,
+}
+
+@group(0) @binding(0) var<uniform> p : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>((f32((vert_idx + push_constants.first_vertex)) + p.f));
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+// Test a shader with a user-declared variable called push_constants, to force
+// renaming of the Tint-provided push_constants variable.
+TEST_F(OffsetFirstIndexTest, ForceRenamingPushConstantsVar) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> push_constants : u32;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>(f32(vert_idx));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants_1 : PushConstants;
+
+@group(0) @binding(0) var<uniform> push_constants : u32;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>(f32((vert_idx + push_constants_1.first_vertex)));
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, NestedCalls) {
+    auto* src = R"(
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2(vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, NestedCalls_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2(vert_idx);
+  return vec4<f32>();
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, MultipleEntryPoints) {
+    auto* src = R"(
+fn func(i : u32) -> u32 {
+  return i;
+}
+
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx + inst_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(inst_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn func(i : u32) -> u32 {
+  return i;
+}
+
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(((vert_idx + push_constants.first_vertex) + (inst_idx + push_constants.first_instance)));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, MultipleEntryPoints_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx + inst_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(inst_idx);
+  return vec4<f32>();
+}
+
+fn func(i : u32) -> u32 {
+  return i;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(((vert_idx + push_constants.first_vertex) + (inst_idx + push_constants.first_instance)));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+
+fn func(i : u32) -> u32 {
+  return i;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::ast::transform
diff --git a/test/tint/var/uses/push_constant.wgsl.expected.glsl b/test/tint/var/uses/push_constant.wgsl.expected.glsl
index 6ed822e..aea31fb 100644
--- a/test/tint/var/uses/push_constant.wgsl.expected.glsl
+++ b/test/tint/var/uses/push_constant.wgsl.expected.glsl
@@ -1,8 +1,12 @@
 #version 310 es
 
-uniform int a;
+struct a_block {
+  int inner;
+};
+
+layout(location=0) uniform a_block a;
 void uses_a() {
-  int foo = a;
+  int foo = a.inner;
 }
 
 void main1() {
@@ -16,9 +20,13 @@
 }
 #version 310 es
 
-uniform int a;
+struct a_block {
+  int inner;
+};
+
+layout(location=0) uniform a_block a;
 void uses_a() {
-  int foo = a;
+  int foo = a.inner;
 }
 
 void uses_uses_a() {
@@ -36,9 +44,13 @@
 }
 #version 310 es
 
-uniform int b;
+struct b_block {
+  int inner;
+};
+
+layout(location=0) uniform b_block b;
 void uses_b() {
-  int foo = b;
+  int foo = b.inner;
 }
 
 void main3() {
diff --git a/webgpu-cts/compat-expectations.txt b/webgpu-cts/compat-expectations.txt
index 704869e..64687a7 100644
--- a/webgpu-cts/compat-expectations.txt
+++ b/webgpu-cts/compat-expectations.txt
@@ -381,91 +381,6 @@
 crbug.com/dawn/2123 [ nvidia-0x2184 ] webgpu:api,validation,capability_checks,limits,maxStorageBuffersPerShaderStage:createPipeline,at_over:limitTest="atMaximum";testValueName="atLimit";async=true;bindingCombination="vertexAndFragmentWithPossibleVertexStageOverflow";order="forward";bindGroupTest="differentGroups" [ Failure ]
 crbug.com/dawn/2123 [ nvidia-0x2184 ] webgpu:api,validation,capability_checks,limits,maxStorageBuffersPerShaderStage:createPipeline,at_over:limitTest="atMaximum";testValueName="atLimit";async=true;bindingCombination="vertexAndFragmentWithPossibleVertexStageOverflow";order="forward";bindGroupTest="sameGroup" [ Failure ]
 
-# Instancing issues on NVidia GL
-# count=[1-9].*first_instance=[1-9].*instance_count=[1-9]
-# These only occur on ANGLE's NVidia GL and GLES backends. ANGLE's SwiftShader
-# and Vk backends are fine.
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=0;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=3;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=1;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=0;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=false;indirect=false;vertex_buffer_offset=32;index_buffer_offset="_undef_";base_vertex="_undef_" [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=0;index_buffer_offset=16;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=0;base_vertex=9 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=0 [ Failure ]
-crbug.com/dawn/2119 webgpu:api,operation,rendering,draw:arguments:first=3;count=6;first_instance=2;instance_count=4;indexed=true;indirect=false;vertex_buffer_offset=32;index_buffer_offset=16;base_vertex=9 [ Failure ]
-
 # Zero init issues, but only with batch__=9 (NVidia driver bug)
 crbug.com/dawn/2125 [ nvidia-0x2184 ] webgpu:shader,execution,zero_init:compute,zero_init:addressSpace="function";workgroupSize=[1,1,1];batch__=9 [ Failure ]
 crbug.com/dawn/2125 [ nvidia-0x2184 ] webgpu:shader,execution,zero_init:compute,zero_init:addressSpace="private";workgroupSize=[1,1,1];batch__=9 [ Failure ]