Using binding information for SPIR-V/Tint interface

This CL removes the explicit external texture and multiplanar options
from the SPIR-V writer and instead uses `binding` information which is
populated by Dawn.

On the Tint side the needed transform data is created from the requested
result from Dawn.

Bug: tint:1501
Change-Id: I36ce6272b3b4e8e4b47a0bcaf59bec193acb7038
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/152864
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
diff --git a/src/dawn/native/StreamImplTint.cpp b/src/dawn/native/StreamImplTint.cpp
index 1b7db58..3cc052c 100644
--- a/src/dawn/native/StreamImplTint.cpp
+++ b/src/dawn/native/StreamImplTint.cpp
@@ -57,6 +57,32 @@
     return {};
 }
 
+#if TINT_BUILD_SPV_WRITER
+// static
+template <>
+void stream::Stream<tint::spirv::writer::Bindings>::Write(
+    stream::Sink* sink,
+    const tint::spirv::writer::Bindings& bindings) {
+    StreamInTintObject(bindings, sink);
+}
+
+// static
+template <>
+void stream::Stream<tint::spirv::writer::binding::ExternalTexture>::Write(
+    stream::Sink* sink,
+    const tint::spirv::writer::binding::ExternalTexture& et) {
+    StreamInTintObject(et, sink);
+}
+
+// static
+template <>
+void stream::Stream<tint::spirv::writer::binding::BindingInfo>::Write(
+    stream::Sink* sink,
+    const tint::spirv::writer::binding::BindingInfo& point) {
+    StreamInTintObject(point, sink);
+}
+#endif  // TINT_BUILD_SPV_WRITER
+
 // static
 template <>
 void stream::Stream<tint::ExternalTextureOptions::BindingPoints>::Write(
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.cpp b/src/dawn/native/vulkan/ShaderModuleVk.cpp
index 79df5a8..a1ac4c3 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.cpp
+++ b/src/dawn/native/vulkan/ShaderModuleVk.cpp
@@ -165,11 +165,12 @@
 
 ShaderModule::~ShaderModule() = default;
 
+#if TINT_BUILD_SPV_WRITER
+
 #define SPIRV_COMPILATION_REQUEST_MEMBERS(X)                                                     \
     X(SingleShaderStage, stage)                                                                  \
     X(const tint::Program*, inputProgram)                                                        \
-    X(tint::BindingRemapperOptions, bindingRemapper)                                             \
-    X(tint::ExternalTextureOptions, externalTextureOptions)                                      \
+    X(tint::spirv::writer::Bindings, bindings)                                                   \
     X(std::optional<tint::ast::transform::SubstituteOverride::Config>, substituteOverrideConfig) \
     X(LimitsForCompilationRequest, limits)                                                       \
     X(std::string_view, entryPointName)                                                          \
@@ -187,6 +188,8 @@
 DAWN_MAKE_CACHE_REQUEST(SpirvCompilationRequest, SPIRV_COMPILATION_REQUEST_MEMBERS);
 #undef SPIRV_COMPILATION_REQUEST_MEMBERS
 
+#endif  // TINT_BUILD_SPV_WRITER
+
 ResultOrError<ShaderModule::ModuleAndSpirv> ShaderModule::GetHandleAndSpirv(
     SingleShaderStage stage,
     const ProgrammableStage& programmableStage,
@@ -207,60 +210,95 @@
         return std::move(*handleAndSpirv);
     }
 
+#if TINT_BUILD_SPV_WRITER
     // Creation of module and spirv is deferred to this point when using tint generator
 
-    // Remap BindingNumber to BindingIndex in WGSL shader
-    using BindingPoint = tint::BindingPoint;
-
-    tint::BindingRemapperOptions bindingRemapper;
+    tint::spirv::writer::Bindings bindings;
 
     const BindingInfoArray& moduleBindingInfo =
         GetEntryPoint(programmableStage.entryPoint.c_str()).bindings;
 
     for (BindGroupIndex group : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
         const BindGroupLayout* bgl = ToBackend(layout->GetBindGroupLayout(group));
-        const auto& groupBindingInfo = moduleBindingInfo[group];
-        for (const auto& [binding, _] : groupBindingInfo) {
-            BindingIndex bindingIndex = bgl->GetBindingIndex(binding);
-            BindingPoint srcBindingPoint{static_cast<uint32_t>(group),
-                                         static_cast<uint32_t>(binding)};
 
-            BindingPoint dstBindingPoint{static_cast<uint32_t>(group),
-                                         static_cast<uint32_t>(bindingIndex)};
-            if (srcBindingPoint != dstBindingPoint) {
-                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
+        for (const auto& [binding, bindingInfo] : moduleBindingInfo[group]) {
+            tint::BindingPoint srcBindingPoint{static_cast<uint32_t>(group),
+                                               static_cast<uint32_t>(binding)};
+
+            tint::BindingPoint dstBindingPoint{
+                static_cast<uint32_t>(group), static_cast<uint32_t>(bgl->GetBindingIndex(binding))};
+
+            switch (bindingInfo.bindingType) {
+                case BindingInfoType::Buffer:
+                    switch (bindingInfo.buffer.type) {
+                        case wgpu::BufferBindingType::Uniform:
+                            bindings.uniform.emplace(
+                                srcBindingPoint,
+                                tint::spirv::writer::binding::Uniform{dstBindingPoint.group,
+                                                                      dstBindingPoint.binding});
+                            break;
+                        case kInternalStorageBufferBinding:
+                        case wgpu::BufferBindingType::Storage:
+                        case wgpu::BufferBindingType::ReadOnlyStorage:
+                            bindings.storage.emplace(
+                                srcBindingPoint,
+                                tint::spirv::writer::binding::Storage{dstBindingPoint.group,
+                                                                      dstBindingPoint.binding});
+                            break;
+                        case wgpu::BufferBindingType::Undefined:
+                            DAWN_UNREACHABLE();
+                            break;
+                    }
+                    break;
+                case BindingInfoType::Sampler:
+                    bindings.sampler.emplace(srcBindingPoint,
+                                             tint::spirv::writer::binding::Sampler{
+                                                 dstBindingPoint.group, dstBindingPoint.binding});
+                    break;
+                case BindingInfoType::Texture:
+                    bindings.texture.emplace(srcBindingPoint,
+                                             tint::spirv::writer::binding::Texture{
+                                                 dstBindingPoint.group, dstBindingPoint.binding});
+                    break;
+                case BindingInfoType::StorageTexture:
+                    bindings.storage_texture.emplace(
+                        srcBindingPoint, tint::spirv::writer::binding::StorageTexture{
+                                             dstBindingPoint.group, dstBindingPoint.binding});
+                    break;
+                case BindingInfoType::ExternalTexture: {
+                    const auto& bindingMap = bgl->GetExternalTextureBindingExpansionMap();
+                    const auto& expansion = bindingMap.find(binding);
+                    DAWN_ASSERT(expansion != bindingMap.end());
+
+                    const auto& bindingExpansion = expansion->second;
+                    tint::spirv::writer::binding::BindingInfo plane0{
+                        static_cast<uint32_t>(group),
+                        static_cast<uint32_t>(bgl->GetBindingIndex(bindingExpansion.plane0))};
+                    tint::spirv::writer::binding::BindingInfo plane1{
+                        static_cast<uint32_t>(group),
+                        static_cast<uint32_t>(bgl->GetBindingIndex(bindingExpansion.plane1))};
+                    tint::spirv::writer::binding::BindingInfo metadata{
+                        static_cast<uint32_t>(group),
+                        static_cast<uint32_t>(bgl->GetBindingIndex(bindingExpansion.params))};
+
+                    bindings.external_texture.emplace(
+                        srcBindingPoint,
+                        tint::spirv::writer::binding::ExternalTexture{metadata, plane0, plane1});
+                    break;
+                }
             }
         }
     }
 
-    // Transform external textures into the binding locations specified in the bgl
-    // TODO(dawn:1082): Replace this block with BuildExternalTextureTransformBindings.
-    tint::ExternalTextureOptions externalTextureOptions;
-    for (BindGroupIndex i : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
-        const BindGroupLayoutInternalBase* bgl = layout->GetBindGroupLayout(i);
-
-        for (const auto& [_, expansion] : bgl->GetExternalTextureBindingExpansionMap()) {
-            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),
-                 static_cast<uint32_t>(bgl->GetBindingIndex(expansion.params))}};
-        }
-    }
-
     std::optional<tint::ast::transform::SubstituteOverride::Config> substituteOverrideConfig;
     if (!programmableStage.metadata->overrides.empty()) {
         substituteOverrideConfig = BuildSubstituteOverridesTransformConfig(programmableStage);
     }
 
-#if TINT_BUILD_SPV_WRITER
     SpirvCompilationRequest req = {};
     req.stage = stage;
     req.inputProgram = GetTintProgram();
-    req.bindingRemapper = std::move(bindingRemapper);
-    req.externalTextureOptions = std::move(externalTextureOptions);
+    req.bindings = std::move(bindings);
     req.entryPointName = programmableStage.entryPoint;
     req.isRobustnessEnabled = GetDevice()->IsRobustnessEnabled();
     req.disableWorkgroupInit = GetDevice()->IsToggleEnabled(Toggle::DisableWorkgroupInit);
@@ -348,8 +386,7 @@
             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;
+            options.bindings = r.bindings;
             options.disable_image_robustness = r.disableImageRobustness;
             options.disable_runtime_sized_array_index_clamping =
                 r.disableRuntimeSizedArrayIndexClamping;
diff --git a/src/tint/cmd/loopy/BUILD.bazel b/src/tint/cmd/loopy/BUILD.bazel
index 194e728..4f75977 100644
--- a/src/tint/cmd/loopy/BUILD.bazel
+++ b/src/tint/cmd/loopy/BUILD.bazel
@@ -39,6 +39,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
     "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/spirv/writer/helpers",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/helpers",
diff --git a/src/tint/cmd/loopy/BUILD.cmake b/src/tint/cmd/loopy/BUILD.cmake
index 86eee38..0370b97 100644
--- a/src/tint/cmd/loopy/BUILD.cmake
+++ b/src/tint/cmd/loopy/BUILD.cmake
@@ -40,6 +40,7 @@
   tint_lang_core_type
   tint_lang_hlsl_writer_common
   tint_lang_spirv_reader_common
+  tint_lang_spirv_writer_helpers
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_helpers
diff --git a/src/tint/cmd/loopy/BUILD.gn b/src/tint/cmd/loopy/BUILD.gn
index 4c54f72..4ca63ad 100644
--- a/src/tint/cmd/loopy/BUILD.gn
+++ b/src/tint/cmd/loopy/BUILD.gn
@@ -39,6 +39,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
     "${tint_src_dir}/lang/spirv/reader/common",
+    "${tint_src_dir}/lang/spirv/writer/helpers",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/helpers",
diff --git a/src/tint/cmd/loopy/main.cc b/src/tint/cmd/loopy/main.cc
index e488fad..a12fbc6 100644
--- a/src/tint/cmd/loopy/main.cc
+++ b/src/tint/cmd/loopy/main.cc
@@ -37,6 +37,7 @@
 #endif  // TINT_BUILD_SPV_READER
 
 #if TINT_BUILD_SPV_WRITER
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
@@ -192,8 +193,7 @@
 bool GenerateSpirv(const tint::Program& program) {
 #if TINT_BUILD_SPV_WRITER
     tint::spirv::writer::Options gen_options;
-    gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(program);
+    gen_options.bindings = tint::spirv::writer::GenerateBindings(program);
     auto result = tint::spirv::writer::Generate(program, gen_options);
     if (!result) {
         tint::cmd::PrintWGSL(std::cerr, program);
diff --git a/src/tint/cmd/tint/BUILD.bazel b/src/tint/cmd/tint/BUILD.bazel
index 321c15e..ad986d1 100644
--- a/src/tint/cmd/tint/BUILD.bazel
+++ b/src/tint/cmd/tint/BUILD.bazel
@@ -39,6 +39,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
     "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/spirv/writer/helpers",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
diff --git a/src/tint/cmd/tint/BUILD.cmake b/src/tint/cmd/tint/BUILD.cmake
index 1d500ac..1f0dc3b 100644
--- a/src/tint/cmd/tint/BUILD.cmake
+++ b/src/tint/cmd/tint/BUILD.cmake
@@ -40,6 +40,7 @@
   tint_lang_core_type
   tint_lang_hlsl_writer_common
   tint_lang_spirv_reader_common
+  tint_lang_spirv_writer_helpers
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
diff --git a/src/tint/cmd/tint/BUILD.gn b/src/tint/cmd/tint/BUILD.gn
index e6c897c..40b456b 100644
--- a/src/tint/cmd/tint/BUILD.gn
+++ b/src/tint/cmd/tint/BUILD.gn
@@ -39,6 +39,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
     "${tint_src_dir}/lang/spirv/reader/common",
+    "${tint_src_dir}/lang/spirv/writer/helpers",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/ast/transform",
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 73eae30..1506bce 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -65,6 +65,7 @@
 #endif  // TINT_BUILD_WGSL_READER
 
 #if TINT_BUILD_SPV_WRITER
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
@@ -579,8 +580,7 @@
     tint::spirv::writer::Options gen_options;
     gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
-    gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(program);
+    gen_options.bindings = tint::spirv::writer::GenerateBindings(program);
     gen_options.use_tint_ir = options.use_ir;
 
     auto result = tint::spirv::writer::Generate(program, gen_options);
diff --git a/src/tint/fuzzers/BUILD.gn b/src/tint/fuzzers/BUILD.gn
index a482492..1f576ad 100644
--- a/src/tint/fuzzers/BUILD.gn
+++ b/src/tint/fuzzers/BUILD.gn
@@ -74,6 +74,7 @@
       "${tint_src_dir}/lang/hlsl/writer",
       "${tint_src_dir}/lang/msl/writer",
       "${tint_src_dir}/lang/spirv/writer",
+      "${tint_src_dir}/lang/spirv/writer/helpers",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/helpers",
diff --git a/src/tint/fuzzers/CMakeLists.txt b/src/tint/fuzzers/CMakeLists.txt
index f9c8747..97674f2 100644
--- a/src/tint/fuzzers/CMakeLists.txt
+++ b/src/tint/fuzzers/CMakeLists.txt
@@ -38,6 +38,7 @@
     tint_spvheaders_compile_options(${NAME})
     tint_spvtools_compile_options(${NAME})
   endif()
+  target_link_libraries(${NAME} PRIVATE tint_lang_spirv_writer_helpers)
   target_compile_options(${NAME} PRIVATE -Wno-missing-prototypes)
 endfunction()
 
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
index f822030..85881e4 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
@@ -14,7 +14,7 @@
 
 function(add_tint_ast_fuzzer NAME)
   add_executable(${NAME} ${NAME}.cc ${AST_FUZZER_SOURCES})
-  target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer)
+  target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer tint_lang_spirv_writer_helpers)
   tint_fuzzer_compile_options(${NAME})
   if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
     tint_spvheaders_compile_options(${NAME})
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index 1b6bd5a..582b259 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -41,6 +41,10 @@
 #include "src/tint/utils/diagnostic/printer.h"
 #include "src/tint/utils/math/hash.h"
 
+#if TINT_BUILD_SPV_WRITER
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
+#endif  // TINT_BUILD_SPV_WRITER
+
 namespace tint::fuzzers {
 
 namespace {
@@ -244,10 +248,23 @@
         }
     }
 
+    switch (output_) {
+        case OutputFormat::kMSL:
+            break;
+        case OutputFormat::kHLSL:
+            break;
+        case OutputFormat::kSpv:
+#if TINT_BUILD_SPV_WRITER
+            options_spirv_.bindings = tint::spirv::writer::GenerateBindings(program);
+#endif  // TINT_BUILD_SPV_WRITER
+            break;
+        case OutputFormat::kWGSL:
+            break;
+    }
+
     // For the generates which use MultiPlanar, make sure the configuration options are provided so
     // that the transformer will execute.
-    if (output_ == OutputFormat::kMSL || output_ == OutputFormat::kHLSL ||
-        output_ == OutputFormat::kSpv) {
+    if (output_ == OutputFormat::kMSL || output_ == OutputFormat::kHLSL) {
         // Gather external texture binding information
         // Collect next valid binding number per group
         std::unordered_map<uint32_t, uint32_t> group_to_next_binding_number;
@@ -284,7 +301,6 @@
                 break;
             }
             case OutputFormat::kSpv: {
-                options_spirv_.external_texture_options.bindings_map = new_bindings_map;
                 break;
             }
             default:
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
index f062d97..90e7e21 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
@@ -14,7 +14,7 @@
 
 function(add_tint_regex_fuzzer NAME)
   add_executable(${NAME} ${NAME}.cc ${REGEX_FUZZER_SOURCES})
-  target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer)
+  target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer tint_lang_spirv_writer_helpers)
   tint_fuzzer_compile_options(${NAME})
   tint_spvtools_compile_options(${NAME})
   target_compile_definitions(${NAME} PRIVATE CUSTOM_MUTATOR)
diff --git a/src/tint/lang/spirv/writer/BUILD.bazel b/src/tint/lang/spirv/writer/BUILD.bazel
index 7c2a7b7..7220323 100644
--- a/src/tint/lang/spirv/writer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/BUILD.bazel
@@ -106,7 +106,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/intrinsic",
@@ -155,7 +154,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
diff --git a/src/tint/lang/spirv/writer/BUILD.cmake b/src/tint/lang/spirv/writer/BUILD.cmake
index 953b266..c0169e9 100644
--- a/src/tint/lang/spirv/writer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/BUILD.cmake
@@ -24,6 +24,7 @@
 include(lang/spirv/writer/ast_printer/BUILD.cmake)
 include(lang/spirv/writer/ast_raise/BUILD.cmake)
 include(lang/spirv/writer/common/BUILD.cmake)
+include(lang/spirv/writer/helpers/BUILD.cmake)
 include(lang/spirv/writer/printer/BUILD.cmake)
 include(lang/spirv/writer/raise/BUILD.cmake)
 
@@ -118,7 +119,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
@@ -175,7 +175,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_bench bench
   tint_api_common
-  tint_api_options
   tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
diff --git a/src/tint/lang/spirv/writer/BUILD.gn b/src/tint/lang/spirv/writer/BUILD.gn
index a816952..31d0875 100644
--- a/src/tint/lang/spirv/writer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/BUILD.gn
@@ -109,7 +109,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/intrinsic",
@@ -159,7 +158,6 @@
       deps = [
         "${tint_src_dir}:google_benchmark",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/cmd/bench:bench",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
index 99c7cd5..22c6d85 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
@@ -109,7 +109,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
index 46151c0..81b45cd 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
@@ -115,7 +115,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_ast_printer_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
index c517b43..b855543 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
@@ -112,7 +112,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/type",
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index fa7d322..e8d1c45 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -24,6 +24,7 @@
 #include "src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h"
 #include "src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h"
 #include "src/tint/lang/spirv/writer/ast_raise/while_to_loop.h"
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
 #include "src/tint/lang/wgsl/ast/transform/add_block_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h"
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
@@ -93,16 +94,21 @@
         data.Add<ast::transform::Robustness::Config>(config);
     }
 
+    ExternalTextureOptions external_texture_options{};
+    RemapperData remapper_data{};
+    PopulateRemapperAndMultiplanarOptions(options, remapper_data, external_texture_options);
+
     // BindingRemapper must come before MultiplanarExternalTexture. Note, this is flipped to the
     // other generators which run Multiplanar first and then binding remapper.
     manager.Add<ast::transform::BindingRemapper>();
+
     data.Add<ast::transform::BindingRemapper::Remappings>(
-        options.binding_remapper_options.binding_points,
-        std::unordered_map<BindingPoint, core::Access>{}, /* allow_collisions */ false);
+        remapper_data, std::unordered_map<BindingPoint, core::Access>{},
+        /* allow_collisions */ false);
 
     // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
     data.Add<ast::transform::MultiplanarExternalTexture::NewBindingPoints>(
-        options.external_texture_options.bindings_map);
+        external_texture_options.bindings_map);
     manager.Add<ast::transform::MultiplanarExternalTexture>();
 
     {  // Builtin polyfills
diff --git a/src/tint/lang/spirv/writer/common/BUILD.bazel b/src/tint/lang/spirv/writer/common/BUILD.bazel
index bf196f2..ecd94ab 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/common/BUILD.bazel
@@ -31,6 +31,7 @@
     "instruction.cc",
     "module.cc",
     "operand.cc",
+    "option_builder.cc",
   ],
   hdrs = [
     "binary_writer.h",
@@ -38,13 +39,14 @@
     "instruction.h",
     "module.h",
     "operand.h",
+    "option_builder.h",
     "options.h",
   ],
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
-    "//src/tint/lang/core",
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
@@ -76,7 +78,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/common/BUILD.cmake b/src/tint/lang/spirv/writer/common/BUILD.cmake
index 086cac1..dcb2f58 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/common/BUILD.cmake
@@ -38,14 +38,16 @@
   lang/spirv/writer/common/module.h
   lang/spirv/writer/common/operand.cc
   lang/spirv/writer/common/operand.h
+  lang/spirv/writer/common/option_builder.cc
+  lang/spirv/writer/common/option_builder.h
   lang/spirv/writer/common/options.h
 )
 
 tint_target_add_dependencies(tint_lang_spirv_writer_common lib
   tint_api_common
   tint_api_options
-  tint_lang_core
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
   tint_utils_math
@@ -81,7 +83,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_common_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
diff --git a/src/tint/lang/spirv/writer/common/BUILD.gn b/src/tint/lang/spirv/writer/common/BUILD.gn
index dc4970f..becae92 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.gn
+++ b/src/tint/lang/spirv/writer/common/BUILD.gn
@@ -41,13 +41,15 @@
       "module.h",
       "operand.cc",
       "operand.h",
+      "option_builder.cc",
+      "option_builder.h",
       "options.h",
     ]
     deps = [
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/api/options",
-      "${tint_src_dir}/lang/core",
       "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
       "${tint_src_dir}/utils/macros",
       "${tint_src_dir}/utils/math",
@@ -78,7 +80,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/common/option_builder.cc b/src/tint/lang/spirv/writer/common/option_builder.cc
new file mode 100644
index 0000000..1bec1e9
--- /dev/null
+++ b/src/tint/lang/spirv/writer/common/option_builder.cc
@@ -0,0 +1,224 @@
+// 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/lang/spirv/writer/common/option_builder.h"
+
+#include "src/tint/utils/containers/hashset.h"
+
+namespace tint::spirv::writer {
+
+bool ValidateBindingOptions(const Options& options, diag::List& diagnostics) {
+    tint::Hashset<tint::BindingPoint, 8> seen_wgsl_bindings{};
+    tint::Hashset<binding::BindingInfo, 8> seen_spirv_bindings{};
+
+    auto wgsl_seen = [&diagnostics, &seen_wgsl_bindings](const tint::BindingPoint& info) -> bool {
+        if (seen_wgsl_bindings.Contains(info)) {
+            std::stringstream str;
+            str << "Found duplicate WGSL binding point: " << info;
+
+            diagnostics.add_error(diag::System::Writer, str.str());
+            return true;
+        }
+        seen_wgsl_bindings.Add(info);
+        return false;
+    };
+
+    auto spirv_seen = [&diagnostics,
+                       &seen_spirv_bindings](const binding::BindingInfo& info) -> bool {
+        if (seen_spirv_bindings.Contains(info)) {
+            std::stringstream str;
+            str << "Found duplicate SPIR-V binding point: [group: " << info.group
+                << ", binding: " << info.binding << "]";
+            diagnostics.add_error(diag::System::Writer, str.str());
+            return true;
+        }
+        seen_spirv_bindings.Add(info);
+        return false;
+    };
+
+    auto valid = [&wgsl_seen, &spirv_seen](const auto& hsh) -> bool {
+        for (const auto it : hsh) {
+            const auto& src_binding = it.first;
+            const auto& dst_binding = it.second;
+
+            if (wgsl_seen(src_binding)) {
+                return false;
+            }
+
+            if (spirv_seen(dst_binding)) {
+                return false;
+            }
+        }
+        return true;
+    };
+
+    if (!valid(options.bindings.uniform)) {
+        diagnostics.add_note(diag::System::Writer, "When processing uniform", {});
+        return false;
+    }
+    if (!valid(options.bindings.storage)) {
+        diagnostics.add_note(diag::System::Writer, "When processing storage", {});
+        return false;
+    }
+    if (!valid(options.bindings.texture)) {
+        diagnostics.add_note(diag::System::Writer, "When processing texture", {});
+        return false;
+    }
+    if (!valid(options.bindings.storage_texture)) {
+        diagnostics.add_note(diag::System::Writer, "When processing storage_texture", {});
+        return false;
+    }
+    if (!valid(options.bindings.sampler)) {
+        diagnostics.add_note(diag::System::Writer, "When processing sampler", {});
+        return false;
+    }
+
+    for (const auto it : options.bindings.external_texture) {
+        const auto& src_binding = it.first;
+        const auto& plane0 = it.second.plane0;
+        const auto& plane1 = it.second.plane1;
+        const auto& metadata = it.second.metadata;
+
+        // Validate with the actual source regardless of what the remapper will do
+        if (wgsl_seen(src_binding)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+
+        if (spirv_seen(plane0)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+        if (spirv_seen(plane1)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+        if (spirv_seen(metadata)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+    }
+
+    return true;
+}
+
+// The remapped binding data and external texture data need to coordinate in order to put things in
+// the correct place when we're done.
+//
+// When the data comes in we have a list of all WGSL origin (group,binding) pairs to SPIR-V
+// (group,binding) pairs in the `uniform`, `storage`, `texture`, and `sampler` arrays.
+//
+// The `external_texture` array stores a WGSL origin (group,binding) pair for the external textures
+// which provide `plane0`, `plane1`, and `metadata` SPIR-V (group,binding) pairs.
+//
+// If the remapper is run first, then the `external_texture` will end up being moved from the WGSL
+// point, or the SPIR-V point (or the `plane0` value). There will also, possibly, have been bindings
+// moved aside in order to place the `external_texture` bindings.
+//
+// If multiplanar runs first, care needs to be taken that when the texture is split and we create
+// `plane1` and `metadata` that they do not collide with existing bindings. If they would collide
+// then we need to place them elsewhere and have the remapper place them in the correct locations.
+//
+// # Example
+// WGSL:
+//   @group(0) @binding(0) var<uniform> u: Uniforms;
+//   @group(0) @binding(1) var s: sampler;
+//   @group(0) @binding(2) var t: texture_external;
+//
+// Given that program, Dawn may decide to do the remappings such that:
+//   * WGSL u (0, 0) -> SPIR-V (0, 1)
+//   * WGSL s (0, 1) -> SPIR-V (0, 2)
+//   * WGSL t (0, 2):
+//     * plane0 -> SPIR-V (0, 3)
+//     * plane1 -> SPIR-V (0, 4)
+//     * metadata -> SPIR-V (0, 0)
+//
+// In this case, if we run binding remapper first, then tell multiplanar to look for the texture at
+// (0, 3) instead of the original (0, 2).
+//
+// If multiplanar runs first, then metadata (0, 0) needs to be placed elsewhere and then remapped
+// back to (0, 0) by the remapper. (Otherwise, we'll have two `@group(0) @binding(0)` items in the
+// program.)
+//
+// # Status
+// The below method assumes we run binding remapper first. So it will setup the binding data and
+// switch the value used by the multiplanar.
+void PopulateRemapperAndMultiplanarOptions(const Options& options,
+                                           RemapperData& remapper_data,
+                                           ExternalTextureOptions& external_texture) {
+    auto create_remappings = [&remapper_data](const auto& hsh) {
+        for (const auto it : hsh) {
+            const BindingPoint& src_binding_point = it.first;
+            const binding::Uniform& dst_binding_point = it.second;
+
+            // Bindings which go to the same slot in SPIR-V do not need to be re-bound.
+            if (src_binding_point.group == dst_binding_point.group &&
+                src_binding_point.binding == dst_binding_point.binding) {
+                continue;
+            }
+
+            remapper_data.emplace(src_binding_point,
+                                  BindingPoint{dst_binding_point.group, dst_binding_point.binding});
+        }
+    };
+
+    create_remappings(options.bindings.uniform);
+    create_remappings(options.bindings.storage);
+    create_remappings(options.bindings.texture);
+    create_remappings(options.bindings.storage_texture);
+    create_remappings(options.bindings.sampler);
+
+    // External textures are re-bound to their plane0 location
+    for (const auto it : options.bindings.external_texture) {
+        const BindingPoint& src_binding_point = it.first;
+        const binding::BindingInfo& plane0 = it.second.plane0;
+        const binding::BindingInfo& plane1 = it.second.plane1;
+        const binding::BindingInfo& metadata = it.second.metadata;
+
+        BindingPoint plane0_binding_point{plane0.group, plane0.binding};
+        BindingPoint plane1_binding_point{plane1.group, plane1.binding};
+        BindingPoint metadata_binding_point{metadata.group, metadata.binding};
+
+        // Use the re-bound spir-v plane0 value for the lookup key.
+        external_texture.bindings_map.emplace(
+            plane0_binding_point,
+            ExternalTextureOptions::BindingPoints{plane1_binding_point, metadata_binding_point});
+
+        // Bindings which go to the same slot in SPIR-V do not need to be re-bound.
+        if (src_binding_point.group == plane0.group &&
+            src_binding_point.binding == plane0.binding) {
+            continue;
+        }
+
+        remapper_data.emplace(src_binding_point, BindingPoint{plane0.group, plane0.binding});
+    }
+}
+
+}  // namespace tint::spirv::writer
+
+namespace std {
+
+/// Custom std::hash specialization for tint::spirv::writer::binding::BindingInfo so
+/// they can be used as keys for std::unordered_map and std::unordered_set.
+template <>
+class hash<tint::spirv::writer::binding::BindingInfo> {
+  public:
+    /// @param info the binding to create a hash for
+    /// @return the hash value
+    inline std::size_t operator()(const tint::spirv::writer::binding::BindingInfo& info) const {
+        return tint::Hash(info.group, info.binding);
+    }
+};
+
+}  // namespace std
diff --git a/src/tint/lang/spirv/writer/common/option_builder.h b/src/tint/lang/spirv/writer/common/option_builder.h
new file mode 100644
index 0000000..0187b48
--- /dev/null
+++ b/src/tint/lang/spirv/writer/common/option_builder.h
@@ -0,0 +1,44 @@
+// 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_LANG_SPIRV_WRITER_COMMON_OPTION_BUILDER_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTION_BUILDER_H_
+
+#include <unordered_map>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/api/options/external_texture.h"
+#include "src/tint/lang/spirv/writer/common/options.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
+
+namespace tint::spirv::writer {
+
+using RemapperData = std::unordered_map<BindingPoint, BindingPoint>;
+
+/// @param options the options
+/// @returns true if the binding points are valid
+bool ValidateBindingOptions(const Options& options, diag::List& diagnostics);
+
+/// Populates data from the writer options for the remapper and external texture.
+/// @param options the writer options
+/// @param remapper_data where to put the remapper data
+/// @param external_texture where to store the external texture options
+/// Note, these are populated together because there are dependencies between the two types of data.
+void PopulateRemapperAndMultiplanarOptions(const Options& options,
+                                           RemapperData& remapper_data,
+                                           ExternalTextureOptions& external_texture);
+
+}  // namespace tint::spirv::writer
+
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTION_BUILDER_H_
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index 57e81e8..4fe9652 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -15,11 +15,83 @@
 #ifndef SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTIONS_H_
 #define SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTIONS_H_
 
-#include "src/tint/api/options/binding_remapper.h"
-#include "src/tint/api/options/external_texture.h"
+#include <unordered_map>
+
+#include "src/tint/api/common/binding_point.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::spirv::writer {
+namespace binding {
+
+/// Generic binding point
+struct BindingInfo {
+    /// The group
+    uint32_t group = 0;
+    /// The binding
+    uint32_t binding = 0;
+
+    /// Equality operator
+    /// @param rhs the BindingInfo to compare against
+    /// @returns true if this BindingInfo is equal to `rhs`
+    inline bool operator==(const BindingInfo& rhs) const {
+        return group == rhs.group && binding == rhs.binding;
+    }
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(group, binding);
+};
+using Uniform = BindingInfo;
+using Storage = BindingInfo;
+using Texture = BindingInfo;
+using StorageTexture = BindingInfo;
+using Sampler = BindingInfo;
+
+/// An external texture
+struct ExternalTexture {
+    /// Metadata
+    BindingInfo metadata{};
+    /// Plane0 binding data
+    BindingInfo plane0{};
+    /// Plane1 binding data
+    BindingInfo plane1{};
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(metadata, plane0, plane1);
+};
+
+}  // namespace binding
+
+// Maps the WGSL binding point to the SPIR-V group,binding for uniforms
+using UniformBindings = std::unordered_map<BindingPoint, binding::Uniform>;
+// Maps the WGSL binding point to the SPIR-V group,binding for storage
+using StorageBindings = std::unordered_map<BindingPoint, binding::Storage>;
+// Maps the WGSL binding point to the SPIR-V group,binding for textures
+using TextureBindings = std::unordered_map<BindingPoint, binding::Texture>;
+// Maps the WGSL binding point to the SPIR-V group,binding for storage textures
+using StorageTextureBindings = std::unordered_map<BindingPoint, binding::StorageTexture>;
+// Maps the WGSL binding point to the SPIR-V group,binding for samplers
+using SamplerBindings = std::unordered_map<BindingPoint, binding::Sampler>;
+// Maps the WGSL binding point to the plane0, plane1, and metadata information for external textures
+using ExternalTextureBindings = std::unordered_map<BindingPoint, binding::ExternalTexture>;
+
+/// Binding information
+struct Bindings {
+    /// Uniform bindings
+    UniformBindings uniform{};
+    /// Storage bindings
+    StorageBindings storage{};
+    /// Texture bindings
+    TextureBindings texture{};
+    /// Storage texture bindings
+    StorageTextureBindings storage_texture{};
+    /// Sampler bindings
+    SamplerBindings sampler{};
+    /// External bindings
+    ExternalTextureBindings external_texture{};
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(uniform, storage, texture, sampler, external_texture);
+};
 
 /// Configuration options used for generating SPIR-V.
 struct Options {
@@ -49,17 +121,14 @@
     /// Set to `true` to generate SPIR-V via the Tint IR instead of from the AST.
     bool use_tint_ir = false;
 
-    /// 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 require `SPV_KHR_subgroup_uniform_control_flow` extension and
     /// `SubgroupUniformControlFlowKHR` execution mode for compute stage entry points in generated
     /// SPIRV module. Issue: dawn:464
     bool experimental_require_subgroup_uniform_control_flow = false;
 
+    /// The bindings
+    Bindings bindings;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
                  disable_image_robustness,
@@ -69,9 +138,8 @@
                  emit_vertex_point_size,
                  clamp_frag_depth,
                  use_tint_ir,
-                 external_texture_options,
-                 binding_remapper_options,
-                 experimental_require_subgroup_uniform_control_flow);
+                 experimental_require_subgroup_uniform_control_flow,
+                 bindings);
 };
 
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.bazel b/src/tint/lang/spirv/writer/helpers/BUILD.bazel
new file mode 100644
index 0000000..190110b
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.bazel
@@ -0,0 +1,70 @@
+# 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.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "helpers",
+  srcs = [
+    "generate_bindings.cc",
+  ],
+  hdrs = [
+    "generate_bindings.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.cmake b/src/tint/lang/spirv/writer/helpers/BUILD.cmake
new file mode 100644
index 0000000..31063c4
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.cmake
@@ -0,0 +1,61 @@
+# 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.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_lang_spirv_writer_helpers
+# Kind:      lib
+################################################################################
+tint_add_target(tint_lang_spirv_writer_helpers lib
+  lang/spirv/writer/helpers/generate_bindings.cc
+  lang/spirv/writer/helpers/generate_bindings.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_writer_helpers lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_helpers lib
+    tint_lang_spirv_writer_common
+  )
+endif(TINT_BUILD_SPV_WRITER)
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.gn b/src/tint/lang/spirv/writer/helpers/BUILD.gn
new file mode 100644
index 0000000..b062d79
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.gn
@@ -0,0 +1,60 @@
+# 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.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+libtint_source_set("helpers") {
+  sources = [
+    "generate_bindings.cc",
+    "generate_bindings.h",
+  ]
+  deps = [
+    "${tint_src_dir}/api/common",
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/lang/wgsl",
+    "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/program",
+    "${tint_src_dir}/lang/wgsl/sem",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+
+  if (tint_build_spv_writer) {
+    deps += [ "${tint_src_dir}/lang/spirv/writer/common" ]
+  }
+}
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
new file mode 100644
index 0000000..0285b0b
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
@@ -0,0 +1,119 @@
+// 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/lang/spirv/writer/helpers/generate_bindings.h"
+
+#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/lang/core/type/external_texture.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::spirv::writer {
+
+Bindings GenerateBindings(const Program& program) {
+    // TODO(tint:1491): Use Inspector once we can get binding info for all
+    // variables, not just those referenced by entry points.
+
+    Bindings bindings{};
+
+    std::unordered_set<tint::BindingPoint> seen_binding_points;
+
+    // Collect next valid binding number per group
+    std::unordered_map<uint32_t, uint32_t> group_to_next_binding_number;
+    std::vector<tint::BindingPoint> ext_tex_bps;
+    for (auto* var : program.AST().GlobalVariables()) {
+        if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
+            if (auto bp = sem_var->BindingPoint()) {
+                // This is a bit of a hack. The binding points must be unique over all the `binding`
+                // entries. But, this is looking at _all_ entry points where bindings can overlap.
+                // In the case where both entry points used the same type (uniform, sampler, etc)
+                // then it woudl be fine as it just overwrites with itself. But, if one entry point
+                // has a uniform and the other a sampler at the same (group,binding) pair then we'll
+                // get a validation error due to duplicate WGSL bindings.
+                //
+                // For generate bindings we don't really care as we always map to itself, so if it
+                // exists anywhere, we just pretend that's the only one.
+                if (seen_binding_points.find(*bp) != seen_binding_points.end()) {
+                    continue;
+                }
+                seen_binding_points.emplace(*bp);
+
+                auto& n = group_to_next_binding_number[bp->group];
+                n = std::max(n, bp->binding + 1);
+
+                // Store up the external textures, we'll add them in the next step
+                if (sem_var->Type()->UnwrapRef()->Is<core::type::ExternalTexture>()) {
+                    ext_tex_bps.emplace_back(*bp);
+                    continue;
+                }
+
+                binding::BindingInfo info{bp->group, bp->binding};
+                switch (sem_var->AddressSpace()) {
+                    case core::AddressSpace::kHandle:
+                        Switch(
+                            sem_var->Type()->UnwrapRef(),  //
+                            [&](const core::type::Sampler*) {
+                                bindings.sampler.emplace(*bp, info);
+                            },
+                            [&](const core::type::StorageTexture*) {
+                                bindings.storage_texture.emplace(*bp, info);
+                            },
+                            [&](const core::type::Texture*) {
+                                bindings.texture.emplace(*bp, info);
+                            });
+                        break;
+                    case core::AddressSpace::kStorage:
+                        bindings.storage.emplace(*bp, info);
+                        break;
+                    case core::AddressSpace::kUniform:
+                        bindings.uniform.emplace(*bp, info);
+                        break;
+
+                    case core::AddressSpace::kUndefined:
+                    case core::AddressSpace::kPixelLocal:
+                    case core::AddressSpace::kPrivate:
+                    case core::AddressSpace::kPushConstant:
+                    case core::AddressSpace::kIn:
+                    case core::AddressSpace::kOut:
+                    case core::AddressSpace::kFunction:
+                    case core::AddressSpace::kWorkgroup:
+                        break;
+                }
+            }
+        }
+    }
+
+    for (auto bp : ext_tex_bps) {
+        uint32_t g = bp.group;
+        uint32_t& next_num = group_to_next_binding_number[g];
+
+        binding::BindingInfo plane0{bp.group, bp.binding};
+        binding::BindingInfo plane1{g, next_num++};
+        binding::BindingInfo metadata{g, next_num++};
+
+        bindings.external_texture.emplace(bp, binding::ExternalTexture{metadata, plane0, plane1});
+    }
+
+    return bindings;
+}
+
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.h b/src/tint/lang/spirv/writer/helpers/generate_bindings.h
new file mode 100644
index 0000000..d97120f
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.h
@@ -0,0 +1,31 @@
+// 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_LANG_SPIRV_WRITER_HELPERS_GENERATE_BINDINGS_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_HELPERS_GENERATE_BINDINGS_H_
+
+#include "src/tint/lang/spirv/writer/common/options.h"
+
+// Forward declarations
+namespace tint {
+class Program;
+}
+
+namespace tint::spirv::writer {
+
+Bindings GenerateBindings(const Program& program);
+
+}  // namespace tint::spirv::writer
+
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_HELPERS_GENERATE_BINDINGS_H_
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.bazel b/src/tint/lang/spirv/writer/printer/BUILD.bazel
index f595547..21bb414 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/printer/BUILD.bazel
@@ -33,7 +33,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.cmake b/src/tint/lang/spirv/writer/printer/BUILD.cmake
index 81c1626..cdadaec 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/printer/BUILD.cmake
@@ -34,7 +34,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_printer lib
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.gn b/src/tint/lang/spirv/writer/printer/BUILD.gn
index 476271a..e57f56c 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/printer/BUILD.gn
@@ -32,7 +32,6 @@
     ]
     deps = [
       "${tint_src_dir}/api/common",
-      "${tint_src_dir}/api/options",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index c2fc514..7add4af 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -31,6 +31,7 @@
 #include "src/tint/lang/core/ir/transform/robustness.h"
 #include "src/tint/lang/core/ir/transform/std140.h"
 #include "src/tint/lang/core/ir/transform/zero_init_workgroup_memory.h"
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
 #include "src/tint/lang/spirv/writer/raise/builtin_polyfill.h"
 #include "src/tint/lang/spirv/writer/raise/expand_implicit_splats.h"
 #include "src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h"
@@ -49,8 +50,11 @@
         }                                \
     } while (false)
 
-    RUN_TRANSFORM(core::ir::transform::BindingRemapper, module,
-                  options.binding_remapper_options.binding_points);
+    ExternalTextureOptions external_texture_options{};
+    RemapperData remapper_data{};
+    PopulateRemapperAndMultiplanarOptions(options, remapper_data, external_texture_options);
+
+    RUN_TRANSFORM(core::ir::transform::BindingRemapper, module, remapper_data);
 
     core::ir::transform::BinaryPolyfillConfig binary_polyfills;
     binary_polyfills.bitshift_modulo = true;
@@ -84,7 +88,7 @@
     }
 
     RUN_TRANSFORM(core::ir::transform::MultiplanarExternalTexture, module,
-                  options.external_texture_options);
+                  external_texture_options);
 
     if (!options.disable_workgroup_init &&
         !options.use_zero_initialize_workgroup_memory_extension) {
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index 1a47183..94484f4 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -18,6 +18,7 @@
 #include <utility>
 
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
 #include "src/tint/lang/spirv/writer/printer/printer.h"
 #include "src/tint/lang/spirv/writer/raise/raise.h"
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
@@ -40,6 +41,13 @@
     bool zero_initialize_workgroup_memory =
         !options.disable_workgroup_init && options.use_zero_initialize_workgroup_memory_extension;
 
+    {
+        diag::List validation_diagnostics;
+        if (!ValidateBindingOptions(options, validation_diagnostics)) {
+            return Failure{validation_diagnostics};
+        }
+    }
+
     Output output;
 
     if (options.use_tint_ir) {