spirv-reader: Implement OpImageSampleProj* instructions

Bug: tint:1143
Change-Id: Ic07245a2c5afdb2400f3a9777b6fd42f70dab3c8
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/64700
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: David Neto <dneto@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 77c0d84..39a06ef 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -478,12 +478,16 @@
     case SpvOpImageSampleExplicitLod:
     case SpvOpImageSampleDrefImplicitLod:
     case SpvOpImageSampleDrefExplicitLod:
+    // WGSL doesn't have *Proj* texturing; spirv reader emulates it.
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
     case SpvOpImageGather:
     case SpvOpImageDrefGather:
     case SpvOpImageQueryLod:
       return true;
     default:
-      // WGSL doesn't have *Proj* texturing.
       break;
   }
   return false;
@@ -497,9 +501,13 @@
     case SpvOpImageSampleExplicitLod:
     case SpvOpImageSampleDrefImplicitLod:
     case SpvOpImageSampleDrefExplicitLod:
+      // WGSL doesn't have *Proj* texturing; spirv reader emulates it.
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
       return true;
     default:
-      // WGSL doesn't have *Proj* texturing.
       break;
   }
   return false;
@@ -5256,11 +5264,15 @@
   switch (opcode) {
     case SpvOpImageSampleImplicitLod:
     case SpvOpImageSampleExplicitLod:
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
       is_non_dref_sample = true;
       builtin_name = "textureSample";
       break;
     case SpvOpImageSampleDrefImplicitLod:
     case SpvOpImageSampleDrefExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
       is_dref_sample = true;
       builtin_name = "textureSampleCompare";
       if (arg_index < num_args) {
@@ -5304,7 +5316,8 @@
       }
       break;
     default:
-      return Fail() << "internal error: sampled image access";
+      return Fail() << "internal error: unrecognized image access: "
+                    << inst.PrettyPrint();
   }
 
   // Loop over the image operands, looking for extra operands to the builtin.
@@ -5601,7 +5614,20 @@
            << texture_type->TypeInfo().name << " prompted by "
            << inst.PrettyPrint();
   }
-  const auto num_coords_required = num_axes + (is_arrayed ? 1 : 0);
+  bool is_proj = false;
+  switch (inst.opcode()) {
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
+      is_proj = true;
+      break;
+    default:
+      break;
+  }
+
+  const auto num_coords_required =
+      num_axes + (is_arrayed ? 1 : 0) + (is_proj ? 1 : 0);
   uint32_t num_coords_supplied = 0;
   auto* component_type = raw_coords.type;
   if (component_type->IsFloatScalar() || component_type->IsIntegerScalar()) {
@@ -5629,13 +5655,20 @@
   // it to a signed value of the same shape (scalar or vector).
   // Use a lambda to make it easy to only generate the expressions when we
   // will actually use them.
-  auto prefix_swizzle_expr = [this, num_axes, component_type,
+  auto prefix_swizzle_expr = [this, num_axes, component_type, is_proj,
                               raw_coords]() -> ast::Expression* {
     auto* swizzle_type =
         (num_axes == 1) ? component_type : ty_.Vector(component_type, num_axes);
     auto* swizzle = create<ast::MemberAccessorExpression>(
         Source{}, raw_coords.expr, PrefixSwizzle(num_axes));
-    return ToSignedIfUnsigned({swizzle_type, swizzle}).expr;
+    if (is_proj) {
+      auto* q = create<ast::MemberAccessorExpression>(Source{}, raw_coords.expr,
+                                                      Swizzle(num_axes));
+      auto* proj_div = builder_.Div(swizzle, q);
+      return ToSignedIfUnsigned({swizzle_type, proj_div}).expr;
+    } else {
+      return ToSignedIfUnsigned({swizzle_type, swizzle}).expr;
+    }
   };
 
   if (is_arrayed) {
@@ -5650,7 +5683,7 @@
     // Convert it to a signed integer type, if needed
     result.push_back(ToI32({component_type, array_index}).expr);
   } else {
-    if (num_coords_supplied == num_coords_required) {
+    if (num_coords_supplied == num_coords_required && !is_proj) {
       // Pass the value through, with possible unsigned->signed conversion.
       result.push_back(ToSignedIfUnsigned(raw_coords).expr);
     } else {
diff --git a/src/reader/spirv/parser_impl_handle_test.cc b/src/reader/spirv/parser_impl_handle_test.cc
index 99c7d53..4c84b8e 100644
--- a/src/reader/spirv/parser_impl_handle_test.cc
+++ b/src/reader/spirv/parser_impl_handle_test.cc
@@ -3170,6 +3170,864 @@
             ScalarConstructor[not set]{0.000000}
           })"}}));
 
+/////
+// Projection sampling
+/////
+
+// Test matrix for projection sampling:
+// sampling
+//   Dimensions: 1D, 2D, 3D, 2DShadow
+//   Variations: Proj { ImplicitLod { | Bias } | ExplicitLod { Lod | Grad | } }
+//   x { | ConstOffset }
+// depth-compare sampling
+//   Dimensions: 2D
+//   Variations: Proj Dref { ImplicitLod { | Bias } | ExplicitLod { Lod | Grad |
+//   } } x { | ConstOffset }
+//
+// Expanded:
+//    ImageSampleProjImplicitLod        // { | ConstOffset }
+//    ImageSampleProjImplicitLod_Bias   // { | ConstOffset }
+//    ImageSampleProjExplicitLod_Lod    // { | ConstOffset }
+//    ImageSampleProjExplicitLod_Grad   // { | ConstOffset }
+//
+//    ImageSampleProjImplicitLod_DepthTexture
+//
+//    ImageSampleProjDrefImplicitLod        // { | ConstOffset }
+//    ImageSampleProjDrefExplicitLod_Lod    // { | ConstOffset }
+
+INSTANTIATE_TEST_SUITE_P(
+    ImageSampleProjImplicitLod,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+
+        // OpImageSampleProjImplicitLod 1D
+        ImageAccessCase{"%float 1D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords12",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_1d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSample}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords12}
+                  Identifier[not set]{x}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords12}
+                  Identifier[not set]{y}
+                }
+              }
+            )
+          })"},
+
+        // OpImageSampleProjImplicitLod 2D
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords123",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSample}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+            )
+          })"},
+
+        // OpImageSampleProjImplicitLod 3D
+        ImageAccessCase{"%float 3D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords1234",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_3d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSample}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords1234}
+                  Identifier[not set]{xyz}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords1234}
+                  Identifier[not set]{w}
+                }
+              }
+            )
+          })"},
+
+        // OpImageSampleProjImplicitLod 2D with ConstOffset
+        // (Don't need to test with 1D or 3D, as the hard part was the splatted
+        // division.) This case tests handling of the ConstOffset
+        ImageAccessCase{
+            "%float 2D 0 0 0 1 Unknown",
+            "%result = OpImageSampleProjImplicitLod "
+            "%v4float %sampled_image %coords123 ConstOffset %offsets2d",
+            R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+            R"(
+          Call[not set]{
+            Identifier[not set]{textureSample}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              TypeConstructor[not set]{
+                __vec_2__i32
+                ScalarConstructor[not set]{3}
+                ScalarConstructor[not set]{4}
+              }
+            )
+          })"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    ImageSampleProjImplicitLod_Bias,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+
+        // OpImageSampleProjImplicitLod with Bias
+        // Only testing 2D
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords123 Bias %float_7",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleBias}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              ScalarConstructor[not set]{7.000000}
+            )
+          })"},
+
+        // OpImageSampleProjImplicitLod with Bias and signed ConstOffset
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords123 Bias|ConstOffset "
+                        "%float_7 %offsets2d",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleBias}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              ScalarConstructor[not set]{7.000000}
+              TypeConstructor[not set]{
+                __vec_2__i32
+                ScalarConstructor[not set]{3}
+                ScalarConstructor[not set]{4}
+              }
+            )
+          })"},
+
+        // OpImageSampleProjImplicitLod with Bias and unsigned ConstOffset
+        // Convert ConstOffset to signed
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords123 Bias|ConstOffset "
+                        "%float_7 %u_offsets2d",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleBias}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              ScalarConstructor[not set]{7.000000}
+              TypeConstructor[not set]{
+                __vec_2__i32
+                TypeConstructor[not set]{
+                  __vec_2__u32
+                  ScalarConstructor[not set]{3u}
+                  ScalarConstructor[not set]{4u}
+                }
+              }
+            )
+          })"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    ImageSampleProjExplicitLod_Lod,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+        // OpImageSampleProjExplicitLod 2D
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjExplicitLod "
+                        "%v4float %sampled_image %coords123 Lod %f1",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleLevel}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              Identifier[not set]{f1}
+            )
+          })"},
+
+        // OpImageSampleProjExplicitLod 2D Lod with ConstOffset
+        ImageAccessCase{
+            "%float 2D 0 0 0 1 Unknown",
+            "%result = OpImageSampleProjExplicitLod "
+            "%v4float %sampled_image %coords123 Lod|ConstOffset %f1 %offsets2d",
+            R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+            R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleLevel}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              Identifier[not set]{f1}
+              TypeConstructor[not set]{
+                __vec_2__i32
+                ScalarConstructor[not set]{3}
+                ScalarConstructor[not set]{4}
+              }
+            )
+          })"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    ImageSampleProjExplicitLod_Grad,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+        // OpImageSampleProjExplicitLod 2D Grad
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjExplicitLod "
+                        "%v4float %sampled_image %coords123 Grad %vf12 %vf21",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleGrad}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              Identifier[not set]{vf12}
+              Identifier[not set]{vf21}
+            )
+          })"},
+
+        // OpImageSampleProjExplicitLod 2D Lod Grad ConstOffset
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjExplicitLod "
+                        "%v4float %sampled_image %coords123 Grad|ConstOffset "
+                        "%vf12 %vf21 %offsets2d",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __sampled_texture_2d__f32
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleGrad}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              Identifier[not set]{vf12}
+              Identifier[not set]{vf21}
+              TypeConstructor[not set]{
+                __vec_2__i32
+                ScalarConstructor[not set]{3}
+                ScalarConstructor[not set]{4}
+              }
+            )
+          })"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    // Ordinary (non-comparison) sampling on a depth texture.
+    ImageSampleProjImplicitLod_DepthTexture,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+        // OpImageSampleProjImplicitLod 2D depth
+        ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords123",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_sampler
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __depth_texture_2d
+  })",
+                        // Sampling the depth texture yields an f32, but the
+                        // SPIR-V operation yiedls vec4<f32>, so fill out the
+                        // remaining components with 0.
+                        R"(
+          TypeConstructor[not set]{
+            __vec_4__f32
+            Call[not set]{
+              Identifier[not set]{textureSample}
+              (
+                Identifier[not set]{x_20}
+                Identifier[not set]{x_10}
+                Binary[not set]{
+                  MemberAccessor[not set]{
+                    Identifier[not set]{coords123}
+                    Identifier[not set]{xy}
+                  }
+                  divide
+                  MemberAccessor[not set]{
+                    Identifier[not set]{coords123}
+                    Identifier[not set]{z}
+                  }
+                }
+              )
+            }
+            ScalarConstructor[not set]{0.000000}
+            ScalarConstructor[not set]{0.000000}
+            ScalarConstructor[not set]{0.000000}
+          })"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    ImageSampleProjDrefImplicitLod,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+
+        // OpImageSampleProjDrefImplicitLod 2D depth-texture
+        ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
+                        "%result = OpImageSampleProjDrefImplicitLod "
+                        "%float %sampled_image %coords123 %f1",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_comparison
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __depth_texture_2d
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleCompare}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              Identifier[not set]{f1}
+            )
+          })"},
+
+        // OpImageSampleProjDrefImplicitLod 2D depth-texture, ConstOffset
+        ImageAccessCase{
+            "%float 2D 1 0 0 1 Unknown",
+            "%result = OpImageSampleProjDrefImplicitLod "
+            "%float %sampled_image %coords123 %f1 ConstOffset %offsets2d",
+            R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_comparison
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __depth_texture_2d
+  })",
+            R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleCompare}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              Identifier[not set]{f1}
+              TypeConstructor[not set]{
+                __vec_2__i32
+                ScalarConstructor[not set]{3}
+                ScalarConstructor[not set]{4}
+              }
+            )
+          })"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    DISABLED_ImageSampleProjDrefExplicitLod_Lod,
+    SpvParserHandleTest_SampledImageAccessTest,
+    ::testing::Values(
+
+        // Lod must be float constant 0 due to a Metal constraint.
+        // Another test checks cases where the Lod is not float constant 0.
+
+        // OpImageSampleProjDrefExplicitLod 2D depth-texture Lod
+        ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
+                        "%result = OpImageSampleProjDrefExplicitLod "
+                        "%float %sampled_image %coords123 %depth Lod %float_0",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_comparison
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __depth_texture_2d
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleCompare}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              ScalarConstructor[not set]{0.200000}
+              ScalarConstructor[not set]{0.000000}
+            )
+          })"},
+
+        // OpImageSampleProjDrefImplicitLod 2D depth-texture, Lod ConstOffset
+        ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
+                        "%result = OpImageSampleProjDrefExplicitLod "
+                        "%float %sampled_image %coords123 %depth "
+                        "Lod|ConstOffset %float_0 %offsets2d",
+                        R"(
+  Variable{
+    Decorations{
+      GroupDecoration{0}
+      BindingDecoration{0}
+    }
+    x_10
+    none
+    undefined
+    __sampler_comparison
+  }
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    none
+    undefined
+    __depth_texture_2d
+  })",
+                        R"(
+          Call[not set]{
+            Identifier[not set]{textureSampleCompareLevel}
+            (
+              Identifier[not set]{x_20}
+              Identifier[not set]{x_10}
+              Binary[not set]{
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{xy}
+                }
+                divide
+                MemberAccessor[not set]{
+                  Identifier[not set]{coords123}
+                  Identifier[not set]{z}
+                }
+              }
+              ScalarConstructor[not set]{0.200000}
+              ScalarConstructor[not set]{0.000000}
+              TypeConstructor[not set]{
+                __vec_2__i32
+                ScalarConstructor[not set]{3}
+                ScalarConstructor[not set]{4}
+              }
+            )
+          })"}));
+
+/////
+// End projection sampling
+/////
+
 using SpvParserHandleTest_ImageAccessTest =
     SpvParserTestBase<::testing::TestWithParam<ImageAccessCase>>;
 
@@ -6368,6 +7226,59 @@
          "0.0",
          {}}}));
 
+INSTANTIATE_TEST_SUITE_P(
+    ImageSampleProjDrefExplicitLod_CheckForLod0,
+    // This is like the previous test, but for Projection sampling.
+    //
+    // Metal requires comparison sampling with explicit Level-of-detail to use
+    // Lod 0.  The SPIR-V reader requires the operand to be parsed as a constant
+    // 0 value. SPIR-V validation requires the Lod parameter to be a floating
+    // point value for non-fetch operations. So only test float values.
+    SpvParserHandleTest_ImageCoordsTest,
+    ::testing::ValuesIn(std::vector<ImageCoordsCase>{
+        // float 0.0 works
+        {"%float 2D 1 0 0 1 Unknown",
+         "%result = OpImageSampleProjDrefExplicitLod %float %sampled_image "
+         "%vf1234 %depth Lod %float_0",
+         "",
+         {R"(Binary[not set]{
+  MemberAccessor[not set]{
+    Identifier[not set]{vf1234}
+    Identifier[not set]{xy}
+  }
+  divide
+  MemberAccessor[not set]{
+    Identifier[not set]{vf1234}
+    Identifier[not set]{z}
+  }
+}
+)"}},
+        // float null works
+        {"%float 2D 1 0 0 1 Unknown",
+         "%result = OpImageSampleProjDrefExplicitLod %float %sampled_image "
+         "%vf1234 %depth Lod %float_0",
+         "",
+         {R"(Binary[not set]{
+  MemberAccessor[not set]{
+    Identifier[not set]{vf1234}
+    Identifier[not set]{xy}
+  }
+  divide
+  MemberAccessor[not set]{
+    Identifier[not set]{vf1234}
+    Identifier[not set]{z}
+  }
+}
+)"}},
+        // float 1.0 fails.
+        {"%float 2D 1 0 0 1 Unknown",
+         "%result = OpImageSampleProjDrefExplicitLod %float %sampled_image "
+         "%vf1234 %depth Lod %float_1",
+         "WGSL comparison sampling without derivatives requires "
+         "level-of-detail "
+         "0.0",
+         {}}}));
+
 TEST_F(SpvParserHandleTest, CombinedImageSampler_IsError) {
   const auto assembly = Preamble() + R"(
      OpEntryPoint Fragment %100 "main"