Move BindingRemapper and MultiplanarExternalTexture to a backend transform.

In most backends, MultiplanarExternalTexture must run before the
BindingRemapper. The exception is the Vulkan SPIR-V backend where
it has to run before Multiplanar. This CL moves both of the transforms
to backend transforms and sets up the ordering as needed in the
generators.

Bug: tint:1855 chromium:1421379
Change-Id: I8223bce40babe901fe08bb1e8f033243837d7b18
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/123522
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/include/tint/tint.h b/include/tint/tint.h
index 0a0c027..0bde567 100644
--- a/include/tint/tint.h
+++ b/include/tint/tint.h
@@ -26,10 +26,8 @@
 #include "src/tint/inspector/inspector.h"
 #include "src/tint/reader/reader.h"
 #include "src/tint/text/unicode.h"
-#include "src/tint/transform/binding_remapper.h"
 #include "src/tint/transform/first_index_offset.h"
 #include "src/tint/transform/manager.h"
-#include "src/tint/transform/multiplanar_external_texture.h"
 #include "src/tint/transform/renamer.h"
 #include "src/tint/transform/single_entry_point.h"
 #include "src/tint/transform/substitute_override.h"
@@ -37,6 +35,8 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/writer/array_length_from_uniform_options.h"
 #include "src/tint/writer/binding_point.h"
+#include "src/tint/writer/binding_remapper_options.h"
+#include "src/tint/writer/external_texture_options.h"
 #include "src/tint/writer/flatten_bindings.h"
 #include "src/tint/writer/writer.h"
 
diff --git a/src/dawn/native/StreamImplTint.cpp b/src/dawn/native/StreamImplTint.cpp
index 2d747b1..d7d81b9 100644
--- a/src/dawn/native/StreamImplTint.cpp
+++ b/src/dawn/native/StreamImplTint.cpp
@@ -51,9 +51,17 @@
 
 // static
 template <>
-void stream::Stream<tint::transform::MultiplanarExternalTexture::BindingPoints>::Write(
+void stream::Stream<tint::writer::ExternalTextureOptions::BindingPoints>::Write(
     stream::Sink* sink,
-    const tint::transform::MultiplanarExternalTexture::BindingPoints& points) {
+    const tint::writer::ExternalTextureOptions::BindingPoints& point) {
+    StreamInTintObject(point, sink);
+}
+
+// static
+template <>
+void stream::Stream<tint::writer::ExternalTextureOptions>::Write(
+    stream::Sink* sink,
+    const tint::writer::ExternalTextureOptions& points) {
     StreamInTintObject(points, sink);
 }
 
@@ -103,4 +111,12 @@
     StreamInTintObject(options, sink);
 }
 
+// static
+template <>
+void stream::Stream<tint::writer::BindingRemapperOptions>::Write(
+    stream::Sink* sink,
+    const tint::writer::BindingRemapperOptions& options) {
+    StreamInTintObject(options, sink);
+}
+
 }  // namespace dawn::native
diff --git a/src/dawn/native/TintUtils.cpp b/src/dawn/native/TintUtils.cpp
index 6e15b1f..bd26afa 100644
--- a/src/dawn/native/TintUtils.cpp
+++ b/src/dawn/native/TintUtils.cpp
@@ -147,18 +147,19 @@
     tlDevice = nullptr;
 }
 
-tint::transform::MultiplanarExternalTexture::BindingsMap BuildExternalTextureTransformBindings(
+tint::writer::ExternalTextureOptions BuildExternalTextureTransformBindings(
     const PipelineLayoutBase* layout) {
-    tint::transform::MultiplanarExternalTexture::BindingsMap newBindingsMap;
+    tint::writer::ExternalTextureOptions options;
     for (BindGroupIndex i : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
         const BindGroupLayoutBase* bgl = layout->GetBindGroupLayout(i);
         for (const auto& [_, expansion] : bgl->GetExternalTextureBindingExpansionMap()) {
-            newBindingsMap[{static_cast<uint32_t>(i), static_cast<uint32_t>(expansion.plane0)}] = {
+            options.bindings_map[{static_cast<uint32_t>(i),
+                                  static_cast<uint32_t>(expansion.plane0)}] = {
                 {static_cast<uint32_t>(i), static_cast<uint32_t>(expansion.plane1)},
                 {static_cast<uint32_t>(i), static_cast<uint32_t>(expansion.params)}};
         }
     }
-    return newBindingsMap;
+    return options;
 }
 
 tint::transform::VertexPulling::Config BuildVertexPullingTransformConfig(
diff --git a/src/dawn/native/TintUtils.h b/src/dawn/native/TintUtils.h
index fb73e4d..b48108e 100644
--- a/src/dawn/native/TintUtils.h
+++ b/src/dawn/native/TintUtils.h
@@ -40,7 +40,7 @@
     ScopedTintICEHandler(ScopedTintICEHandler&&) = delete;
 };
 
-tint::transform::MultiplanarExternalTexture::BindingsMap BuildExternalTextureTransformBindings(
+tint::writer::ExternalTextureOptions BuildExternalTextureTransformBindings(
     const PipelineLayoutBase* layout);
 
 tint::transform::VertexPulling::Config BuildVertexPullingTransformConfig(
diff --git a/src/dawn/native/d3d12/ShaderModuleD3D12.cpp b/src/dawn/native/d3d12/ShaderModuleD3D12.cpp
index de7c2ba..6e5fd02 100644
--- a/src/dawn/native/d3d12/ShaderModuleD3D12.cpp
+++ b/src/dawn/native/d3d12/ShaderModuleD3D12.cpp
@@ -85,10 +85,9 @@
     X(bool, usesNumWorkgroups)                                                              \
     X(uint32_t, numWorkgroupsShaderRegister)                                                \
     X(uint32_t, numWorkgroupsRegisterSpace)                                                 \
-    X(tint::transform::MultiplanarExternalTexture::BindingsMap, newBindingsMap)             \
+    X(tint::writer::ExternalTextureOptions, externalTextureOptions)                         \
     X(tint::writer::ArrayLengthFromUniformOptions, arrayLengthFromUniform)                  \
-    X(tint::transform::BindingRemapper::BindingPoints, remappedBindingPoints)               \
-    X(tint::transform::BindingRemapper::AccessControls, remappedAccessControls)             \
+    X(tint::writer::BindingRemapperOptions, bindingRemapper)                                \
     X(std::optional<tint::transform::SubstituteOverride::Config>, substituteOverrideConfig) \
     X(std::bitset<kMaxInterStageShaderVariables>, interstageLocations)                      \
     X(LimitsForCompilationRequest, limits)                                                  \
@@ -311,12 +310,6 @@
             tint::transform::Renamer::Target::kHlslKeywords);
     }
 
-    if (!r.newBindingsMap.empty()) {
-        transformManager.Add<tint::transform::MultiplanarExternalTexture>();
-        transformInputs.Add<tint::transform::MultiplanarExternalTexture::NewBindingPoints>(
-            std::move(r.newBindingsMap));
-    }
-
     if (r.stage == SingleShaderStage::Vertex) {
         transformManager.Add<tint::transform::FirstIndexOffset>();
         transformInputs.Add<tint::transform::FirstIndexOffset::BindingPoint>(
@@ -331,15 +324,6 @@
             std::move(r.substituteOverrideConfig).value());
     }
 
-    transformManager.Add<tint::transform::BindingRemapper>();
-
-    // D3D12 registers like `t3` and `c3` have the same bindingOffset number in
-    // the remapping but should not be considered a collision because they have
-    // different types.
-    const bool mayCollide = true;
-    transformInputs.Add<tint::transform::BindingRemapper::Remappings>(
-        std::move(r.remappedBindingPoints), std::move(r.remappedAccessControls), mayCollide);
-
     tint::Program transformedProgram;
     tint::transform::DataMap transformOutputs;
     {
@@ -381,6 +365,9 @@
     tint::writer::hlsl::Options options;
     options.disable_robustness = !r.isRobustnessEnabled;
     options.disable_workgroup_init = r.disableWorkgroupInit;
+    options.binding_remapper_options = r.bindingRemapper;
+    options.external_texture_options = r.externalTextureOptions;
+
     if (r.usesNumWorkgroups) {
         options.root_constant_binding_point =
             tint::writer::BindingPoint{r.numWorkgroupsRegisterSpace, r.numWorkgroupsShaderRegister};
@@ -523,11 +510,13 @@
         }
     }
 
-    using tint::transform::BindingRemapper;
     using tint::writer::BindingPoint;
 
-    BindingRemapper::BindingPoints remappedBindingPoints;
-    BindingRemapper::AccessControls remappedAccessControls;
+    tint::writer::BindingRemapperOptions bindingRemapper;
+    // D3D12 registers like `t3` and `c3` have the same bindingOffset number in
+    // the remapping but should not be considered a collision because they have
+    // different types.
+    bindingRemapper.allow_collisions = true;
 
     tint::writer::ArrayLengthFromUniformOptions arrayLengthFromUniform;
     arrayLengthFromUniform.ubo_binding = {layout->GetDynamicStorageBufferLengthsRegisterSpace(),
@@ -549,7 +538,7 @@
             BindingPoint dstBindingPoint{static_cast<uint32_t>(group),
                                          bgl->GetShaderRegister(bindingIndex)};
             if (srcBindingPoint != dstBindingPoint) {
-                remappedBindingPoints.emplace(srcBindingPoint, dstBindingPoint);
+                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
             }
 
             // Declaring a read-only storage buffer in HLSL but specifying a storage
@@ -562,7 +551,8 @@
                       wgpu::BufferBindingType::Storage ||
                   bgl->GetBindingInfo(bindingIndex).buffer.type == kInternalStorageBufferBinding));
             if (forceStorageBufferAsUAV) {
-                remappedAccessControls.emplace(srcBindingPoint, tint::builtin::Access::kReadWrite);
+                bindingRemapper.access_controls.emplace(srcBindingPoint,
+                                                        tint::builtin::Access::kReadWrite);
             }
         }
 
@@ -576,8 +566,8 @@
                 BindingPoint bindingPoint{static_cast<uint32_t>(group),
                                           static_cast<uint32_t>(binding)};
                 // Get the renamed binding point if it was remapped.
-                auto it = remappedBindingPoints.find(bindingPoint);
-                if (it != remappedBindingPoints.end()) {
+                auto it = bindingRemapper.binding_points.find(bindingPoint);
+                if (it != bindingRemapper.binding_points.end()) {
                     bindingPoint = it->second;
                 }
 
@@ -600,9 +590,8 @@
     req.hlsl.usesNumWorkgroups = entryPoint.usesNumWorkgroups;
     req.hlsl.numWorkgroupsShaderRegister = layout->GetNumWorkgroupsShaderRegister();
     req.hlsl.numWorkgroupsRegisterSpace = layout->GetNumWorkgroupsRegisterSpace();
-    req.hlsl.remappedBindingPoints = std::move(remappedBindingPoints);
-    req.hlsl.remappedAccessControls = std::move(remappedAccessControls);
-    req.hlsl.newBindingsMap = BuildExternalTextureTransformBindings(layout);
+    req.hlsl.bindingRemapper = std::move(bindingRemapper);
+    req.hlsl.externalTextureOptions = BuildExternalTextureTransformBindings(layout);
     req.hlsl.arrayLengthFromUniform = std::move(arrayLengthFromUniform);
     req.hlsl.substituteOverrideConfig = std::move(substituteOverrideConfig);
 
diff --git a/src/dawn/native/metal/ShaderModuleMTL.mm b/src/dawn/native/metal/ShaderModuleMTL.mm
index c841d00..9c5b7f7 100644
--- a/src/dawn/native/metal/ShaderModuleMTL.mm
+++ b/src/dawn/native/metal/ShaderModuleMTL.mm
@@ -39,8 +39,8 @@
     X(SingleShaderStage, stage)                                                             \
     X(const tint::Program*, inputProgram)                                                   \
     X(tint::writer::ArrayLengthFromUniformOptions, arrayLengthFromUniform)                  \
-    X(tint::transform::BindingRemapper::BindingPoints, bindingPoints)                       \
-    X(tint::transform::MultiplanarExternalTexture::BindingsMap, externalTextureBindings)    \
+    X(tint::writer::BindingRemapperOptions, bindingRemapper)                                \
+    X(tint::writer::ExternalTextureOptions, externalTextureOptions)                         \
     X(OptionalVertexPullingTransformConfig, vertexPullingTransformConfig)                   \
     X(std::optional<tint::transform::SubstituteOverride::Config>, substituteOverrideConfig) \
     X(LimitsForCompilationRequest, limits)                                                  \
@@ -111,9 +111,10 @@
     errorStream << "Tint MSL failure:" << std::endl;
 
     // Remap BindingNumber to BindingIndex in WGSL shader
-    using BindingRemapper = tint::transform::BindingRemapper;
     using BindingPoint = tint::writer::BindingPoint;
-    BindingRemapper::BindingPoints bindingPoints;
+
+    tint::writer::BindingRemapperOptions bindingRemapper;
+    bindingRemapper.allow_collisions = true;
 
     tint::writer::ArrayLengthFromUniformOptions arrayLengthFromUniform;
     arrayLengthFromUniform.ubo_binding = {0, kBufferLengthBufferSlot};
@@ -135,7 +136,7 @@
                                          static_cast<uint32_t>(bindingNumber)};
             BindingPoint dstBindingPoint{0, shaderIndex};
             if (srcBindingPoint != dstBindingPoint) {
-                bindingPoints.emplace(srcBindingPoint, dstBindingPoint);
+                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
             }
 
             // Use the ShaderIndex as the indices for the buffer size lookups in the array length
@@ -150,8 +151,6 @@
         }
     }
 
-    auto externalTextureBindings = BuildExternalTextureTransformBindings(layout);
-
     std::optional<tint::transform::VertexPulling::Config> vertexPullingTransformConfig;
     if (stage == SingleShaderStage::Vertex &&
         device->IsToggleEnabled(Toggle::MetalEnableVertexPulling)) {
@@ -166,7 +165,7 @@
                                          static_cast<uint8_t>(slot)};
             BindingPoint dstBindingPoint{0, metalIndex};
             if (srcBindingPoint != dstBindingPoint) {
-                bindingPoints.emplace(srcBindingPoint, dstBindingPoint);
+                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
             }
 
             // Use the ShaderIndex as the indices for the buffer size lookups in the array
@@ -184,8 +183,8 @@
     MslCompilationRequest req = {};
     req.stage = stage;
     req.inputProgram = programmableStage.module->GetTintProgram();
-    req.bindingPoints = std::move(bindingPoints);
-    req.externalTextureBindings = std::move(externalTextureBindings);
+    req.bindingRemapper = std::move(bindingRemapper);
+    req.externalTextureOptions = BuildExternalTextureTransformBindings(layout);
     req.vertexPullingTransformConfig = std::move(vertexPullingTransformConfig);
     req.substituteOverrideConfig = std::move(substituteOverrideConfig);
     req.entryPointName = programmableStage.entryPoint.c_str();
@@ -222,12 +221,6 @@
                     tint::transform::Renamer::Target::kMslKeywords);
             }
 
-            if (!r.externalTextureBindings.empty()) {
-                transformManager.Add<tint::transform::MultiplanarExternalTexture>();
-                transformInputs.Add<tint::transform::MultiplanarExternalTexture::NewBindingPoints>(
-                    std::move(r.externalTextureBindings));
-            }
-
             if (r.vertexPullingTransformConfig) {
                 transformManager.Add<tint::transform::VertexPulling>();
                 transformInputs.Add<tint::transform::VertexPulling::Config>(
@@ -242,11 +235,6 @@
                     std::move(r.substituteOverrideConfig).value());
             }
 
-            transformManager.Add<BindingRemapper>();
-            transformInputs.Add<BindingRemapper::Remappings>(std::move(r.bindingPoints),
-                                                             BindingRemapper::AccessControls{},
-                                                             /* mayCollide */ true);
-
             tint::Program program;
             tint::transform::DataMap transformOutputs;
             {
@@ -285,6 +273,8 @@
             options.disable_workgroup_init = r.disableWorkgroupInit;
             options.emit_vertex_point_size = r.emitVertexPointSize;
             options.array_length_from_uniform = r.arrayLengthFromUniform;
+            options.binding_remapper_options = r.bindingRemapper;
+            options.external_texture_options = r.externalTextureOptions;
 
             TRACE_EVENT0(r.tracePlatform.UnsafeGetValue(), General, "tint::writer::msl::Generate");
             auto result = tint::writer::msl::Generate(&program, options);
diff --git a/src/dawn/native/opengl/ShaderModuleGL.cpp b/src/dawn/native/opengl/ShaderModuleGL.cpp
index 149251d..05c5bfa 100644
--- a/src/dawn/native/opengl/ShaderModuleGL.cpp
+++ b/src/dawn/native/opengl/ShaderModuleGL.cpp
@@ -61,7 +61,7 @@
     X(const tint::Program*, inputProgram)                                                   \
     X(std::string, entryPointName)                                                          \
     X(SingleShaderStage, stage)                                                             \
-    X(tint::transform::MultiplanarExternalTexture::BindingsMap, externalTextureBindings)    \
+    X(tint::writer::ExternalTextureOptions, externalTextureOptions)                         \
     X(BindingMap, glBindings)                                                               \
     X(std::optional<tint::transform::SubstituteOverride::Config>, substituteOverrideConfig) \
     X(LimitsForCompilationRequest, limits)                                                  \
@@ -182,7 +182,7 @@
     req.inputProgram = GetTintProgram();
     req.stage = stage;
     req.entryPointName = programmableStage.entryPoint;
-    req.externalTextureBindings = BuildExternalTextureTransformBindings(layout);
+    req.externalTextureOptions = BuildExternalTextureTransformBindings(layout);
     req.glBindings = std::move(glBindings);
     req.substituteOverrideConfig = std::move(substituteOverrideConfig);
     req.limits = LimitsForCompilationRequest::Create(limits.v1);
@@ -197,12 +197,6 @@
             tint::transform::Manager transformManager;
             tint::transform::DataMap transformInputs;
 
-            if (!r.externalTextureBindings.empty()) {
-                transformManager.Add<tint::transform::MultiplanarExternalTexture>();
-                transformInputs.Add<tint::transform::MultiplanarExternalTexture::NewBindingPoints>(
-                    std::move(r.externalTextureBindings));
-            }
-
             if (r.substituteOverrideConfig) {
                 transformManager.Add<tint::transform::SingleEntryPoint>();
                 transformInputs.Add<tint::transform::SingleEntryPoint::Config>(r.entryPointName);
@@ -231,6 +225,8 @@
             // TODO(crbug.com/dawn/1686): Robustness causes shader compilation failures.
             tintOptions.disable_robustness = true;
 
+            tintOptions.external_texture_options = r.externalTextureOptions;
+
             // When textures are accessed without a sampler (e.g., textureLoad()),
             // GetSamplerTextureUses() will return this sentinel value.
             BindingPoint placeholderBindingPoint{static_cast<uint32_t>(kMaxBindGroupsTyped), 0};
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.cpp b/src/dawn/native/vulkan/ShaderModuleVk.cpp
index 2467aa3..8d8aab0 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.cpp
+++ b/src/dawn/native/vulkan/ShaderModuleVk.cpp
@@ -166,8 +166,8 @@
 #define SPIRV_COMPILATION_REQUEST_MEMBERS(X)                                                \
     X(SingleShaderStage, stage)                                                             \
     X(const tint::Program*, inputProgram)                                                   \
-    X(tint::transform::BindingRemapper::BindingPoints, bindingPoints)                       \
-    X(tint::transform::MultiplanarExternalTexture::BindingsMap, newBindingsMap)             \
+    X(tint::writer::BindingRemapperOptions, bindingRemapper)                                \
+    X(tint::writer::ExternalTextureOptions, externalTextureOptions)                         \
     X(std::optional<tint::transform::SubstituteOverride::Config>, substituteOverrideConfig) \
     X(LimitsForCompilationRequest, limits)                                                  \
     X(std::string_view, entryPointName)                                                     \
@@ -204,9 +204,9 @@
     // Creation of module and spirv is deferred to this point when using tint generator
 
     // Remap BindingNumber to BindingIndex in WGSL shader
-    using BindingRemapper = tint::transform::BindingRemapper;
     using BindingPoint = tint::writer::BindingPoint;
-    BindingRemapper::BindingPoints bindingPoints;
+
+    tint::writer::BindingRemapperOptions bindingRemapper;
 
     const BindingInfoArray& moduleBindingInfo =
         GetEntryPoint(programmableStage.entryPoint.c_str()).bindings;
@@ -222,20 +222,21 @@
             BindingPoint dstBindingPoint{static_cast<uint32_t>(group),
                                          static_cast<uint32_t>(bindingIndex)};
             if (srcBindingPoint != dstBindingPoint) {
-                bindingPoints.emplace(srcBindingPoint, dstBindingPoint);
+                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
             }
         }
     }
 
     // Transform external textures into the binding locations specified in the bgl
     // TODO(dawn:1082): Replace this block with BuildExternalTextureTransformBindings.
-    tint::transform::MultiplanarExternalTexture::BindingsMap newBindingsMap;
+    tint::writer::ExternalTextureOptions externalTextureOptions;
     for (BindGroupIndex i : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
         const BindGroupLayoutBase* bgl = layout->GetBindGroupLayout(i);
 
         for (const auto& [_, expansion] : bgl->GetExternalTextureBindingExpansionMap()) {
-            newBindingsMap[{static_cast<uint32_t>(i),
-                            static_cast<uint32_t>(bgl->GetBindingIndex(expansion.plane0))}] = {
+            externalTextureOptions
+                .bindings_map[{static_cast<uint32_t>(i),
+                               static_cast<uint32_t>(bgl->GetBindingIndex(expansion.plane0))}] = {
                 {static_cast<uint32_t>(i),
                  static_cast<uint32_t>(bgl->GetBindingIndex(expansion.plane1))},
                 {static_cast<uint32_t>(i),
@@ -252,8 +253,8 @@
     SpirvCompilationRequest req = {};
     req.stage = stage;
     req.inputProgram = GetTintProgram();
-    req.bindingPoints = std::move(bindingPoints);
-    req.newBindingsMap = std::move(newBindingsMap);
+    req.bindingRemapper = std::move(bindingRemapper);
+    req.externalTextureOptions = std::move(externalTextureOptions);
     req.entryPointName = programmableStage.entryPoint;
     req.isRobustnessEnabled = GetDevice()->IsRobustnessEnabled();
     req.disableWorkgroupInit = GetDevice()->IsToggleEnabled(Toggle::DisableWorkgroupInit);
@@ -293,18 +294,6 @@
                     std::move(r.substituteOverrideConfig).value());
             }
 
-            // Run the binding remapper after SingleEntryPoint to avoid collisions with
-            // unused entryPoints.
-            transformManager.append(std::make_unique<tint::transform::BindingRemapper>());
-            transformInputs.Add<BindingRemapper::Remappings>(std::move(r.bindingPoints),
-                                                             BindingRemapper::AccessControls{},
-                                                             /* mayCollide */ false);
-            if (!r.newBindingsMap.empty()) {
-                transformManager.Add<tint::transform::MultiplanarExternalTexture>();
-                transformInputs.Add<tint::transform::MultiplanarExternalTexture::NewBindingPoints>(
-                    r.newBindingsMap);
-            }
-
             tint::Program program;
             tint::transform::DataMap transformOutputs;
             {
@@ -342,6 +331,8 @@
             options.disable_workgroup_init = r.disableWorkgroupInit;
             options.use_zero_initialize_workgroup_memory_extension =
                 r.useZeroInitializeWorkgroupMemoryExtension;
+            options.binding_remapper_options = r.bindingRemapper;
+            options.external_texture_options = r.externalTextureOptions;
 
             TRACE_EVENT0(r.tracePlatform.UnsafeGetValue(), General,
                          "tint::writer::spirv::Generate()");
diff --git a/src/dawn/tests/unittests/native/StreamTests.cpp b/src/dawn/tests/unittests/native/StreamTests.cpp
index 1c06247..7b1f93e 100644
--- a/src/dawn/tests/unittests/native/StreamTests.cpp
+++ b/src/dawn/tests/unittests/native/StreamTests.cpp
@@ -263,10 +263,10 @@
     EXPECT_CACHE_KEY_EQ(bp, expected);
 }
 
-// Test that ByteVectorSink serializes tint::transform::MultiplanarExternalTexture::BindingPoints
+// Test that ByteVectorSink serializes tint::writer::ExternalTextureOptions::BindingPoints
 // as expected.
 TEST(SerializeTests, TintTransformBindingPoints) {
-    tint::transform::MultiplanarExternalTexture::BindingPoints points{
+    tint::writer::ExternalTextureOptions::BindingPoints points{
         tint::writer::BindingPoint{1, 4},
         tint::writer::BindingPoint{3, 7},
     };
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 11d98bd..20c999c 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -900,6 +900,8 @@
     "writer/array_length_from_uniform_options.cc",
     "writer/array_length_from_uniform_options.h",
     "writer/binding_point.h",
+    "writer/binding_remapper_options.cc",
+    "writer/binding_remapper_options.h",
     "writer/check_supported_extensions.cc",
     "writer/check_supported_extensions.h",
     "writer/external_texture_options.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 07f7aa8..47f13ea 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -544,6 +544,8 @@
   writer/array_length_from_uniform_options.cc
   writer/array_length_from_uniform_options.h
   writer/binding_point.h
+  writer/binding_remapper_options.cc
+  writer/binding_remapper_options.h
   writer/check_supported_extensions.cc
   writer/check_supported_extensions.h
   writer/external_texture_options.cc
diff --git a/src/tint/fuzzers/transform_builder.h b/src/tint/fuzzers/transform_builder.h
index 787abb9..4dcf604 100644
--- a/src/tint/fuzzers/transform_builder.h
+++ b/src/tint/fuzzers/transform_builder.h
@@ -22,6 +22,7 @@
 
 #include "src/tint/fuzzers/data_builder.h"
 #include "src/tint/fuzzers/shuffle_transform.h"
+#include "src/tint/transform/binding_remapper.h"
 #include "src/tint/transform/robustness.h"
 
 namespace tint::fuzzers {
diff --git a/src/tint/writer/binding_remapper_options.cc b/src/tint/writer/binding_remapper_options.cc
new file mode 100644
index 0000000..78f572c
--- /dev/null
+++ b/src/tint/writer/binding_remapper_options.cc
@@ -0,0 +1,29 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/writer/binding_remapper_options.h"
+
+namespace tint::writer {
+
+BindingRemapperOptions::BindingRemapperOptions() = default;
+
+BindingRemapperOptions::~BindingRemapperOptions() = default;
+
+BindingRemapperOptions::BindingRemapperOptions(const BindingRemapperOptions&) = default;
+
+BindingRemapperOptions& BindingRemapperOptions::operator=(const BindingRemapperOptions&) = default;
+
+BindingRemapperOptions::BindingRemapperOptions(BindingRemapperOptions&&) = default;
+
+}  // namespace tint::writer
diff --git a/src/tint/writer/binding_remapper_options.h b/src/tint/writer/binding_remapper_options.h
new file mode 100644
index 0000000..865bcdb
--- /dev/null
+++ b/src/tint/writer/binding_remapper_options.h
@@ -0,0 +1,62 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_WRITER_BINDING_REMAPPER_OPTIONS_H_
+#define SRC_TINT_WRITER_BINDING_REMAPPER_OPTIONS_H_
+
+#include <unordered_map>
+
+#include "src/tint/builtin/access.h"
+#include "src/tint/writer/binding_point.h"
+
+namespace tint::writer {
+
+/// Options used to specify mappings of binding points.
+class BindingRemapperOptions {
+  public:
+    /// BindingPoints is a map of old binding point to new binding point
+    using BindingPoints = std::unordered_map<BindingPoint, BindingPoint>;
+
+    /// AccessControls is a map of old binding point to new access control
+    using AccessControls = std::unordered_map<BindingPoint, builtin::Access>;
+
+    /// Constructor
+    BindingRemapperOptions();
+    /// Destructor
+    ~BindingRemapperOptions();
+    /// Copy constructor
+    BindingRemapperOptions(const BindingRemapperOptions&);
+    /// Copy assignment
+    /// @returns this BindingRemapperOptions
+    BindingRemapperOptions& operator=(const BindingRemapperOptions&);
+    /// Move constructor
+    BindingRemapperOptions(BindingRemapperOptions&&);
+
+    /// A map of old binding point to new binding point
+    BindingPoints binding_points;
+
+    /// A map of old binding point to new access controls
+    AccessControls access_controls;
+
+    /// If true, then validation will be disabled for binding point collisions
+    /// generated by this transform
+    bool allow_collisions = false;
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(binding_points, access_controls, allow_collisions);
+};
+
+}  // namespace tint::writer
+
+#endif  // SRC_TINT_WRITER_BINDING_REMAPPER_OPTIONS_H_
diff --git a/src/tint/writer/hlsl/generator.h b/src/tint/writer/hlsl/generator.h
index 2cdfe6f..9b79ad1 100644
--- a/src/tint/writer/hlsl/generator.h
+++ b/src/tint/writer/hlsl/generator.h
@@ -28,6 +28,7 @@
 #include "src/tint/sem/binding_point.h"
 #include "src/tint/utils/bitset.h"
 #include "src/tint/writer/array_length_from_uniform_options.h"
+#include "src/tint/writer/binding_remapper_options.h"
 #include "src/tint/writer/external_texture_options.h"
 #include "src/tint/writer/text.h"
 
@@ -66,6 +67,9 @@
     /// from which to load buffer sizes.
     ArrayLengthFromUniformOptions array_length_from_uniform = {};
 
+    /// Options used in the bindings remapper
+    BindingRemapperOptions binding_remapper_options = {};
+
     /// Interstage locations actually used as inputs in the next stage of the pipeline.
     /// This is potentially used for truncating unused interstage outputs at current shader stage.
     std::bitset<16> interstage_locations;
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 1cfbf64..cc8ad64 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -43,6 +43,7 @@
 #include "src/tint/switch.h"
 #include "src/tint/transform/add_empty_entry_point.h"
 #include "src/tint/transform/array_length_from_uniform.h"
+#include "src/tint/transform/binding_remapper.h"
 #include "src/tint/transform/builtin_polyfill.h"
 #include "src/tint/transform/calculate_array_length.h"
 #include "src/tint/transform/canonicalize_entry_point_io.h"
@@ -195,6 +196,13 @@
         manager.Add<transform::MultiplanarExternalTexture>();
     }
 
+    // BindingRemapper must come after MultiplanarExternalTexture
+    manager.Add<transform::BindingRemapper>();
+    data.Add<transform::BindingRemapper::Remappings>(
+        options.binding_remapper_options.binding_points,
+        options.binding_remapper_options.access_controls,
+        options.binding_remapper_options.allow_collisions);
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kFull;
diff --git a/src/tint/writer/msl/generator.h b/src/tint/writer/msl/generator.h
index b4e11d9..53805f1 100644
--- a/src/tint/writer/msl/generator.h
+++ b/src/tint/writer/msl/generator.h
@@ -23,6 +23,7 @@
 
 #include "src/tint/reflection.h"
 #include "src/tint/writer/array_length_from_uniform_options.h"
+#include "src/tint/writer/binding_remapper_options.h"
 #include "src/tint/writer/external_texture_options.h"
 #include "src/tint/writer/text.h"
 
@@ -70,6 +71,9 @@
     /// from which to load buffer sizes.
     ArrayLengthFromUniformOptions array_length_from_uniform = {};
 
+    /// Options used in the bindings remapper
+    BindingRemapperOptions binding_remapper_options = {};
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
                  buffer_size_ubo_index,
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index ae49d5d..f4bf13e 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -42,6 +42,7 @@
 #include "src/tint/sem/variable.h"
 #include "src/tint/switch.h"
 #include "src/tint/transform/array_length_from_uniform.h"
+#include "src/tint/transform/binding_remapper.h"
 #include "src/tint/transform/builtin_polyfill.h"
 #include "src/tint/transform/canonicalize_entry_point_io.h"
 #include "src/tint/transform/demote_to_helper.h"
@@ -217,6 +218,13 @@
         manager.Add<transform::MultiplanarExternalTexture>();
     }
 
+    // BindingRemapper must come after MultiplanarExternalTexture
+    manager.Add<transform::BindingRemapper>();
+    data.Add<transform::BindingRemapper::Remappings>(
+        options.binding_remapper_options.binding_points,
+        options.binding_remapper_options.access_controls,
+        options.binding_remapper_options.allow_collisions);
+
     if (!options.disable_workgroup_init) {
         // ZeroInitWorkgroupMemory must come before CanonicalizeEntryPointIO as
         // ZeroInitWorkgroupMemory may inject new builtin parameters.
@@ -225,6 +233,7 @@
 
     // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<transform::CanonicalizeEntryPointIO>();
+    data.Add<transform::CanonicalizeEntryPointIO::Config>(std::move(entry_point_io_cfg));
 
     manager.Add<transform::PromoteInitializersToLet>();
 
@@ -235,6 +244,7 @@
     manager.Add<transform::VectorizeScalarMatrixInitializers>();
     manager.Add<transform::RemovePhonies>();
     manager.Add<transform::SimplifyPointers>();
+
     // ArrayLengthFromUniform must come after SimplifyPointers, as
     // it assumes that the form of the array length argument is &var.array.
     manager.Add<transform::ArrayLengthFromUniform>();
@@ -248,7 +258,7 @@
     // PackedVec3 must come after ExpandCompoundAssignment.
     manager.Add<transform::PackedVec3>();
     manager.Add<transform::ModuleScopeVarToEntryPointParam>();
-    data.Add<transform::CanonicalizeEntryPointIO::Config>(std::move(entry_point_io_cfg));
+
     auto out = manager.Run(in, data);
 
     SanitizedResult result;
diff --git a/src/tint/writer/spirv/generator.h b/src/tint/writer/spirv/generator.h
index f6d2268..d24d938 100644
--- a/src/tint/writer/spirv/generator.h
+++ b/src/tint/writer/spirv/generator.h
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "src/tint/reflection.h"
+#include "src/tint/writer/binding_remapper_options.h"
 #include "src/tint/writer/external_texture_options.h"
 #include "src/tint/writer/writer.h"
 
@@ -52,6 +53,9 @@
     /// Options used in the binding mappings for external textures
     ExternalTextureOptions external_texture_options = {};
 
+    /// Options used in the bindings remapper
+    BindingRemapperOptions binding_remapper_options = {};
+
     /// Set to `true` to initialize workgroup memory with OpConstantNull when
     /// VK_KHR_zero_initialize_workgroup_memory is enabled.
     bool use_zero_initialize_workgroup_memory_extension = false;
diff --git a/src/tint/writer/spirv/generator_impl.cc b/src/tint/writer/spirv/generator_impl.cc
index 1986f83..970a3e5 100644
--- a/src/tint/writer/spirv/generator_impl.cc
+++ b/src/tint/writer/spirv/generator_impl.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/transform/add_block_attribute.h"
 #include "src/tint/transform/add_empty_entry_point.h"
+#include "src/tint/transform/binding_remapper.h"
 #include "src/tint/transform/builtin_polyfill.h"
 #include "src/tint/transform/canonicalize_entry_point_io.h"
 #include "src/tint/transform/clamp_frag_depth.h"
@@ -59,13 +60,18 @@
     // ExpandCompoundAssignment must come before BuiltinPolyfill
     manager.Add<transform::ExpandCompoundAssignment>();
 
-    manager.Add<transform::PreservePadding>();  // Must come before DirectVariableAccess
+    // Must come before DirectVariableAccess
+    manager.Add<transform::PreservePadding>();
 
-    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
+    // Must come before DirectVariableAccess
+    manager.Add<transform::Unshadow>();
 
     manager.Add<transform::RemoveUnreachableStatements>();
     manager.Add<transform::PromoteSideEffectsToDecl>();
-    manager.Add<transform::SimplifyPointers>();  // Required for arrayLength()
+
+    // Required for arrayLength()
+    manager.Add<transform::SimplifyPointers>();
+
     manager.Add<transform::RemovePhonies>();
     manager.Add<transform::VectorizeScalarMatrixInitializers>();
     manager.Add<transform::VectorizeMatrixConversions>();
@@ -78,6 +84,14 @@
         manager.Add<transform::Robustness>();
     }
 
+    // BindingRemapper must come before MultiplanarExternalTexture. Note, this is flipped to the
+    // other generators which run Multiplanar first and then binding remapper.
+    manager.Add<transform::BindingRemapper>();
+    data.Add<transform::BindingRemapper::Remappings>(
+        options.binding_remapper_options.binding_points,
+        options.binding_remapper_options.access_controls,
+        options.binding_remapper_options.allow_collisions);
+
     if (!options.external_texture_options.bindings_map.empty()) {
         // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
         data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(