Implement textureNumLevels()

SPIR-V reader TODO

Bug: tint:140
Bug: tint:437
Change-Id: Icd41b2ef84e62b304e446589eb2e37c38279af35
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/37846
Reviewed-by: David Neto <dneto@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/ast/intrinsic.cc b/src/ast/intrinsic.cc
index 3f6f8ac..2282650 100644
--- a/src/ast/intrinsic.cc
+++ b/src/ast/intrinsic.cc
@@ -212,6 +212,9 @@
     case Intrinsic::kTextureNumLayers:
       out << "textureNumLayers";
       return out;
+    case Intrinsic::kTextureNumLevels:
+      out << "textureNumLevels";
+      return out;
     case Intrinsic::kTextureSample:
       out << "textureSample";
       return out;
@@ -268,8 +271,8 @@
 }
 
 bool IsTextureIntrinsic(Intrinsic i) {
-  return i == Intrinsic::kTextureDimensions || i == Intrinsic::kTextureLoad ||
-         i == Intrinsic::kTextureNumLayers || i == Intrinsic::kTextureSample ||
+  return IsImageQueryIntrinsic(i) || i == Intrinsic::kTextureLoad ||
+         i == Intrinsic::kTextureSample ||
          i == Intrinsic::kTextureSampleLevel ||
          i == Intrinsic::kTextureSampleBias ||
          i == Intrinsic::kTextureSampleCompare ||
@@ -278,7 +281,7 @@
 
 bool IsImageQueryIntrinsic(Intrinsic i) {
   return i == ast::Intrinsic::kTextureDimensions ||
-         i == Intrinsic::kTextureNumLayers;
+         i == Intrinsic::kTextureNumLayers || i == Intrinsic::kTextureNumLevels;
 }
 
 }  // namespace intrinsic
diff --git a/src/ast/intrinsic.h b/src/ast/intrinsic.h
index 36eb5ad..bdf310e 100644
--- a/src/ast/intrinsic.h
+++ b/src/ast/intrinsic.h
@@ -86,6 +86,7 @@
   kTextureDimensions,
   kTextureLoad,
   kTextureNumLayers,
+  kTextureNumLevels,
   kTextureSample,
   kTextureSampleBias,
   kTextureSampleCompare,
diff --git a/src/ast/intrinsic_texture_helper_test.cc b/src/ast/intrinsic_texture_helper_test.cc
index ac3c61a..280236d 100644
--- a/src/ast/intrinsic_texture_helper_test.cc
+++ b/src/ast/intrinsic_texture_helper_test.cc
@@ -543,7 +543,7 @@
       },
       {
           ValidTextureOverload::kNumLayers1dArray,
-          "textureNumLayers(t : texture_1d_array<T>) -> i32",
+          "textureNumLayers(t : texture_1d_array<f32>) -> i32",
           TextureKind::kRegular,
           type::SamplerKind::kSampler,
           type::TextureDimension::k1dArray,
@@ -553,7 +553,7 @@
       },
       {
           ValidTextureOverload::kNumLayers2dArray,
-          "textureNumLayers(t : texture_2d_array<T>) -> i32",
+          "textureNumLayers(t : texture_2d_array<f32>) -> i32",
           TextureKind::kRegular,
           type::SamplerKind::kSampler,
           type::TextureDimension::k2dArray,
@@ -563,7 +563,7 @@
       },
       {
           ValidTextureOverload::kNumLayersCubeArray,
-          "textureNumLayers(t : texture_cube_array<T>) -> i32",
+          "textureNumLayers(t : texture_cube_array<f32>) -> i32",
           TextureKind::kRegular,
           type::SamplerKind::kSampler,
           type::TextureDimension::kCubeArray,
@@ -573,7 +573,7 @@
       },
       {
           ValidTextureOverload::kNumLayersMultisampled_2dArray,
-          "textureNumLayers(t : texture_multisampled_2d_array<T>) -> i32",
+          "textureNumLayers(t : texture_multisampled_2d_array<f32>) -> i32",
           TextureKind::kMultisampled,
           type::SamplerKind::kSampler,
           type::TextureDimension::k2dArray,
@@ -603,7 +603,7 @@
       },
       {
           ValidTextureOverload::kNumLayersStorageWO1dArray,
-          "textureNumLayers(t : texture_storage_1d_array<F>) -> i32",
+          "textureNumLayers(t : texture_storage_1d_array<rgba32float>) -> i32",
           ast::AccessControl::kWriteOnly,
           ast::type::ImageFormat::kRgba32Float,
           type::TextureDimension::k1dArray,
@@ -613,7 +613,7 @@
       },
       {
           ValidTextureOverload::kNumLayersStorageWO2dArray,
-          "textureNumLayers(t : texture_storage_2d_array<F>) -> i32",
+          "textureNumLayers(t : texture_storage_2d_array<rgba32float>) -> i32",
           ast::AccessControl::kWriteOnly,
           ast::type::ImageFormat::kRgba32Float,
           type::TextureDimension::k2dArray,
@@ -622,6 +622,96 @@
           [](Builder* b) { return b->ExprList("texture"); },
       },
       {
+          ValidTextureOverload::kNumLevels2d,
+          "textureNumLevels(t : texture_2d<f32>) -> i32",
+          TextureKind::kRegular,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevels2dArray,
+          "textureNumLevels(t : texture_2d_array<f32>) -> i32",
+          TextureKind::kRegular,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevels3d,
+          "textureNumLevels(t : texture_3d<f32>) -> i32",
+          TextureKind::kRegular,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsCube,
+          "textureNumLevels(t : texture_cube<f32>) -> i32",
+          TextureKind::kRegular,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsCubeArray,
+          "textureNumLevels(t : texture_cube_array<f32>) -> i32",
+          TextureKind::kRegular,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepth2d,
+          "textureNumLevels(t : texture_depth_2d) -> i32",
+          TextureKind::kDepth,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepth2dArray,
+          "textureNumLevels(t : texture_depth_2d_array) -> i32",
+          TextureKind::kDepth,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepthCube,
+          "textureNumLevels(t : texture_depth_cube) -> i32",
+          TextureKind::kDepth,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepthCubeArray,
+          "textureNumLevels(t : texture_depth_cube_array) -> i32",
+          TextureKind::kDepth,
+          type::SamplerKind::kSampler,
+          type::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](Builder* b) { return b->ExprList("texture"); },
+      },
+      {
           ValidTextureOverload::kSample1dF32,
           "textureSample(t      : texture_1d<f32>,\n"
           "              s      : sampler,\n"
@@ -2413,7 +2503,7 @@
       },
       {
           ValidTextureOverload::kStoreWO1dRgba32float,
-          "textureStore(t      : texture_storage_1d<F>,\n"
+          "textureStore(t      : texture_storage_1d<rgba32float>,\n"
           "             coords : i32,\n"
           "             value  : vec4<T>) -> void",
           ast::AccessControl::kWriteOnly,
@@ -2429,7 +2519,7 @@
       },
       {
           ValidTextureOverload::kStoreWO1dArrayRgba32float,
-          "textureStore(t           : texture_storage_1d_array<F>,\n"
+          "textureStore(t           : texture_storage_1d_array<rgba32float>,\n"
           "             coords      : i32,\n"
           "             array_index : i32,\n"
           "             value       : vec4<T>) -> void",
@@ -2447,7 +2537,7 @@
       },
       {
           ValidTextureOverload::kStoreWO2dRgba32float,
-          "textureStore(t      : texture_storage_2d<F>,\n"
+          "textureStore(t      : texture_storage_2d<rgba32float>,\n"
           "             coords : vec2<i32>,\n"
           "             value  : vec4<T>) -> void",
           ast::AccessControl::kWriteOnly,
@@ -2463,7 +2553,7 @@
       },
       {
           ValidTextureOverload::kStoreWO2dArrayRgba32float,
-          "textureStore(t           : texture_storage_2d_array<F>,\n"
+          "textureStore(t           : texture_storage_2d_array<rgba32float>,\n"
           "             coords      : vec2<i32>,\n"
           "             array_index : i32,\n"
           "             value       : vec4<T>) -> void",
@@ -2481,7 +2571,7 @@
       },
       {
           ValidTextureOverload::kStoreWO3dRgba32float,
-          "textureStore(t      : texture_storage_3d<F>,\n"
+          "textureStore(t      : texture_storage_3d<rgba32float>,\n"
           "             coords : vec3<i32>,\n"
           "             value  : vec4<T>) -> void",
           ast::AccessControl::kWriteOnly,
diff --git a/src/ast/intrinsic_texture_helper_test.h b/src/ast/intrinsic_texture_helper_test.h
index ca0d655..c30f1be 100644
--- a/src/ast/intrinsic_texture_helper_test.h
+++ b/src/ast/intrinsic_texture_helper_test.h
@@ -77,6 +77,15 @@
   kNumLayersDepthCubeArray,
   kNumLayersStorageWO1dArray,
   kNumLayersStorageWO2dArray,
+  kNumLevels2d,
+  kNumLevels2dArray,
+  kNumLevels3d,
+  kNumLevelsCube,
+  kNumLevelsCubeArray,
+  kNumLevelsDepth2d,
+  kNumLevelsDepth2dArray,
+  kNumLevelsDepthCube,
+  kNumLevelsDepthCubeArray,
   kSample1dF32,
   kSample1dArrayF32,
   kSample2dF32,
diff --git a/src/type_determiner.cc b/src/type_determiner.cc
index 57ba101..84f9b6f 100644
--- a/src/type_determiner.cc
+++ b/src/type_determiner.cc
@@ -567,6 +567,7 @@
         }
         break;
       case ast::Intrinsic::kTextureNumLayers:
+      case ast::Intrinsic::kTextureNumLevels:
         param.idx.texture = param.count++;
         break;
       case ast::Intrinsic::kTextureLoad:
@@ -696,6 +697,7 @@
         break;
       }
       case ast::Intrinsic::kTextureNumLayers:
+      case ast::Intrinsic::kTextureNumLevels:
         return_type = mod_->create<ast::type::I32>();
         break;
       case ast::Intrinsic::kTextureStore:
@@ -1034,6 +1036,8 @@
     ident->set_intrinsic(ast::Intrinsic::kTextureDimensions);
   } else if (name == "textureNumLayers") {
     ident->set_intrinsic(ast::Intrinsic::kTextureNumLayers);
+  } else if (name == "textureNumLevels") {
+    ident->set_intrinsic(ast::Intrinsic::kTextureNumLevels);
   } else if (name == "textureLoad") {
     ident->set_intrinsic(ast::Intrinsic::kTextureLoad);
   } else if (name == "textureStore") {
diff --git a/src/type_determiner_test.cc b/src/type_determiner_test.cc
index dcd3519..542d0be 100644
--- a/src/type_determiner_test.cc
+++ b/src/type_determiner_test.cc
@@ -1765,6 +1765,7 @@
         IntrinsicData{"textureDimensions", ast::Intrinsic::kTextureDimensions},
         IntrinsicData{"textureLoad", ast::Intrinsic::kTextureLoad},
         IntrinsicData{"textureNumLayers", ast::Intrinsic::kTextureNumLayers},
+        IntrinsicData{"textureNumLevels", ast::Intrinsic::kTextureNumLevels},
         IntrinsicData{"textureSample", ast::Intrinsic::kTextureSample},
         IntrinsicData{"textureSampleBias", ast::Intrinsic::kTextureSampleBias},
         IntrinsicData{"textureSampleCompare",
@@ -2938,6 +2939,16 @@
     case ValidTextureOverload::kNumLayersStorageWO1dArray:
     case ValidTextureOverload::kNumLayersStorageWO2dArray:
       return R"(textureNumLayers(texture))";
+    case ValidTextureOverload::kNumLevels2d:
+    case ValidTextureOverload::kNumLevels2dArray:
+    case ValidTextureOverload::kNumLevels3d:
+    case ValidTextureOverload::kNumLevelsCube:
+    case ValidTextureOverload::kNumLevelsCubeArray:
+    case ValidTextureOverload::kNumLevelsDepth2d:
+    case ValidTextureOverload::kNumLevelsDepth2dArray:
+    case ValidTextureOverload::kNumLevelsDepthCube:
+    case ValidTextureOverload::kNumLevelsDepthCubeArray:
+      return R"(textureNumLevels(texture))";
     case ValidTextureOverload::kDimensions2dLevel:
     case ValidTextureOverload::kDimensions2dArrayLevel:
     case ValidTextureOverload::kDimensions3dLevel:
@@ -3193,6 +3204,8 @@
     }
   } else if (std::string(param.function) == "textureNumLayers") {
     EXPECT_EQ(call->result_type(), ty.i32);
+  } else if (std::string(param.function) == "textureNumLevels") {
+    EXPECT_EQ(call->result_type(), ty.i32);
   } else if (std::string(param.function) == "textureStore") {
     EXPECT_EQ(call->result_type(), ty.void_);
   } else {
diff --git a/src/writer/hlsl/generator_impl.cc b/src/writer/hlsl/generator_impl.cc
index a750271..187020d 100644
--- a/src/writer/hlsl/generator_impl.cc
+++ b/src/writer/hlsl/generator_impl.cc
@@ -697,7 +697,8 @@
 
   switch (ident->intrinsic()) {
     case ast::Intrinsic::kTextureDimensions:
-    case ast::Intrinsic::kTextureNumLayers: {
+    case ast::Intrinsic::kTextureNumLayers:
+    case ast::Intrinsic::kTextureNumLevels: {
       // Declare a variable to hold the queried texture info
       auto dims = generate_name(kTempNamePrefix);
 
@@ -706,85 +707,120 @@
         return false;
       }
 
-      auto get_dimensions = [&](std::initializer_list<const char*>&& suffixes) {
-        pre << texture_name.str() << ".GetDimensions(";
-        if (pidx.level != kNotUsed) {
-          pre << pidx.level << ", ";
-        }
-        bool first = true;
-        for (auto* suffix : suffixes) {
-          if (!first) {
-            pre << ", ";
-          }
-          first = false;
-          pre << dims << suffix;
-        }
-        pre << ");";
-      };
+      int num_dimensions = 0;
+      const char* swizzle = "";
+      bool add_mip_level_in = false;
 
-      const char* dims_swizzle = "";
-      const char* num_els_swizzle = "";
-
-      std::stringstream ss;
-      switch (texture_type->dim()) {
-        case ast::type::TextureDimension::kNone:
-          error_ = "texture dimension is kNone";
-          return false;
-        case ast::type::TextureDimension::k1d:
-          pre << "int " << dims << ";\n";
-          get_dimensions({""});
-          break;
-        case ast::type::TextureDimension::k1dArray:
-          pre << "int2 " << dims << ";\n";
-          get_dimensions({".x", ".y"});
-          dims_swizzle = ".x";
-          num_els_swizzle = ".y";
-          break;
-        case ast::type::TextureDimension::k2d:
-          pre << "int2 " << dims << ";\n";
-          get_dimensions({".x", ".y"});
-          break;
-        case ast::type::TextureDimension::k2dArray:
-          pre << "int3 " << dims << ";\n";
-          get_dimensions({".x", ".y", ".z"});
-          dims_swizzle = ".xy";
-          num_els_swizzle = ".z";
-          break;
-        case ast::type::TextureDimension::k3d:
-          pre << "int3 " << dims << ";\n";
-          get_dimensions({".x", ".y", ".z"});
-          break;
-        case ast::type::TextureDimension::kCube:
-          // width == height == depth for cubes
-          // See https://github.com/gpuweb/gpuweb/issues/1345
-          pre << "int2 " << dims << ";\n";
-          get_dimensions({".x", ".y"});
-          dims_swizzle = ".xyy";  // [width, height, height]
-          break;
-        case ast::type::TextureDimension::kCubeArray:
-          // width == height == depth for cubes
-          // See https://github.com/gpuweb/gpuweb/issues/1345
-          pre << "int3 " << dims << ";\n";
-          get_dimensions({".x", ".y", ".z"});
-          dims_swizzle = ".xyy";  // [width, height, height]
-          num_els_swizzle = ".z";
-          break;
-      }
-
-      // The result of the textureDimensions() call is now in temporary
-      // variable. This may be packed with other data, so the final expression
-      // may require a swizzle.
       switch (ident->intrinsic()) {
         case ast::Intrinsic::kTextureDimensions:
-          out << dims << dims_swizzle;
-          return true;
+          switch (texture_type->dim()) {
+            case ast::type::TextureDimension::kNone:
+              error_ = "texture dimension is kNone";
+              return false;
+            case ast::type::TextureDimension::k1d:
+              num_dimensions = 1;
+              break;
+            case ast::type::TextureDimension::k1dArray:
+              num_dimensions = 2;
+              swizzle = ".x";
+              break;
+            case ast::type::TextureDimension::k2d:
+              num_dimensions = 2;
+              break;
+            case ast::type::TextureDimension::k2dArray:
+              num_dimensions = 3;
+              swizzle = ".xy";
+              break;
+            case ast::type::TextureDimension::k3d:
+              num_dimensions = 3;
+              break;
+            case ast::type::TextureDimension::kCube:
+              // width == height == depth for cubes
+              // See https://github.com/gpuweb/gpuweb/issues/1345
+              num_dimensions = 2;
+              swizzle = ".xyy";  // [width, height, height]
+              break;
+            case ast::type::TextureDimension::kCubeArray:
+              // width == height == depth for cubes
+              // See https://github.com/gpuweb/gpuweb/issues/1345
+              num_dimensions = 3;
+              swizzle = ".xyy";  // [width, height, height]
+              break;
+          }
+          break;
         case ast::Intrinsic::kTextureNumLayers:
-          out << dims << num_els_swizzle;
-          return true;
+          switch (texture_type->dim()) {
+            default:
+              error_ = "texture dimension is not arrayed";
+              return false;
+            case ast::type::TextureDimension::k1dArray:
+              num_dimensions = 2;
+              swizzle = ".y";
+              break;
+            case ast::type::TextureDimension::k2dArray:
+            case ast::type::TextureDimension::kCubeArray:
+              num_dimensions = 3;
+              swizzle = ".z";
+              break;
+          }
+          break;
+        case ast::Intrinsic::kTextureNumLevels:
+          add_mip_level_in = true;
+          switch (texture_type->dim()) {
+            default:
+              error_ = "texture dimension does not support mips";
+              return false;
+            case ast::type::TextureDimension::k2d:
+            case ast::type::TextureDimension::kCube:
+              num_dimensions = 3;
+              swizzle = ".z";
+              break;
+            case ast::type::TextureDimension::k2dArray:
+            case ast::type::TextureDimension::k3d:
+            case ast::type::TextureDimension::kCubeArray:
+              num_dimensions = 4;
+              swizzle = ".w";
+              break;
+          }
+          break;
         default:
-          error_ = "Unhandled intrinsic";
+          error_ = "unexpected intrinsic";
           return false;
       }
+
+      if (num_dimensions == 1) {
+        pre << "int " << dims << ";\n";
+      } else {
+        pre << "int" << num_dimensions << " " << dims << ";\n";
+      }
+
+      pre << texture_name.str() << ".GetDimensions(";
+      if (pidx.level != kNotUsed) {
+        pre << pidx.level << ", ";
+      } else if (add_mip_level_in) {
+        pre << "0, ";
+      }
+
+      if (num_dimensions == 1) {
+        pre << dims;
+      } else {
+        assert(num_dimensions > 0);
+        assert(num_dimensions <= 4);
+        static constexpr char xyzw[] = {'x', 'y', 'z', 'w'};
+        for (int i = 0; i < num_dimensions; i++) {
+          if (i > 0) {
+            pre << ", ";
+          }
+          pre << dims << "." << xyzw[i];
+        }
+      }
+      pre << ");";
+
+      // The out parameters of the GetDimensions() call is now in temporary
+      // `dims` variable. This may be packed with other data, so the final
+      // expression may require a swizzle.
+      out << dims << swizzle;
+      return true;
     }
     default:
       break;
diff --git a/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc b/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc
index 972283c..4a15b4c 100644
--- a/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc
+++ b/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc
@@ -157,6 +157,27 @@
           "GetDimensions(_tint_tmp.x, _tint_tmp.y, _tint_tmp.z);",
           "_tint_tmp.z",
       };
+    case ValidTextureOverload::kNumLevels2d:
+    case ValidTextureOverload::kNumLevelsCube:
+    case ValidTextureOverload::kNumLevelsDepth2d:
+    case ValidTextureOverload::kNumLevelsDepthCube:
+      return {
+          "int3 _tint_tmp;\n"
+          "texture_tint_0."
+          "GetDimensions(0, _tint_tmp.x, _tint_tmp.y, _tint_tmp.z);",
+          "_tint_tmp.z",
+      };
+    case ValidTextureOverload::kNumLevels2dArray:
+    case ValidTextureOverload::kNumLevels3d:
+    case ValidTextureOverload::kNumLevelsCubeArray:
+    case ValidTextureOverload::kNumLevelsDepth2dArray:
+    case ValidTextureOverload::kNumLevelsDepthCubeArray:
+      return {
+          "int4 _tint_tmp;\n"
+          "texture_tint_0.GetDimensions(0, "
+          "_tint_tmp.x, _tint_tmp.y, _tint_tmp.z, _tint_tmp.w);",
+          "_tint_tmp.w",
+      };
     case ValidTextureOverload::kSample1dF32:
       return R"(texture_tint_0.Sample(sampler_tint_0, 1.0f))";
     case ValidTextureOverload::kSample1dArrayF32:
diff --git a/src/writer/msl/generator_impl.cc b/src/writer/msl/generator_impl.cc
index 30c1887..f3a55e4 100644
--- a/src/writer/msl/generator_impl.cc
+++ b/src/writer/msl/generator_impl.cc
@@ -679,6 +679,14 @@
       out_ << ".get_array_size())";
       return true;
     }
+    case ast::Intrinsic::kTextureNumLevels: {
+      out_ << "int(";
+      if (!EmitExpression(params[pidx.texture])) {
+        return false;
+      }
+      out_ << ".get_num_mip_levels())";
+      return true;
+    }
     default:
       break;
   }
diff --git a/src/writer/msl/generator_impl_intrinsic_texture_test.cc b/src/writer/msl/generator_impl_intrinsic_texture_test.cc
index 67bbdc7..962a18b 100644
--- a/src/writer/msl/generator_impl_intrinsic_texture_test.cc
+++ b/src/writer/msl/generator_impl_intrinsic_texture_test.cc
@@ -80,6 +80,16 @@
     case ValidTextureOverload::kNumLayersStorageWO1dArray:
     case ValidTextureOverload::kNumLayersStorageWO2dArray:
       return R"(int(texture_tint_0.get_array_size()))";
+    case ValidTextureOverload::kNumLevels2d:
+    case ValidTextureOverload::kNumLevels2dArray:
+    case ValidTextureOverload::kNumLevels3d:
+    case ValidTextureOverload::kNumLevelsCube:
+    case ValidTextureOverload::kNumLevelsCubeArray:
+    case ValidTextureOverload::kNumLevelsDepth2d:
+    case ValidTextureOverload::kNumLevelsDepth2dArray:
+    case ValidTextureOverload::kNumLevelsDepthCube:
+    case ValidTextureOverload::kNumLevelsDepthCubeArray:
+      return R"(int(texture_tint_0.get_num_mip_levels()))";
     case ValidTextureOverload::kSample1dF32:
       return R"(texture_tint_0.sample(sampler_tint_0, 1.0f))";
     case ValidTextureOverload::kSample1dArrayF32:
diff --git a/src/writer/spirv/builder.cc b/src/writer/spirv/builder.cc
index 8887d63..7f913a5 100644
--- a/src/writer/spirv/builder.cc
+++ b/src/writer/spirv/builder.cc
@@ -2245,6 +2245,12 @@
       }
       break;
     }
+    case ast::Intrinsic::kTextureNumLevels: {
+      op = spv::Op::OpImageQueryLevels;
+      append_result_type_and_id_to_spirv_params();
+      spirv_params.emplace_back(gen_param(pidx.texture));
+      break;
+    }
     case ast::Intrinsic::kTextureLoad: {
       op = texture_type->Is<ast::type::StorageTexture>()
                ? spv::Op::OpImageRead
diff --git a/src/writer/spirv/builder_intrinsic_texture_test.cc b/src/writer/spirv/builder_intrinsic_texture_test.cc
index abb588f..9267280 100644
--- a/src/writer/spirv/builder_intrinsic_texture_test.cc
+++ b/src/writer/spirv/builder_intrinsic_texture_test.cc
@@ -910,6 +910,170 @@
               R"(
 OpCapability ImageQuery
 )"};
+    case ValidTextureOverload::kNumLevels2d:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 2D 0 0 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevels2dArray:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 2D 0 1 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevels3d:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 3D 0 0 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevelsCube:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 Cube 0 0 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevelsCubeArray:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 Cube 0 1 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability SampledCubeArray
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevelsDepth2d:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 2D 1 0 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevelsDepth2dArray:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 2D 1 1 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevelsDepthCube:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 Cube 1 0 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability ImageQuery
+)"};
+    case ValidTextureOverload::kNumLevelsDepthCubeArray:
+      return {R"(
+%4 = OpTypeFloat 32
+%3 = OpTypeImage %4 Cube 1 1 0 1 Unknown
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
+%7 = OpTypeSampler
+%6 = OpTypePointer UniformConstant %7
+%5 = OpVariable %6 UniformConstant
+%9 = OpTypeInt 32 1
+)",
+              R"(
+%10 = OpLoad %3 %1
+%8 = OpImageQueryLevels %9 %10
+)",
+              R"(
+OpCapability SampledCubeArray
+OpCapability ImageQuery
+)"};
     case ValidTextureOverload::kSample1dF32:
       return {
           R"(