[ir] Add robustness for texture builtins

Clamp the coordinates, level, and array index arguments to
`textureLoad`, `textureStore`, and `textureDimensions`.

Bug: tint:1718
Change-Id: I77b17b9aae05a7ec31ea57b65dad0c17c9959985
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/151120
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/lang/core/ir/transform/robustness.cc b/src/tint/lang/core/ir/transform/robustness.cc
index 43c25ff3..ed03c80 100644
--- a/src/tint/lang/core/ir/transform/robustness.cc
+++ b/src/tint/lang/core/ir/transform/robustness.cc
@@ -20,6 +20,9 @@
 #include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/type/depth_texture.h"
+#include "src/tint/lang/core/type/sampled_texture.h"
+#include "src/tint/lang/core/type/texture.h"
 
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
@@ -48,6 +51,7 @@
         Vector<ir::Access*, 64> accesses;
         Vector<ir::LoadVectorElement*, 64> vector_loads;
         Vector<ir::StoreVectorElement*, 64> vector_stores;
+        Vector<ir::CoreBuiltinCall*, 64> texture_calls;
         for (auto* inst : ir->instructions.Objects()) {
             if (inst->Alive()) {
                 tint::Switch(
@@ -78,6 +82,16 @@
                         if (ShouldClamp(ptr->AddressSpace())) {
                             vector_stores.Push(sve);
                         }
+                    },
+                    [&](ir::CoreBuiltinCall* call) {
+                        // Check if this is a texture builtin that needs to be clamped.
+                        if (config.clamp_texture) {
+                            if (call->Func() == core::Function::kTextureDimensions ||
+                                call->Func() == core::Function::kTextureLoad ||
+                                call->Func() == core::Function::kTextureStore) {
+                                texture_calls.Push(call);
+                            }
+                        }
                     });
             }
         }
@@ -107,7 +121,13 @@
             });
         }
 
-        // TODO(jrprice): Handle texture builtins.
+        // Clamp indices and coordinates for texture builtins calls.
+        for (auto* call : texture_calls) {
+            b.InsertBefore(call, [&] {  //
+                ClampTextureCallArgs(call);
+            });
+        }
+
         // TODO(jrprice): Handle config.bindings_ignored.
         // TODO(jrprice): Handle config.disable_runtime_sized_array_index_clamping.
     }
@@ -139,6 +159,21 @@
         return false;
     }
 
+    /// Convert a value to a u32 if needed.
+    /// @param value the value to convert
+    /// @returns the converted value, or @p value if it is already a u32
+    ir::Value* CastToU32(ir::Value* value) {
+        if (value->Type()->is_unsigned_integer_scalar_or_vector()) {
+            return value;
+        }
+
+        const type::Type* type = ty.u32();
+        if (auto* vec = value->Type()->As<type::Vector>()) {
+            type = ty.vec(type, vec->Width());
+        }
+        return b.Convert(type, value)->Result();
+    }
+
     /// Clamp operand @p op_idx of @p inst to ensure it is within @p limit.
     /// @param inst the instruction
     /// @param op_idx the index of the operand that should be clamped
@@ -154,13 +189,8 @@
             clamped_idx = b.Constant(u32(std::min(const_idx->Value()->ValueAs<uint32_t>(),
                                                   const_limit->Value()->ValueAs<uint32_t>())));
         } else {
-            // Convert the index to u32 if needed.
-            if (idx->Type()->is_signed_integer_scalar()) {
-                idx = b.Convert(ty.u32(), idx)->Result();
-            }
-
             // Clamp it to the dynamic limit.
-            clamped_idx = b.Call(ty.u32(), core::Function::kMin, idx, limit)->Result();
+            clamped_idx = b.Call(ty.u32(), core::Function::kMin, CastToU32(idx), limit)->Result();
         }
 
         // Replace the index operand with the clamped version.
@@ -219,6 +249,81 @@
                              : type->Elements().type;
         }
     }
+
+    /// Clamp the indices and coordinates of a texture builtin call instruction to ensure they are
+    /// within the limits of the texture that they are accessing.
+    /// @param call the texture builtin call instruction
+    void ClampTextureCallArgs(ir::CoreBuiltinCall* call) {
+        const auto& args = call->Args();
+        auto* texture = args[0]->Type()->As<type::Texture>();
+
+        // Helper for clamping the level argument.
+        // Keep hold of the clamped value to use for clamping the coordinates.
+        Value* clamped_level = nullptr;
+        auto clamp_level = [&](uint32_t idx) {
+            auto* num_levels = b.Call(ty.u32(), core::Function::kTextureNumLevels, args[0]);
+            auto* limit = b.Subtract(ty.u32(), num_levels, 1_u);
+            clamped_level =
+                b.Call(ty.u32(), core::Function::kMin, CastToU32(args[idx]), limit)->Result();
+            call->SetOperand(CoreBuiltinCall::kArgsOperandOffset + idx, clamped_level);
+        };
+
+        // Helper for clamping the coordinates.
+        auto clamp_coords = [&](uint32_t idx) {
+            const type::Type* type = ty.u32();
+            auto* one = b.Constant(1_u);
+            if (auto* vec = args[idx]->Type()->As<type::Vector>()) {
+                type = ty.vec(type, vec->Width());
+                one = b.Splat(type, one, vec->Width());
+            }
+            auto* dims = clamped_level ? b.Call(type, core::Function::kTextureDimensions, args[0],
+                                                clamped_level)
+                                       : b.Call(type, core::Function::kTextureDimensions, args[0]);
+            auto* limit = b.Subtract(type, dims, one);
+            call->SetOperand(
+                CoreBuiltinCall::kArgsOperandOffset + idx,
+                b.Call(type, core::Function::kMin, CastToU32(args[idx]), limit)->Result());
+        };
+
+        // Helper for clamping the array index.
+        auto clamp_array_index = [&](uint32_t idx) {
+            auto* num_layers = b.Call(ty.u32(), core::Function::kTextureNumLayers, args[0]);
+            auto* limit = b.Subtract(ty.u32(), num_layers, 1_u);
+            call->SetOperand(
+                CoreBuiltinCall::kArgsOperandOffset + idx,
+                b.Call(ty.u32(), core::Function::kMin, CastToU32(args[idx]), limit)->Result());
+        };
+
+        // Select which arguments to clamp based on the function overload.
+        switch (call->Func()) {
+            case core::Function::kTextureDimensions: {
+                if (args.Length() > 1) {
+                    clamp_level(1u);
+                }
+                break;
+            }
+            case core::Function::kTextureLoad: {
+                clamp_coords(1u);
+                uint32_t next_arg = 2u;
+                if (type::IsTextureArray(texture->dim())) {
+                    clamp_array_index(next_arg++);
+                }
+                if (texture->IsAnyOf<type::SampledTexture, type::DepthTexture>()) {
+                    clamp_level(next_arg++);
+                }
+                break;
+            }
+            case core::Function::kTextureStore: {
+                clamp_coords(1u);
+                if (type::IsTextureArray(texture->dim())) {
+                    clamp_array_index(2u);
+                }
+                break;
+            }
+            default:
+                break;
+        }
+    }
 };
 
 }  // namespace
diff --git a/src/tint/lang/core/ir/transform/robustness_test.cc b/src/tint/lang/core/ir/transform/robustness_test.cc
index 6bda7fd..6bb4be0 100644
--- a/src/tint/lang/core/ir/transform/robustness_test.cc
+++ b/src/tint/lang/core/ir/transform/robustness_test.cc
@@ -18,8 +18,14 @@
 
 #include "src/tint/lang/core/ir/transform/helper_test.h"
 #include "src/tint/lang/core/type/array.h"
+#include "src/tint/lang/core/type/depth_multisampled_texture.h"
+#include "src/tint/lang/core/type/depth_texture.h"
+#include "src/tint/lang/core/type/external_texture.h"
 #include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/multisampled_texture.h"
 #include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/sampled_texture.h"
+#include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/struct.h"
 #include "src/tint/lang/core/type/vector.h"
 
@@ -1870,5 +1876,1711 @@
     EXPECT_EQ(GetParam() ? expect : src, str());
 }
 
+////////////////////////////////////////////////////////////////
+// Test clamping texture builtin calls.
+////////////////////////////////////////////////////////////////
+
+TEST_P(IR_RobustnessTest, TextureDimensions) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    auto* func = b.Function("foo", ty.vec2<u32>());
+    b.Append(func->Block(), [&] {
+        auto* handle = b.Load(texture);
+        auto* dims = b.Call(ty.vec2<u32>(), core::Function::kTextureDimensions, handle);
+        b.Return(func, dims);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%foo = func():vec2<u32> -> %b2 {
+  %b2 = block {
+    %3:texture_2d<f32> = load %texture
+    %4:vec2<u32> = textureDimensions %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureDimensions_WithLevel) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    auto* func = b.Function("foo", ty.vec2<u32>());
+    auto* level = b.FunctionParam("level", ty.u32());
+    func->SetParams({level});
+    b.Append(func->Block(), [&] {
+        auto* handle = b.Load(texture);
+        auto* dims = b.Call(ty.vec2<u32>(), core::Function::kTextureDimensions, handle, level);
+        b.Return(func, dims);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%foo = func(%level:u32):vec2<u32> -> %b2 {
+  %b2 = block {
+    %4:texture_2d<f32> = load %texture
+    %5:vec2<u32> = textureDimensions %4, %level
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%foo = func(%level:u32):vec2<u32> -> %b2 {
+  %b2 = block {
+    %4:texture_2d<f32> = load %texture
+    %5:u32 = textureNumLevels %4
+    %6:u32 = sub %5, 1u
+    %7:u32 = min %level, %6
+    %8:vec2<u32> = textureDimensions %4, %7
+    ret %8
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled1D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k1d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.i32());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.u32());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_1d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_1d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_1d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_1d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_1d<f32> = load %texture
+    %6:u32 = textureDimensions %5
+    %7:u32 = sub %6, 1u
+    %8:u32 = convert %coords
+    %9:u32 = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_1d<f32> = load %texture
+    %19:u32 = textureDimensions %18
+    %20:u32 = sub %19, 1u
+    %21:u32 = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled2D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_2d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_2d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_2d<f32> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_2d<f32> = load %texture
+    %19:vec2<u32> = textureDimensions %18
+    %20:vec2<u32> = sub %19, vec2<u32>(1u)
+    %21:vec2<u32> = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled2DArray) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()),
+               read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d_array<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %6:texture_2d_array<f32> = load %texture
+    %7:vec4<f32> = textureLoad %6, %coords, %layer, %level
+    ret %7
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %12:texture_2d_array<f32> = load %texture
+    %13:vec4<f32> = textureLoad %12, %coords_1, %layer_1, %level_1
+    ret %13
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d_array<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %6:texture_2d_array<f32> = load %texture
+    %7:vec2<u32> = textureDimensions %6
+    %8:vec2<u32> = sub %7, vec2<u32>(1u)
+    %9:vec2<u32> = convert %coords
+    %10:vec2<u32> = min %9, %8
+    %11:u32 = textureNumLayers %6
+    %12:u32 = sub %11, 1u
+    %13:u32 = convert %layer
+    %14:u32 = min %13, %12
+    %15:u32 = textureNumLevels %6
+    %16:u32 = sub %15, 1u
+    %17:u32 = convert %level
+    %18:u32 = min %17, %16
+    %19:vec4<f32> = textureLoad %6, %10, %14, %18
+    ret %19
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %24:texture_2d_array<f32> = load %texture
+    %25:vec2<u32> = textureDimensions %24
+    %26:vec2<u32> = sub %25, vec2<u32>(1u)
+    %27:vec2<u32> = min %coords_1, %26
+    %28:u32 = textureNumLayers %24
+    %29:u32 = sub %28, 1u
+    %30:u32 = min %layer_1, %29
+    %31:u32 = textureNumLevels %24
+    %32:u32 = sub %31, 1u
+    %33:u32 = min %level_1, %32
+    %34:vec4<f32> = textureLoad %24, %27, %30, %33
+    ret %34
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled3D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k3d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_3d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_3d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_3d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_3d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_3d<f32> = load %texture
+    %6:vec3<u32> = textureDimensions %5
+    %7:vec3<u32> = sub %6, vec3<u32>(1u)
+    %8:vec3<u32> = convert %coords
+    %9:vec3<u32> = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_3d<f32> = load %texture
+    %19:vec3<u32> = textureDimensions %18
+    %20:vec3<u32> = sub %19, vec3<u32>(1u)
+    %21:vec3<u32> = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Multisampled2D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::MultisampledTexture>(type::TextureDimension::k2d, ty.f32()),
+               read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_multisampled_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_multisampled_2d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_multisampled_2d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_multisampled_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_multisampled_2d<f32> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:vec4<f32> = textureLoad %5, %9, %level
+    ret %10
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %14:texture_multisampled_2d<f32> = load %texture
+    %15:vec2<u32> = textureDimensions %14
+    %16:vec2<u32> = sub %15, vec2<u32>(1u)
+    %17:vec2<u32> = min %coords_1, %16
+    %18:vec4<f32> = textureLoad %14, %17, %level_1
+    ret %18
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Depth2D) {
+    auto* texture = b.Var(
+        "texture", ty.ptr(handle, ty.Get<type::DepthTexture>(type::TextureDimension::k2d), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_2d = load %texture
+    %6:f32 = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_depth_2d = load %texture
+    %11:f32 = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_2d = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:f32 = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_depth_2d = load %texture
+    %19:vec2<u32> = textureDimensions %18
+    %20:vec2<u32> = sub %19, vec2<u32>(1u)
+    %21:vec2<u32> = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:f32 = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Depth2DArray) {
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle, ty.Get<type::DepthTexture>(type::TextureDimension::k2dArray), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d_array, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %6:texture_depth_2d_array = load %texture
+    %7:f32 = textureLoad %6, %coords, %layer, %level
+    ret %7
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %12:texture_depth_2d_array = load %texture
+    %13:f32 = textureLoad %12, %coords_1, %layer_1, %level_1
+    ret %13
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d_array, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %6:texture_depth_2d_array = load %texture
+    %7:vec2<u32> = textureDimensions %6
+    %8:vec2<u32> = sub %7, vec2<u32>(1u)
+    %9:vec2<u32> = convert %coords
+    %10:vec2<u32> = min %9, %8
+    %11:u32 = textureNumLayers %6
+    %12:u32 = sub %11, 1u
+    %13:u32 = convert %layer
+    %14:u32 = min %13, %12
+    %15:u32 = textureNumLevels %6
+    %16:u32 = sub %15, 1u
+    %17:u32 = convert %level
+    %18:u32 = min %17, %16
+    %19:f32 = textureLoad %6, %10, %14, %18
+    ret %19
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %24:texture_depth_2d_array = load %texture
+    %25:vec2<u32> = textureDimensions %24
+    %26:vec2<u32> = sub %25, vec2<u32>(1u)
+    %27:vec2<u32> = min %coords_1, %26
+    %28:u32 = textureNumLayers %24
+    %29:u32 = sub %28, 1u
+    %30:u32 = min %layer_1, %29
+    %31:u32 = textureNumLevels %24
+    %32:u32 = sub %31, 1u
+    %33:u32 = min %level_1, %32
+    %34:f32 = textureLoad %24, %27, %30, %33
+    ret %34
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_DepthMultisampled2D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::DepthMultisampledTexture>(type::TextureDimension::k2d), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* index = b.FunctionParam("index", ty.i32());
+        func->SetParams({coords, index});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, index);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* index = b.FunctionParam("index", ty.u32());
+        func->SetParams({coords, index});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, index);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_multisampled_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %index:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_multisampled_2d = load %texture
+    %6:f32 = textureLoad %5, %coords, %index
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %index_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %index_1: 'index'
+  %b3 = block {
+    %10:texture_depth_multisampled_2d = load %texture
+    %11:f32 = textureLoad %10, %coords_1, %index_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_multisampled_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %index:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_multisampled_2d = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:f32 = textureLoad %5, %9, %index
+    ret %10
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %index_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %index_1: 'index'
+  %b3 = block {
+    %14:texture_depth_multisampled_2d = load %texture
+    %15:vec2<u32> = textureDimensions %14
+    %16:vec2<u32> = sub %15, vec2<u32>(1u)
+    %17:vec2<u32> = min %coords_1, %16
+    %18:f32 = textureLoad %14, %17, %index_1
+    ret %18
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_External) {
+    auto* texture = b.Var("texture", ty.ptr(handle, ty.Get<type::ExternalTexture>(), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_external, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_external = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_external = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_external, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_external = load %texture
+    %5:vec2<u32> = textureDimensions %4
+    %6:vec2<u32> = sub %5, vec2<u32>(1u)
+    %7:vec2<u32> = convert %coords
+    %8:vec2<u32> = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_external = load %texture
+    %13:vec2<u32> = textureDimensions %12
+    %14:vec2<u32> = sub %13, vec2<u32>(1u)
+    %15:vec2<u32> = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage1D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k1d, format, read_write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.i32());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.u32());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %5:u32 = textureDimensions %4
+    %6:u32 = sub %5, 1u
+    %7:u32 = convert %coords
+    %8:u32 = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %13:u32 = textureDimensions %12
+    %14:u32 = sub %13, 1u
+    %15:u32 = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage2D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k2d, format, read_write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %5:vec2<u32> = textureDimensions %4
+    %6:vec2<u32> = sub %5, vec2<u32>(1u)
+    %7:vec2<u32> = convert %coords
+    %8:vec2<u32> = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %13:vec2<u32> = textureDimensions %12
+    %14:vec2<u32> = sub %13, vec2<u32>(1u)
+    %15:vec2<u32> = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage2DArray) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle,
+               ty.Get<type::StorageTexture>(type::TextureDimension::k2dArray, format, read_write,
+                                            type::StorageTexture::SubtypeFor(format, ty)),
+               read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        func->SetParams({coords, layer});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        func->SetParams({coords, layer});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %layer
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer'
+  %b3 = block {
+    %10:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %layer_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:u32 = textureNumLayers %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %layer
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer'
+  %b3 = block {
+    %18:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %19:vec2<u32> = textureDimensions %18
+    %20:vec2<u32> = sub %19, vec2<u32>(1u)
+    %21:vec2<u32> = min %coords_1, %20
+    %22:u32 = textureNumLayers %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %layer_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage3D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k3d, format, read_write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<i32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<u32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %5:vec3<u32> = textureDimensions %4
+    %6:vec3<u32> = sub %5, vec3<u32>(1u)
+    %7:vec3<u32> = convert %coords
+    %8:vec3<u32> = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %13:vec3<u32> = textureDimensions %12
+    %14:vec3<u32> = sub %13, vec3<u32>(1u)
+    %15:vec3<u32> = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage1D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k1d, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.i32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.u32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_1d<rgba8unorm, write> = load %texture
+    %6:vec4<f32> = textureStore %5, %coords, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %10:texture_storage_1d<rgba8unorm, write> = load %texture
+    %11:vec4<f32> = textureStore %10, %coords_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_1d<rgba8unorm, write> = load %texture
+    %6:u32 = textureDimensions %5
+    %7:u32 = sub %6, 1u
+    %8:u32 = convert %coords
+    %9:u32 = min %8, %7
+    %10:vec4<f32> = textureStore %5, %9, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %14:texture_storage_1d<rgba8unorm, write> = load %texture
+    %15:u32 = textureDimensions %14
+    %16:u32 = sub %15, 1u
+    %17:u32 = min %coords_1, %16
+    %18:vec4<f32> = textureStore %14, %17, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage2D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k2d, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d<rgba8unorm, write> = load %texture
+    %6:vec4<f32> = textureStore %5, %coords, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %10:texture_storage_2d<rgba8unorm, write> = load %texture
+    %11:vec4<f32> = textureStore %10, %coords_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d<rgba8unorm, write> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:vec4<f32> = textureStore %5, %9, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %14:texture_storage_2d<rgba8unorm, write> = load %texture
+    %15:vec2<u32> = textureDimensions %14
+    %16:vec2<u32> = sub %15, vec2<u32>(1u)
+    %17:vec2<u32> = min %coords_1, %16
+    %18:vec4<f32> = textureStore %14, %17, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage2DArray) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k2dArray, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, layer, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, layer, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, layer, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, layer, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %6:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %7:vec4<f32> = textureStore %6, %coords, %layer, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %value_1: 'value'
+  %b3 = block {
+    %12:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %13:vec4<f32> = textureStore %12, %coords_1, %layer_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %6:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %7:vec2<u32> = textureDimensions %6
+    %8:vec2<u32> = sub %7, vec2<u32>(1u)
+    %9:vec2<u32> = convert %coords
+    %10:vec2<u32> = min %9, %8
+    %11:u32 = textureNumLayers %6
+    %12:u32 = sub %11, 1u
+    %13:u32 = convert %layer
+    %14:u32 = min %13, %12
+    %15:vec4<f32> = textureStore %6, %10, %14, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %value_1: 'value'
+  %b3 = block {
+    %20:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %21:vec2<u32> = textureDimensions %20
+    %22:vec2<u32> = sub %21, vec2<u32>(1u)
+    %23:vec2<u32> = min %coords_1, %22
+    %24:u32 = textureNumLayers %20
+    %25:u32 = sub %24, 1u
+    %26:u32 = min %layer_1, %25
+    %27:vec4<f32> = textureStore %20, %23, %26, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage3D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k3d, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec3<i32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec3<u32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_3d<rgba8unorm, write> = load %texture
+    %6:vec4<f32> = textureStore %5, %coords, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %10:texture_storage_3d<rgba8unorm, write> = load %texture
+    %11:vec4<f32> = textureStore %10, %coords_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_3d<rgba8unorm, write> = load %texture
+    %6:vec3<u32> = textureDimensions %5
+    %7:vec3<u32> = sub %6, vec3<u32>(1u)
+    %8:vec3<u32> = convert %coords
+    %9:vec3<u32> = min %8, %7
+    %10:vec4<f32> = textureStore %5, %9, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %14:texture_storage_3d<rgba8unorm, write> = load %texture
+    %15:vec3<u32> = textureDimensions %14
+    %16:vec3<u32> = sub %15, vec3<u32>(1u)
+    %17:vec3<u32> = min %coords_1, %16
+    %18:vec4<f32> = textureStore %14, %17, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
 }  // namespace
 }  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/spirv/writer/common/helper_test.h b/src/tint/lang/spirv/writer/common/helper_test.h
index b1e7f5c..5e727a9 100644
--- a/src/tint/lang/spirv/writer/common/helper_test.h
+++ b/src/tint/lang/spirv/writer/common/helper_test.h
@@ -100,9 +100,10 @@
 
     /// Run the specified writer on the IR module and validate the result.
     /// @param writer the writer to use for SPIR-V generation
+    /// @param options the optional writer options to use when raising the IR
     /// @returns true if generation and validation succeeded
-    bool Generate(Printer& writer) {
-        auto raised = raise::Raise(&mod, {});
+    bool Generate(Printer& writer, Options options = {}) {
+        auto raised = raise::Raise(&mod, options);
         if (!raised) {
             err_ = raised.Failure();
             return false;
@@ -126,8 +127,9 @@
     }
 
     /// Run the writer on the IR module and validate the result.
+    /// @param options the optional writer options to use when raising the IR
     /// @returns true if generation and validation succeeded
-    bool Generate() { return Generate(writer_); }
+    bool Generate(Options options = {}) { return Generate(writer_, options); }
 
     /// Validate the generated SPIR-V using the SPIR-V Tools Validator.
     /// @param binary the SPIR-V binary module to validate
diff --git a/src/tint/lang/spirv/writer/texture_builtin_test.cc b/src/tint/lang/spirv/writer/texture_builtin_test.cc
index c257bf7..1b228ec 100644
--- a/src/tint/lang/spirv/writer/texture_builtin_test.cc
+++ b/src/tint/lang/spirv/writer/texture_builtin_test.cc
@@ -191,7 +191,9 @@
             }
         });
 
-        ASSERT_TRUE(Generate()) << Error() << output_;
+        Options options;
+        options.disable_image_robustness = true;
+        ASSERT_TRUE(Generate(options)) << Error() << output_;
         for (auto& inst : params.instructions) {
             EXPECT_INST(inst);
         }
@@ -1928,12 +1930,103 @@
         b.Return(func);
     });
 
-    ASSERT_TRUE(Generate()) << Error() << output_;
+    Options options;
+    options.disable_image_robustness = true;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
     EXPECT_INST(R"(
          %13 = OpVectorShuffle %v4float %value %value 2 1 0 3
                OpImageWrite %texture %coords %13 None
 )");
 }
 
+////////////////////////////////////////////////////////////////
+//// Texture robustness enabled.
+////////////////////////////////////////////////////////////////
+
+TEST_F(SpirvWriterTest, TextureDimensions_WithRobustness) {
+    auto* texture_ty =
+        ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+
+    auto* texture = b.FunctionParam("texture", texture_ty);
+    auto* level = b.FunctionParam("level", ty.i32());
+    auto* func = b.Function("foo", ty.vec2<u32>());
+    func->SetParams({texture, level});
+    b.Append(func->Block(), [&] {
+        auto* dims = b.Call(ty.vec2<u32>(), core::Function::kTextureDimensions, texture, level);
+        b.Return(func, dims);
+        mod.SetName(dims, "dims");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+         %11 = OpImageQueryLevels %uint %texture
+         %12 = OpISub %uint %11 %uint_1
+         %14 = OpBitcast %uint %level
+         %15 = OpExtInst %uint %16 UMin %14 %12
+       %dims = OpImageQuerySizeLod %v2uint %texture %15
+)");
+}
+
+TEST_F(SpirvWriterTest, TextureLoad_WithRobustness) {
+    auto* texture_ty =
+        ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+
+    auto* texture = b.FunctionParam("texture", texture_ty);
+    auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+    auto* level = b.FunctionParam("level", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({texture, coords, level});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, texture, coords, level);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+         %13 = OpImageQuerySizeLod %v2uint %texture %uint_0
+         %15 = OpISub %v2uint %13 %16
+         %18 = OpExtInst %v2uint %19 UMin %coords %15
+         %20 = OpImageQueryLevels %uint %texture
+         %21 = OpISub %uint %20 %uint_1
+         %22 = OpBitcast %uint %level
+         %23 = OpExtInst %uint %19 UMin %22 %21
+     %result = OpImageFetch %v4float %texture %18 Lod %23
+)");
+}
+
+TEST_F(SpirvWriterTest, TextureStore_WithRobustness) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture_ty = ty.Get<core::type::StorageTexture>(
+        core::type::TextureDimension::k2dArray, format, core::Access::kWrite,
+        core::type::StorageTexture::SubtypeFor(format, ty));
+
+    auto* texture = b.FunctionParam("texture", texture_ty);
+    auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+    auto* layer = b.FunctionParam("layer", ty.i32());
+    auto* value = b.FunctionParam("value", ty.vec4<f32>());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({texture, coords, layer, value});
+    b.Append(func->Block(), [&] {
+        b.Call(ty.void_(), core::Function::kTextureStore, texture, coords, layer, value);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+         %15 = OpImageQuerySize %v3uint %texture
+         %17 = OpVectorShuffle %v2uint %15 %15 0 1
+         %18 = OpISub %v2uint %17 %19
+         %21 = OpExtInst %v2uint %22 UMin %coords %18
+         %23 = OpImageQuerySize %v3uint %texture
+         %24 = OpCompositeExtract %uint %23 2
+         %25 = OpISub %uint %24 %uint_1
+         %26 = OpBitcast %uint %layer
+         %27 = OpExtInst %uint %22 UMin %26 %25
+         %28 = OpCompositeConstruct %v3uint %21 %27
+               OpImageWrite %texture %28 %value None
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/robustness_test.cc b/src/tint/lang/wgsl/ast/transform/robustness_test.cc
index c2d3fce..e2074ef 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness_test.cc
@@ -1788,7 +1788,7 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-// Texture
+// Texture builtin calls.
 ////////////////////////////////////////////////////////////////////////////////
 
 TEST_P(RobustnessTest, TextureDimensions) {