spirv-opt: Add LegalizeMultidimArrayPass (#6535)

This pass transforms multidimensional arrays of resources (e.g.,
Texture2D g_Textures[2][3]) into single-dimensional arrays (e.g.,
Texture2D g_Textures[6]) and updates all access chains that reference
them.

This transformation is required for Vulkan compatibility, as
multidimensional
resource arrays are not allowed in some storage classes.

Specifically, it helps avoid:
- VUID-StandaloneSpirv-Uniform-06807
- VUID-StandaloneSpirv-UniformConstant-04655

Fixes https://github.com/microsoft/DirectXShaderCompiler/issues/7922
diff --git a/Android.mk b/Android.mk
index f9e2360..9e200f8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -144,6 +144,7 @@
 		source/opt/invocation_interlock_placement_pass.cpp \
 		source/opt/ir_context.cpp \
 		source/opt/ir_loader.cpp \
+		source/opt/legalize_multidim_array_pass.cpp \
 		source/opt/licm_pass.cpp \
 		source/opt/liveness.cpp \
 		source/opt/local_access_chain_convert_pass.cpp \
diff --git a/BUILD.gn b/BUILD.gn
index 7e0aa44..c16755d 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -520,6 +520,8 @@
     "source/opt/ir_loader.cpp",
     "source/opt/ir_loader.h",
     "source/opt/iterator.h",
+    "source/opt/legalize_multidim_array_pass.cpp",
+    "source/opt/legalize_multidim_array_pass.h",
     "source/opt/licm_pass.cpp",
     "source/opt/licm_pass.h",
     "source/opt/liveness.cpp",
diff --git a/include/spirv-tools/optimizer.hpp b/include/spirv-tools/optimizer.hpp
index b25ad47..fd4527b 100644
--- a/include/spirv-tools/optimizer.hpp
+++ b/include/spirv-tools/optimizer.hpp
@@ -645,6 +645,11 @@
 // Works best after LICM and local multi store elimination pass.
 Optimizer::PassToken CreateLoopUnswitchPass();
 
+// Creates a pass to legalize multidimensional arrays for Vulkan.
+// This pass will replace multidimensional arrays of resources with a single
+// dimensional array. Combine-access-chains should be run before this pass.
+Optimizer::PassToken CreateLegalizeMultidimArrayPass();
+
 // Create global value numbering pass.
 // This pass will look for instructions where the same value is computed on all
 // paths leading to the instruction.  Those instructions are deleted.
diff --git a/source/opt/CMakeLists.txt b/source/opt/CMakeLists.txt
index ff32dfa..a0ca5b8 100644
--- a/source/opt/CMakeLists.txt
+++ b/source/opt/CMakeLists.txt
@@ -75,6 +75,7 @@
   ir_context.h
   ir_loader.h
   licm_pass.h
+  legalize_multidim_array_pass.h
   liveness.h
   local_access_chain_convert_pass.h
   local_redundancy_elimination.h
@@ -197,6 +198,7 @@
   ir_context.cpp
   ir_loader.cpp
   licm_pass.cpp
+  legalize_multidim_array_pass.cpp
   liveness.cpp
   local_access_chain_convert_pass.cpp
   local_redundancy_elimination.cpp
diff --git a/source/opt/legalize_multidim_array_pass.cpp b/source/opt/legalize_multidim_array_pass.cpp
new file mode 100644
index 0000000..cd207b0
--- /dev/null
+++ b/source/opt/legalize_multidim_array_pass.cpp
@@ -0,0 +1,275 @@
+// Copyright (c) 2026 Google LLC
+//
+// 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 "source/opt/legalize_multidim_array_pass.h"
+
+#include "source/opt/constants.h"
+#include "source/opt/desc_sroa_util.h"
+#include "source/opt/ir_builder.h"
+#include "source/opt/ir_context.h"
+#include "source/opt/type_manager.h"
+
+namespace spvtools {
+namespace opt {
+
+Pass::Status LegalizeMultidimArrayPass::Process() {
+  std::vector<Instruction*> vars_to_legalize;
+
+  for (auto& var : context()->types_values()) {
+    if (var.opcode() != spv::Op::OpVariable) continue;
+    if (!IsMultidimArrayOfResources(&var)) continue;
+    if (!CanLegalize(&var)) {
+      context()->EmitErrorMessage("Unable to legalize multidimensional array: ",
+                                  &var);
+      return Status::Failure;
+    }
+    vars_to_legalize.push_back(&var);
+  }
+
+  if (vars_to_legalize.empty()) return Status::SuccessWithoutChange;
+
+  for (auto* var : vars_to_legalize) {
+    uint32_t old_ptr_type_id = var->type_id();
+    uint32_t new_ptr_type_id = FlattenArrayType(var);
+    if (new_ptr_type_id == 0) return Status::Failure;
+    if (!RewriteAccessChains(var, old_ptr_type_id)) return Status::Failure;
+  }
+
+  return Status::SuccessWithChange;
+}
+
+bool LegalizeMultidimArrayPass::IsMultidimArrayOfResources(Instruction* var) {
+  if (!descsroautil::IsDescriptorArray(context(), var)) return false;
+
+  uint32_t type_id = var->type_id();
+  Instruction* type_inst = context()->get_def_use_mgr()->GetDef(type_id);
+  uint32_t pointee_type_id = type_inst->GetSingleWordInOperand(1);
+  std::vector<uint32_t> dims;
+  uint32_t element_type_id = 0;
+  GetArrayDimensions(pointee_type_id, &dims, &element_type_id);
+
+  return dims.size() > 1;
+}
+
+void LegalizeMultidimArrayPass::GetArrayDimensions(uint32_t type_id,
+                                                   std::vector<uint32_t>* dims,
+                                                   uint32_t* element_type_id) {
+  assert(dims != nullptr && "dims cannot be null.");
+  dims->clear();
+
+  Instruction* type_inst = context()->get_def_use_mgr()->GetDef(type_id);
+  while (type_inst->opcode() == spv::Op::OpTypeArray) {
+    uint32_t length_id = type_inst->GetSingleWordInOperand(1);
+    Instruction* length_inst = context()->get_def_use_mgr()->GetDef(length_id);
+    // Assume OpConstant. According to the spec the length could also be an
+    // OpSpecConstantOp. However, DXC will not generate that type of code. The
+    // code to handle spec constants will be much more complicated.
+    assert(length_inst->opcode() == spv::Op::OpConstant);
+    uint32_t length = length_inst->GetSingleWordInOperand(0);
+    dims->push_back(length);
+    type_id = type_inst->GetSingleWordInOperand(0);
+    type_inst = context()->get_def_use_mgr()->GetDef(type_id);
+  }
+  *element_type_id = type_id;
+}
+
+uint32_t LegalizeMultidimArrayPass::FlattenArrayType(Instruction* var) {
+  analysis::TypeManager* type_mgr = context()->get_type_mgr();
+  analysis::ConstantManager* constant_mgr = context()->get_constant_mgr();
+
+  uint32_t ptr_type_id = var->type_id();
+  Instruction* ptr_type_inst =
+      context()->get_def_use_mgr()->GetDef(ptr_type_id);
+  uint32_t pointee_type_id = ptr_type_inst->GetSingleWordInOperand(1);
+
+  std::vector<uint32_t> dims;
+  uint32_t element_type_id = 0;
+  GetArrayDimensions(pointee_type_id, &dims, &element_type_id);
+
+  uint32_t total_elements = 1;
+  for (uint32_t dim : dims) {
+    total_elements *= dim;
+  }
+
+  const analysis::Constant* total_elements_const =
+      constant_mgr->GetIntConst(total_elements, 32, false);
+
+  Instruction* total_elements_inst =
+      constant_mgr->GetDefiningInstruction(total_elements_const);
+  uint32_t total_elements_id = total_elements_inst->result_id();
+
+  // Create new OpTypeArray.
+  analysis::Type* element_type = type_mgr->GetType(element_type_id);
+  analysis::Array::LengthInfo length_info = {
+      total_elements_id,
+      {analysis::Array::LengthInfo::kConstant, total_elements}};
+  analysis::Array new_array_type(element_type, length_info);
+  uint32_t new_array_type_id = type_mgr->GetTypeInstruction(&new_array_type);
+
+  // Create new OpTypePointer.
+  spv::StorageClass sc =
+      static_cast<spv::StorageClass>(ptr_type_inst->GetSingleWordInOperand(0));
+  analysis::Pointer new_ptr_type(type_mgr->GetType(new_array_type_id), sc);
+  uint32_t new_ptr_type_id = type_mgr->GetTypeInstruction(&new_ptr_type);
+
+  var->SetResultType(new_ptr_type_id);
+  context()->UpdateDefUse(var);
+
+  // Move the var after the new pointer type to avoid a def-before-use.
+  var->InsertAfter(get_def_use_mgr()->GetDef(new_ptr_type_id));
+
+  return new_ptr_type_id;
+}
+
+bool LegalizeMultidimArrayPass::RewriteAccessChains(Instruction* var,
+                                                    uint32_t old_ptr_type_id) {
+  uint32_t var_id = var->result_id();
+  std::vector<Instruction*> users;
+  // Use a worklist to handle transitive uses (e.g. through OpCopyObject)
+  std::vector<Instruction*> worklist;
+
+  context()->get_def_use_mgr()->ForEachUser(
+      var_id, [&worklist](Instruction* user) { worklist.push_back(user); });
+
+  Instruction* old_ptr_type_inst =
+      context()->get_def_use_mgr()->GetDef(old_ptr_type_id);
+  uint32_t old_pointee_type_id = old_ptr_type_inst->GetSingleWordInOperand(1);
+  std::vector<uint32_t> dims;
+  uint32_t element_type_id = 0;
+  GetArrayDimensions(old_pointee_type_id, &dims, &element_type_id);
+  assert(dims.size() != 0 &&
+         "This variable should have been rejected earlier.");
+
+  // Calculate strides once
+  std::vector<uint32_t> strides(dims.size());
+  strides[dims.size() - 1] = 1;
+  for (int i = static_cast<int>(dims.size()) - 2; i >= 0; --i) {
+    strides[i] = strides[i + 1] * dims[i + 1];
+  }
+
+  // Pre-calculate uint type id
+  uint32_t uint_type_id = context()->get_type_mgr()->GetUIntTypeId();
+  if (uint_type_id == 0) return false;
+
+  while (!worklist.empty()) {
+    Instruction* user = worklist.back();
+    worklist.pop_back();
+
+    if (user->opcode() == spv::Op::OpAccessChain ||
+        user->opcode() == spv::Op::OpInBoundsAccessChain) {
+      uint32_t num_indices = user->NumInOperands() - 1;
+      assert(num_indices >= dims.size());
+
+      InstructionBuilder builder(context(), user, IRContext::kAnalysisDefUse);
+
+      uint32_t linearized_idx_id = 0;
+      for (uint32_t i = 0; i < dims.size(); ++i) {
+        uint32_t idx_id = user->GetSingleWordInOperand(i + 1);
+
+        uint32_t term_id = idx_id;
+        if (strides[i] != 1) {
+          const analysis::Constant* stride_const =
+              context()->get_constant_mgr()->GetConstant(
+                  context()->get_type_mgr()->GetType(uint_type_id),
+                  {strides[i]});
+          Instruction* stride_inst =
+              context()->get_constant_mgr()->GetDefiningInstruction(
+                  stride_const);
+
+          Instruction* mul_inst = builder.AddBinaryOp(
+              uint_type_id, spv::Op::OpIMul, idx_id, stride_inst->result_id());
+          if (mul_inst == nullptr) return false;
+          term_id = mul_inst->result_id();
+        }
+
+        if (linearized_idx_id == 0) {
+          linearized_idx_id = term_id;
+        } else {
+          Instruction* add_inst = builder.AddBinaryOp(
+              uint_type_id, spv::Op::OpIAdd, linearized_idx_id, term_id);
+          if (add_inst == nullptr) return false;
+          linearized_idx_id = add_inst->result_id();
+        }
+      }
+
+      // Create new AccessChain.
+      Instruction::OperandList new_operands;
+      new_operands.push_back(user->GetInOperand(0));
+      new_operands.push_back({SPV_OPERAND_TYPE_ID, {linearized_idx_id}});
+      for (uint32_t i = static_cast<uint32_t>(dims.size()); i < num_indices;
+           ++i) {
+        new_operands.push_back(user->GetInOperand(i + 1));
+      }
+      user->SetInOperands(std::move(new_operands));
+      context()->UpdateDefUse(user);
+    } else if (user->opcode() == spv::Op::OpCopyObject) {
+      // The type of the variable has changed so the result type of the
+      // OpCopyObject will change as well.
+
+      uint32_t operand_id = user->GetSingleWordInOperand(0);
+      Instruction* operand_inst =
+          context()->get_def_use_mgr()->GetDef(operand_id);
+      user->SetResultType(operand_inst->type_id());
+      context()->UpdateDefUse(user);
+
+      // Add users of this copy to worklist
+      context()->get_def_use_mgr()->ForEachUser(
+          user->result_id(),
+          [&worklist](Instruction* u) { worklist.push_back(u); });
+    }
+  }
+  return true;
+}
+
+bool LegalizeMultidimArrayPass::CheckUse(Instruction* inst,
+                                         uint32_t max_depth) {
+  if (inst->opcode() == spv::Op::OpAccessChain ||
+      inst->opcode() == spv::Op::OpInBoundsAccessChain) {
+    uint32_t num_indices = inst->NumInOperands() - 1;
+    return num_indices >= max_depth;
+  } else if (inst->opcode() == spv::Op::OpCopyObject) {
+    bool ok = true;
+    return !context()->get_def_use_mgr()->WhileEachUser(
+        inst->result_id(),
+        [&](Instruction* u) { return !CheckUse(u, max_depth); });
+    return ok;
+  } else if (inst->IsDecoration() || inst->opcode() == spv::Op::OpName ||
+             inst->opcode() == spv::Op::OpMemberName) {
+    // Metadata is fine.
+    return true;
+  }
+
+  // Direct use of array or partial array without AccessChain is not allowed.
+  return false;
+}
+
+bool LegalizeMultidimArrayPass::CanLegalize(Instruction* var) {
+  bool ok = true;
+  uint32_t ptr_type_id = var->type_id();
+  Instruction* ptr_type_inst =
+      context()->get_def_use_mgr()->GetDef(ptr_type_id);
+  uint32_t pointee_type_id = ptr_type_inst->GetSingleWordInOperand(1);
+  std::vector<uint32_t> dims;
+  uint32_t element_type_id = 0;
+  GetArrayDimensions(pointee_type_id, &dims, &element_type_id);
+
+  context()->get_def_use_mgr()->ForEachUser(
+      var->result_id(), [&](Instruction* u) {
+        if (!CheckUse(u, static_cast<uint32_t>(dims.size()))) ok = false;
+      });
+  return ok;
+}
+
+}  // namespace opt
+}  // namespace spvtools
diff --git a/source/opt/legalize_multidim_array_pass.h b/source/opt/legalize_multidim_array_pass.h
new file mode 100644
index 0000000..61c912d
--- /dev/null
+++ b/source/opt/legalize_multidim_array_pass.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2026 Google LLC
+//
+// 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 SOURCE_OPT_LEGALIZE_MULTIDIM_ARRAY_PASS_H_
+#define SOURCE_OPT_LEGALIZE_MULTIDIM_ARRAY_PASS_H_
+
+#include "source/opt/pass.h"
+
+namespace spvtools {
+namespace opt {
+
+// Pass to legalize multidimensional arrays of resources for Vulkan.
+// It transforms multidimensional arrays into single-dimensional ones.
+class LegalizeMultidimArrayPass : public Pass {
+ public:
+  const char* name() const override { return "legalize-multidim-array"; }
+  Status Process() override;
+
+ private:
+  // Returns true if |var| is a multidimensional array of resources.
+  bool IsMultidimArrayOfResources(Instruction* var);
+
+  // Flattens the multidimensional array type of |var| and returns the new type
+  // id.
+  uint32_t FlattenArrayType(Instruction* var);
+
+  // Rewrites all access chains that use |var|.
+  bool RewriteAccessChains(Instruction* var, uint32_t old_ptr_type_id);
+
+  // Returns true if all uses of |var| can be legalized.
+  bool CanLegalize(Instruction* var);
+
+  // Recursively checks if the uses of |inst| can be legalized.
+  bool CheckUse(Instruction* inst, uint32_t max_depth);
+
+  // Returns the dimensions of the array type |type_id|.
+  void GetArrayDimensions(uint32_t type_id, std::vector<uint32_t>* dims,
+                          uint32_t* element_type_id);
+};
+
+}  // namespace opt
+}  // namespace spvtools
+
+#endif  // SOURCE_OPT_LEGALIZE_MULTIDIM_ARRAY_PASS_H_
diff --git a/source/opt/optimizer.cpp b/source/opt/optimizer.cpp
index 1380204..e8f64d5 100644
--- a/source/opt/optimizer.cpp
+++ b/source/opt/optimizer.cpp
@@ -171,7 +171,9 @@
           .RegisterPass(CreateRemoveUnusedInterfaceVariablesPass())
           .RegisterPass(CreateInterpolateFixupPass())
           .RegisterPass(CreateInvocationInterlockPlacementPass())
-          .RegisterPass(CreateOpExtInstWithForwardReferenceFixupPass());
+          .RegisterPass(CreateOpExtInstWithForwardReferenceFixupPass())
+          .RegisterPass(CreateCombineAccessChainsPass())
+          .RegisterPass(CreateLegalizeMultidimArrayPass());
 }
 
 Optimizer& Optimizer::RegisterLegalizationPasses() {
@@ -399,6 +401,8 @@
     RegisterPass(CreateFoldSpecConstantOpAndCompositePass());
   } else if (pass_name == "loop-unswitch") {
     RegisterPass(CreateLoopUnswitchPass());
+  } else if (pass_name == "legalize-multidim-array") {
+    RegisterPass(CreateLegalizeMultidimArrayPass());
   } else if (pass_name == "scalar-replacement") {
     if (pass_args.size() == 0) {
       RegisterPass(CreateScalarReplacementPass(0));
@@ -961,6 +965,11 @@
       MakeUnique<opt::LoopUnswitchPass>());
 }
 
+Optimizer::PassToken CreateLegalizeMultidimArrayPass() {
+  return MakeUnique<Optimizer::PassToken::Impl>(
+      MakeUnique<opt::LegalizeMultidimArrayPass>());
+}
+
 Optimizer::PassToken CreateRedundancyEliminationPass() {
   return MakeUnique<Optimizer::PassToken::Impl>(
       MakeUnique<opt::RedundancyEliminationPass>());
diff --git a/source/opt/passes.h b/source/opt/passes.h
index d005377..533fc21 100644
--- a/source/opt/passes.h
+++ b/source/opt/passes.h
@@ -52,6 +52,7 @@
 #include "source/opt/interface_var_sroa.h"
 #include "source/opt/interp_fixup_pass.h"
 #include "source/opt/invocation_interlock_placement_pass.h"
+#include "source/opt/legalize_multidim_array_pass.h"
 #include "source/opt/licm_pass.h"
 #include "source/opt/local_access_chain_convert_pass.h"
 #include "source/opt/local_redundancy_elimination.h"
diff --git a/test/opt/CMakeLists.txt b/test/opt/CMakeLists.txt
index 9df0bb7..12096f5 100644
--- a/test/opt/CMakeLists.txt
+++ b/test/opt/CMakeLists.txt
@@ -70,6 +70,7 @@
        ir_loader_test.cpp
        iterator_test.cpp
        line_debug_info_test.cpp
+       legalize_multidim_array_test.cpp
        local_access_chain_convert_test.cpp
        local_redundancy_elimination_test.cpp
        local_single_block_elim.cpp
diff --git a/test/opt/legalize_multidim_array_test.cpp b/test/opt/legalize_multidim_array_test.cpp
new file mode 100644
index 0000000..c7bb6b2
--- /dev/null
+++ b/test/opt/legalize_multidim_array_test.cpp
@@ -0,0 +1,639 @@
+// Copyright (c) 2026 Google LLC
+//
+// 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 <string>
+
+#include "test/opt/assembly_builder.h"
+#include "test/opt/pass_fixture.h"
+#include "test/opt/pass_utils.h"
+
+namespace spvtools {
+namespace opt {
+namespace {
+
+using LegalizeMultidimArrayTest = PassTest<::testing::Test>;
+
+TEST_F(LegalizeMultidimArrayTest, Flatten2DResourceArray) {
+  // HLSL:
+  // Texture2D g_Textures[2][3];
+  // SamplerState g_Sampler;
+  // float4 main(float2 uv : TEXCOORD) : SV_Target {
+  //   return g_Textures[0][1].Sample(g_Sampler, uv);
+  // }
+  const std::string text = R"(
+; CHECK: %uint_6 = OpConstant %uint 6
+; CHECK: %_arr_type_2d_image_uint_6 = OpTypeArray %type_2d_image %uint_6
+; CHECK: %_ptr_UniformConstant__arr_type_2d_image_uint_6 = OpTypePointer UniformConstant %_arr_type_2d_image_uint_6
+; CHECK: %g_Textures = OpVariable %_ptr_UniformConstant__arr_type_2d_image_uint_6 UniformConstant
+; CHECK: [[mul:%\w+]] = OpIMul %uint %int_0 %uint_3
+; CHECK: [[idx:%\w+]] = OpIAdd %uint [[mul]] %int_1
+; CHECK: [[ptr:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures [[idx]]
+; CHECK: OpLoad %type_2d_image [[ptr]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %in_var_TEXCOORD %out_var_SV_Target
+               OpExecutionMode %main OriginUpperLeft
+               OpSource HLSL 600
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpName %type_sampler "type.sampler"
+               OpName %g_Sampler "g_Sampler"
+               OpName %main "main"
+               OpName %src_main "src.main"
+               OpDecorate %in_var_TEXCOORD Location 0
+               OpDecorate %out_var_SV_Target Location 0
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+               OpDecorate %g_Sampler DescriptorSet 0
+               OpDecorate %g_Sampler Binding 1
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_3 = OpTypeArray %type_2d_image %uint_3
+%_arr__arr_type_2d_image_uint_3_uint_2 = OpTypeArray %_arr_type_2d_image_uint_3 %uint_2
+%_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 = OpTypePointer UniformConstant %_arr__arr_type_2d_image_uint_3_uint_2
+%type_sampler = OpTypeSampler
+%_ptr_UniformConstant_type_sampler = OpTypePointer UniformConstant %type_sampler
+    %v2float = OpTypeVector %float 2
+%_ptr_Input_v2float = OpTypePointer Input %v2float
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+       %void = OpTypeVoid
+         %24 = OpTypeFunction %void
+%_ptr_Function_v2float = OpTypePointer Function %v2float
+         %31 = OpTypeFunction %v4float %_ptr_Function_v2float
+%_ptr_UniformConstant__arr_type_2d_image_uint_3 = OpTypePointer UniformConstant %_arr_type_2d_image_uint_3
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+%type_sampled_image = OpTypeSampledImage %type_2d_image
+ %g_Textures = OpVariable %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 UniformConstant
+  %g_Sampler = OpVariable %_ptr_UniformConstant_type_sampler UniformConstant
+%in_var_TEXCOORD = OpVariable %_ptr_Input_v2float Input
+%out_var_SV_Target = OpVariable %_ptr_Output_v4float Output
+       %main = OpFunction %void None %24
+         %25 = OpLabel
+%param_var_uv = OpVariable %_ptr_Function_v2float Function
+         %28 = OpLoad %v2float %in_var_TEXCOORD
+               OpStore %param_var_uv %28
+         %29 = OpFunctionCall %v4float %src_main %param_var_uv
+               OpStore %out_var_SV_Target %29
+               OpReturn
+               OpFunctionEnd
+   %src_main = OpFunction %v4float None %31
+         %uv = OpFunctionParameter %_ptr_Function_v2float
+   %bb_entry = OpLabel
+         %37 = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures %int_0 %int_1
+         %38 = OpLoad %type_2d_image %37
+         %39 = OpLoad %type_sampler %g_Sampler
+         %40 = OpLoad %v2float %uv
+         %42 = OpSampledImage %type_sampled_image %38 %39
+         %43 = OpImageSampleImplicitLod %v4float %42 %40 None
+               OpReturnValue %43
+               OpFunctionEnd
+  )";
+
+  const std::string expected = R"(
+)";
+
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text,
+                                                   /*do_validation=*/true);
+}
+
+// Test that the pass fails when the access chain is split into multiple access
+// chains. We expect CombineAccessChains to be run before this pass to avoid
+// this.
+TEST_F(LegalizeMultidimArrayTest, IndirectUseViaPartialAccessChain) {
+  // HLSL source approximation:
+  // Texture2D g_Textures[2][3];
+  // ...
+  // Texture2D row[3] = g_Textures[0];
+  // return row[1].Sample(...);
+  //
+  // In SPIR-V, this often looks like:
+  // %ptr_row = OpAccessChain %_ptr_UniformConstant_arr_type_2d_image_uint_3
+  // %g_Textures %int_0 %ptr_tex = OpAccessChain
+  // %_ptr_UniformConstant_type_2d_image %ptr_row %int_1 OpLoad %type_2d_image
+  // %ptr_tex
+
+  const std::string text = R"(
+  ; CHECK: Unable to legalize multidimensional array
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %out_var_SV_Target
+               OpExecutionMode %main OriginUpperLeft
+               OpSource HLSL 600
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpName %main "main"
+               OpDecorate %out_var_SV_Target Location 0
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_3 = OpTypeArray %type_2d_image %uint_3
+%_arr__arr_type_2d_image_uint_3_uint_2 = OpTypeArray %_arr_type_2d_image_uint_3 %uint_2
+%_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 = OpTypePointer UniformConstant %_arr__arr_type_2d_image_uint_3_uint_2
+%_ptr_UniformConstant__arr_type_2d_image_uint_3 = OpTypePointer UniformConstant %_arr_type_2d_image_uint_3
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+       %void = OpTypeVoid
+         %24 = OpTypeFunction %void
+ %g_Textures = OpVariable %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 UniformConstant
+%out_var_SV_Target = OpVariable %_ptr_Output_v4float Output
+       %main = OpFunction %void None %24
+         %25 = OpLabel
+         %37 = OpAccessChain %_ptr_UniformConstant__arr_type_2d_image_uint_3 %g_Textures %int_0
+         %38 = OpAccessChain %_ptr_UniformConstant_type_2d_image %37 %int_1
+         %39 = OpLoad %type_2d_image %38
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  SinglePassRunAndFail<LegalizeMultidimArrayPass>(text);
+}
+
+TEST_F(LegalizeMultidimArrayTest, Flatten3DResourceArray) {
+  // Texture2D g_Textures[2][3][4];
+  // Access: g_Textures[0][1][2]
+  const std::string text = R"(
+; CHECK: %uint_24 = OpConstant %uint 24
+; CHECK: %_arr_type_2d_image_uint_24 = OpTypeArray %type_2d_image %uint_24
+; CHECK: %_ptr_UniformConstant__arr_type_2d_image_uint_24 = OpTypePointer UniformConstant %_arr_type_2d_image_uint_24
+; CHECK: %g_Textures = OpVariable %_ptr_UniformConstant__arr_type_2d_image_uint_24 UniformConstant
+; CHECK: [[mul1:%\w+]] = OpIMul %uint %int_0 %uint_12
+; CHECK: [[mul2:%\w+]] = OpIMul %uint %int_1 %uint_4
+; CHECK: [[add1:%\w+]] = OpIAdd %uint [[mul1]] [[mul2]]
+; CHECK: [[final_idx:%\w+]] = OpIAdd %uint [[add1]] %int_2
+; CHECK: [[ptr:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures [[final_idx]]
+; CHECK: OpLoad %type_2d_image [[ptr]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+       %uint = OpTypeInt 32 0
+     %uint_0 = OpConstant %uint 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+     %uint_4 = OpConstant %uint 4
+     %uint_12 = OpConstant %uint 12
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_4 = OpTypeArray %type_2d_image %uint_4
+%_arr_arr_type_2d_image_uint_4_uint_3 = OpTypeArray %_arr_type_2d_image_uint_4 %uint_3
+%_arr_arr_arr_type_2d_image_uint_4_uint_3_uint_2 = OpTypeArray %_arr_arr_type_2d_image_uint_4_uint_3 %uint_2
+%_ptr_UniformConstant_arr_3d = OpTypePointer UniformConstant %_arr_arr_arr_type_2d_image_uint_4_uint_3_uint_2
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Textures = OpVariable %_ptr_UniformConstant_arr_3d UniformConstant
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures %int_0 %int_1 %int_2
+         %val = OpLoad %type_2d_image %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, FlattenSamplerArray) {
+  // SamplerState g_Samplers[2][2];
+  const std::string text = R"(
+; CHECK: %uint_4 = OpConstant %uint 4
+; CHECK: %_arr_type_sampler_uint_4 = OpTypeArray %type_sampler %uint_4
+; CHECK: %_ptr_UniformConstant__arr_type_sampler_uint_4 = OpTypePointer UniformConstant %_arr_type_sampler_uint_4
+; CHECK: %g_Samplers = OpVariable %_ptr_UniformConstant__arr_type_sampler_uint_4 UniformConstant
+; CHECK: [[mul:%\w+]] = OpIMul %uint %int_0 %uint_2
+; CHECK: [[idx:%\w+]] = OpIAdd %uint [[mul]] %int_1
+; CHECK: [[ptr:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_sampler %g_Samplers [[idx]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %type_sampler "type.sampler"
+               OpName %g_Samplers "g_Samplers"
+               OpDecorate %g_Samplers DescriptorSet 0
+               OpDecorate %g_Samplers Binding 0
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+%type_sampler = OpTypeSampler
+%_arr_type_sampler_uint_2 = OpTypeArray %type_sampler %uint_2
+%_arr_arr_type_sampler_uint_2_uint_2 = OpTypeArray %_arr_type_sampler_uint_2 %uint_2
+%_ptr_UniformConstant_arr_2d = OpTypePointer UniformConstant %_arr_arr_type_sampler_uint_2_uint_2
+%_ptr_UniformConstant_type_sampler = OpTypePointer UniformConstant %type_sampler
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Samplers = OpVariable %_ptr_UniformConstant_arr_2d UniformConstant
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr = OpAccessChain %_ptr_UniformConstant_type_sampler %g_Samplers %int_0 %int_1
+         %val = OpLoad %type_sampler %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, FlattenStorageBufferArray) {
+  // struct S { float f; };
+  // S buffers[2][3];
+  // buffers[0][1].f
+  const std::string text = R"(
+; CHECK: %uint_6 = OpConstant %uint 6
+; CHECK: %_arr_S_uint_6 = OpTypeArray %S %uint_6
+; CHECK: %_ptr_StorageBuffer__arr_S_uint_6 = OpTypePointer StorageBuffer %_arr_S_uint_6
+; CHECK: %g_Buffers = OpVariable %_ptr_StorageBuffer__arr_S_uint_6 StorageBuffer
+; CHECK: [[mul:%\w+]] = OpIMul %uint %int_0 %uint_3
+; CHECK: [[idx:%\w+]] = OpIAdd %uint [[mul]] %int_1
+; CHECK: [[ptr:%\w+]] = OpAccessChain %_ptr_StorageBuffer_float %g_Buffers [[idx]] %int_0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %S "S"
+               OpName %g_Buffers "g_Buffers"
+               OpDecorate %g_Buffers DescriptorSet 0
+               OpDecorate %g_Buffers Binding 0
+               OpMemberDecorate %S 0 Offset 0
+               OpDecorate %S Block
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+          %S = OpTypeStruct %float
+%_arr_S_uint_3 = OpTypeArray %S %uint_3
+%_arr__arr_S_uint_3_uint_2 = OpTypeArray %_arr_S_uint_3 %uint_2
+%_ptr_StorageBuffer_arr_2d = OpTypePointer StorageBuffer %_arr__arr_S_uint_3_uint_2
+%_ptr_StorageBuffer_float = OpTypePointer StorageBuffer %float
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+  %g_Buffers = OpVariable %_ptr_StorageBuffer_arr_2d StorageBuffer
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr = OpAccessChain %_ptr_StorageBuffer_float %g_Buffers %int_0 %int_1 %int_0
+         %val = OpLoad %float %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, FlattenUniformArray) {
+  // Uniform buffer array: MyBlock buffers[2][3];
+  // Access: buffers[0][1].member
+  const std::string text = R"(
+; CHECK: %uint_6 = OpConstant %uint 6
+; CHECK: %_arr_MyBlock_uint_6 = OpTypeArray %MyBlock %uint_6
+; CHECK: %_ptr_Uniform__arr_MyBlock_uint_6 = OpTypePointer Uniform %_arr_MyBlock_uint_6
+; CHECK: %g_Uniforms = OpVariable %_ptr_Uniform__arr_MyBlock_uint_6 Uniform
+; CHECK: [[mul:%\w+]] = OpIMul %uint %int_0 %uint_3
+; CHECK: [[idx:%\w+]] = OpIAdd %uint [[mul]] %int_1
+; CHECK: [[ptr:%\w+]] = OpAccessChain %_ptr_Uniform_float %g_Uniforms [[idx]] %int_0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %MyBlock "MyBlock"
+               OpName %g_Uniforms "g_Uniforms"
+               OpDecorate %g_Uniforms DescriptorSet 0
+               OpDecorate %g_Uniforms Binding 0
+               OpMemberDecorate %MyBlock 0 Offset 0
+               OpDecorate %MyBlock Block
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+    %MyBlock = OpTypeStruct %float
+%_arr_MyBlock_uint_3 = OpTypeArray %MyBlock %uint_3
+%_arr__arr_MyBlock_uint_3_uint_2 = OpTypeArray %_arr_MyBlock_uint_3 %uint_2
+%_ptr_Uniform__arr__arr_MyBlock_uint_3_uint_2 = OpTypePointer Uniform %_arr__arr_MyBlock_uint_3_uint_2
+%_ptr_Uniform_float = OpTypePointer Uniform %float
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Uniforms = OpVariable %_ptr_Uniform__arr__arr_MyBlock_uint_3_uint_2 Uniform
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr = OpAccessChain %_ptr_Uniform_float %g_Uniforms %int_0 %int_1 %int_0
+         %val = OpLoad %float %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, AccessChainThroughCopyObject) {
+  // Texture2D g_Textures[2][3];
+  // %copy = OpCopyObject %ptr_type %g_Textures
+  // %ptr = OpAccessChain %... %copy %int_0 %int_1
+  const std::string text = R"(
+; CHECK: %uint_6 = OpConstant %uint 6
+; CHECK: %_arr_type_2d_image_uint_6 = OpTypeArray %type_2d_image %uint_6
+; CHECK: %_ptr_UniformConstant__arr_type_2d_image_uint_6 = OpTypePointer UniformConstant %_arr_type_2d_image_uint_6
+; CHECK: %g_Textures = OpVariable %_ptr_UniformConstant__arr_type_2d_image_uint_6 UniformConstant
+; CHECK: [[copy:%\w+]] = OpCopyObject %_ptr_UniformConstant__arr_type_2d_image_uint_6 %g_Textures
+; CHECK: [[mul:%\w+]] = OpIMul %uint %int_0 %uint_3
+; CHECK: [[idx:%\w+]] = OpIAdd %uint [[mul]] %int_1
+; CHECK: [[ptr:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_2d_image [[copy]] [[idx]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_3 = OpTypeArray %type_2d_image %uint_3
+%_arr__arr_type_2d_image_uint_3_uint_2 = OpTypeArray %_arr_type_2d_image_uint_3 %uint_2
+%_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 = OpTypePointer UniformConstant %_arr__arr_type_2d_image_uint_3_uint_2
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Textures = OpVariable %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 UniformConstant
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %copy = OpCopyObject %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 %g_Textures
+         %ptr = OpAccessChain %_ptr_UniformConstant_type_2d_image %copy %int_0 %int_1
+         %val = OpLoad %type_2d_image %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, DynamicIndices) {
+  // Access with non-constant indices.
+  // g_Textures[var_i][var_j]
+  const std::string text = R"(
+; CHECK: [[idx1:%\w+]] = OpLoad %int %idx_var_1
+; CHECK: [[idx2:%\w+]] = OpLoad %int %idx_var_2
+; CHECK: [[mul:%\w+]] = OpIMul %uint [[idx1]] %uint_3
+; CHECK: [[add:%\w+]] = OpIAdd %uint [[mul]] [[idx2]]
+; CHECK: OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures [[add]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpName %idx_var_1 "idx_var_1"
+               OpName %idx_var_2 "idx_var_2"
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_3 = OpTypeArray %type_2d_image %uint_3
+%_arr__arr_type_2d_image_uint_3_uint_2 = OpTypeArray %_arr_type_2d_image_uint_3 %uint_2
+%_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 = OpTypePointer UniformConstant %_arr__arr_type_2d_image_uint_3_uint_2
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+%_ptr_Function_int = OpTypePointer Function %int
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Textures = OpVariable %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 UniformConstant
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+    %idx_var_1 = OpVariable %_ptr_Function_int Function
+    %idx_var_2 = OpVariable %_ptr_Function_int Function
+         %i = OpLoad %int %idx_var_1
+         %j = OpLoad %int %idx_var_2
+         %ptr = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures %i %j
+         %val = OpLoad %type_2d_image %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, IgnoreFunctionScopeArray) {
+  // Function scope array [2][3] should NOT be legalized.
+  const std::string text = R"(
+; CHECK: %_arr__arr_float_uint_3_uint_2 = OpTypeArray %_arr_float_uint_3 %uint_2
+; CHECK: %_ptr_Function__arr__arr_float_uint_3_uint_2 = OpTypePointer Function %_arr__arr_float_uint_3_uint_2
+; CHECK: %var = OpVariable %_ptr_Function__arr__arr_float_uint_3_uint_2 Function
+; CHECK: OpAccessChain %_ptr_Function_float %var %int_0 %int_1
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %var "var"
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%_arr_float_uint_3 = OpTypeArray %float %uint_3
+%_arr__arr_float_uint_3_uint_2 = OpTypeArray %_arr_float_uint_3 %uint_2
+%_ptr_Function__arr__arr_float_uint_3_uint_2 = OpTypePointer Function %_arr__arr_float_uint_3_uint_2
+%_ptr_Function_float = OpTypePointer Function %float
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %var = OpVariable %_ptr_Function__arr__arr_float_uint_3_uint_2 Function
+         %ptr = OpAccessChain %_ptr_Function_float %var %int_0 %int_1
+         %val = OpLoad %float %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, IgnoreWorkgroupScopeArray) {
+  // Workgroup scope array [2][3] should NOT be legalized.
+  const std::string text = R"(
+; CHECK: %_arr__arr_float_uint_3_uint_2 = OpTypeArray %_arr_float_uint_3 %uint_2
+; CHECK: %_ptr_Workgroup__arr__arr_float_uint_3_uint_2 = OpTypePointer Workgroup %_arr__arr_float_uint_3_uint_2
+; CHECK: %var = OpVariable %_ptr_Workgroup__arr__arr_float_uint_3_uint_2 Workgroup
+; CHECK: OpAccessChain %_ptr_Workgroup_float %var %int_0 %int_1
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %var "var"
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%_arr_float_uint_3 = OpTypeArray %float %uint_3
+%_arr__arr_float_uint_3_uint_2 = OpTypeArray %_arr_float_uint_3 %uint_2
+%_ptr_Workgroup__arr__arr_float_uint_3_uint_2 = OpTypePointer Workgroup %_arr__arr_float_uint_3_uint_2
+%_ptr_Workgroup_float = OpTypePointer Workgroup %float
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %var = OpVariable %_ptr_Workgroup__arr__arr_float_uint_3_uint_2 Workgroup
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr = OpAccessChain %_ptr_Workgroup_float %var %int_0 %int_1
+         %val = OpLoad %float %ptr
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, MultipleAccessChains) {
+  // Access g_Textures[0][1] and g_Textures[1][2] in the same function.
+  const std::string text = R"(
+; CHECK: [[mul1:%\w+]] = OpIMul %uint %int_0 %uint_3
+; CHECK: [[idx1:%\w+]] = OpIAdd %uint [[mul1]] %int_1
+; CHECK: [[ptr1:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures [[idx1]]
+; CHECK: OpLoad %type_2d_image [[ptr1]]
+; CHECK: [[mul2:%\w+]] = OpIMul %uint %int_1 %uint_3
+; CHECK: [[idx2:%\w+]] = OpIAdd %uint [[mul2]] %int_2
+; CHECK: [[ptr2:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures [[idx2]]
+; CHECK: OpLoad %type_2d_image [[ptr2]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_3 = OpTypeArray %type_2d_image %uint_3
+%_arr__arr_type_2d_image_uint_3_uint_2 = OpTypeArray %_arr_type_2d_image_uint_3 %uint_2
+%_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 = OpTypePointer UniformConstant %_arr__arr_type_2d_image_uint_3_uint_2
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Textures = OpVariable %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 UniformConstant
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr1 = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures %int_0 %int_1
+         %val1 = OpLoad %type_2d_image %ptr1
+         %ptr2 = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures %int_1 %int_2
+         %val2 = OpLoad %type_2d_image %ptr2
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+TEST_F(LegalizeMultidimArrayTest, MultipleResources) {
+  // Two different resource arrays:
+  // Texture2D g_Textures[2][3];
+  // SamplerState g_Samplers[2][2];
+  const std::string text = R"(
+; CHECK: %g_Textures = OpVariable %_ptr_UniformConstant__arr_type_2d_image_uint_6 UniformConstant
+; CHECK: %g_Samplers = OpVariable %_ptr_UniformConstant__arr_type_sampler_uint_4 UniformConstant
+; CHECK: [[mul1:%\w+]] = OpIMul %uint %int_0 %uint_3
+; CHECK: [[idx1:%\w+]] = OpIAdd %uint [[mul1]] %int_1
+; CHECK: [[ptr1:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures [[idx1]]
+; CHECK: OpLoad %type_2d_image [[ptr1]]
+; CHECK: [[mul2:%\w+]] = OpIMul %uint %int_1 %uint_2
+; CHECK: [[idx2:%\w+]] = OpIAdd %uint [[mul2]] %int_1
+; CHECK: [[ptr2:%\w+]] = OpAccessChain %_ptr_UniformConstant_type_sampler %g_Samplers [[idx2]]
+; CHECK: OpLoad %type_sampler [[ptr2]]
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+               OpName %type_2d_image "type.2d.image"
+               OpName %g_Textures "g_Textures"
+               OpName %type_sampler "type.sampler"
+               OpName %g_Samplers "g_Samplers"
+               OpDecorate %g_Textures DescriptorSet 0
+               OpDecorate %g_Textures Binding 0
+               OpDecorate %g_Samplers DescriptorSet 0
+               OpDecorate %g_Samplers Binding 1
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+       %uint = OpTypeInt 32 0
+     %uint_2 = OpConstant %uint 2
+     %uint_3 = OpConstant %uint 3
+      %float = OpTypeFloat 32
+%type_2d_image = OpTypeImage %float 2D 2 0 0 1 Unknown
+%_arr_type_2d_image_uint_3 = OpTypeArray %type_2d_image %uint_3
+%_arr__arr_type_2d_image_uint_3_uint_2 = OpTypeArray %_arr_type_2d_image_uint_3 %uint_2
+%_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 = OpTypePointer UniformConstant %_arr__arr_type_2d_image_uint_3_uint_2
+%_ptr_UniformConstant_type_2d_image = OpTypePointer UniformConstant %type_2d_image
+%type_sampler = OpTypeSampler
+%_arr_type_sampler_uint_2 = OpTypeArray %type_sampler %uint_2
+%_arr_arr_type_sampler_uint_2_uint_2 = OpTypeArray %_arr_type_sampler_uint_2 %uint_2
+%_ptr_UniformConstant_arr_2d_sampler = OpTypePointer UniformConstant %_arr_arr_type_sampler_uint_2_uint_2
+%_ptr_UniformConstant_type_sampler = OpTypePointer UniformConstant %type_sampler
+       %void = OpTypeVoid
+       %main_func = OpTypeFunction %void
+ %g_Textures = OpVariable %_ptr_UniformConstant__arr__arr_type_2d_image_uint_3_uint_2 UniformConstant
+ %g_Samplers = OpVariable %_ptr_UniformConstant_arr_2d_sampler UniformConstant
+       %main = OpFunction %void None %main_func
+         %label = OpLabel
+         %ptr1 = OpAccessChain %_ptr_UniformConstant_type_2d_image %g_Textures %int_0 %int_1
+         %val1 = OpLoad %type_2d_image %ptr1
+         %ptr2 = OpAccessChain %_ptr_UniformConstant_type_sampler %g_Samplers %int_1 %int_1
+         %val2 = OpLoad %type_sampler %ptr2
+               OpReturn
+               OpFunctionEnd
+  )";
+  SinglePassRunAndMatch<LegalizeMultidimArrayPass>(text, true);
+}
+
+}  // namespace
+}  // namespace opt
+}  // namespace spvtools
\ No newline at end of file
diff --git a/test/opt/pass_fixture.h b/test/opt/pass_fixture.h
index d67d364..a578cc4 100644
--- a/test/opt/pass_fixture.h
+++ b/test/opt/pass_fixture.h
@@ -220,14 +220,14 @@
   // messages.
   template <typename PassT, typename... Args>
   void SinglePassRunAndFail(const std::string& original, Args&&... args) {
-    context_ = BuildModule(env_, consumer_, original, assemble_options_);
-    EXPECT_NE(nullptr, context()) << "Assembling failed for shader:\n"
-                                  << original << std::endl;
     std::ostringstream errs;
     auto error_consumer = [&errs](spv_message_level_t, const char*,
                                   const spv_position_t&, const char* message) {
       errs << message << std::endl;
     };
+    context_ = BuildModule(env_, error_consumer, original, assemble_options_);
+    EXPECT_NE(nullptr, context()) << "Assembling failed for shader:\n"
+                                  << original << std::endl;
     auto pass = MakeUnique<PassT>(std::forward<Args>(args)...);
     pass->SetMessageConsumer(error_consumer);
     const auto status = pass->Run(context());
diff --git a/tools/opt/opt.cpp b/tools/opt/opt.cpp
index 1b0b372..19852e6 100644
--- a/tools/opt/opt.cpp
+++ b/tools/opt/opt.cpp
@@ -279,6 +279,10 @@
                option --relax-logical-pointer to the validator.)",
          GetLegalizationPasses().c_str());
   printf(R"(
+  --legalize-multidim-array
+               Replace multidimensional arrays of resources with single-dimensional
+               arrays. Run combine-access-chains before this pass.)");
+  printf(R"(
   --local-redundancy-elimination
                Looks for instructions in the same basic block that compute the
                same value, and deletes the redundant ones.)");