Add MultiplanarExternalTextureTransform and Tests

Implements MultiplanarExternalTextureTransform to allow transforming a
texture_external binding into two texture_2d<f32> bindings and a uniform
buffer binding. Transforms textureSampleLevel and textureLoad calls with
a texture_external parameter into custom functions that can handle both
single-plane RGBA or bi-planar YUV. Includes tests.

Bug: dawn:1082
Change-Id: Icb6d8b0f3773feca01c833171f07230c3531f3aa
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/68620
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 8140244..fb8c8e7 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -453,6 +453,8 @@
     "transform/manager.h",
     "transform/module_scope_var_to_entry_point_param.cc",
     "transform/module_scope_var_to_entry_point_param.h",
+    "transform/multiplanar_external_texture.cc",
+    "transform/multiplanar_external_texture.h",
     "transform/num_workgroups_from_uniform.cc",
     "transform/num_workgroups_from_uniform.h",
     "transform/pad_array_elements.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 866edf9..3bb6ae7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -317,6 +317,8 @@
   transform/manager.h
   transform/module_scope_var_to_entry_point_param.cc
   transform/module_scope_var_to_entry_point_param.h
+  transform/multiplanar_external_texture.cc
+  transform/multiplanar_external_texture.h
   transform/num_workgroups_from_uniform.cc
   transform/num_workgroups_from_uniform.h
   transform/pad_array_elements.cc
@@ -954,6 +956,7 @@
       transform/inline_pointer_lets_test.cc
       transform/loop_to_for_loop_test.cc
       transform/module_scope_var_to_entry_point_param_test.cc
+      transform/multiplanar_external_texture_test.cc
       transform/num_workgroups_from_uniform_test.cc
       transform/pad_array_elements_test.cc
       transform/promote_initializers_to_const_var_test.cc
diff --git a/src/transform/multiplanar_external_texture.cc b/src/transform/multiplanar_external_texture.cc
new file mode 100644
index 0000000..599f593
--- /dev/null
+++ b/src/transform/multiplanar_external_texture.cc
@@ -0,0 +1,356 @@
+// Copyright 2021 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/transform/multiplanar_external_texture.h"
+
+#include <string>
+#include <vector>
+
+#include "src/ast/function.h"
+#include "src/program_builder.h"
+#include "src/sem/call.h"
+#include "src/sem/variable.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::transform::MultiplanarExternalTexture);
+TINT_INSTANTIATE_TYPEINFO(
+    tint::transform::MultiplanarExternalTexture::NewBindingPoints);
+
+namespace tint {
+namespace transform {
+namespace {
+
+/// This struct stores symbols for new bindings created as a result of
+/// transforming a texture_external instance.
+struct NewBindingSymbols {
+  Symbol ext_tex_params_binding_sym;
+  Symbol ext_tex_plane_1_binding_sym;
+};
+}  // namespace
+
+/// State holds the current transform state
+struct MultiplanarExternalTexture::State {
+  /// Symbol for the ExternalTextureParams struct
+  Symbol external_texture_params_struct_sym;
+
+  /// Symbol for the textureLoadExternal function
+  Symbol texture_load_external_sym;
+
+  /// Symbol for the textureSampleExternal function
+  Symbol texture_sample_external_sym;
+
+  /// Storage for new bindings that have been created corresponding to an
+  /// original texture_external binding.
+  std::unordered_map<Symbol, NewBindingSymbols> new_binding_symbols;
+};
+
+MultiplanarExternalTexture::NewBindingPoints::NewBindingPoints(
+    BindingsMap inputBindingsMap)
+    : bindings_map(std::move(inputBindingsMap)) {}
+MultiplanarExternalTexture::NewBindingPoints::~NewBindingPoints() = default;
+
+MultiplanarExternalTexture::MultiplanarExternalTexture() = default;
+MultiplanarExternalTexture::~MultiplanarExternalTexture() = default;
+
+// Within this transform, an instance of a texture_external binding is unpacked
+// into two texture_2d<f32> bindings representing two possible planes of a
+// single texture and a uniform buffer binding representing a struct of
+// parameters. Calls to textureLoad or textureSampleLevel that contain a
+// texture_external parameter will be transformed into a newly generated version
+// of the function, which can perform the desired operation on a single RGBA
+// plane or on seperate Y and UV planes.
+void MultiplanarExternalTexture::Run(CloneContext& ctx,
+                                     const DataMap& inputs,
+                                     DataMap&) {
+  State state;
+  auto& b = *ctx.dst;
+
+  auto* new_binding_points = inputs.Get<NewBindingPoints>();
+
+  if (!new_binding_points) {
+    b.Diagnostics().add_error(
+        diag::System::Transform,
+        "missing new binding point data for " + std::string(TypeInfo().name));
+    return;
+  }
+
+  auto& sem = ctx.src->Sem();
+
+  // For each texture_external binding, we replace it with a texture_2d<f32>
+  // binding and create two additional bindings (one texture_2d<f32> to
+  // represent the secondary plane and one uniform buffer for the
+  // ExternalTextureParams struct).
+  ctx.ReplaceAll([&](const ast::Variable* var) -> const ast::Variable* {
+    if (!::tint::Is<ast::ExternalTexture>(var->type)) {
+      return nullptr;
+    }
+
+    // If the decorations are empty, then this must be a texture_external being
+    // passed as a function parameter. We need to unpack this into multiple
+    // parameters - but this hasn't been implemented so produce an error.
+    if (var->decorations.empty()) {
+      b.Diagnostics().add_error(
+          diag::System::Transform,
+          "transforming a texture_external passed as a user-defined function "
+          "parameter has not been implemented.");
+      return nullptr;
+    }
+
+    // If we find a texture_external binding, we know we must emit the
+    // ExternalTextureParams struct.
+    if (!state.external_texture_params_struct_sym.IsValid()) {
+      ast::StructMemberList member_list = {
+          b.Member("numPlanes", b.ty.u32()), b.Member("vr", b.ty.f32()),
+          b.Member("ug", b.ty.f32()), b.Member("vg", b.ty.f32()),
+          b.Member("ub", b.ty.f32())};
+
+      state.external_texture_params_struct_sym =
+          b.Symbols().New("ExternalTextureParams");
+
+      b.Structure(state.external_texture_params_struct_sym, member_list,
+                  ast::DecorationList{b.StructBlock()});
+    }
+
+    // The binding points for the newly introduced bindings must have been
+    // provided to this transform. We fetch the new binding points by
+    // providing the original texture_external binding points into the
+    // passed map.
+    BindingPoint bp = {var->BindingPoint().group->value,
+                       var->BindingPoint().binding->value};
+    BindingPoints bps;
+    BindingsMap::const_iterator it = new_binding_points->bindings_map.find(bp);
+    if (it == new_binding_points->bindings_map.end()) {
+      b.Diagnostics().add_error(
+          diag::System::Transform,
+          "missing new binding points for texture_external at binding {" +
+              std::to_string(bp.group) + "," + std::to_string(bp.binding) +
+              "}");
+      return nullptr;
+    } else {
+      bps = it->second;
+    }
+
+    // Symbols for the newly created bindings must be saved so they can be
+    // passed as parameters later. These are placed in a map and keyed by
+    // the symbol associated with the texture_external binding that
+    // corresponds with the new bindings.
+    NewBindingSymbols new_binding_syms;
+    new_binding_syms.ext_tex_plane_1_binding_sym =
+        b.Symbols().New("ext_tex_plane_1");
+    b.Global(new_binding_syms.ext_tex_plane_1_binding_sym,
+             b.ty.sampled_texture(ast::TextureDimension::k2d, b.ty.f32()),
+             b.GroupAndBinding(bps.plane_1.group, bps.plane_1.binding));
+
+    new_binding_syms.ext_tex_params_binding_sym =
+        b.Symbols().New("ext_tex_params");
+    b.Global(new_binding_syms.ext_tex_params_binding_sym,
+             b.ty.type_name("ExternalTextureParams"),
+             ast::StorageClass::kUniform,
+             b.GroupAndBinding(bps.params.group, bps.params.binding));
+
+    // Replace the original texture_external binding with a texture_2d<f32>
+    // binding.
+    auto cloned_sym = ctx.Clone(var->symbol);
+    ast::DecorationList cloned_decorations = ctx.Clone(var->decorations);
+    const ast::Expression* cloned_constructor = ctx.Clone(var->constructor);
+    state.new_binding_symbols[cloned_sym] = new_binding_syms;
+
+    return b.Var(cloned_sym,
+                 b.ty.sampled_texture(ast::TextureDimension::k2d, b.ty.f32()),
+                 cloned_constructor, cloned_decorations);
+  });
+
+  // Transform the original textureLoad and textureSampleLevel calls into
+  // textureLoadExternal and textureSampleExternal calls.
+  ctx.ReplaceAll([&](const ast::CallExpression* expr)
+                     -> const ast::CallExpression* {
+    auto* intrinsic = sem.Get(expr)->Target()->As<sem::Intrinsic>();
+
+    if (!intrinsic ||
+        !intrinsic->Parameters()[0]->Type()->Is<sem::ExternalTexture>() ||
+        intrinsic->Parameters().empty() ||
+        intrinsic->Type() == sem::IntrinsicType::kTextureDimensions) {
+      return nullptr;
+    }
+
+    const ast::Expression* ext_tex_plane_0_binding_param =
+        ctx.Clone(expr->args[0]);
+
+    // Lookup the symbols for the new bindings using the symbol from the
+    // original texture_external.
+    Symbol ext_tex_plane_1_binding_sym =
+        state
+            .new_binding_symbols[ext_tex_plane_0_binding_param
+                                     ->As<ast::IdentifierExpression>()
+                                     ->symbol]
+            .ext_tex_plane_1_binding_sym;
+    Symbol ext_tex_params_binding_sym =
+        state
+            .new_binding_symbols[ext_tex_plane_0_binding_param
+                                     ->As<ast::IdentifierExpression>()
+                                     ->symbol]
+            .ext_tex_params_binding_sym;
+
+    // If valid new binding locations were not provided earlier, we would
+    // have been unable to create these symbols. An error message was
+    // emitted earlier, so just return early to avoid internal compiler
+    // errors and retain a clean error message.
+    if (!ext_tex_plane_1_binding_sym.IsValid() ||
+        !ext_tex_params_binding_sym.IsValid()) {
+      return nullptr;
+    }
+
+    ast::IdentifierExpression* exp;
+    ast::ExpressionList params;
+
+    if (intrinsic->Type() == sem::IntrinsicType::kTextureLoad) {
+      if (expr->args.size() != 2) {
+        TINT_ICE(Transform, b.Diagnostics())
+            << "expected textureLoad call with a texture_external "
+               "to "
+               "have 2 parameters, found "
+            << expr->args.size() << " parameters";
+      }
+
+      if (!state.texture_load_external_sym.IsValid()) {
+        state.texture_load_external_sym =
+            b.Symbols().New("textureLoadExternal");
+
+        // Emit the textureLoadExternal function.
+        ast::VariableList var_list = {
+            b.Param("plane0", b.ty.sampled_texture(ast::TextureDimension::k2d,
+                                                   b.ty.f32())),
+            b.Param("plane1", b.ty.sampled_texture(ast::TextureDimension::k2d,
+                                                   b.ty.f32())),
+            b.Param("coord", b.ty.vec2(b.ty.i32())),
+            b.Param("params",
+                    b.ty.type_name(state.external_texture_params_struct_sym))};
+
+        ast::StatementList statement_list =
+            createTexFnExtStatementList(b, sem::IntrinsicType::kTextureLoad);
+
+        b.Func(state.texture_load_external_sym, var_list, b.ty.vec4(b.ty.f32()),
+               statement_list, {});
+      }
+
+      exp =
+          b.create<ast::IdentifierExpression>(state.texture_load_external_sym);
+      params = {ext_tex_plane_0_binding_param,
+                b.Expr(ext_tex_plane_1_binding_sym), ctx.Clone(expr->args[1]),
+                b.Expr(ext_tex_params_binding_sym)};
+    } else if (intrinsic->Type() == sem::IntrinsicType::kTextureSampleLevel) {
+      if (expr->args.size() != 3) {
+        TINT_ICE(Transform, b.Diagnostics())
+            << "expected textureSampleLevel call with a "
+               "texture_external to have 3 parameters, found "
+            << expr->args.size() << " parameters";
+      }
+
+      if (!state.texture_sample_external_sym.IsValid()) {
+        state.texture_sample_external_sym =
+            b.Symbols().New("textureSampleExternal");
+
+        // Emit the textureSampleExternal function.
+        ast::VariableList varList = {
+            b.Param("plane0", b.ty.sampled_texture(ast::TextureDimension::k2d,
+                                                   b.ty.f32())),
+            b.Param("plane1", b.ty.sampled_texture(ast::TextureDimension::k2d,
+                                                   b.ty.f32())),
+            b.Param("smp", b.ty.sampler(ast::SamplerKind::kSampler)),
+            b.Param("coord", b.ty.vec2(b.ty.f32())),
+            b.Param("params",
+                    b.ty.type_name(state.external_texture_params_struct_sym))};
+
+        ast::StatementList statementList = createTexFnExtStatementList(
+            b, sem::IntrinsicType::kTextureSampleLevel);
+
+        b.Func(state.texture_sample_external_sym, varList,
+               b.ty.vec4(b.ty.f32()), statementList, {});
+      }
+      exp = b.create<ast::IdentifierExpression>(
+          state.texture_sample_external_sym);
+      params = {ext_tex_plane_0_binding_param,
+                b.Expr(ext_tex_plane_1_binding_sym), ctx.Clone(expr->args[1]),
+                ctx.Clone(expr->args[2]), b.Expr(ext_tex_params_binding_sym)};
+    }
+
+    return b.Call(exp, params);
+  });
+
+  ctx.Clone();
+}
+
+// Constructs a StatementList containing all the statements making up the bodies
+// of the textureSampleExternal and textureLoadExternal functions.
+ast::StatementList MultiplanarExternalTexture::createTexFnExtStatementList(
+    ProgramBuilder& b,
+    sem::IntrinsicType callType) {
+  using f32 = ProgramBuilder::f32;
+  const ast::CallExpression* single_plane_call;
+  const ast::CallExpression* plane_0_call;
+  const ast::CallExpression* plane_1_call;
+  if (callType == sem::IntrinsicType::kTextureSampleLevel) {
+    // textureSampleLevel(plane0, smp, coord.xy, 0.0);
+    single_plane_call =
+        b.Call("textureSampleLevel", "plane0", "smp", "coord", 0.0f);
+    // textureSampleLevel(plane0, smp, coord.xy, 0.0);
+    plane_0_call = b.Call("textureSampleLevel", "plane0", "smp", "coord", 0.0f);
+    // textureSampleLevel(plane1, smp, coord.xy, 0.0);
+    plane_1_call = b.Call("textureSampleLevel", "plane1", "smp", "coord", 0.0f);
+  } else if (callType == sem::IntrinsicType::kTextureLoad) {
+    // textureLoad(plane0, coords.xy, 0);
+    single_plane_call = b.Call("textureLoad", "plane0", "coord", 0);
+    // textureLoad(plane0, coords.xy, 0);
+    plane_0_call = b.Call("textureLoad", "plane0", "coord", 0);
+    // textureLoad(plane1, coords.xy, 0);
+    plane_1_call = b.Call("textureLoad", "plane1", "coord", 0);
+  }
+
+  return {
+      // if (params.numPlanes == 1u) {
+      //    return singlePlaneCall
+      // }
+      b.If(b.create<ast::BinaryExpression>(
+               ast::BinaryOp::kEqual, b.MemberAccessor("params", "numPlanes"),
+               b.Expr(1u)),
+           b.Block(b.Return(single_plane_call))),
+      // let y = plane0Call.r - 0.0625;
+      b.Decl(b.Const("y", nullptr,
+                     b.Sub(b.MemberAccessor(plane_0_call, "r"), 0.0625f))),
+      // let uv = plane1Call.rg - 0.5;
+      b.Decl(b.Const("uv", nullptr,
+                     b.Sub(b.MemberAccessor(plane_1_call, "rg"), 0.5f))),
+      // let u = uv.x;
+      b.Decl(b.Const("u", nullptr, b.MemberAccessor("uv", "x"))),
+      // let v = uv.y;
+      b.Decl(b.Const("v", nullptr, b.MemberAccessor("uv", "y"))),
+      // let r = 1.164 * y + params.vr * v;
+      b.Decl(b.Const("r", nullptr,
+                     b.Add(b.Mul(1.164f, "y"),
+                           b.Mul(b.MemberAccessor("params", "vr"), "v")))),
+      // let g = 1.164 * y - params.ug * u - params.vg * v;
+      b.Decl(b.Const("g", nullptr,
+                     b.Sub(b.Sub(b.Mul(1.164f, "y"),
+                                 b.Mul(b.MemberAccessor("params", "ug"), "u")),
+                           b.Mul(b.MemberAccessor("params", "vg"), "v")))),
+      // let b = 1.164 * y + params.ub * u;
+      b.Decl(b.Const("b", nullptr,
+                     b.Add(b.Mul(1.164f, "y"),
+                           b.Mul(b.MemberAccessor("params", "ub"), "u")))),
+      // return vec4<f32>(r, g, b, 1.0);
+      b.Return(b.vec4<f32>("r", "g", "b", 1.0f)),
+  };
+}
+
+}  // namespace transform
+}  // namespace tint
diff --git a/src/transform/multiplanar_external_texture.h b/src/transform/multiplanar_external_texture.h
new file mode 100644
index 0000000..718dd3a
--- /dev/null
+++ b/src/transform/multiplanar_external_texture.h
@@ -0,0 +1,105 @@
+// Copyright 2021 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.
+
+#ifndef SRC_TRANSFORM_MULTIPLANAR_EXTERNAL_TEXTURE_H_
+#define SRC_TRANSFORM_MULTIPLANAR_EXTERNAL_TEXTURE_H_
+
+#include <unordered_map>
+#include <utility>
+
+#include "src/ast/struct_member.h"
+#include "src/sem/binding_point.h"
+#include "src/sem/intrinsic_type.h"
+#include "src/transform/transform.h"
+
+namespace tint {
+namespace transform {
+
+/// BindingPoint is an alias to sem::BindingPoint
+using BindingPoint = sem::BindingPoint;
+
+/// This struct identifies the binding groups and locations for new bindings to
+/// use when transforming a texture_external instance.
+struct BindingPoints {
+  /// The desired binding location of the texture_2d representing plane #1 when
+  /// a texture_external binding is expanded.
+  BindingPoint plane_1;
+  /// The desired binding location of the ExternalTextureParams uniform when a
+  /// texture_external binding is expanded.
+  BindingPoint params;
+};
+
+/// Within the MultiplanarExternalTexture transform, each instance of a
+/// texture_external binding is unpacked into two texture_2d<f32> bindings
+/// representing two possible planes of a texture and a uniform buffer binding
+/// representing a struct of parameters. Calls to textureLoad or
+/// textureSampleLevel that contain a texture_external parameter will be
+/// transformed into a newly generated version of the function, which can
+/// perform the desired operation on a single RGBA plane or on seperate Y and UV
+/// planes.
+class MultiplanarExternalTexture
+    : public Castable<MultiplanarExternalTexture, Transform> {
+ public:
+  /// BindingsMap is a map where the key is the binding location of a
+  /// texture_external and the value is a struct containing the desired
+  /// locations for new bindings expanded from the texture_external instance.
+  using BindingsMap = std::unordered_map<BindingPoint, BindingPoints>;
+
+  /// NewBindingPoints is consumed by the MultiplanarExternalTexture transform.
+  /// Data holds information about location of each texture_external binding and
+  /// which binding slots it should expand into.
+  struct NewBindingPoints : public Castable<Data, transform::Data> {
+    /// Constructor
+    /// @param bm a map to the new binding slots to use.
+    explicit NewBindingPoints(BindingsMap bm);
+
+    /// Destructor
+    ~NewBindingPoints() override;
+
+    /// A map of new binding points to use.
+    const BindingsMap bindings_map;
+  };
+
+  /// Constructor
+  MultiplanarExternalTexture();
+  /// Destructor
+  ~MultiplanarExternalTexture() override;
+
+ protected:
+  struct State;
+
+  /// Runs the transform using the CloneContext built for transforming a
+  /// program. Run() is responsible for calling Clone() on the CloneContext.
+  /// @param ctx the CloneContext primed with the input program and
+  /// ProgramBuilder
+  /// @param inputs optional extra transform-specific input data
+  /// @param outputs optional extra transform-specific output data
+  void Run(CloneContext& ctx, const DataMap& inputs, DataMap& outputs) override;
+
+  /// Creates the statement list for the TextureSampleExternal and
+  /// TextureLoadExternal functions.
+  /// @param b a reference to the ProgramBuilder associated with the destination
+  /// context
+  /// @param callType determines the kind of param list to emit (either
+  /// textureLoad or textureSampleLevel)
+  /// @returns a statement list that is used to create the TextureSampleExternal
+  /// and TextureLoadExternal functions.
+  ast::StatementList createTexFnExtStatementList(ProgramBuilder& b,
+                                                 sem::IntrinsicType callType);
+};
+
+}  // namespace transform
+}  // namespace tint
+
+#endif  // SRC_TRANSFORM_MULTIPLANAR_EXTERNAL_TEXTURE_H_
diff --git a/src/transform/multiplanar_external_texture_test.cc b/src/transform/multiplanar_external_texture_test.cc
new file mode 100644
index 0000000..489a437
--- /dev/null
+++ b/src/transform/multiplanar_external_texture_test.cc
@@ -0,0 +1,408 @@
+// Copyright 2021 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/transform/multiplanar_external_texture.h"
+#include "src/transform/test_helper.h"
+
+namespace tint {
+namespace transform {
+namespace {
+
+using MultiplanarExternalTextureTest = TransformTest;
+
+// Running the transform without passing in data for the new bindings should
+// result in an error.
+TEST_F(MultiplanarExternalTextureTest, ErrorNoPassedData) {
+  auto* src = R"(
+[[group(0), binding(0)]] var s : sampler;
+[[group(0), binding(1)]] var ext_tex : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSampleLevel(ext_tex, s, coord.xy);
+}
+)";
+  auto* expect =
+      R"(error: missing new binding point data for tint::transform::MultiplanarExternalTexture)";
+
+  auto got = Run<MultiplanarExternalTexture>(src);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Running the transform with incorrect binding data should result in an error.
+TEST_F(MultiplanarExternalTextureTest, ErrorIncorrectBindingPont) {
+  auto* src = R"(
+[[group(0), binding(0)]] var s : sampler;
+[[group(0), binding(1)]] var ext_tex : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSampleLevel(ext_tex, s, coord.xy);
+}
+)";
+
+  auto* expect =
+      R"(error: missing new binding points for texture_external at binding {0,1})";
+
+  DataMap data;
+  // This bindings map specifies 0,0 as the location of the texture_external,
+  // which is incorrect.
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Tests that the transform works with a textureDimensions call.
+TEST_F(MultiplanarExternalTextureTest, Dimensions) {
+  auto* src = R"(
+[[group(0), binding(0)]] var ext_tex : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  var dim : vec2<i32>;
+  dim = textureDimensions(ext_tex);
+  return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+}
+)";
+
+  auto* expect = R"(
+[[block]]
+struct ExternalTextureParams {
+  numPlanes : u32;
+  vr : f32;
+  ug : f32;
+  vg : f32;
+  ub : f32;
+};
+
+[[group(0), binding(1)]] var ext_tex_plane_1 : texture_2d<f32>;
+
+[[group(0), binding(2)]] var<uniform> ext_tex_params : ExternalTextureParams;
+
+[[group(0), binding(0)]] var ext_tex : texture_2d<f32>;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  var dim : vec2<i32>;
+  dim = textureDimensions(ext_tex);
+  return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+}
+)";
+
+  DataMap data;
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Test that the transform works with a textureSampleLevel call.
+TEST_F(MultiplanarExternalTextureTest, BasicTextureSampleLevel) {
+  auto* src = R"(
+[[group(0), binding(0)]] var s : sampler;
+[[group(0), binding(1)]] var ext_tex : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSampleLevel(ext_tex, s, coord.xy);
+}
+)";
+
+  auto* expect = R"(
+[[group(0), binding(0)]] var s : sampler;
+
+[[block]]
+struct ExternalTextureParams {
+  numPlanes : u32;
+  vr : f32;
+  ug : f32;
+  vg : f32;
+  ub : f32;
+};
+
+[[group(0), binding(2)]] var ext_tex_plane_1 : texture_2d<f32>;
+
+[[group(0), binding(3)]] var<uniform> ext_tex_params : ExternalTextureParams;
+
+[[group(0), binding(1)]] var ext_tex : texture_2d<f32>;
+
+fn textureSampleExternal(plane0 : texture_2d<f32>, plane1 : texture_2d<f32>, smp : sampler, coord : vec2<f32>, params : ExternalTextureParams) -> vec4<f32> {
+  if ((params.numPlanes == 1u)) {
+    return textureSampleLevel(plane0, smp, coord, 0.0);
+  }
+  let y = (textureSampleLevel(plane0, smp, coord, 0.0).r - 0.0625);
+  let uv = (textureSampleLevel(plane1, smp, coord, 0.0).rg - 0.5);
+  let u = uv.x;
+  let v = uv.y;
+  let r = ((1.164000034 * y) + (params.vr * v));
+  let g = (((1.164000034 * y) - (params.ug * u)) - (params.vg * v));
+  let b = ((1.164000034 * y) + (params.ub * u));
+  return vec4<f32>(r, g, b, 1.0);
+}
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSampleExternal(ext_tex, ext_tex_plane_1, s, coord.xy, ext_tex_params);
+}
+)";
+
+  DataMap data;
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{{{0, 1}, {{0, 2}, {0, 3}}}});
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Tests that the transform works with a textureLoad call.
+TEST_F(MultiplanarExternalTextureTest, BasicTextureLoad) {
+  auto* src = R"(
+[[group(0), binding(0)]] var ext_tex : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureLoad(ext_tex, vec2<i32>(1, 1));
+}
+)";
+
+  auto* expect = R"(
+[[block]]
+struct ExternalTextureParams {
+  numPlanes : u32;
+  vr : f32;
+  ug : f32;
+  vg : f32;
+  ub : f32;
+};
+
+[[group(0), binding(1)]] var ext_tex_plane_1 : texture_2d<f32>;
+
+[[group(0), binding(2)]] var<uniform> ext_tex_params : ExternalTextureParams;
+
+[[group(0), binding(0)]] var ext_tex : texture_2d<f32>;
+
+fn textureLoadExternal(plane0 : texture_2d<f32>, plane1 : texture_2d<f32>, coord : vec2<i32>, params : ExternalTextureParams) -> vec4<f32> {
+  if ((params.numPlanes == 1u)) {
+    return textureLoad(plane0, coord, 0);
+  }
+  let y = (textureLoad(plane0, coord, 0).r - 0.0625);
+  let uv = (textureLoad(plane1, coord, 0).rg - 0.5);
+  let u = uv.x;
+  let v = uv.y;
+  let r = ((1.164000034 * y) + (params.vr * v));
+  let g = (((1.164000034 * y) - (params.ug * u)) - (params.vg * v));
+  let b = ((1.164000034 * y) + (params.ub * u));
+  return vec4<f32>(r, g, b, 1.0);
+}
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureLoadExternal(ext_tex, ext_tex_plane_1, vec2<i32>(1, 1), ext_tex_params);
+}
+)";
+
+  DataMap data;
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Tests that the transform works with both a textureSampleLevel and textureLoad
+// call.
+TEST_F(MultiplanarExternalTextureTest, TextureSampleAndTextureLoad) {
+  auto* src = R"(
+[[group(0), binding(0)]] var s : sampler;
+[[group(0), binding(1)]] var ext_tex : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSampleLevel(ext_tex, s, coord.xy) + textureLoad(ext_tex, vec2<i32>(1, 1));
+}
+)";
+
+  auto* expect = R"(
+[[group(0), binding(0)]] var s : sampler;
+
+[[block]]
+struct ExternalTextureParams {
+  numPlanes : u32;
+  vr : f32;
+  ug : f32;
+  vg : f32;
+  ub : f32;
+};
+
+[[group(0), binding(2)]] var ext_tex_plane_1 : texture_2d<f32>;
+
+[[group(0), binding(3)]] var<uniform> ext_tex_params : ExternalTextureParams;
+
+[[group(0), binding(1)]] var ext_tex : texture_2d<f32>;
+
+fn textureSampleExternal(plane0 : texture_2d<f32>, plane1 : texture_2d<f32>, smp : sampler, coord : vec2<f32>, params : ExternalTextureParams) -> vec4<f32> {
+  if ((params.numPlanes == 1u)) {
+    return textureSampleLevel(plane0, smp, coord, 0.0);
+  }
+  let y = (textureSampleLevel(plane0, smp, coord, 0.0).r - 0.0625);
+  let uv = (textureSampleLevel(plane1, smp, coord, 0.0).rg - 0.5);
+  let u = uv.x;
+  let v = uv.y;
+  let r = ((1.164000034 * y) + (params.vr * v));
+  let g = (((1.164000034 * y) - (params.ug * u)) - (params.vg * v));
+  let b = ((1.164000034 * y) + (params.ub * u));
+  return vec4<f32>(r, g, b, 1.0);
+}
+
+fn textureLoadExternal(plane0 : texture_2d<f32>, plane1 : texture_2d<f32>, coord : vec2<i32>, params : ExternalTextureParams) -> vec4<f32> {
+  if ((params.numPlanes == 1u)) {
+    return textureLoad(plane0, coord, 0);
+  }
+  let y = (textureLoad(plane0, coord, 0).r - 0.0625);
+  let uv = (textureLoad(plane1, coord, 0).rg - 0.5);
+  let u = uv.x;
+  let v = uv.y;
+  let r = ((1.164000034 * y) + (params.vr * v));
+  let g = (((1.164000034 * y) - (params.ug * u)) - (params.vg * v));
+  let b = ((1.164000034 * y) + (params.ub * u));
+  return vec4<f32>(r, g, b, 1.0);
+}
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return (textureSampleExternal(ext_tex, ext_tex_plane_1, s, coord.xy, ext_tex_params) + textureLoadExternal(ext_tex, ext_tex_plane_1, vec2<i32>(1, 1), ext_tex_params));
+}
+)";
+
+  DataMap data;
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{{{0, 1}, {{0, 2}, {0, 3}}}});
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Tests that the transform works with many instances of texture_external.
+TEST_F(MultiplanarExternalTextureTest, ManyTextureSampleLevel) {
+  auto* src = R"(
+[[group(0), binding(0)]] var s : sampler;
+[[group(0), binding(1)]] var ext_tex : texture_external;
+[[group(0), binding(2)]] var ext_tex_1 : texture_external;
+[[group(0), binding(3)]] var ext_tex_2 : texture_external;
+[[group(1), binding(0)]] var ext_tex_3 : texture_external;
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSampleLevel(ext_tex, s, coord.xy) + textureSampleLevel(ext_tex_1, s, coord.xy) + textureSampleLevel(ext_tex_2, s, coord.xy) + textureSampleLevel(ext_tex_3, s, coord.xy);
+}
+)";
+
+  auto* expect = R"(
+[[group(0), binding(0)]] var s : sampler;
+
+[[block]]
+struct ExternalTextureParams {
+  numPlanes : u32;
+  vr : f32;
+  ug : f32;
+  vg : f32;
+  ub : f32;
+};
+
+[[group(0), binding(4)]] var ext_tex_plane_1 : texture_2d<f32>;
+
+[[group(0), binding(5)]] var<uniform> ext_tex_params : ExternalTextureParams;
+
+[[group(0), binding(1)]] var ext_tex : texture_2d<f32>;
+
+[[group(0), binding(6)]] var ext_tex_plane_1_1 : texture_2d<f32>;
+
+[[group(0), binding(7)]] var<uniform> ext_tex_params_1 : ExternalTextureParams;
+
+[[group(0), binding(2)]] var ext_tex_1 : texture_2d<f32>;
+
+[[group(0), binding(8)]] var ext_tex_plane_1_2 : texture_2d<f32>;
+
+[[group(0), binding(9)]] var<uniform> ext_tex_params_2 : ExternalTextureParams;
+
+[[group(0), binding(3)]] var ext_tex_2 : texture_2d<f32>;
+
+[[group(1), binding(1)]] var ext_tex_plane_1_3 : texture_2d<f32>;
+
+[[group(1), binding(2)]] var<uniform> ext_tex_params_3 : ExternalTextureParams;
+
+[[group(1), binding(0)]] var ext_tex_3 : texture_2d<f32>;
+
+fn textureSampleExternal(plane0 : texture_2d<f32>, plane1 : texture_2d<f32>, smp : sampler, coord : vec2<f32>, params : ExternalTextureParams) -> vec4<f32> {
+  if ((params.numPlanes == 1u)) {
+    return textureSampleLevel(plane0, smp, coord, 0.0);
+  }
+  let y = (textureSampleLevel(plane0, smp, coord, 0.0).r - 0.0625);
+  let uv = (textureSampleLevel(plane1, smp, coord, 0.0).rg - 0.5);
+  let u = uv.x;
+  let v = uv.y;
+  let r = ((1.164000034 * y) + (params.vr * v));
+  let g = (((1.164000034 * y) - (params.ug * u)) - (params.vg * v));
+  let b = ((1.164000034 * y) + (params.ub * u));
+  return vec4<f32>(r, g, b, 1.0);
+}
+
+[[stage(fragment)]]
+fn main([[builtin(position)]] coord : vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return (((textureSampleExternal(ext_tex, ext_tex_plane_1, s, coord.xy, ext_tex_params) + textureSampleExternal(ext_tex_1, ext_tex_plane_1_1, s, coord.xy, ext_tex_params_1)) + textureSampleExternal(ext_tex_2, ext_tex_plane_1_2, s, coord.xy, ext_tex_params_2)) + textureSampleExternal(ext_tex_3, ext_tex_plane_1_3, s, coord.xy, ext_tex_params_3));
+}
+)";
+
+  DataMap data;
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{
+          {{0, 1}, {{0, 4}, {0, 5}}},
+          {{0, 2}, {{0, 6}, {0, 7}}},
+          {{0, 3}, {{0, 8}, {0, 9}}},
+          {{1, 0}, {{1, 1}, {1, 2}}},
+      });
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+// Tests that the texture_external passed as a function parameter produces the
+// correct output.
+TEST_F(MultiplanarExternalTextureTest, ExternalTexturePassedAsParam) {
+  auto* src = R"(
+fn f(t : texture_external, s : sampler) {
+      textureSampleLevel(t, s, vec2<f32>(1.0, 2.0));
+  }
+
+  [[group(0), binding(0)]] var ext_tex : texture_external;
+  [[group(0), binding(1)]] var smp : sampler;
+
+  [[stage(fragment)]]
+  fn main() {
+      f(ext_tex, smp);
+  }
+)";
+
+  auto* expect =
+      "error: transforming a texture_external passed as a user-defined "
+      "function parameter has not been implemented.";
+  DataMap data;
+  data.Add<MultiplanarExternalTexture::NewBindingPoints>(
+      MultiplanarExternalTexture::BindingsMap{
+          {{0, 0}, {{0, 2}, {0, 3}}},
+      });
+  auto got = Run<MultiplanarExternalTexture>(src, data);
+  EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace transform
+}  // namespace tint
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 2a1883c..907018a 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -316,6 +316,7 @@
     "../src/transform/inline_pointer_lets_test.cc",
     "../src/transform/loop_to_for_loop_test.cc",
     "../src/transform/module_scope_var_to_entry_point_param_test.cc",
+    "../src/transform/multiplanar_external_texture_test.cc",
     "../src/transform/num_workgroups_from_uniform_test.cc",
     "../src/transform/pad_array_elements_test.cc",
     "../src/transform/promote_initializers_to_const_var_test.cc",