[ir][spirv-writer] Handle texture sample builtins

Use the BuiltinPolyfillSpirv transform to replace texture sample
builtins with calls to SPIR-V intrinsic functions that will create the
`OpSampledImage` and then execute an `OpImageSample*` instruction.

A `LiteralOperand` subclass of `ir::Constant` is used to represent the
literal 'image operands' operand, and a `SampledImage` subclass of
`type::Type` is used to represent the `OpSampledImage` type.

Bug: tint:1906
Change-Id: Id3fd166f1cf5772fd75aed5cbeb8c3c02ea65197
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/141230
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>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 467d112..729d601 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -2088,6 +2088,7 @@
         "writer/spirv/ir/generator_impl_ir_switch_test.cc",
         "writer/spirv/ir/generator_impl_ir_swizzle_test.cc",
         "writer/spirv/ir/generator_impl_ir_test.cc",
+        "writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc",
         "writer/spirv/ir/generator_impl_ir_type_test.cc",
         "writer/spirv/ir/generator_impl_ir_unary_test.cc",
         "writer/spirv/ir/generator_impl_ir_var_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index b53cffd..e7b6cb2 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -1350,6 +1350,7 @@
         writer/spirv/ir/generator_impl_ir_switch_test.cc
         writer/spirv/ir/generator_impl_ir_swizzle_test.cc
         writer/spirv/ir/generator_impl_ir_test.cc
+        writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc
         writer/spirv/ir/generator_impl_ir_type_test.cc
         writer/spirv/ir/generator_impl_ir_unary_test.cc
         writer/spirv/ir/generator_impl_ir_var_test.cc
diff --git a/src/tint/ir/intrinsic_call.cc b/src/tint/ir/intrinsic_call.cc
index 4063213..1170418 100644
--- a/src/tint/ir/intrinsic_call.cc
+++ b/src/tint/ir/intrinsic_call.cc
@@ -40,6 +40,21 @@
         case IntrinsicCall::Kind::kSpirvSelect:
             out << "spirv.select";
             break;
+        case IntrinsicCall::Kind::kSpirvSampledImage:
+            out << "spirv.sampled_image";
+            break;
+        case IntrinsicCall::Kind::kSpirvImageSampleImplicitLod:
+            out << "spirv.image_sample_implicit_lod";
+            break;
+        case IntrinsicCall::Kind::kSpirvImageSampleExplicitLod:
+            out << "spirv.image_sample_explicit_lod";
+            break;
+        case IntrinsicCall::Kind::kSpirvImageSampleDrefImplicitLod:
+            out << "spirv.image_sample_dref_implicit_lod";
+            break;
+        case IntrinsicCall::Kind::kSpirvImageSampleDrefExplicitLod:
+            out << "spirv.image_sample_dref_implicit_lod";
+            break;
     }
     return out;
 }
diff --git a/src/tint/ir/intrinsic_call.h b/src/tint/ir/intrinsic_call.h
index 33cbb03..f535972 100644
--- a/src/tint/ir/intrinsic_call.h
+++ b/src/tint/ir/intrinsic_call.h
@@ -31,6 +31,11 @@
         // SPIR-V backend intrinsics.
         kSpirvDot,
         kSpirvSelect,
+        kSpirvSampledImage,
+        kSpirvImageSampleImplicitLod,
+        kSpirvImageSampleExplicitLod,
+        kSpirvImageSampleDrefImplicitLod,
+        kSpirvImageSampleDrefExplicitLod,
     };
 
     /// Constructor
diff --git a/src/tint/ir/transform/builtin_polyfill_spirv.cc b/src/tint/ir/transform/builtin_polyfill_spirv.cc
index 1aa82e5..4833847 100644
--- a/src/tint/ir/transform/builtin_polyfill_spirv.cc
+++ b/src/tint/ir/transform/builtin_polyfill_spirv.cc
@@ -16,10 +16,17 @@
 
 #include <utility>
 
+#include "spirv/unified1/spirv.h"
 #include "src/tint/ir/builder.h"
 #include "src/tint/ir/module.h"
+#include "src/tint/type/depth_multisampled_texture.h"
+#include "src/tint/type/depth_texture.h"
+#include "src/tint/type/sampled_texture.h"
+#include "src/tint/type/texture.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ir::transform::BuiltinPolyfillSpirv);
+TINT_INSTANTIATE_TYPEINFO(tint::ir::transform::BuiltinPolyfillSpirv::LiteralOperand);
+TINT_INSTANTIATE_TYPEINFO(tint::ir::transform::BuiltinPolyfillSpirv::SampledImage);
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -52,6 +59,12 @@
                 switch (builtin->Func()) {
                     case builtin::Function::kDot:
                     case builtin::Function::kSelect:
+                    case builtin::Function::kTextureSample:
+                    case builtin::Function::kTextureSampleBias:
+                    case builtin::Function::kTextureSampleCompare:
+                    case builtin::Function::kTextureSampleCompareLevel:
+                    case builtin::Function::kTextureSampleGrad:
+                    case builtin::Function::kTextureSampleLevel:
                         worklist.Push(builtin);
                         break;
                     default:
@@ -70,6 +83,14 @@
                 case builtin::Function::kSelect:
                     replacement = Select(builtin);
                     break;
+                case builtin::Function::kTextureSample:
+                case builtin::Function::kTextureSampleBias:
+                case builtin::Function::kTextureSampleCompare:
+                case builtin::Function::kTextureSampleCompareLevel:
+                case builtin::Function::kTextureSampleGrad:
+                case builtin::Function::kTextureSampleLevel:
+                    replacement = TextureSample(builtin);
+                    break;
                 default:
                     break;
             }
@@ -152,10 +173,178 @@
         call->InsertBefore(builtin);
         return call->Result();
     }
+
+    /// Handle a textureSample*() builtin.
+    /// @param builtin the builtin call instruction
+    /// @returns the replacement value
+    Value* TextureSample(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* 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 =
+            b.Call(ty.Get<SampledImage>(texture_ty), IntrinsicCall::Kind::kSpirvSampledImage,
+                   utils::Vector{texture, sampler});
+        sampled_image->InsertBefore(builtin);
+
+        // Append the array index to the coordinates if provided.
+        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();
+        }
+
+        // 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;
+        switch (builtin->Func()) {
+            case builtin::Function::kTextureSample:
+                intrinsic = IntrinsicCall::Kind::kSpirvImageSampleImplicitLod;
+                operands.offset = next_arg();
+                break;
+            case builtin::Function::kTextureSampleBias:
+                intrinsic = IntrinsicCall::Kind::kSpirvImageSampleImplicitLod;
+                operands.bias = next_arg();
+                operands.offset = next_arg();
+                break;
+            case builtin::Function::kTextureSampleCompare:
+                intrinsic = IntrinsicCall::Kind::kSpirvImageSampleDrefImplicitLod;
+                depth = next_arg();
+                operands.offset = next_arg();
+                break;
+            case builtin::Function::kTextureSampleCompareLevel:
+                intrinsic = IntrinsicCall::Kind::kSpirvImageSampleDrefExplicitLod;
+                depth = next_arg();
+                operands.lod = b.Constant(0_f);
+                operands.offset = next_arg();
+                break;
+            case builtin::Function::kTextureSampleGrad:
+                intrinsic = IntrinsicCall::Kind::kSpirvImageSampleExplicitLod;
+                operands.ddx = next_arg();
+                operands.ddy = next_arg();
+                operands.offset = next_arg();
+                break;
+            case builtin::Function::kTextureSampleLevel:
+                intrinsic = IntrinsicCall::Kind::kSpirvImageSampleExplicitLod;
+                operands.lod = next_arg();
+                operands.offset = next_arg();
+                break;
+            default:
+                return nullptr;
+        }
+
+        // Start building the argument list for the intrinsic.
+        // The first two operands are always the sampled image and then the coordinates, followed by
+        // the depth reference if used.
+        utils::Vector<Value*, 8> intrinsic_args;
+        intrinsic_args.Push(sampled_image->Result());
+        intrinsic_args.Push(coords);
+        if (depth) {
+            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);
+
+        // Call the intrinsic.
+        // If this is a depth comparison, the result is always f32, otherwise vec4f.
+        auto* result_ty = depth ? static_cast<const type::Type*>(ty.f32()) : ty.vec4<f32>();
+        auto* texture_call = b.Call(result_ty, intrinsic, std::move(intrinsic_args));
+        texture_call->InsertBefore(builtin);
+
+        auto* result = texture_call->Result();
+
+        // If this is not a depth comparison but we are sampling a depth texture, extract the first
+        // component to get the scalar f32 that SPIR-V expects.
+        if (!depth && texture_ty->IsAnyOf<type::DepthTexture, type::DepthMultisampledTexture>()) {
+            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 {
     State{ir}.Process();
 }
 
+BuiltinPolyfillSpirv::LiteralOperand::LiteralOperand(const constant::Value* value) : Base(value) {}
+
+BuiltinPolyfillSpirv::LiteralOperand::~LiteralOperand() = default;
+
+BuiltinPolyfillSpirv::SampledImage::SampledImage(const type::Type* image)
+    : Base(static_cast<size_t>(
+               utils::Hash(utils::TypeInfo::Of<BuiltinPolyfillSpirv::SampledImage>().full_hashcode,
+                           image)),
+           type::Flags{}),
+      image_(image) {}
+
+BuiltinPolyfillSpirv::SampledImage* BuiltinPolyfillSpirv::SampledImage::Clone(
+    type::CloneContext& ctx) const {
+    auto* image = image_->Clone(ctx);
+    return ctx.dst.mgr->Get<BuiltinPolyfillSpirv::SampledImage>(image);
+}
+
 }  // namespace tint::ir::transform
diff --git a/src/tint/ir/transform/builtin_polyfill_spirv.h b/src/tint/ir/transform/builtin_polyfill_spirv.h
index 8909263..d323f68 100644
--- a/src/tint/ir/transform/builtin_polyfill_spirv.h
+++ b/src/tint/ir/transform/builtin_polyfill_spirv.h
@@ -15,7 +15,16 @@
 #ifndef SRC_TINT_IR_TRANSFORM_BUILTIN_POLYFILL_SPIRV_H_
 #define SRC_TINT_IR_TRANSFORM_BUILTIN_POLYFILL_SPIRV_H_
 
+#include <string>
+
+#include "src/tint/ir/constant.h"
 #include "src/tint/ir/transform/transform.h"
+#include "src/tint/type/type.h"
+
+// Forward declarations
+namespace tint::type {
+class Texture;
+}  // namespace tint::type
 
 namespace tint::ir::transform {
 
@@ -31,6 +40,44 @@
     /// @copydoc Transform::Run
     void Run(ir::Module* module, const DataMap& inputs, DataMap& outputs) const override;
 
+    /// LiteralOperand is a type of constant value that is intended to be emitted as a literal in
+    /// the SPIR-V instruction stream.
+    class LiteralOperand final : public utils::Castable<LiteralOperand, ir::Constant> {
+      public:
+        /// Constructor
+        /// @param value the operand value
+        explicit LiteralOperand(const constant::Value* value);
+        /// Destructor
+        ~LiteralOperand() override;
+    };
+
+    /// SampledImage represents an OpTypeSampledImage in SPIR-V.
+    class SampledImage final : public utils::Castable<SampledImage, type::Type> {
+      public:
+        /// Constructor
+        /// @param image the image type
+        explicit SampledImage(const type::Type* image);
+
+        /// @param other the other node to compare against
+        /// @returns true if the this type is equal to @p other
+        bool Equals(const UniqueNode& other) const override {
+            return &other.TypeInfo() == &TypeInfo();
+        }
+
+        /// @returns the friendly name for this type
+        std::string FriendlyName() const override { return "spirv.sampled_image"; }
+
+        /// @param ctx the clone context
+        /// @returns a clone of this type
+        SampledImage* Clone(type::CloneContext& ctx) const override;
+
+        /// @returns the image type
+        const type::Type* Image() const { return image_; }
+
+      private:
+        const type::Type* image_;
+    };
+
   private:
     struct State;
 };
diff --git a/src/tint/ir/transform/builtin_polyfill_spirv_test.cc b/src/tint/ir/transform/builtin_polyfill_spirv_test.cc
index 007a702..6f655c4 100644
--- a/src/tint/ir/transform/builtin_polyfill_spirv_test.cc
+++ b/src/tint/ir/transform/builtin_polyfill_spirv_test.cc
@@ -17,6 +17,8 @@
 #include <utility>
 
 #include "src/tint/ir/transform/test_helper.h"
+#include "src/tint/type/depth_texture.h"
+#include "src/tint/type/sampled_texture.h"
 
 namespace tint::ir::transform {
 namespace {
@@ -260,5 +262,785 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSample_1D) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k1d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(ty.f32(), builtin::Function::kTextureSample, t, s, coords);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_1d<f32>, %s:sampler, %coords:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:f32 = textureSample %t, %s, %coords
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_1d<f32>, %s:sampler, %coords:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:spirv.sampled_image = spirv.sampled_image %t, %s
+    %6:vec4<f32> = spirv.image_sample_implicit_lod %5, %coords, 0u
+    ret %6
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSample_2D) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(ty.f32(), builtin::Function::kTextureSample, t, s, coords);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:f32 = textureSample %t, %s, %coords
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:spirv.sampled_image = spirv.sampled_image %t, %s
+    %6:vec4<f32> = spirv.image_sample_implicit_lod %5, %coords, 0u
+    ret %6
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSample_2D_Offset) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSample, t, s, coords,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:vec4<f32> = textureSample %t, %s, %coords, vec2<i32>(1i)
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %5:spirv.sampled_image = spirv.sampled_image %t, %s
+    %6:vec4<f32> = spirv.image_sample_implicit_lod %5, %coords, 8u, vec2<i32>(1i)
+    ret %6
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSample_2DArray_Offset) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, array_idx});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSample, t, s, coords, array_idx,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:vec4<f32> = textureSample %t, %s, %coords, %array_idx, vec2<i32>(1i)
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:f32 = convert %array_idx
+    %8:vec3<f32> = construct %coords, %7
+    %9:vec4<f32> = spirv.image_sample_implicit_lod %6, %8, 8u, vec2<i32>(1i)
+    ret %9
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleBias_2D) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* bias = b.FunctionParam("bias", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, bias});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(ty.f32(), builtin::Function::kTextureSampleBias, t, s, coords, bias);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %bias:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:f32 = textureSampleBias %t, %s, %coords, %bias
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %bias:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:vec4<f32> = spirv.image_sample_implicit_lod %6, %coords, 1u, %bias
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleBias_2D_Offset) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* bias = b.FunctionParam("bias", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, bias});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSampleBias, t, s, coords, bias,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %bias:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:vec4<f32> = textureSampleBias %t, %s, %coords, %bias, vec2<i32>(1i)
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %bias:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:vec4<f32> = spirv.image_sample_implicit_lod %6, %coords, 9u, %bias, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleBias_2DArray_Offset) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* bias = b.FunctionParam("bias", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, array_idx, bias});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSampleBias, t, s, coords, array_idx, bias,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:vec4<f32> = textureSampleBias %t, %s, %coords, %array_idx, %bias, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:spirv.sampled_image = spirv.sampled_image %t, %s
+    %8:f32 = convert %array_idx
+    %9:vec3<f32> = construct %coords, %8
+    %10:vec4<f32> = spirv.image_sample_implicit_lod %7, %9, 9u, %bias, vec2<i32>(1i)
+    ret %10
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleCompare_2D) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2d));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* dref = b.FunctionParam("dref", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, s, coords, dref});
+
+    b.With(func->Block(), [&] {
+        auto* result =
+            b.Call(ty.f32(), builtin::Function::kTextureSampleCompare, t, s, coords, dref);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:f32 = textureSampleCompare %t, %s, %coords, %dref
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 0u
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleCompare_2D_Offset) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2d));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* dref = b.FunctionParam("dref", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, s, coords, dref});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.f32(), builtin::Function::kTextureSampleCompare, t, s, coords, dref,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:f32 = textureSampleCompare %t, %s, %coords, %dref, vec2<i32>(1i)
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 8u, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleCompare_2DArray_Offset) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2dArray));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* bias = b.FunctionParam("bias", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, s, coords, array_idx, bias});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.f32(), builtin::Function::kTextureSampleCompare, t, s, coords, array_idx, bias,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+  %b1 = block {
+    %7:f32 = textureSampleCompare %t, %s, %coords, %array_idx, %bias, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+  %b1 = block {
+    %7:spirv.sampled_image = spirv.sampled_image %t, %s
+    %8:f32 = convert %array_idx
+    %9:vec3<f32> = construct %coords, %8
+    %10:f32 = spirv.image_sample_dref_implicit_lod %7, %9, %bias, 8u, vec2<i32>(1i)
+    ret %10
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleCompareLevel_2D) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2d));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* dref = b.FunctionParam("dref", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, s, coords, dref});
+
+    b.With(func->Block(), [&] {
+        auto* result =
+            b.Call(ty.f32(), builtin::Function::kTextureSampleCompareLevel, t, s, coords, dref);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:f32 = textureSampleCompareLevel %t, %s, %coords, %dref
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 2u, 0.0f
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleCompareLevel_2D_Offset) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2d));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* dref = b.FunctionParam("dref", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, s, coords, dref});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.f32(), builtin::Function::kTextureSampleCompareLevel, t, s, coords, dref,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:f32 = textureSampleCompareLevel %t, %s, %coords, %dref, vec2<i32>(1i)
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 10u, 0.0f, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleCompareLevel_2DArray_Offset) {
+    auto* t = b.FunctionParam("t", ty.Get<type::DepthTexture>(type::TextureDimension::k2dArray));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* bias = b.FunctionParam("bias", ty.f32());
+    auto* func = b.Function("foo", ty.f32());
+    func->SetParams({t, s, coords, array_idx, bias});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.f32(), builtin::Function::kTextureSampleCompareLevel, t, s, coords, array_idx, bias,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+  %b1 = block {
+    %7:f32 = textureSampleCompareLevel %t, %s, %coords, %array_idx, %bias, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+  %b1 = block {
+    %7:spirv.sampled_image = spirv.sampled_image %t, %s
+    %8:f32 = convert %array_idx
+    %9:vec3<f32> = construct %coords, %8
+    %10:f32 = spirv.image_sample_dref_implicit_lod %7, %9, %bias, 10u, 0.0f, vec2<i32>(1i)
+    ret %10
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleGrad_2D) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* ddx = b.FunctionParam("ddx", ty.vec2<f32>());
+    auto* ddy = b.FunctionParam("ddy", ty.vec2<f32>());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, ddx, ddy});
+
+    b.With(func->Block(), [&] {
+        auto* result =
+            b.Call(ty.f32(), builtin::Function::kTextureSampleBias, t, s, coords, ddx, ddy);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:f32 = textureSampleBias %t, %s, %coords, %ddx, %ddy
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:spirv.sampled_image = spirv.sampled_image %t, %s
+    %8:vec4<f32> = spirv.image_sample_implicit_lod %7, %coords, 9u, %ddx, %ddy
+    ret %8
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleGrad_2D_Offset) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* ddx = b.FunctionParam("ddx", ty.vec2<f32>());
+    auto* ddy = b.FunctionParam("ddy", ty.vec2<f32>());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, ddx, ddy});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSampleBias, t, s, coords, ddx, ddy,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:vec4<f32> = textureSampleBias %t, %s, %coords, %ddx, %ddy, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:spirv.sampled_image = spirv.sampled_image %t, %s
+    %8:vec4<f32> = spirv.image_sample_implicit_lod %7, %coords, 9u, %ddx, %ddy
+    ret %8
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleGrad_2DArray_Offset) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* ddx = b.FunctionParam("ddx", ty.vec2<f32>());
+    auto* ddy = b.FunctionParam("ddy", ty.vec2<f32>());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, array_idx, ddx, ddy});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSampleBias, t, s, coords, array_idx, ddx,
+            ddy,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %8:vec4<f32> = textureSampleBias %t, %s, %coords, %array_idx, %ddx, %ddy, vec2<i32>(1i)
+    ret %8
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
+  %b1 = block {
+    %8:spirv.sampled_image = spirv.sampled_image %t, %s
+    %9:f32 = convert %array_idx
+    %10:vec3<f32> = construct %coords, %9
+    %11:vec4<f32> = spirv.image_sample_implicit_lod %8, %10, 9u, %ddx, %ddy
+    ret %11
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleLevel_2D) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* lod = b.FunctionParam("lod", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(ty.f32(), builtin::Function::kTextureSampleLevel, t, s, coords, lod);
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %lod:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:f32 = textureSampleLevel %t, %s, %coords, %lod
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %lod:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:vec4<f32> = spirv.image_sample_explicit_lod %6, %coords, 2u, %lod
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleLevel_2D_Offset) {
+    auto* t =
+        b.FunctionParam("t", ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* lod = b.FunctionParam("lod", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSampleLevel, t, s, coords, lod,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %lod:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:vec4<f32> = textureSampleLevel %t, %s, %coords, %lod, vec2<i32>(1i)
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %lod:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %6:spirv.sampled_image = spirv.sampled_image %t, %s
+    %7:vec4<f32> = spirv.image_sample_explicit_lod %6, %coords, 10u, %lod, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillSpirvTest, TextureSampleLevel_2DArray_Offset) {
+    auto* t = b.FunctionParam(
+        "t", ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()));
+    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
+    auto* array_idx = b.FunctionParam("array_idx", ty.i32());
+    auto* lod = b.FunctionParam("lod", ty.f32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({t, s, coords, array_idx, lod});
+
+    b.With(func->Block(), [&] {
+        auto* result = b.Call(
+            ty.vec4<f32>(), builtin::Function::kTextureSampleLevel, t, s, coords, array_idx, lod,
+            b.Constant(mod.constant_values.Splat(ty.vec2<i32>(), mod.constant_values.Get(1_i), 2)));
+        b.Return(func, result);
+    });
+
+    auto* src = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %lod:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:vec4<f32> = textureSampleLevel %t, %s, %coords, %array_idx, %lod, vec2<i32>(1i)
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %lod:f32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %7:spirv.sampled_image = spirv.sampled_image %t, %s
+    %8:f32 = convert %array_idx
+    %9:vec3<f32> = construct %coords, %8
+    %10:vec4<f32> = spirv.image_sample_explicit_lod %7, %9, 10u, %lod, vec2<i32>(1i)
+    ret %10
+  }
+}
+)";
+
+    Run<BuiltinPolyfillSpirv>();
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::ir::transform
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.cc b/src/tint/writer/spirv/ir/generator_impl_ir.cc
index 024aa59..479cbd9 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.cc
@@ -147,6 +147,15 @@
             return s;
         },
 
+        // Dedup a SampledImage if its underlying image will be deduped.
+        [&](const ir::transform::BuiltinPolyfillSpirv::SampledImage* si) -> const type::Type* {
+            auto* img = DedupType(si->Image(), types);
+            if (img != si->Image()) {
+                return types.Get<ir::transform::BuiltinPolyfillSpirv::SampledImage>(img);
+            }
+            return si;
+        },
+
         [&](Default) { return ty; });
 }
 
@@ -236,6 +245,11 @@
 }
 
 uint32_t GeneratorImplIr::Constant(ir::Constant* constant) {
+    // If it is a literal operand, just return the value.
+    if (auto* literal = constant->As<ir::transform::BuiltinPolyfillSpirv::LiteralOperand>()) {
+        return literal->Value()->ValueAs<uint32_t>();
+    }
+
     auto id = Constant(constant->Value());
 
     // Set the name for the SPIR-V result ID if provided in the module.
@@ -375,6 +389,9 @@
             [&](const type::Struct* str) { EmitStructType(id, str, addrspace); },
             [&](const type::Texture* tex) { EmitTextureType(id, tex); },
             [&](const type::Sampler*) { module_.PushType(spv::Op::OpTypeSampler, {id}); },
+            [&](const ir::transform::BuiltinPolyfillSpirv::SampledImage* s) {
+                module_.PushType(spv::Op::OpTypeSampledImage, {id, Type(s->Image())});
+            },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_) << "unhandled type: " << ty->FriendlyName();
             });
@@ -1403,6 +1420,21 @@
         case ir::IntrinsicCall::Kind::kSpirvSelect:
             op = spv::Op::OpSelect;
             break;
+        case ir::IntrinsicCall::Kind::kSpirvSampledImage:
+            op = spv::Op::OpSampledImage;
+            break;
+        case ir::IntrinsicCall::Kind::kSpirvImageSampleImplicitLod:
+            op = spv::Op::OpImageSampleImplicitLod;
+            break;
+        case ir::IntrinsicCall::Kind::kSpirvImageSampleExplicitLod:
+            op = spv::Op::OpImageSampleExplicitLod;
+            break;
+        case ir::IntrinsicCall::Kind::kSpirvImageSampleDrefImplicitLod:
+            op = spv::Op::OpImageSampleDrefImplicitLod;
+            break;
+        case ir::IntrinsicCall::Kind::kSpirvImageSampleDrefExplicitLod:
+            op = spv::Op::OpImageSampleDrefExplicitLod;
+            break;
     }
 
     OperandList operands = {Type(call->Result()->Type()), id};
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
new file mode 100644
index 0000000..90711e7
--- /dev/null
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_texture_builtin_test.cc
@@ -0,0 +1,597 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/writer/spirv/ir/test_helper_ir.h"
+
+#include "src/tint/builtin/function.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::writer::spirv {
+namespace {
+
+/// An additional argument to a texture builtin.
+struct Arg {
+    /// The argument name.
+    const char* name;
+    /// The vector width of the argument (1 means scalar).
+    uint32_t width;
+    /// The element type of the argument.
+    TestElementType type;
+};
+
+/// A parameterized texture builtin function test case.
+struct TextureBuiltinTestCase {
+    /// The builtin function.
+    enum builtin::Function function;
+    /// The builtin function arguments.
+    utils::Vector<Arg, 4> optional_args;
+    /// The expected SPIR-V instruction string for the texture call.
+    const char* texture_call;
+};
+
+std::string PrintCase(testing::TestParamInfo<TextureBuiltinTestCase> cc) {
+    utils::StringStream ss;
+    ss << cc.param.function;
+    for (const auto& arg : cc.param.optional_args) {
+        ss << "_" << arg.name;
+    }
+    return ss.str();
+}
+
+class TextureBuiltinTest : public SpvGeneratorImplTestWithParam<TextureBuiltinTestCase> {
+  protected:
+    void Run(const type::Texture* texture_ty,
+             const type::Sampler* sampler_ty,
+             const type::Type* coord_ty,
+             const type::Type* return_ty) {
+        auto params = GetParam();
+
+        auto* t = b.FunctionParam("t", texture_ty);
+        auto* s = b.FunctionParam("s", sampler_ty);
+        auto* coord = b.FunctionParam("coords", coord_ty);
+        auto* func = b.Function("foo", return_ty);
+        func->SetParams({t, s, coord});
+
+        b.With(func->Block(), [&] {
+            utils::Vector<ir::Value*, 4> args = {t, s, coord};
+            uint32_t arg_value = 1;
+            for (const auto& arg : params.optional_args) {
+                auto* value = MakeScalarValue(arg.type, arg_value++);
+                if (arg.width > 1) {
+                    value = b.Constant(mod.constant_values.Splat(ty.vec(value->Type(), arg.width),
+                                                                 value->Value(), arg.width));
+                }
+                args.Push(value);
+                mod.SetName(value, arg.name);
+            }
+            auto* result = b.Call(return_ty, params.function, std::move(args));
+            b.Return(func, result);
+            mod.SetName(result, "result");
+        });
+
+        ASSERT_TRUE(Generate()) << Error() << output_;
+        EXPECT_INST(params.texture_call);
+    }
+};
+
+using Texture1D = TextureBuiltinTest;
+TEST_P(Texture1D, Emit) {
+    Run(ty.Get<type::SampledTexture>(type::TextureDimension::k1d, ty.f32()),
+        ty.sampler(),   // sampler type
+        ty.f32(),       // coord type
+        ty.vec4<f32>()  // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+}
+INSTANTIATE_TEST_SUITE_P(SpvGeneratorImplTest,
+                         Texture1D,
+                         testing::Values(TextureBuiltinTestCase{
+                             builtin::Function::kTextureSample,
+                             {},
+                             "OpImageSampleImplicitLod %v4float %11 %coords None",
+                         }),
+                         PrintCase);
+
+using Texture2D = TextureBuiltinTest;
+TEST_P(Texture2D, Emit) {
+    Run(ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()),
+        ty.sampler(),    // sampler type
+        ty.vec2<f32>(),  // coord type
+        ty.vec4<f32>()   // return type
+    );
+    EXPECT_INST("%12 = OpSampledImage %13 %t %s");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    Texture2D,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {},
+            "OpImageSampleImplicitLod %v4float %12 %coords None",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"offset", 2, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %coords ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleBias,
+            {{"bias", 1, kF32}},
+            "OpImageSampleImplicitLod %v4float %12 %coords Bias %bias",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleBias,
+            {{"bias", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %coords Bias|ConstOffset %bias %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleGrad,
+            {{"ddx", 2, kF32}, {"ddy", 2, kF32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Grad %ddx %ddy",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleGrad,
+            {{"ddx", 2, kF32}, {"ddy", 2, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Grad|ConstOffset %ddx %ddy %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"lod", 1, kF32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Lod %lod",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"lod", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Lod|ConstOffset %lod %offset",
+        }),
+    PrintCase);
+
+using Texture2DArray = TextureBuiltinTest;
+TEST_P(Texture2DArray, Emit) {
+    Run(ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()),
+        ty.sampler(),    // sampler type
+        ty.vec2<f32>(),  // coord type
+        ty.vec4<f32>()   // return type
+    );
+    EXPECT_INST("%12 = OpSampledImage %13 %t %s");
+    EXPECT_INST("%14 = OpConvertSToF %float %array_idx");
+    EXPECT_INST("%18 = OpCompositeConstruct %v3float %coords %14");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    Texture2DArray,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"array_idx", 1, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %18 None",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"array_idx", 1, kI32}, {"offset", 2, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %18 ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleBias,
+            {{"array_idx", 1, kI32}, {"bias", 1, kF32}},
+            "OpImageSampleImplicitLod %v4float %12 %18 Bias %bias",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleBias,
+            {{"array_idx", 1, kI32}, {"bias", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %18 Bias|ConstOffset %bias %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleGrad,
+            {{"array_idx", 1, kI32}, {"ddx", 2, kF32}, {"ddy", 2, kF32}},
+            "OpImageSampleExplicitLod %v4float %12 %18 Grad %ddx %ddy",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleGrad,
+            {{"array_idx", 1, kI32}, {"ddx", 2, kF32}, {"ddy", 2, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleExplicitLod %v4float %12 %18 Grad|ConstOffset %ddx %ddy %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"array_idx", 1, kI32}, {"lod", 1, kF32}},
+            "OpImageSampleExplicitLod %v4float %12 %18 Lod %lod",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"array_idx", 1, kI32}, {"lod", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleExplicitLod %v4float %12 %18 Lod|ConstOffset %lod %offset",
+        }),
+    PrintCase);
+
+using Texture3D = TextureBuiltinTest;
+TEST_P(Texture3D, Emit) {
+    Run(ty.Get<type::SampledTexture>(type::TextureDimension::k3d, ty.f32()),
+        ty.sampler(),    // sampler type
+        ty.vec3<f32>(),  // coord type
+        ty.vec4<f32>()   // return type
+    );
+    EXPECT_INST("%12 = OpSampledImage %13 %t %s");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    Texture3D,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {},
+            "OpImageSampleImplicitLod %v4float %12 %coords None",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"offset", 3, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %coords ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleBias,
+            {{"bias", 1, kF32}},
+            "OpImageSampleImplicitLod %v4float %12 %coords Bias %bias",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleBias,
+            {{"bias", 1, kF32}, {"offset", 3, kI32}},
+            "OpImageSampleImplicitLod %v4float %12 %coords Bias|ConstOffset %bias %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleGrad,
+            {{"ddx", 3, kF32}, {"ddy", 3, kF32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Grad %ddx %ddy",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleGrad,
+            {{"ddx", 3, kF32}, {"ddy", 3, kF32}, {"offset", 3, kI32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Grad|ConstOffset %ddx %ddy %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"lod", 1, kF32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Lod %lod",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"lod", 1, kF32}, {"offset", 3, kI32}},
+            "OpImageSampleExplicitLod %v4float %12 %coords Lod|ConstOffset %lod %offset",
+        }),
+    PrintCase);
+
+using TextureCube = TextureBuiltinTest;
+TEST_P(TextureCube, Emit) {
+    Run(ty.Get<type::SampledTexture>(type::TextureDimension::kCube, ty.f32()),
+        ty.sampler(),    // sampler type
+        ty.vec3<f32>(),  // coord type
+        ty.vec4<f32>()   // return type
+    );
+    EXPECT_INST("%12 = OpSampledImage %13 %t %s");
+}
+INSTANTIATE_TEST_SUITE_P(SpvGeneratorImplTest,
+                         TextureCube,
+                         testing::Values(
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSample,
+                                 {},
+                                 "OpImageSampleImplicitLod %v4float %12 %coords None",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleBias,
+                                 {{"bias", 1, kF32}},
+                                 "OpImageSampleImplicitLod %v4float %12 %coords Bias %bias",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleGrad,
+                                 {{"ddx", 3, kF32}, {"ddy", 3, kF32}},
+                                 "OpImageSampleExplicitLod %v4float %12 %coords Grad %ddx %ddy",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleLevel,
+                                 {{"lod", 1, kF32}},
+                                 "OpImageSampleExplicitLod %v4float %12 %coords Lod %lod",
+                             }),
+                         PrintCase);
+
+using TextureCubeArray = TextureBuiltinTest;
+TEST_P(TextureCubeArray, Emit) {
+    Run(ty.Get<type::SampledTexture>(type::TextureDimension::kCubeArray, ty.f32()),
+        ty.sampler(),    // sampler type
+        ty.vec3<f32>(),  // coord type
+        ty.vec4<f32>()   // return type
+    );
+    EXPECT_INST("%12 = OpSampledImage %13 %t %s");
+    EXPECT_INST("%14 = OpConvertSToF %float %array_idx");
+    EXPECT_INST("%17 = OpCompositeConstruct %v4float %coords %14");
+}
+INSTANTIATE_TEST_SUITE_P(SpvGeneratorImplTest,
+                         TextureCubeArray,
+                         testing::Values(
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSample,
+                                 {{"array_idx", 1, kI32}},
+                                 "OpImageSampleImplicitLod %v4float %12 %17 None",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleBias,
+                                 {{"array_idx", 1, kI32}, {"bias", 1, kF32}},
+                                 "OpImageSampleImplicitLod %v4float %12 %17 Bias %bias",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleGrad,
+                                 {{"array_idx", 1, kI32}, {"ddx", 3, kF32}, {"ddy", 3, kF32}},
+                                 "OpImageSampleExplicitLod %v4float %12 %17 Grad %ddx %ddy",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleLevel,
+                                 {{"array_idx", 1, kI32}, {"lod", 1, kF32}},
+                                 "OpImageSampleExplicitLod %v4float %12 %17 Lod %lod",
+                             }),
+                         PrintCase);
+
+using TextureDepth2D = TextureBuiltinTest;
+TEST_P(TextureDepth2D, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::k2d),
+        ty.sampler(),    // sampler type
+        ty.vec2<f32>(),  // coord type
+        ty.f32()         // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+    EXPECT_INST("%result = OpCompositeExtract %float");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    TextureDepth2D,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {},
+            "OpImageSampleImplicitLod %v4float %11 %coords None",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"offset", 2, kI32}},
+            "OpImageSampleImplicitLod %v4float %11 %coords ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"lod", 1, kI32}},
+            "OpImageSampleExplicitLod %v4float %11 %coords Lod %13",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"lod", 1, kI32}, {"offset", 2, kI32}},
+            "OpImageSampleExplicitLod %v4float %11 %coords Lod|ConstOffset %13 %offset",
+        }),
+    PrintCase);
+
+using TextureDepth2D_DepthComparison = TextureBuiltinTest;
+TEST_P(TextureDepth2D_DepthComparison, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::k2d),
+        ty.comparison_sampler(),  // sampler type
+        ty.vec2<f32>(),           // coord type
+        ty.f32()                  // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    TextureDepth2D_DepthComparison,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompare,
+            {{"depth", 1, kF32}},
+            "OpImageSampleDrefImplicitLod %float %11 %coords %depth",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompare,
+            {{"depth", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleDrefImplicitLod %float %11 %coords %depth ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompareLevel,
+            {{"depth_l0", 1, kF32}},
+            "OpImageSampleDrefExplicitLod %float %11 %coords %depth_l0 Lod %float_0",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompareLevel,
+            {{"depth_l0", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleDrefExplicitLod %float %11 %coords %depth_l0 Lod|ConstOffset %float_0 "
+            "%offset",
+        }),
+    PrintCase);
+
+using TextureDepth2DArray = TextureBuiltinTest;
+TEST_P(TextureDepth2DArray, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::k2dArray),
+        ty.sampler(),    // sampler type
+        ty.vec2<f32>(),  // coord type
+        ty.f32()         // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+    EXPECT_INST("%13 = OpConvertSToF %float %array_idx");
+    EXPECT_INST("%17 = OpCompositeConstruct %v3float %coords %13");
+    EXPECT_INST("%result = OpCompositeExtract %float");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    TextureDepth2DArray,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"array_idx", 1, kI32}},
+            "OpImageSampleImplicitLod %v4float %11 %17 None",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSample,
+            {{"array_idx", 1, kI32}, {"offset", 2, kI32}},
+            "OpImageSampleImplicitLod %v4float %11 %17 ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"array_idx", 1, kI32}, {"lod", 1, kI32}},
+            "OpImageSampleExplicitLod %v4float %11 %17 Lod %18",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleLevel,
+            {{"array_idx", 1, kI32}, {"lod", 1, kI32}, {"offset", 2, kI32}},
+            "OpImageSampleExplicitLod %v4float %11 %17 Lod|ConstOffset %18 %offset",
+        }),
+    PrintCase);
+
+using TextureDepth2DArray_DepthComparison = TextureBuiltinTest;
+TEST_P(TextureDepth2DArray_DepthComparison, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::k2dArray),
+        ty.comparison_sampler(),  // sampler type
+        ty.vec2<f32>(),           // coord type
+        ty.f32()                  // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+    EXPECT_INST("%13 = OpConvertSToF %float %array_idx");
+    EXPECT_INST("%17 = OpCompositeConstruct %v3float %coords %13");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    TextureDepth2DArray_DepthComparison,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompare,
+            {{"array_idx", 1, kI32}, {"depth", 1, kF32}},
+            "OpImageSampleDrefImplicitLod %float %11 %17 %depth",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompare,
+            {{"array_idx", 1, kI32}, {"depth", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleDrefImplicitLod %float %11 %17 %depth ConstOffset %offset",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompareLevel,
+            {{"array_idx", 1, kI32}, {"depth_l0", 1, kF32}},
+            "OpImageSampleDrefExplicitLod %float %11 %17 %depth_l0 Lod %float_0",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompareLevel,
+            {{"array_idx", 1, kI32}, {"depth_l0", 1, kF32}, {"offset", 2, kI32}},
+            "OpImageSampleDrefExplicitLod %float %11 %17 %depth_l0 Lod|ConstOffset %float_0 "
+            "%offset",
+        }),
+    PrintCase);
+
+using TextureDepthCube = TextureBuiltinTest;
+TEST_P(TextureDepthCube, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::kCube),
+        ty.sampler(),    // sampler type
+        ty.vec3<f32>(),  // coord type
+        ty.f32()         // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+    EXPECT_INST("%result = OpCompositeExtract %float");
+}
+INSTANTIATE_TEST_SUITE_P(SpvGeneratorImplTest,
+                         TextureDepthCube,
+                         testing::Values(
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSample,
+                                 {},
+                                 "OpImageSampleImplicitLod %v4float %11 %coords None",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleLevel,
+                                 {{"lod", 1, kI32}},
+                                 "OpImageSampleExplicitLod %v4float %11 %coords Lod %13",
+                             }),
+                         PrintCase);
+
+using TextureDepthCube_DepthComparison = TextureBuiltinTest;
+TEST_P(TextureDepthCube_DepthComparison, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::kCube),
+        ty.comparison_sampler(),  // sampler typea
+        ty.vec3<f32>(),           // coord type
+        ty.f32()                  // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    TextureDepthCube_DepthComparison,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompare,
+            {{"depth", 1, kF32}},
+            "OpImageSampleDrefImplicitLod %float %11 %coords %depth",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompareLevel,
+            {{"depth_l0", 1, kF32}},
+            "OpImageSampleDrefExplicitLod %float %11 %coords %depth_l0 Lod %float_0",
+        }),
+    PrintCase);
+
+using TextureDepthCubeArray = TextureBuiltinTest;
+TEST_P(TextureDepthCubeArray, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::kCubeArray),
+        ty.sampler(),    // sampler type
+        ty.vec3<f32>(),  // coord type
+        ty.f32()         // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+    EXPECT_INST("%13 = OpConvertSToF %float %array_idx");
+    EXPECT_INST("%17 = OpCompositeConstruct %v4float %coords %13");
+    EXPECT_INST("%result = OpCompositeExtract %float");
+}
+INSTANTIATE_TEST_SUITE_P(SpvGeneratorImplTest,
+                         TextureDepthCubeArray,
+                         testing::Values(
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSample,
+                                 {{"array_idx", 1, kI32}},
+                                 "OpImageSampleImplicitLod %v4float %11 %17 None",
+                             },
+                             TextureBuiltinTestCase{
+                                 builtin::Function::kTextureSampleLevel,
+                                 {{"array_idx", 1, kI32}, {"lod", 1, kI32}},
+                                 "OpImageSampleExplicitLod %v4float %11 %17 Lod %18",
+                             }),
+                         PrintCase);
+
+using TextureDepthCubeArray_DepthComparison = TextureBuiltinTest;
+TEST_P(TextureDepthCubeArray_DepthComparison, Emit) {
+    Run(ty.Get<type::DepthTexture>(type::TextureDimension::kCubeArray),
+        ty.comparison_sampler(),  // sampler type
+        ty.vec3<f32>(),           // coord type
+        ty.f32()                  // return type
+    );
+    EXPECT_INST("%11 = OpSampledImage %12 %t %s");
+    EXPECT_INST("%13 = OpConvertSToF %float %array_idx");
+    EXPECT_INST("%17 = OpCompositeConstruct %v4float %coords %13");
+}
+INSTANTIATE_TEST_SUITE_P(
+    SpvGeneratorImplTest,
+    TextureDepthCubeArray_DepthComparison,
+    testing::Values(
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompare,
+            {{"array_idx", 1, kI32}, {"depth", 1, kF32}},
+            "OpImageSampleDrefImplicitLod %float %11 %17 %depth",
+        },
+        TextureBuiltinTestCase{
+            builtin::Function::kTextureSampleCompareLevel,
+            {{"array_idx", 1, kI32}, {"depth_l0", 1, kF32}},
+            "OpImageSampleDrefExplicitLod %float %11 %17 %depth_l0 Lod %float_0",
+        }),
+    PrintCase);
+
+}  // namespace
+}  // namespace tint::writer::spirv
diff --git a/src/tint/writer/spirv/ir/test_helper_ir.h b/src/tint/writer/spirv/ir/test_helper_ir.h
index 3d86819..54af5ad 100644
--- a/src/tint/writer/spirv/ir/test_helper_ir.h
+++ b/src/tint/writer/spirv/ir/test_helper_ir.h
@@ -176,19 +176,20 @@
 
     /// Helper to make a scalar value with the scalar type `type`.
     /// @param type the element type
+    /// @param value the optional value to use
     /// @returns the scalar value
-    ir::Value* MakeScalarValue(TestElementType type) {
+    ir::Constant* MakeScalarValue(TestElementType type, uint32_t value = 1) {
         switch (type) {
             case kBool:
                 return b.Constant(true);
             case kI32:
-                return b.Constant(i32(1));
+                return b.Constant(i32(value));
             case kU32:
-                return b.Constant(u32(1));
+                return b.Constant(u32(value));
             case kF32:
-                return b.Constant(f32(1));
+                return b.Constant(f32(value));
             case kF16:
-                return b.Constant(f16(1));
+                return b.Constant(f16(value));
         }
         return nullptr;
     }
@@ -196,7 +197,7 @@
     /// Helper to make a vector value with an element type of `type`.
     /// @param type the element type
     /// @returns the vector value
-    ir::Value* MakeVectorValue(TestElementType type) {
+    ir::Constant* MakeVectorValue(TestElementType type) {
         switch (type) {
             case kBool:
                 return b.Constant(mod.constant_values.Composite(