[spirv-writer] Combine access instruction chains

Some Qualcomm drivers lose matrix stride information for OpAccessChain
instructions that produce pointers to matrices. Use a transform to
combine chains of access instructions so that generated OpAccessChain
instructions always start from the root.

Bug: tint:1906
Change-Id: I371b11aa993da713144a6616dfa25943a986f9e5
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/154101
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index 8dabe5c..62c2113 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -32,6 +32,7 @@
     "binding_remapper.cc",
     "block_decorated_structs.cc",
     "builtin_polyfill.cc",
+    "combine_access_instructions.cc",
     "conversion_polyfill.cc",
     "demote_to_helper.cc",
     "direct_variable_access.cc",
@@ -49,6 +50,7 @@
     "binding_remapper.h",
     "block_decorated_structs.h",
     "builtin_polyfill.h",
+    "combine_access_instructions.h",
     "conversion_polyfill.h",
     "demote_to_helper.h",
     "direct_variable_access.h",
@@ -94,6 +96,7 @@
     "binding_remapper_test.cc",
     "block_decorated_structs_test.cc",
     "builtin_polyfill_test.cc",
+    "combine_access_instructions_test.cc",
     "conversion_polyfill_test.cc",
     "demote_to_helper_test.cc",
     "direct_variable_access_test.cc",
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index b2a8967..c0b7f14 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -38,6 +38,8 @@
   lang/core/ir/transform/block_decorated_structs.h
   lang/core/ir/transform/builtin_polyfill.cc
   lang/core/ir/transform/builtin_polyfill.h
+  lang/core/ir/transform/combine_access_instructions.cc
+  lang/core/ir/transform/combine_access_instructions.h
   lang/core/ir/transform/conversion_polyfill.cc
   lang/core/ir/transform/conversion_polyfill.h
   lang/core/ir/transform/demote_to_helper.cc
@@ -92,6 +94,7 @@
   lang/core/ir/transform/binding_remapper_test.cc
   lang/core/ir/transform/block_decorated_structs_test.cc
   lang/core/ir/transform/builtin_polyfill_test.cc
+  lang/core/ir/transform/combine_access_instructions_test.cc
   lang/core/ir/transform/conversion_polyfill_test.cc
   lang/core/ir/transform/demote_to_helper_test.cc
   lang/core/ir/transform/direct_variable_access_test.cc
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index d38f791..6d2212e 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -43,6 +43,8 @@
     "block_decorated_structs.h",
     "builtin_polyfill.cc",
     "builtin_polyfill.h",
+    "combine_access_instructions.cc",
+    "combine_access_instructions.h",
     "conversion_polyfill.cc",
     "conversion_polyfill.h",
     "demote_to_helper.cc",
@@ -94,6 +96,7 @@
       "binding_remapper_test.cc",
       "block_decorated_structs_test.cc",
       "builtin_polyfill_test.cc",
+      "combine_access_instructions_test.cc",
       "conversion_polyfill_test.cc",
       "demote_to_helper_test.cc",
       "direct_variable_access_test.cc",
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions.cc b/src/tint/lang/core/ir/transform/combine_access_instructions.cc
new file mode 100644
index 0000000..71b42c9
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions.cc
@@ -0,0 +1,80 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/combine_access_instructions.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    Module& ir;
+
+    /// Process the module.
+    void Process() {
+        // Loop over every instruction looking for access instructions.
+        for (auto* inst : ir.instructions.Objects()) {
+            if (auto* access = inst->As<ir::Access>(); access && access->Alive()) {
+                // Look for places where the result of this access instruction is used as a base
+                // pointer for another access instruction.
+                access->Result()->ForEachUse([&](Usage use) {
+                    auto* child = use.instruction->As<ir::Access>();
+                    if (child && use.operand_index == ir::Access::kObjectOperandOffset) {
+                        // Push the indices of the parent access instruction into the child.
+                        Vector<ir::Value*, 4> operands;
+                        operands.Push(access->Object());
+                        for (auto* idx : access->Indices()) {
+                            operands.Push(idx);
+                        }
+                        for (auto* idx : child->Indices()) {
+                            operands.Push(idx);
+                        }
+                        child->SetOperands(std::move(operands));
+                    }
+                });
+
+                // If there are no other uses of the access instruction, remove it.
+                if (access->Result()->Usages().IsEmpty()) {
+                    access->Destroy();
+                }
+            }
+        }
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> CombineAccessInstructions(Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "CombineAccessInstructions transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions.h b/src/tint/lang/core/ir/transform/combine_access_instructions.h
new file mode 100644
index 0000000..55005e2
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions.h
@@ -0,0 +1,34 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_COMBINE_ACCESS_INSTRUCTIONS_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_COMBINE_ACCESS_INSTRUCTIONS_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// CombineAccessInstructions is a transform that combines chains of access instructions.
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> CombineAccessInstructions(Module& module);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_COMBINE_ACCESS_INSTRUCTIONS_H_
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions_test.cc b/src/tint/lang/core/ir/transform/combine_access_instructions_test.cc
new file mode 100644
index 0000000..7c79865
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions_test.cc
@@ -0,0 +1,874 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/combine_access_instructions.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using IR_CombineAccessInstructionsTest = TransformTest;
+
+TEST_F(IR_CombineAccessInstructionsTest, NoModify_NoChaining) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_root = b.Access(ty.ptr(uniform, structure), buffer);
+        b.Load(access_root);
+
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        b.Load(access_arr);
+
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), buffer, 0_u, 1_u);
+        b.Load(access_mat);
+
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), buffer, 0_u, 1_u, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, MyStruct, read_write> = access %buffer
+    %4:MyStruct = load %3
+    %5:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %6:array<mat3x3<f32>, 4> = load %5
+    %7:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %8:mat3x3<f32> = load %7
+    %9:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %10:vec3<f32> = load %9
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, SimpleChain) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %6:vec3<f32> = load %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %4:vec3<f32> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, NoIndices_Root) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_root = b.Access(ty.ptr(uniform, structure), buffer);
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), access_root, 0_u);
+        b.Load(access_arr);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, MyStruct, read_write> = access %buffer
+    %4:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %3, 0u
+    %5:array<mat3x3<f32>, 4> = load %4
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:array<mat3x3<f32>, 4> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, NoIndices_Middle) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_copy = b.Access(ty.ptr(uniform, arr), access_arr);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_copy, 1_u);
+        b.Load(access_mat);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %3
+    %5:ptr<uniform, mat3x3<f32>, read_write> = access %4, 1u
+    %6:mat3x3<f32> = load %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %4:mat3x3<f32> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, NoIndices_End) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        auto* access_copy = b.Access(ty.ptr(uniform, mat), access_mat);
+        b.Load(access_copy);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, mat3x3<f32>, read_write> = access %4
+    %6:mat3x3<f32> = load %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %4:mat3x3<f32> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, MutipleChains_FromRoot) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+
+        {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 2_u);
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 0_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 3_u);
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 1_u);
+            b.Load(access_vec);
+        }
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, mat3x3<f32>, read_write> = access %3, 2u
+    %8:ptr<uniform, vec3<f32>, read_write> = access %7, 0u
+    %9:vec3<f32> = load %8
+    %10:ptr<uniform, mat3x3<f32>, read_write> = access %3, 3u
+    %11:ptr<uniform, vec3<f32>, read_write> = access %10, 1u
+    %12:vec3<f32> = load %11
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %4:vec3<f32> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 2u, 0u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 3u, 1u
+    %8:vec3<f32> = load %7
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, MutipleChains_FromMiddle) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+
+        {
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 0_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 1_u);
+            b.Load(access_vec);
+        }
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, vec3<f32>, read_write> = access %4, 0u
+    %8:vec3<f32> = load %7
+    %9:ptr<uniform, vec3<f32>, read_write> = access %4, 1u
+    %10:vec3<f32> = load %9
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %4:vec3<f32> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 0u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 1u
+    %8:vec3<f32> = load %7
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, OtherUses_Root) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        b.Load(access_arr);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:array<mat3x3<f32>, 4> = load %3
+    %5:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %6:ptr<uniform, vec3<f32>, read_write> = access %5, 2u
+    %7:vec3<f32> = load %6
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:array<mat3x3<f32>, 4> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %6:vec3<f32> = load %5
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, OtherUses_Middle) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        b.Load(access_mat);
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:mat3x3<f32> = load %4
+    %6:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %7:vec3<f32> = load %6
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %4:mat3x3<f32> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %6:vec3<f32> = load %5
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, AccessResultUsesAsAccessIndex) {
+    auto* func = b.Function("foo", ty.f32());
+    auto* indices = b.FunctionParam("indices", ty.array<u32, 4>());
+    auto* values = b.FunctionParam("values", ty.array<f32, 4>());
+    b.Append(func->Block(), [&] {
+        auto* access_index = b.Access(ty.u32(), indices, 1_u);
+        auto* access_value = b.Access(ty.f32(), values, access_index);
+        b.Return(func, access_value);
+    });
+
+    auto* src = R"(
+%foo = func():f32 -> %b1 {
+  %b1 = block {
+    %2:u32 = access %indices, 1u
+    %4:f32 = access %values, %2
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, UseInDifferentBlock_If) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* ifelse = b.If(true);
+        b.Append(ifelse->True(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+            b.Load(access_mat);
+            b.ExitIf(ifelse);
+        });
+        b.Append(ifelse->False(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 2_u);
+            b.Load(access_mat);
+            b.ExitIf(ifelse);
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    if true [t: %b3, f: %b4] {  # if_1
+      %b3 = block {  # true
+        %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+        %5:mat3x3<f32> = load %4
+        exit_if  # if_1
+      }
+      %b4 = block {  # false
+        %6:ptr<uniform, mat3x3<f32>, read_write> = access %3, 2u
+        %7:mat3x3<f32> = load %6
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    if true [t: %b3, f: %b4] {  # if_1
+      %b3 = block {  # true
+        %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+        %4:mat3x3<f32> = load %3
+        exit_if  # if_1
+      }
+      %b4 = block {  # false
+        %5:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 2u
+        %6:mat3x3<f32> = load %5
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, UseInDifferentBlock_Loop) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 2_u);
+            b.Load(access_mat);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 3_u);
+            b.Load(access_mat);
+            b.BreakIf(loop, true);
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    loop [b: %b3, c: %b4] {  # loop_1
+      %b3 = block {  # body
+        %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 2u
+        %5:mat3x3<f32> = load %4
+        continue %b4
+      }
+      %b4 = block {  # continuing
+        %6:ptr<uniform, mat3x3<f32>, read_write> = access %3, 3u
+        %7:mat3x3<f32> = load %6
+        break_if true %b3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    loop [b: %b3, c: %b4] {  # loop_1
+      %b3 = block {  # body
+        %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 2u
+        %4:mat3x3<f32> = load %3
+        continue %b4
+      }
+      %b4 = block {  # continuing
+        %5:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 3u
+        %6:mat3x3<f32> = load %5
+        break_if true %b3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/spirv/writer/access_test.cc b/src/tint/lang/spirv/writer/access_test.cc
index ffdd185..967cdf5 100644
--- a/src/tint/lang/spirv/writer/access_test.cc
+++ b/src/tint/lang/spirv/writer/access_test.cc
@@ -22,11 +22,11 @@
 
 TEST_F(SpirvWriterTest, Access_Array_Value_ConstantIndex) {
     auto* arr_val = b.FunctionParam("arr", ty.array(ty.i32(), 4));
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({arr_val});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), arr_val, 1_u);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
@@ -35,11 +35,11 @@
 }
 
 TEST_F(SpirvWriterTest, Access_Array_Pointer_ConstantIndex) {
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     b.Append(func->Block(), [&] {
         auto* arr_var = b.Var("arr", ty.ptr<function, array<i32, 4>>());
         auto* result = b.Access(ty.ptr<function, i32>(), arr_var, 1_u);
-        b.Return(func);
+        b.Return(func, b.Load(result));
         mod.SetName(result, "result");
     });
 
@@ -49,31 +49,31 @@
 
 TEST_F(SpirvWriterTest, Access_Array_Pointer_DynamicIndex) {
     auto* idx = b.FunctionParam("idx", ty.i32());
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({idx});
     b.Append(func->Block(), [&] {
         auto* arr_var = b.Var("arr", ty.ptr<function, array<i32, 4>>());
         auto* result = b.Access(ty.ptr<function, i32>(), arr_var, idx);
-        b.Return(func);
+        b.Return(func, b.Load(result));
         mod.SetName(result, "result");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-         %13 = OpBitcast %uint %idx
-         %14 = OpExtInst %uint %15 UMin %13 %uint_3
-     %result = OpAccessChain %_ptr_Function_int %arr %14
+         %12 = OpBitcast %uint %idx
+         %13 = OpExtInst %uint %14 UMin %12 %uint_3
+     %result = OpAccessChain %_ptr_Function_int %arr %13
 )");
 }
 
 TEST_F(SpirvWriterTest, Access_Matrix_Value_ConstantIndex) {
     auto* mat_val = b.FunctionParam("mat", ty.mat2x2(ty.f32()));
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.vec2<f32>());
     func->SetParams({mat_val});
     b.Append(func->Block(), [&] {
         auto* result_vector = b.Access(ty.vec2(ty.f32()), mat_val, 1_u);
         auto* result_scalar = b.Access(ty.f32(), mat_val, 1_u, 0_u);
-        b.Return(func);
+        b.Return(func, b.Multiply(ty.vec2<f32>(), result_vector, result_scalar));
         mod.SetName(result_vector, "result_vector");
         mod.SetName(result_scalar, "result_scalar");
     });
@@ -127,11 +127,11 @@
 
 TEST_F(SpirvWriterTest, Access_Vector_Value_ConstantIndex) {
     auto* vec_val = b.FunctionParam("vec", ty.vec4(ty.i32()));
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({vec_val});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), vec_val, 1_u);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
@@ -142,82 +142,84 @@
 TEST_F(SpirvWriterTest, Access_Vector_Value_DynamicIndex) {
     auto* vec_val = b.FunctionParam("vec", ty.vec4(ty.i32()));
     auto* idx = b.FunctionParam("idx", ty.i32());
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({vec_val, idx});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), vec_val, idx);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-         %10 = OpBitcast %uint %idx
-         %11 = OpExtInst %uint %12 UMin %10 %uint_3
-     %result = OpVectorExtractDynamic %int %vec %11
+          %9 = OpBitcast %uint %idx
+         %10 = OpExtInst %uint %11 UMin %9 %uint_3
+     %result = OpVectorExtractDynamic %int %vec %10
 )");
 }
 
 TEST_F(SpirvWriterTest, Access_NestedVector_Value_DynamicIndex) {
     auto* val = b.FunctionParam("arr", ty.array(ty.array(ty.vec4(ty.i32()), 4), 4));
     auto* idx = b.FunctionParam("idx", ty.i32());
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({val, idx});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), val, 1_u, 2_u, idx);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-         %13 = OpBitcast %uint %idx
-         %14 = OpExtInst %uint %15 UMin %13 %uint_3
-         %18 = OpCompositeExtract %v4int %arr 1 2
-     %result = OpVectorExtractDynamic %int %18 %14
+         %12 = OpBitcast %uint %idx
+         %13 = OpExtInst %uint %14 UMin %12 %uint_3
+         %17 = OpCompositeExtract %v4int %arr 1 2
+     %result = OpVectorExtractDynamic %int %17 %13
 )");
 }
 
 TEST_F(SpirvWriterTest, Access_Struct_Value_ConstantIndex) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("a"), ty.i32()},
                                                    {mod.symbols.Register("b"), ty.vec4<i32>()},
                                                });
     auto* str_val = b.FunctionParam("str", str);
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({str_val});
     b.Append(func->Block(), [&] {
-        auto* result_a = b.Access(ty.f32(), str_val, 0_u);
+        auto* result_a = b.Access(ty.i32(), str_val, 0_u);
         auto* result_b = b.Access(ty.i32(), str_val, 1_u, 2_u);
-        b.Return(func);
+        b.Return(func, b.Add(ty.i32(), result_a, result_b));
         mod.SetName(result_a, "result_a");
         mod.SetName(result_b, "result_b");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result_a = OpCompositeExtract %float %str 0");
+    EXPECT_INST("%result_a = OpCompositeExtract %int %str 0");
     EXPECT_INST("%result_b = OpCompositeExtract %int %str 1 2");
 }
 
 TEST_F(SpirvWriterTest, Access_Struct_Pointer_ConstantIndex) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("a"), ty.i32()},
                                                    {mod.symbols.Register("b"), ty.vec4<i32>()},
                                                });
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.vec4<i32>());
     b.Append(func->Block(), [&] {
         auto* str_var = b.Var("str", ty.ptr(function, str, read_write));
-        auto* result_a = b.Access(ty.ptr<function, f32>(), str_var, 0_u);
+        auto* result_a = b.Access(ty.ptr<function, i32>(), str_var, 0_u);
         auto* result_b = b.Access(ty.ptr<function, vec4<i32>>(), str_var, 1_u);
-        b.Return(func);
+        auto* val_a = b.Load(result_a);
+        auto* val_b = b.Load(result_b);
+        b.Return(func, b.Add(ty.vec4<i32>(), val_a, val_b));
         mod.SetName(result_a, "result_a");
         mod.SetName(result_b, "result_b");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result_a = OpAccessChain %_ptr_Function_float %str %uint_0");
+    EXPECT_INST("%result_a = OpAccessChain %_ptr_Function_int %str %uint_0");
     EXPECT_INST("%result_b = OpAccessChain %_ptr_Function_v4int %str %uint_1");
 }
 
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 724997f..c2fc514 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/core/ir/transform/binding_remapper.h"
 #include "src/tint/lang/core/ir/transform/block_decorated_structs.h"
 #include "src/tint/lang/core/ir/transform/builtin_polyfill.h"
+#include "src/tint/lang/core/ir/transform/combine_access_instructions.h"
 #include "src/tint/lang/core/ir/transform/conversion_polyfill.h"
 #include "src/tint/lang/core/ir/transform/demote_to_helper.h"
 #include "src/tint/lang/core/ir/transform/direct_variable_access.h"
@@ -90,6 +91,7 @@
         RUN_TRANSFORM(core::ir::transform::ZeroInitWorkgroupMemory, module);
     }
 
+    // PreservePadding must come before DirectVariableAccess.
     RUN_TRANSFORM(core::ir::transform::PreservePadding, module);
 
     core::ir::transform::DirectVariableAccessOptions dva_options;
@@ -100,6 +102,12 @@
     RUN_TRANSFORM(core::ir::transform::AddEmptyEntryPoint, module);
     RUN_TRANSFORM(core::ir::transform::Bgra8UnormPolyfill, module);
     RUN_TRANSFORM(core::ir::transform::BlockDecoratedStructs, module);
+
+    // CombineAccessInstructions must come after DirectVariableAccess and BlockDecoratedStructs.
+    // We run this transform as some Qualcomm drivers struggle with partial access chains that
+    // produce pointers to matrices.
+    RUN_TRANSFORM(core::ir::transform::CombineAccessInstructions, module);
+
     RUN_TRANSFORM(BuiltinPolyfill, module);
     RUN_TRANSFORM(core::ir::transform::DemoteToHelper, module);
     RUN_TRANSFORM(ExpandImplicitSplats, module);
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
index 88d564f..59cd9c4 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -117,7 +117,7 @@
     // Find the access instructions that need replacing.
     Vector<AccessToReplace, 4> worklist;
     for (auto* inst : ir.instructions.Objects()) {
-        if (auto* access = inst->As<core::ir::Access>()) {
+        if (auto* access = inst->As<core::ir::Access>(); access && access->Alive()) {
             if (auto to_replace = ShouldReplace(access)) {
                 worklist.Push(to_replace.value());
             }