[ir] Add PreservePadding transform

Use it in the SPIR-V writer, followed by DirectVariableAccess.

Bug: tint:1906
Change-Id: Ie7aa96a527f5798028867a5c1565a35762a7009e
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/153863
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index e4c3240..20133bc 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -35,6 +35,7 @@
     "demote_to_helper.cc",
     "direct_variable_access.cc",
     "multiplanar_external_texture.cc",
+    "preserve_padding.cc",
     "robustness.cc",
     "shader_io.cc",
     "std140.cc",
@@ -50,6 +51,7 @@
     "demote_to_helper.h",
     "direct_variable_access.h",
     "multiplanar_external_texture.h",
+    "preserve_padding.h",
     "robustness.h",
     "shader_io.h",
     "std140.h",
@@ -94,6 +96,7 @@
     "direct_variable_access_test.cc",
     "helper_test.h",
     "multiplanar_external_texture_test.cc",
+    "preserve_padding_test.cc",
     "robustness_test.cc",
     "std140_test.cc",
     "zero_init_workgroup_memory_test.cc",
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 1164934..ba85854 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -44,6 +44,8 @@
   lang/core/ir/transform/direct_variable_access.h
   lang/core/ir/transform/multiplanar_external_texture.cc
   lang/core/ir/transform/multiplanar_external_texture.h
+  lang/core/ir/transform/preserve_padding.cc
+  lang/core/ir/transform/preserve_padding.h
   lang/core/ir/transform/robustness.cc
   lang/core/ir/transform/robustness.h
   lang/core/ir/transform/shader_io.cc
@@ -92,6 +94,7 @@
   lang/core/ir/transform/direct_variable_access_test.cc
   lang/core/ir/transform/helper_test.h
   lang/core/ir/transform/multiplanar_external_texture_test.cc
+  lang/core/ir/transform/preserve_padding_test.cc
   lang/core/ir/transform/robustness_test.cc
   lang/core/ir/transform/std140_test.cc
   lang/core/ir/transform/zero_init_workgroup_memory_test.cc
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index 9ee4ae0..f6ce734 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -49,6 +49,8 @@
     "direct_variable_access.h",
     "multiplanar_external_texture.cc",
     "multiplanar_external_texture.h",
+    "preserve_padding.cc",
+    "preserve_padding.h",
     "robustness.cc",
     "robustness.h",
     "shader_io.cc",
@@ -95,6 +97,7 @@
       "direct_variable_access_test.cc",
       "helper_test.h",
       "multiplanar_external_texture_test.cc",
+      "preserve_padding_test.cc",
       "robustness_test.cc",
       "std140_test.cc",
       "zero_init_workgroup_memory_test.cc",
diff --git a/src/tint/lang/core/ir/transform/preserve_padding.cc b/src/tint/lang/core/ir/transform/preserve_padding.cc
new file mode 100644
index 0000000..18f18f6
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/preserve_padding.cc
@@ -0,0 +1,173 @@
+// 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/preserve_padding.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;
+
+    /// The IR builder.
+    Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// The symbol table.
+    SymbolTable& sym{ir.symbols};
+
+    /// Map from a type to a helper function that will store a decomposed value.
+    Hashmap<const core::type::Type*, Function*, 4> helpers{};
+
+    /// Process the module.
+    void Process() {
+        // Find host-visible stores of types that contain padding bytes.
+        Vector<Store*, 8> worklist;
+        for (auto inst : ir.instructions.Objects()) {
+            if (auto* store = inst->As<Store>(); store && store->Alive()) {
+                auto* ptr = store->To()->Type()->As<core::type::Pointer>();
+                if (ptr->AddressSpace() == core::AddressSpace::kStorage &&
+                    ContainsPadding(ptr->StoreType())) {
+                    worklist.Push(store);
+                }
+            }
+        }
+
+        // Replace the stores we found with calls to helper functions that decompose the accesses.
+        for (auto* store : worklist) {
+            auto* replacement = MakeStore(store->To(), store->From());
+            store->ReplaceWith(replacement);
+            store->Destroy();
+        }
+    }
+
+    /// Check if a type contains padding bytes.
+    /// @param type the type to check
+    /// @returns true if the type contains padding bytes
+    bool ContainsPadding(const type::Type* type) {
+        return tint::Switch(
+            type,  //
+            [&](const type::Array* arr) {
+                auto* elem_ty = arr->ElemType();
+                if (arr->Stride() > elem_ty->Size()) {
+                    return true;
+                }
+                return ContainsPadding(elem_ty);
+            },
+            [&](const type::Matrix* mat) {
+                return mat->ColumnStride() > mat->ColumnType()->Size();
+            },
+            [&](const type::Struct* str) {
+                uint32_t current_offset = 0;
+                for (auto* member : str->Members()) {
+                    if (member->Offset() > current_offset) {
+                        return true;
+                    }
+                    if (ContainsPadding(member->Type())) {
+                        return true;
+                    }
+                    current_offset += member->Type()->Size();
+                }
+                return (current_offset < str->Size());
+            });
+    }
+
+    /// Create an instruction that stores a (possibly padded) type to memory, decomposing the access
+    /// into separate components to preserve padding if necessary.
+    /// @param to the pointer to store to
+    /// @param value the value to store
+    /// @returns the instruction that performs the store
+    Instruction* MakeStore(Value* to, Value* value) {
+        auto* store_type = value->Type();
+
+        // If there are no padding bytes in this type, just use a regular store instruction.
+        if (!ContainsPadding(store_type)) {
+            return b.Store(to, value);
+        }
+
+        // The type contains padding bytes, so call a helper function that decomposes the accesses.
+        auto* helper = helpers.GetOrCreate(store_type, [&] {
+            auto* func = b.Function("tint_store_and_preserve_padding", ty.void_());
+            auto* target = b.FunctionParam("target", ty.ptr(storage, store_type));
+            auto* value_param = b.FunctionParam("value_param", store_type);
+            func->SetParams({target, value_param});
+
+            b.Append(func->Block(), [&] {
+                tint::Switch(
+                    store_type,  //
+                    [&](const type::Array* arr) {
+                        b.LoopRange(
+                            ty, 0_u, u32(arr->ConstantCount().value()), 1_u, [&](Value* idx) {
+                                auto* el_ptr =
+                                    b.Access(ty.ptr(storage, arr->ElemType()), target, idx);
+                                auto* el_value = b.Access(arr->ElemType(), value_param, idx);
+                                MakeStore(el_ptr->Result(), el_value->Result());
+                            });
+                    },
+                    [&](const type::Matrix* mat) {
+                        for (uint32_t i = 0; i < mat->columns(); i++) {
+                            auto* col_ptr =
+                                b.Access(ty.ptr(storage, mat->ColumnType()), target, u32(i));
+                            auto* col_value = b.Access(mat->ColumnType(), value_param, u32(i));
+                            MakeStore(col_ptr->Result(), col_value->Result());
+                        }
+                    },
+                    [&](const type::Struct* str) {
+                        for (auto* member : str->Members()) {
+                            auto* sub_ptr = b.Access(ty.ptr(storage, member->Type()), target,
+                                                     u32(member->Index()));
+                            auto* sub_value =
+                                b.Access(member->Type(), value_param, u32(member->Index()));
+                            MakeStore(sub_ptr->Result(), sub_value->Result());
+                        }
+                    });
+
+                b.Return(func);
+            });
+
+            return func;
+        });
+
+        return b.Call(helper, to, value);
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> PreservePadding(Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "PreservePadding transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/preserve_padding.h b/src/tint/lang/core/ir/transform/preserve_padding.h
new file mode 100644
index 0000000..fb0082d
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/preserve_padding.h
@@ -0,0 +1,37 @@
+// 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_PRESERVE_PADDING_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_PRESERVE_PADDING_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// PreservePadding is a transform that decomposes stores of whole structure and array types to
+/// preserve padding bytes.
+///
+/// @note assumes that DirectVariableAccess will be run afterwards for backends that need it.
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> PreservePadding(Module& module);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_PRESERVE_PADDING_H_
diff --git a/src/tint/lang/core/ir/transform/preserve_padding_test.cc b/src/tint/lang/core/ir/transform/preserve_padding_test.cc
new file mode 100644
index 0000000..88fcebc
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/preserve_padding_test.cc
@@ -0,0 +1,1291 @@
+// 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/preserve_padding.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+#include "src/tint/lang/core/type/array.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/struct.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+class IR_PreservePaddingTest : public TransformTest {
+  protected:
+    const type::Struct* MakeStructWithoutPadding() {
+        auto* structure =
+            ty.Struct(mod.symbols.New("MyStruct"), {
+                                                       {mod.symbols.New("a"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("b"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("c"), ty.vec4<u32>()},
+                                                   });
+        return structure;
+    }
+
+    const type::Struct* MakeStructWithTrailingPadding() {
+        auto* structure =
+            ty.Struct(mod.symbols.New("MyStruct"), {
+                                                       {mod.symbols.New("a"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("b"), ty.u32()},
+                                                   });
+        return structure;
+    }
+
+    const type::Struct* MakeStructWithInternalPadding() {
+        auto* structure =
+            ty.Struct(mod.symbols.New("MyStruct"), {
+                                                       {mod.symbols.New("a"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("b"), ty.u32()},
+                                                       {mod.symbols.New("c"), ty.vec4<u32>()},
+                                                   });
+        return structure;
+    }
+};
+
+TEST_F(IR_PreservePaddingTest, NoModify_Workgroup) {
+    auto* structure = MakeStructWithTrailingPadding();
+    auto* buffer = b.Var("buffer", ty.ptr(workgroup, structure));
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<workgroup, MyStruct, read_write> = var
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_Private) {
+    auto* structure = MakeStructWithTrailingPadding();
+    auto* buffer = b.Var("buffer", ty.ptr(private_, structure));
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<private, MyStruct, read_write> = var
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_Function) {
+    auto* structure = MakeStructWithTrailingPadding();
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        auto* buffer = b.Var("buffer", ty.ptr(function, structure));
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%foo = func(%value:MyStruct):void -> %b1 {
+  %b1 = block {
+    %buffer:ptr<function, MyStruct, read_write> = var
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_StructWithoutPadding) {
+    auto* structure = MakeStructWithoutPadding();
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:vec4<u32> @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_MatrixWithoutPadding) {
+    auto* mat = ty.mat4x4<f32>();
+    auto* buffer = b.Var("buffer", ty.ptr(storage, mat));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", mat);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, mat4x4<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat4x4<f32>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_ArrayWithoutPadding) {
+    auto* arr = ty.array<vec4<f32>>();
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<vec4<f32>>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<vec4<f32>>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_Vec3) {
+    auto* buffer = b.Var("buffer", ty.ptr(storage, ty.vec3<f32>()));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", ty.vec3<f32>());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, vec3<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:vec3<f32>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_LoadStructWithTrailingPadding) {
+    auto* structure = MakeStructWithTrailingPadding();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", structure);
+    b.Append(func->Block(), [&] {
+        auto* load = b.Load(buffer);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():MyStruct -> %b2 {
+  %b2 = block {
+    %3:MyStruct = load %buffer
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Struct_TrailingPadding) {
+    auto* structure = MakeStructWithTrailingPadding();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, MyStruct, read_write>, %value_param:MyStruct):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, vec4<u32>, read_write> = access %target, 0u
+    %9:vec4<u32> = access %value_param, 0u
+    store %8, %9
+    %10:ptr<storage, u32, read_write> = access %target, 1u
+    %11:u32 = access %value_param, 1u
+    store %10, %11
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Struct_InternalPadding) {
+    auto* structure = MakeStructWithInternalPadding();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, MyStruct, read_write>, %value_param:MyStruct):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, vec4<u32>, read_write> = access %target, 0u
+    %9:vec4<u32> = access %value_param, 0u
+    store %8, %9
+    %10:ptr<storage, u32, read_write> = access %target, 1u
+    %11:u32 = access %value_param, 1u
+    store %10, %11
+    %12:ptr<storage, vec4<u32>, read_write> = access %target, 2u
+    %13:vec4<u32> = access %value_param, 2u
+    store %12, %13
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NestedStructWithPadding) {
+    auto* inner = MakeStructWithInternalPadding();
+    auto* outer = ty.Struct(mod.symbols.New("Outer"), {
+                                                          {mod.symbols.New("inner"), inner},
+                                                      });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, outer));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", outer);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+Outer = struct @align(16) {
+  inner:MyStruct @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, Outer, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:Outer):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+Outer = struct @align(16) {
+  inner:MyStruct @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, Outer, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:Outer):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, Outer, read_write>, %value_param:Outer):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, MyStruct, read_write> = access %target, 0u
+    %9:MyStruct = access %value_param, 0u
+    %10:void = call %tint_store_and_preserve_padding_1, %8, %9
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, MyStruct, read_write>, %value_param_1:MyStruct):void -> %b4 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b4 = block {
+    %14:ptr<storage, vec4<u32>, read_write> = access %target_1, 0u
+    %15:vec4<u32> = access %value_param_1, 0u
+    store %14, %15
+    %16:ptr<storage, u32, read_write> = access %target_1, 1u
+    %17:u32 = access %value_param_1, 1u
+    store %16, %17
+    %18:ptr<storage, vec4<u32>, read_write> = access %target_1, 2u
+    %19:vec4<u32> = access %value_param_1, 2u
+    store %18, %19
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, StructWithPadding_InArray) {
+    auto* structure = MakeStructWithTrailingPadding();
+    auto* arr = ty.array(structure, 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<MyStruct, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<MyStruct, 4>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<MyStruct, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<MyStruct, 4>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<MyStruct, 4>, read_write>, %value_param:array<MyStruct, 4>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 4u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, MyStruct, read_write> = access %target, %idx:u32
+        %11:MyStruct = access %value_param, %idx:u32
+        %12:void = call %tint_store_and_preserve_padding_1, %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %14:u32 = add %idx:u32, 1u
+        next_iteration %b5 %14
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, MyStruct, read_write>, %value_param_1:MyStruct):void -> %b8 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b8 = block {
+    %17:ptr<storage, vec4<u32>, read_write> = access %target_1, 0u
+    %18:vec4<u32> = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, u32, read_write> = access %target_1, 1u
+    %20:u32 = access %value_param_1, 1u
+    store %19, %20
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Mat3x3) {
+    auto* mat = ty.mat3x3<f32>();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, mat));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", mat);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, mat3x3<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, mat3x3<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, mat3x3<f32>, read_write>, %value_param:mat3x3<f32>):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, vec3<f32>, read_write> = access %target, 0u
+    %9:vec3<f32> = access %value_param, 0u
+    store %8, %9
+    %10:ptr<storage, vec3<f32>, read_write> = access %target, 1u
+    %11:vec3<f32> = access %value_param, 1u
+    store %10, %11
+    %12:ptr<storage, vec3<f32>, read_write> = access %target, 2u
+    %13:vec3<f32> = access %value_param, 2u
+    store %12, %13
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Mat3x3_InStruct) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), mat},
+                                                                 {mod.symbols.New("b"), mat},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:mat3x3<f32> @offset(0)
+  b:mat3x3<f32> @offset(48)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:mat3x3<f32> @offset(0)
+  b:mat3x3<f32> @offset(48)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, MyStruct, read_write>, %value_param:MyStruct):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, mat3x3<f32>, read_write> = access %target, 0u
+    %9:mat3x3<f32> = access %value_param, 0u
+    %10:void = call %tint_store_and_preserve_padding_1, %8, %9
+    %12:ptr<storage, mat3x3<f32>, read_write> = access %target, 1u
+    %13:mat3x3<f32> = access %value_param, 1u
+    %14:void = call %tint_store_and_preserve_padding_1, %12, %13
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, mat3x3<f32>, read_write>, %value_param_1:mat3x3<f32>):void -> %b4 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b4 = block {
+    %17:ptr<storage, vec3<f32>, read_write> = access %target_1, 0u
+    %18:vec3<f32> = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, vec3<f32>, read_write> = access %target_1, 1u
+    %20:vec3<f32> = access %value_param_1, 1u
+    store %19, %20
+    %21:ptr<storage, vec3<f32>, read_write> = access %target_1, 2u
+    %22:vec3<f32> = access %value_param_1, 2u
+    store %21, %22
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Mat3x3_Array) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<mat3x3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<mat3x3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<mat3x3<f32>, 4>, read_write>, %value_param:array<mat3x3<f32>, 4>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 4u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, mat3x3<f32>, read_write> = access %target, %idx:u32
+        %11:mat3x3<f32> = access %value_param, %idx:u32
+        %12:void = call %tint_store_and_preserve_padding_1, %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %14:u32 = add %idx:u32, 1u
+        next_iteration %b5 %14
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, mat3x3<f32>, read_write>, %value_param_1:mat3x3<f32>):void -> %b8 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b8 = block {
+    %17:ptr<storage, vec3<f32>, read_write> = access %target_1, 0u
+    %18:vec3<f32> = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, vec3<f32>, read_write> = access %target_1, 1u
+    %20:vec3<f32> = access %value_param_1, 1u
+    store %19, %20
+    %21:ptr<storage, vec3<f32>, read_write> = access %target_1, 2u
+    %22:vec3<f32> = access %value_param_1, 2u
+    store %21, %22
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Vec3_Array) {
+    auto* arr = ty.array(ty.vec3<f32>(), 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<vec3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<vec3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<vec3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<vec3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<vec3<f32>, 4>, read_write>, %value_param:array<vec3<f32>, 4>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 4u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, vec3<f32>, read_write> = access %target, %idx:u32
+        %11:vec3<f32> = access %value_param, %idx:u32
+        store %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %12:u32 = add %idx:u32, 1u
+        next_iteration %b5 %12
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, ComplexNesting) {
+    auto* inner =
+        ty.Struct(mod.symbols.New("Inner"), {
+                                                {mod.symbols.New("a"), ty.u32()},
+                                                {mod.symbols.New("b"), ty.array<vec3<f32>, 4>()},
+                                                {mod.symbols.New("c"), ty.mat3x3<f32>()},
+                                                {mod.symbols.New("d"), ty.u32()},
+                                            });
+
+    auto* outer =
+        ty.Struct(mod.symbols.New("Outer"), {
+                                                {mod.symbols.New("a"), ty.u32()},
+                                                {mod.symbols.New("inner"), inner},
+                                                {mod.symbols.New("inner_arr"), ty.array(inner, 4)},
+                                                {mod.symbols.New("b"), ty.u32()},
+                                            });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, ty.array(outer, 3)));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", ty.array(outer, 3));
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+Inner = struct @align(16) {
+  a:u32 @offset(0)
+  b:array<vec3<f32>, 4> @offset(16)
+  c:mat3x3<f32> @offset(80)
+  d:u32 @offset(128)
+}
+
+Outer = struct @align(16) {
+  a_1:u32 @offset(0)
+  inner:Inner @offset(16)
+  inner_arr:array<Inner, 4> @offset(160)
+  b_1:u32 @offset(736)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<Outer, 3>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<Outer, 3>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Inner = struct @align(16) {
+  a:u32 @offset(0)
+  b:array<vec3<f32>, 4> @offset(16)
+  c:mat3x3<f32> @offset(80)
+  d:u32 @offset(128)
+}
+
+Outer = struct @align(16) {
+  a_1:u32 @offset(0)
+  inner:Inner @offset(16)
+  inner_arr:array<Inner, 4> @offset(160)
+  b_1:u32 @offset(736)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<Outer, 3>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<Outer, 3>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<Outer, 3>, read_write>, %value_param:array<Outer, 3>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 3u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, Outer, read_write> = access %target, %idx:u32
+        %11:Outer = access %value_param, %idx:u32
+        %12:void = call %tint_store_and_preserve_padding_1, %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %14:u32 = add %idx:u32, 1u
+        next_iteration %b5 %14
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, Outer, read_write>, %value_param_1:Outer):void -> %b8 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b8 = block {
+    %17:ptr<storage, u32, read_write> = access %target_1, 0u
+    %18:u32 = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, Inner, read_write> = access %target_1, 1u
+    %20:Inner = access %value_param_1, 1u
+    %21:void = call %tint_store_and_preserve_padding_2, %19, %20
+    %23:ptr<storage, array<Inner, 4>, read_write> = access %target_1, 2u
+    %24:array<Inner, 4> = access %value_param_1, 2u
+    %25:void = call %tint_store_and_preserve_padding_3, %23, %24
+    %27:ptr<storage, u32, read_write> = access %target_1, 3u
+    %28:u32 = access %value_param_1, 3u
+    store %27, %28
+    ret
+  }
+}
+%tint_store_and_preserve_padding_2 = func(%target_2:ptr<storage, Inner, read_write>, %value_param_2:Inner):void -> %b9 {  # %tint_store_and_preserve_padding_2: 'tint_store_and_preserve_padding', %target_2: 'target', %value_param_2: 'value_param'
+  %b9 = block {
+    %31:ptr<storage, u32, read_write> = access %target_2, 0u
+    %32:u32 = access %value_param_2, 0u
+    store %31, %32
+    %33:ptr<storage, array<vec3<f32>, 4>, read_write> = access %target_2, 1u
+    %34:array<vec3<f32>, 4> = access %value_param_2, 1u
+    %35:void = call %tint_store_and_preserve_padding_4, %33, %34
+    %37:ptr<storage, mat3x3<f32>, read_write> = access %target_2, 2u
+    %38:mat3x3<f32> = access %value_param_2, 2u
+    %39:void = call %tint_store_and_preserve_padding_5, %37, %38
+    %41:ptr<storage, u32, read_write> = access %target_2, 3u
+    %42:u32 = access %value_param_2, 3u
+    store %41, %42
+    ret
+  }
+}
+%tint_store_and_preserve_padding_4 = func(%target_3:ptr<storage, array<vec3<f32>, 4>, read_write>, %value_param_3:array<vec3<f32>, 4>):void -> %b10 {  # %tint_store_and_preserve_padding_4: 'tint_store_and_preserve_padding', %target_3: 'target', %value_param_3: 'value_param'
+  %b10 = block {
+    loop [i: %b11, b: %b12, c: %b13] {  # loop_2
+      %b11 = block {  # initializer
+        next_iteration %b12 0u
+      }
+      %b12 = block (%idx_1:u32) {  # body
+        %46:bool = gte %idx_1:u32, 4u
+        if %46 [t: %b14] {  # if_2
+          %b14 = block {  # true
+            exit_loop  # loop_2
+          }
+        }
+        %47:ptr<storage, vec3<f32>, read_write> = access %target_3, %idx_1:u32
+        %48:vec3<f32> = access %value_param_3, %idx_1:u32
+        store %47, %48
+        continue %b13
+      }
+      %b13 = block {  # continuing
+        %49:u32 = add %idx_1:u32, 1u
+        next_iteration %b12 %49
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_5 = func(%target_4:ptr<storage, mat3x3<f32>, read_write>, %value_param_4:mat3x3<f32>):void -> %b15 {  # %tint_store_and_preserve_padding_5: 'tint_store_and_preserve_padding', %target_4: 'target', %value_param_4: 'value_param'
+  %b15 = block {
+    %52:ptr<storage, vec3<f32>, read_write> = access %target_4, 0u
+    %53:vec3<f32> = access %value_param_4, 0u
+    store %52, %53
+    %54:ptr<storage, vec3<f32>, read_write> = access %target_4, 1u
+    %55:vec3<f32> = access %value_param_4, 1u
+    store %54, %55
+    %56:ptr<storage, vec3<f32>, read_write> = access %target_4, 2u
+    %57:vec3<f32> = access %value_param_4, 2u
+    store %56, %57
+    ret
+  }
+}
+%tint_store_and_preserve_padding_3 = func(%target_5:ptr<storage, array<Inner, 4>, read_write>, %value_param_5:array<Inner, 4>):void -> %b16 {  # %tint_store_and_preserve_padding_3: 'tint_store_and_preserve_padding', %target_5: 'target', %value_param_5: 'value_param'
+  %b16 = block {
+    loop [i: %b17, b: %b18, c: %b19] {  # loop_3
+      %b17 = block {  # initializer
+        next_iteration %b18 0u
+      }
+      %b18 = block (%idx_2:u32) {  # body
+        %61:bool = gte %idx_2:u32, 4u
+        if %61 [t: %b20] {  # if_3
+          %b20 = block {  # true
+            exit_loop  # loop_3
+          }
+        }
+        %62:ptr<storage, Inner, read_write> = access %target_5, %idx_2:u32
+        %63:Inner = access %value_param_5, %idx_2:u32
+        %64:void = call %tint_store_and_preserve_padding_2, %62, %63
+        continue %b19
+      }
+      %b19 = block {  # continuing
+        %65:u32 = add %idx_2:u32, 1u
+        next_iteration %b18 %65
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, MultipleStoresSameType) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", mat);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 0_u), value);
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 1_u), value);
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 2_u), value);
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 3_u), value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 0u
+    store %4, %value
+    %5:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 1u
+    store %5, %value
+    %6:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 2u
+    store %6, %value
+    %7:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 3u
+    store %7, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 0u
+    %5:void = call %tint_store_and_preserve_padding, %4, %value
+    %7:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 1u
+    %8:void = call %tint_store_and_preserve_padding, %7, %value
+    %9:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 2u
+    %10:void = call %tint_store_and_preserve_padding, %9, %value
+    %11:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 3u
+    %12:void = call %tint_store_and_preserve_padding, %11, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, mat3x3<f32>, read_write>, %value_param:mat3x3<f32>):void -> %b3 {
+  %b3 = block {
+    %15:ptr<storage, vec3<f32>, read_write> = access %target, 0u
+    %16:vec3<f32> = access %value_param, 0u
+    store %15, %16
+    %17:ptr<storage, vec3<f32>, read_write> = access %target, 1u
+    %18:vec3<f32> = access %value_param, 1u
+    store %17, %18
+    %19:ptr<storage, vec3<f32>, read_write> = access %target, 2u
+    %20:vec3<f32> = access %value_param, 2u
+    store %19, %20
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index ec72695..6d0213f 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -23,7 +23,9 @@
 #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/demote_to_helper.h"
+#include "src/tint/lang/core/ir/transform/direct_variable_access.h"
 #include "src/tint/lang/core/ir/transform/multiplanar_external_texture.h"
+#include "src/tint/lang/core/ir/transform/preserve_padding.h"
 #include "src/tint/lang/core/ir/transform/robustness.h"
 #include "src/tint/lang/core/ir/transform/std140.h"
 #include "src/tint/lang/core/ir/transform/zero_init_workgroup_memory.h"
@@ -83,6 +85,13 @@
         RUN_TRANSFORM(core::ir::transform::ZeroInitWorkgroupMemory, module);
     }
 
+    RUN_TRANSFORM(core::ir::transform::PreservePadding, module);
+
+    core::ir::transform::DirectVariableAccessOptions dva_options;
+    dva_options.transform_function = true;
+    dva_options.transform_private = true;
+    RUN_TRANSFORM(core::ir::transform::DirectVariableAccess, module, dva_options);
+
     RUN_TRANSFORM(core::ir::transform::AddEmptyEntryPoint, module);
     RUN_TRANSFORM(core::ir::transform::Bgra8UnormPolyfill, module);
     RUN_TRANSFORM(core::ir::transform::BlockDecoratedStructs, module);