[ir][spirv-writer] Handle textureLoad builtin

This maps to OpImageFetch. Refactor some logic from TextureSample that
can be reused for TextureLoad.

Bug: tint:1906
Change-Id: Ie1ea9d707b43eee9bff0db5cc4ef9367305028f8
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/141541
Reviewed-by: Ben Clayton <bclayton@google.com>
Auto-Submit: James Price <jrprice@google.com>
Commit-Queue: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
diff --git a/src/tint/ir/intrinsic_call.cc b/src/tint/ir/intrinsic_call.cc
index 81a4f4e..6a34103 100644
--- a/src/tint/ir/intrinsic_call.cc
+++ b/src/tint/ir/intrinsic_call.cc
@@ -76,6 +76,9 @@
         case IntrinsicCall::Kind::kSpirvDot:
             out << "spirv.dot";
             break;
+        case IntrinsicCall::Kind::kSpirvImageFetch:
+            out << "spirv.image_fetch";
+            break;
         case IntrinsicCall::Kind::kSpirvImageSampleImplicitLod:
             out << "spirv.image_sample_implicit_lod";
             break;
diff --git a/src/tint/ir/intrinsic_call.h b/src/tint/ir/intrinsic_call.h
index 7d667ca..674f8bf 100644
--- a/src/tint/ir/intrinsic_call.h
+++ b/src/tint/ir/intrinsic_call.h
@@ -43,6 +43,7 @@
         kSpirvAtomicUMin,
         kSpirvAtomicXor,
         kSpirvDot,
+        kSpirvImageFetch,
         kSpirvImageSampleImplicitLod,
         kSpirvImageSampleExplicitLod,
         kSpirvImageSampleDrefImplicitLod,
diff --git a/src/tint/ir/transform/builtin_polyfill_spirv.cc b/src/tint/ir/transform/builtin_polyfill_spirv.cc
index 274cfb3..9865126 100644
--- a/src/tint/ir/transform/builtin_polyfill_spirv.cc
+++ b/src/tint/ir/transform/builtin_polyfill_spirv.cc
@@ -22,6 +22,7 @@
 #include "src/tint/type/builtin_structs.h"
 #include "src/tint/type/depth_multisampled_texture.h"
 #include "src/tint/type/depth_texture.h"
+#include "src/tint/type/multisampled_texture.h"
 #include "src/tint/type/sampled_texture.h"
 #include "src/tint/type/texture.h"
 
@@ -71,6 +72,7 @@
                     case builtin::Function::kAtomicXor:
                     case builtin::Function::kDot:
                     case builtin::Function::kSelect:
+                    case builtin::Function::kTextureLoad:
                     case builtin::Function::kTextureSample:
                     case builtin::Function::kTextureSampleBias:
                     case builtin::Function::kTextureSampleCompare:
@@ -108,6 +110,9 @@
                 case builtin::Function::kSelect:
                     replacement = Select(builtin);
                     break;
+                case builtin::Function::kTextureLoad:
+                    replacement = TextureLoad(builtin);
+                    break;
                 case builtin::Function::kTextureSample:
                 case builtin::Function::kTextureSampleBias:
                 case builtin::Function::kTextureSampleCompare:
@@ -302,6 +307,94 @@
         return call->Result();
     }
 
+    /// ImageOperands represents the optional image operands for an image instruction.
+    struct ImageOperands {
+        /// Bias
+        Value* bias = nullptr;
+        /// Lod
+        Value* lod = nullptr;
+        /// Grad (dx)
+        Value* ddx = nullptr;
+        /// Grad (dy)
+        Value* ddy = nullptr;
+        /// ConstOffset
+        Value* offset = nullptr;
+        /// Sample
+        Value* sample = nullptr;
+    };
+
+    /// Append optional image operands to an image intrinsic argument list.
+    /// @param operands the operands
+    /// @param args the argument list
+    /// @param insertion_point the insertion point for new instructions
+    /// @param requires_float_lod true if the lod needs to be a floating point value
+    void AppendImageOperands(ImageOperands& operands,
+                             utils::Vector<Value*, 8>& args,
+                             Instruction* insertion_point,
+                             bool requires_float_lod) {
+        // Add a placeholder argument for the image operand mask, which we will fill in when we have
+        // processed the image operands.
+        uint32_t image_operand_mask = 0u;
+        size_t mask_idx = args.Length();
+        args.Push(nullptr);
+
+        // Add each of the optional image operands if used, updating the image operand mask.
+        if (operands.bias) {
+            image_operand_mask |= SpvImageOperandsBiasMask;
+            args.Push(operands.bias);
+        }
+        if (operands.lod) {
+            image_operand_mask |= SpvImageOperandsLodMask;
+            if (requires_float_lod && operands.lod->Type()->is_integer_scalar()) {
+                auto* convert = b.Convert(ty.f32(), operands.lod);
+                convert->InsertBefore(insertion_point);
+                operands.lod = convert->Result();
+            }
+            args.Push(operands.lod);
+        }
+        if (operands.ddx) {
+            image_operand_mask |= SpvImageOperandsGradMask;
+            args.Push(operands.ddx);
+            args.Push(operands.ddy);
+        }
+        if (operands.offset) {
+            image_operand_mask |= SpvImageOperandsConstOffsetMask;
+            args.Push(operands.offset);
+        }
+        if (operands.sample) {
+            image_operand_mask |= SpvImageOperandsSampleMask;
+            args.Push(operands.sample);
+        }
+
+        // Replace the image operand mask with the final mask value, as a literal operand.
+        auto* literal = ir->constant_values.Get(u32(image_operand_mask));
+        args[mask_idx] = ir->values.Create<LiteralOperand>(literal);
+    }
+
+    /// Append an array index to a coordinate vector.
+    /// @param coords the coordinate vector
+    /// @param array_idx the array index
+    /// @param insertion_point the insertion point for new instructions
+    /// @returns the modified coordinate vector
+    Value* AppendArrayIndex(Value* coords, Value* array_idx, Instruction* insertion_point) {
+        auto* vec = coords->Type()->As<type::Vector>();
+        auto* element_ty = vec->type();
+
+        // Convert the index to match the coordinate type if needed.
+        if (array_idx->Type() != element_ty) {
+            auto* array_idx_converted = b.Convert(element_ty, array_idx);
+            array_idx_converted->InsertBefore(insertion_point);
+            array_idx = array_idx_converted->Result();
+        }
+
+        // Construct a new coordinate vector.
+        auto num_coords = vec->Width();
+        auto* coord_ty = ty.vec(element_ty, num_coords + 1);
+        auto* construct = b.Construct(coord_ty, utils::Vector{coords, array_idx});
+        construct->InsertBefore(insertion_point);
+        return construct->Result();
+    }
+
     /// Handle a textureSample*() builtin.
     /// @param builtin the builtin call instruction
     /// @returns the replacement value
@@ -316,8 +409,6 @@
         auto* sampler = next_arg();
         auto* coords = next_arg();
         auto* texture_ty = texture->Type()->As<type::Texture>();
-        auto* array_idx = IsTextureArray(texture_ty->dim()) ? next_arg() : nullptr;
-        Value* depth = nullptr;
 
         // Use OpSampledImage to create an OpTypeSampledImage object.
         auto* sampled_image =
@@ -326,29 +417,15 @@
         sampled_image->InsertBefore(builtin);
 
         // Append the array index to the coordinates if provided.
+        auto* array_idx = IsTextureArray(texture_ty->dim()) ? next_arg() : nullptr;
         if (array_idx) {
-            // Convert the index to an f32.
-            auto* array_idx_f32 = b.Convert(ty.f32(), array_idx);
-            array_idx_f32->InsertBefore(builtin);
-
-            // Construct a new coordinate vector.
-            auto num_coords = coords->Type()->As<type::Vector>()->Width();
-            auto* coord_ty = ty.vec(ty.f32(), num_coords + 1);
-            auto* construct = b.Construct(coord_ty, utils::Vector{coords, array_idx_f32->Result()});
-            construct->InsertBefore(builtin);
-            coords = construct->Result();
+            coords = AppendArrayIndex(coords, array_idx, builtin);
         }
 
         // Determine which SPIR-V intrinsic to use and which optional image operands are needed.
         enum IntrinsicCall::Kind intrinsic;
-        struct ImageOperands {
-            Value* bias = nullptr;
-            Value* lod = nullptr;
-            Value* ddx = nullptr;
-            Value* ddy = nullptr;
-            Value* offset = nullptr;
-            Value* sample = nullptr;
-        } operands;
+        Value* depth = nullptr;
+        ImageOperands operands;
         switch (builtin->Func()) {
             case builtin::Function::kTextureSample:
                 intrinsic = IntrinsicCall::Kind::kSpirvImageSampleImplicitLod;
@@ -395,44 +472,8 @@
             intrinsic_args.Push(depth);
         }
 
-        // Add a placeholder argument for the image operand mask, which we'll fill in when we've
-        // processed the image operands.
-        uint32_t image_operand_mask = 0u;
-        size_t mask_idx = intrinsic_args.Length();
-        intrinsic_args.Push(nullptr);
-
-        // Add each of the optional image operands if used, updating the image operand mask.
-        if (operands.bias) {
-            image_operand_mask |= SpvImageOperandsBiasMask;
-            intrinsic_args.Push(operands.bias);
-        }
-        if (operands.lod) {
-            image_operand_mask |= SpvImageOperandsLodMask;
-            if (operands.lod->Type()->is_integer_scalar()) {
-                // Some builtins take the lod as an integer, but SPIR-V always requires an f32.
-                auto* convert = b.Convert(ty.f32(), operands.lod);
-                convert->InsertBefore(builtin);
-                operands.lod = convert->Result();
-            }
-            intrinsic_args.Push(operands.lod);
-        }
-        if (operands.ddx) {
-            image_operand_mask |= SpvImageOperandsGradMask;
-            intrinsic_args.Push(operands.ddx);
-            intrinsic_args.Push(operands.ddy);
-        }
-        if (operands.offset) {
-            image_operand_mask |= SpvImageOperandsConstOffsetMask;
-            intrinsic_args.Push(operands.offset);
-        }
-        if (operands.sample) {
-            image_operand_mask |= SpvImageOperandsSampleMask;
-            intrinsic_args.Push(operands.sample);
-        }
-
-        // Replace the image operand mask with the final mask value, as a literal operand.
-        auto* literal = ir->constant_values.Get(u32(image_operand_mask));
-        intrinsic_args[mask_idx] = ir->values.Create<LiteralOperand>(literal);
+        // Add the optional image operands, if any.
+        AppendImageOperands(operands, intrinsic_args, builtin, /* requires_float_lod */ true);
 
         // Call the intrinsic.
         // If this is a depth comparison, the result is always f32, otherwise vec4f.
@@ -452,6 +493,63 @@
 
         return result;
     }
+
+    /// Handle a textureLoad() builtin.
+    /// @param builtin the builtin call instruction
+    /// @returns the replacement value
+    Value* TextureLoad(CoreBuiltinCall* builtin) {
+        // Helper to get the next argument from the call, or nullptr if there are no more arguments.
+        uint32_t arg_idx = 0;
+        auto next_arg = [&]() {
+            return arg_idx < builtin->Args().Length() ? builtin->Args()[arg_idx++] : nullptr;
+        };
+
+        auto* texture = next_arg();
+        auto* coords = next_arg();
+        auto* texture_ty = texture->Type()->As<type::Texture>();
+
+        // Append the array index to the coordinates if provided.
+        auto* array_idx = IsTextureArray(texture_ty->dim()) ? next_arg() : nullptr;
+        if (array_idx) {
+            coords = AppendArrayIndex(coords, array_idx, builtin);
+        }
+
+        // Start building the argument list for the intrinsic.
+        // The first two operands are always the texture and then the coordinates.
+        utils::Vector<Value*, 8> intrinsic_args;
+        intrinsic_args.Push(texture);
+        intrinsic_args.Push(coords);
+
+        // Add the optional image operands, if any.
+        ImageOperands operands;
+        if (texture_ty->IsAnyOf<type::MultisampledTexture, type::DepthMultisampledTexture>()) {
+            operands.sample = next_arg();
+        } else {
+            operands.lod = next_arg();
+        }
+        AppendImageOperands(operands, intrinsic_args, builtin, /* requires_float_lod */ false);
+
+        // Call the intrinsic.
+        // The result is always a vec4 in SPIR-V.
+        auto* result_ty = builtin->Result()->Type();
+        bool expects_scalar_result = result_ty->Is<type::Scalar>();
+        if (expects_scalar_result) {
+            result_ty = ty.vec4(result_ty);
+        }
+        auto* texture_call =
+            b.Call(result_ty, IntrinsicCall::Kind::kSpirvImageFetch, std::move(intrinsic_args));
+        texture_call->InsertBefore(builtin);
+        auto* result = texture_call->Result();
+
+        // If we are expecting a scalar result, extract the first component.
+        if (expects_scalar_result) {
+            auto* extract = b.Access(ty.f32(), result, 0_u);
+            extract->InsertBefore(builtin);
+            result = extract->Result();
+        }
+
+        return result;
+    }
 };
 
 void BuiltinPolyfillSpirv::Run(ir::Module* ir, const DataMap&, DataMap&) const {
diff --git a/src/tint/ir/transform/builtin_polyfill_spirv_test.cc b/src/tint/ir/transform/builtin_polyfill_spirv_test.cc
index e8ab19a..37c5988 100644
--- a/src/tint/ir/transform/builtin_polyfill_spirv_test.cc
+++ b/src/tint/ir/transform/builtin_polyfill_spirv_test.cc
@@ -20,6 +20,7 @@
 #include "src/tint/type/atomic.h"
 #include "src/tint/type/builtin_structs.h"
 #include "src/tint/type/depth_texture.h"
+#include "src/tint/type/multisampled_texture.h"
 #include "src/tint/type/sampled_texture.h"
 
 namespace tint::ir::transform {
@@ -897,6 +898,199 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureLoad_2D) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+    auto* lod = b.FunctionParam("lod", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, coords, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(ty.vec4<f32>(), builtin::Function::kTextureLoad, t, coords, lod);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %coords:vec2<i32>, %lod:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:vec4<f32> = textureLoad %t, %coords, %lod
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %coords:vec2<i32>, %lod:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:vec4<f32> = spirv.image_fetch %t, %coords, 2u, %lod
+    ret %5
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureLoad_2DArray) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()));
+    auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* lod = b.FunctionParam("lod", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, coords, array_idx, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result =
+            b.Call(ty.vec4<f32>(), builtin::Function::kTextureLoad, t, coords, array_idx, lod);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d_array<f32>, %coords:vec2<i32>, %array_idx:i32, %lod:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:vec4<f32> = textureLoad %t, %coords, %array_idx, %lod
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d_array<f32>, %coords:vec2<i32>, %array_idx:i32, %lod:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:vec3<i32> = construct %coords, %array_idx
+    %7:vec4<f32> = spirv.image_fetch %t, %6, 2u, %lod
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureLoad_2DArray_IndexDifferentType) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()));
+    auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.u32());
+    auto* lod = b.FunctionParam("lod", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, coords, array_idx, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result =
+            b.Call(ty.vec4<f32>(), builtin::Function::kTextureLoad, t, coords, array_idx, lod);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d_array<f32>, %coords:vec2<i32>, %array_idx:u32, %lod:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:vec4<f32> = textureLoad %t, %coords, %array_idx, %lod
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d_array<f32>, %coords:vec2<i32>, %array_idx:u32, %lod:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:i32 = convert %array_idx
+    %7:vec3<i32> = construct %coords, %6
+    %8:vec4<f32> = spirv.image_fetch %t, %7, 2u, %lod
+    ret %8
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureLoad_Multisampled2D) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::MultisampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+    auto* sample_idx = b.FunctionParam("sample_idx", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, coords, sample_idx});
+
+    b.With(func->Block(), [&] {
+        auto* result =
+            b.Call(ty.vec4<f32>(), builtin::Function::kTextureLoad, t, coords, sample_idx);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_multisampled_2d<f32>, %coords:vec2<i32>, %sample_idx:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:vec4<f32> = textureLoad %t, %coords, %sample_idx
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_multisampled_2d<f32>, %coords:vec2<i32>, %sample_idx:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:vec4<f32> = spirv.image_fetch %t, %coords, 64u, %sample_idx
+    ret %5
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureLoad_Depth2D) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2d));
+    auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+    auto* lod = b.FunctionParam("lod", ty.i32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, coords, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(ty.f32(), builtin::Function::kTextureLoad, t, coords, lod);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d, %coords:vec2<i32>, %lod:i32):f32 -> %b1 {
+  %b1 = block {
+    %5:f32 = textureLoad %t, %coords, %lod
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d, %coords:vec2<i32>, %lod:i32):f32 -> %b1 {
+  %b1 = block {
+    %5:vec4<f32> = spirv.image_fetch %t, %coords, 2u, %lod
+    %6:f32 = access %5, 0u
+    ret %6
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
 TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSample_1D) {
     auto* t =
         b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k1d, ty.f32()));
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.cc b/src/tint/writer/spirv/ir/generator_impl_ir.cc
index 95e82ee..276b4ea 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.cc
@@ -1478,6 +1478,9 @@
         case ir::IntrinsicCall::Kind::kSpirvDot:
             op = spv::Op::OpDot;
             break;
+        case ir::IntrinsicCall::Kind::kSpirvImageFetch:
+            op = spv::Op::OpImageFetch;
+            break;
         case ir::IntrinsicCall::Kind::kSpirvImageSampleImplicitLod:
             op = spv::Op::OpImageSampleImplicitLod;
             break;
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc
index f931747..d38beb0 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/writer/spirv/ir/test_helper_ir.h"
 
 #include "src/tint/builtin/function.h"
+#include "src/tint/type/depth_multisampled_texture.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -23,7 +24,9 @@
 
 enum TextureType {
     kSampledTexture,
+    kMultisampledTexture,
     kDepthTexture,
+    kDepthMultisampledTexture,
 };
 
 enum SamplerUsage {
@@ -63,9 +66,15 @@
         case kSampledTexture:
             out << "SampleTexture";
             break;
+        case kMultisampledTexture:
+            out << "MultisampleTexture";
+            break;
         case kDepthTexture:
             out << "DepthTexture";
             break;
+        case kDepthMultisampledTexture:
+            out << "DepthMultisampledTexture";
+            break;
     }
     return out;
 }
@@ -87,8 +96,12 @@
         switch (type) {
             case kSampledTexture:
                 return ty.Get<type::SampledTexture>(dim, MakeScalarType(texel_type));
+            case kMultisampledTexture:
+                return ty.Get<type::MultisampledTexture>(dim, MakeScalarType(texel_type));
             case kDepthTexture:
                 return ty.Get<type::DepthTexture>(dim);
+            case kDepthMultisampledTexture:
+                return ty.Get<type::DepthMultisampledTexture>(dim);
         }
         return nullptr;
     }
@@ -924,5 +937,124 @@
         }),
     PrintCase);
 
+////////////////////////////////////////////////////////////////
+//// textureLoad
+////////////////////////////////////////////////////////////////
+using TextureLoad = TextureBuiltinTest;
+TEST_P(TextureLoad, Emit) {
+    Run(builtin::Function::kTextureLoad, kNoSampler);
+}
+INSTANTIATE_TEST_SUITE_P(SpvGeneratorImplTest,
+                         TextureLoad,
+                         testing::Values(
+                             TextureBuiltinTestCase{
+                                 kSampledTexture,
+                                 type::TextureDimension::k1d,
+                                 /* texel type */ kF32,
+                                 {{"coord", 1, kI32}, {"lod", 1, kI32}},
+                                 {"result", 4, kF32},
+                                 {
+                                     "OpImageFetch %v4float %t %coord Lod %lod",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kSampledTexture,
+                                 type::TextureDimension::k2d,
+                                 /* texel type */ kF32,
+                                 {{"coords", 2, kI32}, {"lod", 1, kI32}},
+                                 {"result", 4, kF32},
+                                 {
+                                     "OpImageFetch %v4float %t %coords Lod %lod",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kSampledTexture,
+                                 type::TextureDimension::k2dArray,
+                                 /* texel type */ kF32,
+                                 {{"coords", 2, kI32}, {"array_idx", 1, kI32}, {"lod", 1, kI32}},
+                                 {"result", 4, kF32},
+                                 {
+                                     "%10 = OpCompositeConstruct %v3int %coords %array_idx",
+                                     "OpImageFetch %v4float %t %10 Lod %lod",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kSampledTexture,
+                                 type::TextureDimension::k3d,
+                                 /* texel type */ kF32,
+                                 {{"coords", 3, kI32}, {"lod", 1, kI32}},
+                                 {"result", 4, kF32},
+                                 {
+                                     "OpImageFetch %v4float %t %coords Lod %lod",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kMultisampledTexture,
+                                 type::TextureDimension::k2d,
+                                 /* texel type */ kF32,
+                                 {{"coords", 2, kI32}, {"sample_idx", 1, kI32}},
+                                 {"result", 4, kF32},
+                                 {
+                                     "OpImageFetch %v4float %t %coords Sample %sample_idx",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kDepthTexture,
+                                 type::TextureDimension::k2d,
+                                 /* texel type */ kF32,
+                                 {{"coords", 2, kI32}, {"lod", 1, kI32}},
+                                 {"result", 1, kF32},
+                                 {
+                                     "OpImageFetch %v4float %t %coords Lod %lod",
+                                     "%result = OpCompositeExtract %float",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kDepthTexture,
+                                 type::TextureDimension::k2dArray,
+                                 /* texel type */ kF32,
+                                 {{"coords", 2, kI32}, {"array_idx", 1, kI32}, {"lod", 1, kI32}},
+                                 {"result", 1, kF32},
+                                 {
+                                     "%9 = OpCompositeConstruct %v3int %coords %array_idx",
+                                     "OpImageFetch %v4float %t %9 Lod %lod",
+                                     "%result = OpCompositeExtract %float",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kDepthMultisampledTexture,
+                                 type::TextureDimension::k2d,
+                                 /* texel type */ kF32,
+                                 {{"coords", 3, kI32}, {"sample_idx", 1, kI32}},
+                                 {"result", 1, kF32},
+                                 {
+                                     "OpImageFetch %v4float %t %coords Sample %sample_idx",
+                                     "%result = OpCompositeExtract %float",
+                                 },
+                             },
+
+                             // Test some textures with integer texel types.
+                             TextureBuiltinTestCase{
+                                 kSampledTexture,
+                                 type::TextureDimension::k2d,
+                                 /* texel type */ kI32,
+                                 {{"coords", 2, kI32}, {"lod", 1, kI32}},
+                                 {"result", 4, kI32},
+                                 {
+                                     "OpImageFetch %v4int %t %coords Lod %lod",
+                                 },
+                             },
+                             TextureBuiltinTestCase{
+                                 kSampledTexture,
+                                 type::TextureDimension::k2d,
+                                 /* texel type */ kU32,
+                                 {{"coords", 2, kI32}, {"lod", 1, kI32}},
+                                 {"result", 4, kU32},
+                                 {
+                                     "OpImageFetch %v4uint %t %coords Lod %lod",
+                                 },
+                             }),
+                         PrintCase);
+
 }  // namespace
 }  // namespace tint::writer::spirv