dawn: use binding information for HLSL interface

Bug: tint:2133
Change-Id: Ia06a4ca92cb524ba01dbc5de4fb716f4bf80d1e0
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/170682
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
diff --git a/src/dawn/native/d3d11/ShaderModuleD3D11.cpp b/src/dawn/native/d3d11/ShaderModuleD3D11.cpp
index 7968a61..6ab1873 100644
--- a/src/dawn/native/d3d11/ShaderModuleD3D11.cpp
+++ b/src/dawn/native/d3d11/ShaderModuleD3D11.cpp
@@ -112,44 +112,72 @@
             break;
     }
 
-    tint::BindingRemapperOptions bindingRemapper;
-
     const BindingInfoArray& moduleBindingInfo = entryPoint.bindings;
 
-    for (BindGroupIndex group : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
-        const BindGroupLayout* groupLayout = ToBackend(layout->GetBindGroupLayout(group));
-        const auto& indices = layout->GetBindingIndexInfo()[group];
-        const auto& groupBindingInfo = moduleBindingInfo[group];
+    tint::hlsl::writer::Bindings bindings;
 
-        for (const auto& [binding, bindingInfo] : groupBindingInfo) {
-            BindingIndex bindingIndex = groupLayout->GetBindingIndex(binding);
+    for (BindGroupIndex group : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
+        const BindGroupLayout* bgl = ToBackend(layout->GetBindGroupLayout(group));
+        const auto& indices = layout->GetBindingIndexInfo()[group];
+        const BindingGroupInfoMap& moduleGroupBindingInfo = moduleBindingInfo[group];
+
+        for (const auto& [binding, shaderBindingInfo] : moduleGroupBindingInfo) {
+            BindingIndex bindingIndex = bgl->GetBindingIndex(binding);
             tint::BindingPoint srcBindingPoint{static_cast<uint32_t>(group),
                                                static_cast<uint32_t>(binding)};
             tint::BindingPoint dstBindingPoint{0u, indices[bindingIndex]};
-            if (srcBindingPoint != dstBindingPoint) {
-                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
-            }
-        }
+            auto* const bufferBindingInfo =
+                std::get_if<BufferBindingInfo>(&shaderBindingInfo.bindingInfo);
 
-        // Tint will add two bindings (plane1, params) for one external texture binding.
-        // We need to remap the binding points for the two bindings.
-        // we cannot specified the final slot of those two bindings in
-        // req.hlsl.externalTextureOptions because the final slots may be conflict with
-        // existing other bindings, and then they will be remapped again with bindingRemapper
-        // incorrectly. So we have to use intermediate binding slots in
-        // req.hlsl.externalTextureOptions, and then map them to the final slots with
-        // bindingRemapper.
-        for (const auto& [_, expansion] : groupLayout->GetExternalTextureBindingExpansionMap()) {
-            uint32_t plane1Slot = indices[groupLayout->GetBindingIndex(expansion.plane1)];
-            uint32_t paramsSlot = indices[groupLayout->GetBindingIndex(expansion.params)];
-            bindingRemapper.binding_points.emplace(
-                tint::BindingPoint{static_cast<uint32_t>(group),
-                                   static_cast<uint32_t>(expansion.plane1)},
-                tint::BindingPoint{0u, plane1Slot});
-            bindingRemapper.binding_points.emplace(
-                tint::BindingPoint{static_cast<uint32_t>(group),
-                                   static_cast<uint32_t>(expansion.params)},
-                tint::BindingPoint{0u, paramsSlot});
+            if (bufferBindingInfo) {
+                switch (bufferBindingInfo->type) {
+                    case wgpu::BufferBindingType::Uniform:
+                        bindings.uniform.emplace(
+                            srcBindingPoint, tint::hlsl::writer::binding::Uniform{
+                                                 dstBindingPoint.group, dstBindingPoint.binding});
+                        break;
+                    case kInternalStorageBufferBinding:
+                    case wgpu::BufferBindingType::Storage:
+                    case wgpu::BufferBindingType::ReadOnlyStorage:
+                        bindings.storage.emplace(
+                            srcBindingPoint, tint::hlsl::writer::binding::Storage{
+                                                 dstBindingPoint.group, dstBindingPoint.binding});
+                        break;
+                    case wgpu::BufferBindingType::Undefined:
+                        DAWN_UNREACHABLE();
+                        break;
+                }
+            } else if (std::holds_alternative<SamplerBindingInfo>(shaderBindingInfo.bindingInfo)) {
+                bindings.sampler.emplace(
+                    srcBindingPoint, tint::hlsl::writer::binding::Sampler{dstBindingPoint.group,
+                                                                          dstBindingPoint.binding});
+            } else if (std::holds_alternative<SampledTextureBindingInfo>(
+                           shaderBindingInfo.bindingInfo)) {
+                bindings.texture.emplace(
+                    srcBindingPoint, tint::hlsl::writer::binding::Texture{dstBindingPoint.group,
+                                                                          dstBindingPoint.binding});
+            } else if (std::holds_alternative<StorageTextureBindingInfo>(
+                           shaderBindingInfo.bindingInfo)) {
+                bindings.storage_texture.emplace(
+                    srcBindingPoint, tint::hlsl::writer::binding::StorageTexture{
+                                         dstBindingPoint.group, dstBindingPoint.binding});
+            } else if (std::holds_alternative<ExternalTextureBindingInfo>(
+                           shaderBindingInfo.bindingInfo)) {
+                const auto& etBindingMap = bgl->GetExternalTextureBindingExpansionMap();
+                const auto& expansion = etBindingMap.find(binding);
+                DAWN_ASSERT(expansion != etBindingMap.end());
+
+                const auto& bindingExpansion = expansion->second;
+                tint::hlsl::writer::binding::BindingInfo plane0{
+                    0u, indices[bgl->GetBindingIndex(bindingExpansion.plane0)]};
+                tint::hlsl::writer::binding::BindingInfo plane1{
+                    0u, indices[bgl->GetBindingIndex(bindingExpansion.plane1)]};
+                tint::hlsl::writer::binding::BindingInfo metadata{
+                    0u, indices[bgl->GetBindingIndex(bindingExpansion.params)]};
+                bindings.external_texture.emplace(
+                    srcBindingPoint,
+                    tint::hlsl::writer::binding::ExternalTexture{metadata, plane0, plane1});
+            }
         }
     }
 
@@ -173,7 +201,10 @@
         // D3D11 (HLSL SM5.0) doesn't support spaces, so we have to put the firstIndex in the
         // default space(0)
         tint::BindingPoint dstBindingPoint{0u, PipelineLayout::kFirstIndexOffsetConstantBufferSlot};
-        bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
+
+        bindings.uniform.emplace(
+            srcBindingPoint,
+            tint::hlsl::writer::binding::Uniform{dstBindingPoint.group, dstBindingPoint.binding});
     }
 
     req.hlsl.substituteOverrideConfig = std::move(substituteOverrideConfig);
@@ -184,8 +215,7 @@
     req.hlsl.tintOptions.disable_robustness = !device->IsRobustnessEnabled();
     req.hlsl.tintOptions.disable_workgroup_init =
         device->IsToggleEnabled(Toggle::DisableWorkgroupInit);
-    req.hlsl.tintOptions.binding_remapper_options = std::move(bindingRemapper);
-    req.hlsl.tintOptions.external_texture_options = BuildExternalTextureTransformBindings(layout);
+    req.hlsl.tintOptions.bindings = std::move(bindings);
 
     if (entryPoint.usesNumWorkgroups) {
         // D3D11 (HLSL SM5.0) doesn't support spaces, so we have to put the numWorkgroups in the
diff --git a/src/dawn/native/d3d12/ShaderModuleD3D12.cpp b/src/dawn/native/d3d12/ShaderModuleD3D12.cpp
index 431efd2..10390e1 100644
--- a/src/dawn/native/d3d12/ShaderModuleD3D12.cpp
+++ b/src/dawn/native/d3d12/ShaderModuleD3D12.cpp
@@ -179,17 +179,16 @@
 
     using tint::BindingPoint;
 
-    tint::BindingRemapperOptions bindingRemapper;
-    std::unordered_map<BindingPoint, tint::core::Access> accessControls;
-
     tint::ArrayLengthFromUniformOptions arrayLengthFromUniform;
     arrayLengthFromUniform.ubo_binding = {layout->GetDynamicStorageBufferLengthsRegisterSpace(),
                                           layout->GetDynamicStorageBufferLengthsShaderRegister()};
 
+    tint::hlsl::writer::Bindings bindings;
+
     const BindingInfoArray& moduleBindingInfo = entryPoint.bindings;
     for (BindGroupIndex group : IterateBitSet(layout->GetBindGroupLayoutsMask())) {
         const BindGroupLayout* bgl = ToBackend(layout->GetBindGroupLayout(group));
-        const auto& moduleGroupBindingInfo = moduleBindingInfo[group];
+        const BindingGroupInfoMap& moduleGroupBindingInfo = moduleBindingInfo[group];
 
         // d3d12::BindGroupLayout packs the bindings per HLSL register-space. We modify
         // the Tint AST to make the "bindings" decoration match the offset chosen by
@@ -201,80 +200,123 @@
                                          static_cast<uint32_t>(binding)};
             BindingPoint dstBindingPoint{static_cast<uint32_t>(group),
                                          bgl->GetShaderRegister(bindingIndex)};
-            if (srcBindingPoint != dstBindingPoint) {
-                bindingRemapper.binding_points.emplace(srcBindingPoint, dstBindingPoint);
-            }
 
-            const auto* bufferBindingInfo =
+            auto* const bufferBindingInfo =
                 std::get_if<BufferBindingInfo>(&shaderBindingInfo.bindingInfo);
-            if (bufferBindingInfo == nullptr) {
-                continue;
+
+            if (bufferBindingInfo) {
+                switch (bufferBindingInfo->type) {
+                    case wgpu::BufferBindingType::Uniform:
+                        bindings.uniform.emplace(
+                            srcBindingPoint, tint::hlsl::writer::binding::Uniform{
+                                                 dstBindingPoint.group, dstBindingPoint.binding});
+                        break;
+                    case kInternalStorageBufferBinding:
+                    case wgpu::BufferBindingType::Storage:
+                    case wgpu::BufferBindingType::ReadOnlyStorage:
+                        bindings.storage.emplace(
+                            srcBindingPoint, tint::hlsl::writer::binding::Storage{
+                                                 dstBindingPoint.group, dstBindingPoint.binding});
+                        break;
+                    case wgpu::BufferBindingType::Undefined:
+                        DAWN_UNREACHABLE();
+                        break;
+                }
+            } else if (std::holds_alternative<SamplerBindingInfo>(shaderBindingInfo.bindingInfo)) {
+                bindings.sampler.emplace(
+                    srcBindingPoint, tint::hlsl::writer::binding::Sampler{dstBindingPoint.group,
+                                                                          dstBindingPoint.binding});
+            } else if (std::holds_alternative<SampledTextureBindingInfo>(
+                           shaderBindingInfo.bindingInfo)) {
+                bindings.texture.emplace(
+                    srcBindingPoint, tint::hlsl::writer::binding::Texture{dstBindingPoint.group,
+                                                                          dstBindingPoint.binding});
+            } else if (std::holds_alternative<StorageTextureBindingInfo>(
+                           shaderBindingInfo.bindingInfo)) {
+                bindings.storage_texture.emplace(
+                    srcBindingPoint, tint::hlsl::writer::binding::StorageTexture{
+                                         dstBindingPoint.group, dstBindingPoint.binding});
+            } else if (std::holds_alternative<ExternalTextureBindingInfo>(
+                           shaderBindingInfo.bindingInfo)) {
+                const auto& etBindingMap = bgl->GetExternalTextureBindingExpansionMap();
+                const auto& expansion = etBindingMap.find(binding);
+                DAWN_ASSERT(expansion != etBindingMap.end());
+
+                const auto& bindingExpansion = expansion->second;
+                tint::hlsl::writer::binding::BindingInfo plane0{
+                    static_cast<uint32_t>(group),
+                    bgl->GetShaderRegister(bgl->GetBindingIndex(bindingExpansion.plane0))};
+                tint::hlsl::writer::binding::BindingInfo plane1{
+                    static_cast<uint32_t>(group),
+                    bgl->GetShaderRegister(bgl->GetBindingIndex(bindingExpansion.plane1))};
+                tint::hlsl::writer::binding::BindingInfo metadata{
+                    static_cast<uint32_t>(group),
+                    bgl->GetShaderRegister(bgl->GetBindingIndex(bindingExpansion.params))};
+                bindings.external_texture.emplace(
+                    srcBindingPoint,
+                    tint::hlsl::writer::binding::ExternalTexture{metadata, plane0, plane1});
             }
 
-            const auto& bindingLayout =
-                std::get<BufferBindingLayout>(bgl->GetBindingInfo(bindingIndex).bindingLayout);
+            if (bufferBindingInfo) {
+                const auto& bindingLayout =
+                    std::get<BufferBindingLayout>(bgl->GetBindingInfo(bindingIndex).bindingLayout);
 
-            // Declaring a read-only storage buffer in HLSL but specifying a storage
-            // buffer in the BGL produces the wrong output. Force read-only storage
-            // buffer bindings to be treated as UAV instead of SRV. Internal storage
-            // buffer is a storage buffer used in the internal pipeline.
-            const bool forceStorageBufferAsUAV =
-                (bufferBindingInfo->type == wgpu::BufferBindingType::ReadOnlyStorage &&
-                 (bindingLayout.type == wgpu::BufferBindingType::Storage ||
-                  bindingLayout.type == kInternalStorageBufferBinding));
-            if (forceStorageBufferAsUAV) {
-                accessControls.emplace(srcBindingPoint, tint::core::Access::kReadWrite);
-            }
-
-            // On D3D12 backend all storage buffers without Dynamic Buffer Offset will always be
-            // bound to root descriptor tables, where D3D12 runtime can guarantee that OOB-read will
-            // always return 0 and OOB-write will always take no action, so we don't need to do
-            // robustness transform on them. Note that we still need to do robustness transform on
-            // uniform buffers because only sized array is allowed in uniform buffers, so FXC will
-            // report compilation error when the indexing to the array in a cBuffer is out of bound
-            // and can be checked at compilation time. Storage buffers are OK because they are
-            // always translated with RWByteAddressBuffers, which has no such sized arrays.
-            //
-            // For example below WGSL shader will cause compilation error when we skip robustness
-            // transform on uniform buffers:
-            //
-            // struct TestData {
-            //     data: array<vec4<u32>, 3>,
-            // };
-            // @group(0) @binding(0) var<uniform> s: TestData;
-            //
-            // fn test() -> u32 {
-            //     let index = 1000000u;
-            //     if (s.data[index][0] != 0u) {    // error X3504: array index out of bounds
-            //         return 0x1004u;
-            //     }
-            //     return 0u;
-            // }
-            if ((bufferBindingInfo->type == wgpu::BufferBindingType::Storage ||
-                 bufferBindingInfo->type == wgpu::BufferBindingType::ReadOnlyStorage) &&
-                !bindingLayout.hasDynamicOffset) {
-                req.hlsl.tintOptions.binding_points_ignored_in_robustness_transform.emplace_back(
-                    srcBindingPoint);
-            }
-        }
-
-        // Add arrayLengthFromUniform options
-        {
-            for (const auto& bindingAndRegisterOffset :
-                 layout->GetDynamicStorageBufferLengthInfo()[group].bindingAndRegisterOffsets) {
-                BindingNumber binding = bindingAndRegisterOffset.binding;
-                uint32_t registerOffset = bindingAndRegisterOffset.registerOffset;
-
-                BindingPoint bindingPoint{static_cast<uint32_t>(group),
-                                          static_cast<uint32_t>(binding)};
-                // Get the renamed binding point if it was remapped.
-                auto it = bindingRemapper.binding_points.find(bindingPoint);
-                if (it != bindingRemapper.binding_points.end()) {
-                    bindingPoint = it->second;
+                // Declaring a read-only storage buffer in HLSL but specifying a storage
+                // buffer in the BGL produces the wrong output. Force read-only storage
+                // buffer bindings to be treated as UAV instead of SRV. Internal storage
+                // buffer is a storage buffer used in the internal pipeline.
+                const bool forceStorageBufferAsUAV =
+                    (bufferBindingInfo->type == wgpu::BufferBindingType::ReadOnlyStorage &&
+                     (bindingLayout.type == wgpu::BufferBindingType::Storage ||
+                      bindingLayout.type == kInternalStorageBufferBinding));
+                if (forceStorageBufferAsUAV) {
+                    bindings.access_controls.emplace(srcBindingPoint,
+                                                     tint::core::Access::kReadWrite);
                 }
 
-                arrayLengthFromUniform.bindpoint_to_size_index.emplace(bindingPoint,
-                                                                       registerOffset);
+                // On D3D12 backend all storage buffers without Dynamic Buffer Offset will always be
+                // bound to root descriptor tables, where D3D12 runtime can guarantee that OOB-read
+                // will always return 0 and OOB-write will always take no action, so we don't need
+                // to do robustness transform on them. Note that we still need to do robustness
+                // transform on uniform buffers because only sized array is allowed in uniform
+                // buffers, so FXC will report compilation error when the indexing to the array in a
+                // cBuffer is out of bound and can be checked at compilation time. Storage buffers
+                // are OK because they are always translated with RWByteAddressBuffers, which has no
+                // such sized arrays.
+                //
+                // For example below WGSL shader will cause compilation error when we skip
+                // robustness transform on uniform buffers:
+                //
+                // struct TestData {
+                //     data: array<vec4<u32>, 3>,
+                // };
+                // @group(0) @binding(0) var<uniform> s: TestData;
+                //
+                // fn test() -> u32 {
+                //     let index = 1000000u;
+                //     if (s.data[index][0] != 0u) {    // error X3504: array index out of bounds
+                //         return 0x1004u;
+                //     }
+                //     return 0u;
+                // }
+                if ((bufferBindingInfo->type == wgpu::BufferBindingType::Storage ||
+                     bufferBindingInfo->type == wgpu::BufferBindingType::ReadOnlyStorage) &&
+                    !bindingLayout.hasDynamicOffset) {
+                    bindings.ignored_by_robustness_transform.emplace_back(srcBindingPoint);
+                }
+            }
+
+            // Add arrayLengthFromUniform options
+            {
+                for (const auto& bindingAndRegisterOffset :
+                     layout->GetDynamicStorageBufferLengthInfo()[group].bindingAndRegisterOffsets) {
+                    BindingNumber bindingNum = bindingAndRegisterOffset.binding;
+                    uint32_t registerOffset = bindingAndRegisterOffset.registerOffset;
+                    BindingPoint bindingPoint{static_cast<uint32_t>(group),
+                                              static_cast<uint32_t>(bindingNum)};
+                    arrayLengthFromUniform.bindpoint_to_size_index.emplace(bindingPoint,
+                                                                           registerOffset);
+                }
             }
         }
     }
@@ -295,9 +337,7 @@
     req.hlsl.tintOptions.disable_robustness = !device->IsRobustnessEnabled();
     req.hlsl.tintOptions.disable_workgroup_init =
         device->IsToggleEnabled(Toggle::DisableWorkgroupInit);
-    req.hlsl.tintOptions.binding_remapper_options = std::move(bindingRemapper);
-    req.hlsl.tintOptions.access_controls = std::move(accessControls);
-    req.hlsl.tintOptions.external_texture_options = BuildExternalTextureTransformBindings(layout);
+    req.hlsl.tintOptions.bindings = std::move(bindings);
 
     if (entryPoint.usesNumWorkgroups) {
         req.hlsl.tintOptions.root_constant_binding_point = tint::BindingPoint{
diff --git a/src/tint/cmd/loopy/BUILD.bazel b/src/tint/cmd/loopy/BUILD.bazel
index 1b0859f..9a2c3ad 100644
--- a/src/tint/cmd/loopy/BUILD.bazel
+++ b/src/tint/cmd/loopy/BUILD.bazel
@@ -51,6 +51,7 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/hlsl/writer/helpers",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
diff --git a/src/tint/cmd/loopy/BUILD.cmake b/src/tint/cmd/loopy/BUILD.cmake
index 2597a47..ad75e1a 100644
--- a/src/tint/cmd/loopy/BUILD.cmake
+++ b/src/tint/cmd/loopy/BUILD.cmake
@@ -52,6 +52,7 @@
   tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
+  tint_lang_hlsl_writer_helpers
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
diff --git a/src/tint/cmd/loopy/BUILD.gn b/src/tint/cmd/loopy/BUILD.gn
index 5176168..f578df9 100644
--- a/src/tint/cmd/loopy/BUILD.gn
+++ b/src/tint/cmd/loopy/BUILD.gn
@@ -51,6 +51,7 @@
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
+    "${tint_src_dir}/lang/hlsl/writer/helpers",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
diff --git a/src/tint/cmd/loopy/main.cc b/src/tint/cmd/loopy/main.cc
index ccd8b23..89bbcb3 100644
--- a/src/tint/cmd/loopy/main.cc
+++ b/src/tint/cmd/loopy/main.cc
@@ -37,6 +37,7 @@
 #endif  // TINT_BUILD_GLSL_WRITER
 
 #if TINT_BUILD_HLSL_WRITER
+#include "src/tint/lang/hlsl/writer/helpers/generate_bindings.h"
 #include "src/tint/lang/hlsl/writer/writer.h"
 #endif  // TINT_BUILD_HLSL_WRITER
 
@@ -282,8 +283,7 @@
 bool GenerateHlsl(const tint::Program& program) {
 #if TINT_BUILD_HLSL_WRITER
     tint::hlsl::writer::Options gen_options;
-    gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(program);
+    gen_options.bindings = tint::hlsl::writer::GenerateBindings(program);
     auto result = tint::hlsl::writer::Generate(program, gen_options);
     if (result != tint::Success) {
         tint::cmd::PrintWGSL(std::cerr, program);
diff --git a/src/tint/cmd/tint/BUILD.bazel b/src/tint/cmd/tint/BUILD.bazel
index fbc1286..d77264b 100644
--- a/src/tint/cmd/tint/BUILD.bazel
+++ b/src/tint/cmd/tint/BUILD.bazel
@@ -51,6 +51,7 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/hlsl/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 9a02dfe..f354d49 100644
--- a/src/tint/cmd/tint/BUILD.cmake
+++ b/src/tint/cmd/tint/BUILD.cmake
@@ -52,6 +52,7 @@
   tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
+  tint_lang_hlsl_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 5d0447f..e51fbf4 100644
--- a/src/tint/cmd/tint/BUILD.gn
+++ b/src/tint/cmd/tint/BUILD.gn
@@ -51,6 +51,7 @@
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
+    "${tint_src_dir}/lang/hlsl/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 7b04c72..38d695b 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -83,6 +83,7 @@
 
 #if TINT_BUILD_HLSL_WRITER
 #include "src/tint/lang/hlsl/validate/validate.h"
+#include "src/tint/lang/hlsl/writer/helpers/generate_bindings.h"
 #include "src/tint/lang/hlsl/writer/writer.h"
 #endif  // TINT_BUILD_HLSL_WRITER
 
@@ -915,8 +916,7 @@
     tint::hlsl::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::hlsl::writer::GenerateBindings(program);
     gen_options.root_constant_binding_point = options.hlsl_root_constant_binding_point;
     gen_options.pixel_local_options = options.pixel_local_options;
     gen_options.polyfill_dot_4x8_packed = options.hlsl_shader_model < kMinShaderModelForDP4aInHLSL;
diff --git a/src/tint/fuzzers/BUILD.gn b/src/tint/fuzzers/BUILD.gn
index b570ed4c..bf83f01 100644
--- a/src/tint/fuzzers/BUILD.gn
+++ b/src/tint/fuzzers/BUILD.gn
@@ -85,6 +85,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/glsl/writer",
       "${tint_src_dir}/lang/hlsl/writer",
+      "${tint_src_dir}/lang/hlsl/writer/helpers",
       "${tint_src_dir}/lang/msl/writer",
       "${tint_src_dir}/lang/msl/writer/helpers",
       "${tint_src_dir}/lang/spirv/writer",
diff --git a/src/tint/fuzzers/CMakeLists.txt b/src/tint/fuzzers/CMakeLists.txt
index 136afd3..134f6ca 100644
--- a/src/tint/fuzzers/CMakeLists.txt
+++ b/src/tint/fuzzers/CMakeLists.txt
@@ -51,6 +51,7 @@
     tint_spvheaders_compile_options(${NAME})
     tint_spvtools_compile_options(${NAME})
   endif()
+  target_link_libraries(${NAME} PRIVATE tint_lang_hlsl_writer_helpers)
   target_link_libraries(${NAME} PRIVATE tint_lang_msl_writer_helpers)
   target_link_libraries(${NAME} PRIVATE tint_lang_spirv_writer_helpers)
   target_compile_options(${NAME} PRIVATE -Wno-missing-prototypes)
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
index fbdb3b9..d4a7050 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
@@ -27,8 +27,9 @@
 
 function(add_tint_ast_fuzzer NAME)
   add_executable(${NAME} ${NAME}.cc ${AST_FUZZER_SOURCES})
-  target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer tint_lang_spirv_writer_helpers)
+  target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer tint_lang_hlsl_writer_helpers)
   target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer tint_lang_msl_writer_helpers)
+  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 be477d3..a50ca8e 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -62,6 +62,10 @@
 #include "src/tint/lang/msl/writer/helpers/generate_bindings.h"
 #endif  // TINT_BUILD_MSL_WRITER
 
+#if TINT_BUILD_HLSL_WRITER
+#include "src/tint/lang/hlsl/writer/helpers/generate_bindings.h"
+#endif  // TINT_BUILD_MSL_WRITER
+
 namespace tint::fuzzers {
 
 namespace {
@@ -272,6 +276,9 @@
 #endif  // TINT_BUILD_MSL_WRITER
             break;
         case OutputFormat::kHLSL:
+#if TINT_BUILD_HLSL_WRITER
+            options_hlsl_.bindings = tint::hlsl::writer::GenerateBindings(program);
+#endif  // TINT_BUILD_HLSL_WRITER
             break;
         case OutputFormat::kSpv:
 #if TINT_BUILD_SPV_WRITER
@@ -282,51 +289,6 @@
             break;
     }
 
-    // For the generates which use MultiPlanar, make sure the configuration options are provided so
-    // that the transformer will execute.
-    if (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;
-        std::vector<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->Attributes().binding_point) {
-                    auto& n = group_to_next_binding_number[bp->group];
-                    n = std::max(n, bp->binding + 1);
-
-                    if (sem_var->Type()->UnwrapRef()->Is<core::type::ExternalTexture>()) {
-                        ext_tex_bps.emplace_back(*bp);
-                    }
-                }
-            }
-        }
-
-        ExternalTextureOptions::BindingsMap new_bindings_map;
-        for (auto bp : ext_tex_bps) {
-            uint32_t g = bp.group;
-            uint32_t& next_num = group_to_next_binding_number[g];
-            auto new_bps = ExternalTextureOptions::BindingPoints{{g, next_num++}, {g, next_num++}};
-
-            new_bindings_map[bp] = new_bps;
-        }
-
-        switch (output_) {
-            case OutputFormat::kMSL: {
-                break;
-            }
-            case OutputFormat::kHLSL: {
-                options_hlsl_.external_texture_options.bindings_map = new_bindings_map;
-                break;
-            }
-            case OutputFormat::kSpv: {
-                break;
-            }
-            default:
-                break;
-        }
-    }
-
     switch (output_) {
         case OutputFormat::kWGSL: {
 #if TINT_BUILD_WGSL_WRITER
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
index b6f0043..337acdd 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
@@ -27,8 +27,9 @@
 
 function(add_tint_regex_fuzzer NAME)
   add_executable(${NAME} ${NAME}.cc ${REGEX_FUZZER_SOURCES})
-  target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer tint_lang_spirv_writer_helpers)
+  target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer tint_lang_hlsl_writer_helpers)
   target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer tint_lang_msl_writer_helpers)
+  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/hlsl/writer/BUILD.cmake b/src/tint/lang/hlsl/writer/BUILD.cmake
index 5a19a3c..a40f368 100644
--- a/src/tint/lang/hlsl/writer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/BUILD.cmake
@@ -37,6 +37,7 @@
 include(lang/hlsl/writer/ast_printer/BUILD.cmake)
 include(lang/hlsl/writer/ast_raise/BUILD.cmake)
 include(lang/hlsl/writer/common/BUILD.cmake)
+include(lang/hlsl/writer/helpers/BUILD.cmake)
 
 if(TINT_BUILD_HLSL_WRITER)
 ################################################################################
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index cc97b5e..4552722 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -50,6 +50,7 @@
 #include "src/tint/lang/hlsl/writer/ast_raise/pixel_local.h"
 #include "src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h"
 #include "src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h"
+#include "src/tint/lang/hlsl/writer/common/option_helpers.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
@@ -206,10 +207,9 @@
         manager.Add<ast::transform::Robustness>();
 
         ast::transform::Robustness::Config config = {};
-
         config.bindings_ignored = std::unordered_set<BindingPoint>(
-            options.binding_points_ignored_in_robustness_transform.cbegin(),
-            options.binding_points_ignored_in_robustness_transform.cend());
+            options.bindings.ignored_by_robustness_transform.cbegin(),
+            options.bindings.ignored_by_robustness_transform.cend());
 
         // Direct3D guarantees to return zero for any resource that is accessed out of bounds, and
         // according to the description of the assembly store_uav_typed, out of bounds addressing
@@ -219,20 +219,24 @@
         data.Add<ast::transform::Robustness::Config>(config);
     }
 
-    // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
-    data.Add<ast::transform::MultiplanarExternalTexture::NewBindingPoints>(
-        options.external_texture_options.bindings_map);
-    manager.Add<ast::transform::MultiplanarExternalTexture>();
+    ExternalTextureOptions external_texture_options{};
+    RemapperData remapper_data{};
+    ArrayLengthFromUniformOptions array_length_from_uniform_options{};
+    PopulateBindingRelatedOptions(options, remapper_data, external_texture_options,
+                                  array_length_from_uniform_options);
 
-    // BindingRemapper must come after MultiplanarExternalTexture
     manager.Add<ast::transform::BindingRemapper>();
-
     // D3D11 and 12 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.
     data.Add<ast::transform::BindingRemapper::Remappings>(
-        options.binding_remapper_options.binding_points, options.access_controls,
-        /* allow_collisions */ true);
+        remapper_data, options.bindings.access_controls, /* allow_collisions */ true);
+
+    // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
+    // MultiplanarExternalTexture must come after BindingRemapper
+    data.Add<ast::transform::MultiplanarExternalTexture::NewBindingPoints>(
+        external_texture_options.bindings_map, /* may_collide */ true);
+    manager.Add<ast::transform::MultiplanarExternalTexture>();
 
     {  // Builtin polyfills
         ast::transform::BuiltinPolyfill::Builtins polyfills;
@@ -330,11 +334,10 @@
     manager.Add<ast::transform::RemovePhonies>();
 
     // Build the config for the internal ArrayLengthFromUniform transform.
-    auto& array_length_from_uniform = options.array_length_from_uniform;
     ast::transform::ArrayLengthFromUniform::Config array_length_from_uniform_cfg(
-        array_length_from_uniform.ubo_binding);
+        array_length_from_uniform_options.ubo_binding);
     array_length_from_uniform_cfg.bindpoint_to_size_index =
-        array_length_from_uniform.bindpoint_to_size_index;
+        std::move(array_length_from_uniform_options.bindpoint_to_size_index);
 
     // DemoteToHelper must come after CanonicalizeEntryPointIO, PromoteSideEffectsToDecl, and
     // ExpandCompoundAssignment.
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.bazel b/src/tint/lang/hlsl/writer/common/BUILD.bazel
index 93cb98f..333c5e5 100644
--- a/src/tint/lang/hlsl/writer/common/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/common/BUILD.bazel
@@ -39,18 +39,27 @@
 cc_library(
   name = "common",
   srcs = [
+    "option_helpers.cc",
     "options.cc",
   ],
   hdrs = [
+    "option_helpers.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",
+    "//src/tint/utils/memory",
     "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
   copts = COPTS,
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.cmake b/src/tint/lang/hlsl/writer/common/BUILD.cmake
index 1a5415e..75293b9 100644
--- a/src/tint/lang/hlsl/writer/common/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/common/BUILD.cmake
@@ -39,6 +39,8 @@
 # Kind:      lib
 ################################################################################
 tint_add_target(tint_lang_hlsl_writer_common lib
+  lang/hlsl/writer/common/option_helpers.cc
+  lang/hlsl/writer/common/option_helpers.h
   lang/hlsl/writer/common/options.cc
   lang/hlsl/writer/common/options.h
 )
@@ -47,8 +49,15 @@
   tint_api_common
   tint_api_options
   tint_lang_core
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
   tint_utils_macros
   tint_utils_math
+  tint_utils_memory
   tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.gn b/src/tint/lang/hlsl/writer/common/BUILD.gn
index 39c9b06..b8d369d 100644
--- a/src/tint/lang/hlsl/writer/common/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/common/BUILD.gn
@@ -40,6 +40,8 @@
 
 libtint_source_set("common") {
   sources = [
+    "option_helpers.cc",
+    "option_helpers.h",
     "options.cc",
     "options.h",
   ]
@@ -47,9 +49,16 @@
     "${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",
+    "${tint_src_dir}/utils/memory",
     "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
 }
diff --git a/src/tint/lang/hlsl/writer/common/option_helpers.cc b/src/tint/lang/hlsl/writer/common/option_helpers.cc
new file mode 100644
index 0000000..aace10b
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/common/option_helpers.cc
@@ -0,0 +1,231 @@
+/// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/hlsl/writer/common/option_helpers.h"
+
+#include <utility>
+
+#include "src/tint/utils/containers/hashset.h"
+
+namespace tint::hlsl::writer {
+
+/// binding::BindingInfo to tint::BindingPoint map
+using InfoToPointMap = tint::Hashmap<binding::BindingInfo, tint::BindingPoint, 8>;
+
+Result<SuccessType> ValidateBindingOptions(const Options& options) {
+    diag::List diagnostics;
+
+    tint::Hashmap<tint::BindingPoint, binding::BindingInfo, 8> seen_wgsl_bindings{};
+
+    InfoToPointMap seen_hlsl_buffer_bindings{};
+    InfoToPointMap seen_hlsl_texture_bindings{};
+    InfoToPointMap seen_hlsl_sampler_bindings{};
+
+    // Both wgsl_seen and spirv_seen check to see if the pair of [src, dst] are unique. If we have
+    // multiple entries that map the same [src, dst] pair, that's fine. We treat it as valid as it's
+    // possible for multiple entry points to use the remapper at the same time. If the pair doesn't
+    // match, then we report an error about a duplicate binding point.
+
+    auto wgsl_seen = [&diagnostics, &seen_wgsl_bindings](const tint::BindingPoint& src,
+                                                         const binding::BindingInfo& dst) -> bool {
+        if (auto binding = seen_wgsl_bindings.Get(src)) {
+            if (*binding != dst) {
+                std::stringstream str;
+                str << "found duplicate WGSL binding point: " << src;
+
+                diagnostics.AddError(diag::System::Writer, str.str());
+                return true;
+            }
+        }
+        seen_wgsl_bindings.Add(src, dst);
+        return false;
+    };
+
+    auto hlsl_seen = [&diagnostics](InfoToPointMap& map, const binding::BindingInfo& src,
+                                    const tint::BindingPoint& dst) -> bool {
+        if (auto binding = map.Get(src)) {
+            if (*binding != dst) {
+                std::stringstream str;
+                str << "found duplicate MSL binding point: [binding: " << src.binding << "]";
+                diagnostics.AddError(diag::System::Writer, str.str());
+                return true;
+            }
+        }
+        map.Add(src, dst);
+        return false;
+    };
+
+    auto valid = [&wgsl_seen, &hlsl_seen](InfoToPointMap& map, 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, dst_binding)) {
+                return false;
+            }
+
+            if (hlsl_seen(map, dst_binding, src_binding)) {
+                return false;
+            }
+        }
+        return true;
+    };
+
+    // Storage and uniform are both [[buffer()]]
+    if (!valid(seen_hlsl_buffer_bindings, options.bindings.uniform)) {
+        diagnostics.AddNote(diag::System::Writer, "when processing uniform", {});
+        return Failure{std::move(diagnostics)};
+    }
+    if (!valid(seen_hlsl_buffer_bindings, options.bindings.storage)) {
+        diagnostics.AddNote(diag::System::Writer, "when processing storage", {});
+        return Failure{std::move(diagnostics)};
+    }
+
+    // Sampler is [[sampler()]]
+    if (!valid(seen_hlsl_sampler_bindings, options.bindings.sampler)) {
+        diagnostics.AddNote(diag::System::Writer, "when processing sampler", {});
+        return Failure{std::move(diagnostics)};
+    }
+
+    // Texture and storage texture are [[texture()]]
+    if (!valid(seen_hlsl_texture_bindings, options.bindings.texture)) {
+        diagnostics.AddNote(diag::System::Writer, "when processing texture", {});
+        return Failure{std::move(diagnostics)};
+    }
+    if (!valid(seen_hlsl_texture_bindings, options.bindings.storage_texture)) {
+        diagnostics.AddNote(diag::System::Writer, "when processing storage_texture", {});
+        return Failure{std::move(diagnostics)};
+    }
+
+    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, plane0)) {
+            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            return Failure{std::move(diagnostics)};
+        }
+
+        // Plane0 & Plane1 are [[texture()]]
+        if (hlsl_seen(seen_hlsl_texture_bindings, plane0, src_binding)) {
+            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            return Failure{std::move(diagnostics)};
+        }
+        if (hlsl_seen(seen_hlsl_texture_bindings, plane1, src_binding)) {
+            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            return Failure{std::move(diagnostics)};
+        }
+        // Metadata is [[buffer()]]
+        if (hlsl_seen(seen_hlsl_buffer_bindings, metadata, src_binding)) {
+            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            return Failure{std::move(diagnostics)};
+        }
+    }
+
+    return Success;
+}
+
+// The remapped binding data and external texture data need to coordinate in order to put things in
+// the correct place when we're done. The binding remapper is run first, so make sure that the
+// external texture uses the new binding point.
+//
+// When the data comes in we have a list of all WGSL origin (group,binding) pairs to HLSL
+// (group,binding) in the `uniform`, `storage`, `texture`, and `sampler` arrays.
+void PopulateBindingRelatedOptions(
+    const Options& options,
+    RemapperData& remapper_data,
+    ExternalTextureOptions& external_texture,
+    ArrayLengthFromUniformOptions& array_length_from_uniform_options) {
+    auto create_remappings = [&remapper_data](const auto& hsh) {
+        for (const auto& it : hsh) {
+            const BindingPoint& src_binding_point = it.first;
+            const binding::BindingInfo& dst_binding_info = it.second;
+
+            BindingPoint dst_binding_point{dst_binding_info.group, dst_binding_info.binding};
+
+            // Skip redundant bindings
+            if (src_binding_point == dst_binding_point) {
+                continue;
+            }
+
+            remapper_data.emplace(src_binding_point, dst_binding_point);
+        }
+    };
+
+    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 HLSL 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 HLSL do not need to be re-bound.
+        if (src_binding_point == plane0_binding_point) {
+            continue;
+        }
+
+        remapper_data.emplace(src_binding_point, plane0_binding_point);
+    }
+
+    // ArrayLengthFromUniformOptions bindpoints may need to be remapped
+    {
+        std::unordered_map<BindingPoint, uint32_t> bindpoint_to_size_index;
+        for (auto& [bindpoint, index] : options.array_length_from_uniform.bindpoint_to_size_index) {
+            auto it = remapper_data.find(bindpoint);
+            if (it != remapper_data.end()) {
+                bindpoint_to_size_index.emplace(it->second, index);
+            } else {
+                bindpoint_to_size_index.emplace(bindpoint, index);
+            }
+        }
+
+        array_length_from_uniform_options.ubo_binding =
+            options.array_length_from_uniform.ubo_binding;
+        array_length_from_uniform_options.bindpoint_to_size_index =
+            std::move(bindpoint_to_size_index);
+    }
+}
+
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/hlsl/writer/common/option_helpers.h b/src/tint/lang/hlsl/writer/common/option_helpers.h
new file mode 100644
index 0000000..bcfc083
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/common/option_helpers.h
@@ -0,0 +1,62 @@
+/// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_HLSL_WRITER_COMMON_OPTION_HELPERS_H_
+#define SRC_TINT_LANG_HLSL_WRITER_COMMON_OPTION_HELPERS_H_
+
+#include <unordered_map>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/api/options/external_texture.h"
+#include "src/tint/lang/hlsl/writer/common/options.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/result/result.h"
+
+namespace tint::hlsl::writer {
+
+/// The remapper data
+using RemapperData = std::unordered_map<BindingPoint, BindingPoint>;
+
+/// @param options the options
+/// @returns success or failure
+Result<SuccessType> ValidateBindingOptions(const Options& options);
+
+/// Populates binding-related option from the writer options
+/// @param options the writer options
+/// @param remapper_data where to put the remapper data
+/// @param external_texture where to store the external texture options
+/// @param array_length_from_uniform_options where to store the ArrayLengthFromUniform options
+/// Note, these are populated together because there are dependencies between the two types of data.
+void PopulateBindingRelatedOptions(
+    const Options& options,
+    RemapperData& remapper_data,
+    ExternalTextureOptions& external_texture,
+    ArrayLengthFromUniformOptions& array_length_from_uniform_options);
+
+}  // namespace tint::hlsl::writer
+
+#endif  // SRC_TINT_LANG_HLSL_WRITER_COMMON_OPTION_HELPERS_H_
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index b8d9a75..eb038de 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -39,10 +39,111 @@
 #include "src/tint/api/options/external_texture.h"
 #include "src/tint/api/options/pixel_local.h"
 #include "src/tint/lang/core/access.h"
+#include "src/tint/utils/math/hash.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::hlsl::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;
+    }
+    /// Inequality operator
+    /// @param rhs the BindingInfo to compare against
+    /// @returns true if this BindingInfo is not equal to `rhs`
+    inline bool operator!=(const BindingInfo& rhs) const { return !(*this == rhs); }
+
+    /// @returns the hash code of the BindingInfo
+    size_t HashCode() const { return Hash(group, binding); }
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(BindingInfo, group, binding);
+};
+/// Ensure that all the fields of BindingInfo are reflected.
+TINT_ASSERT_ALL_FIELDS_REFLECTED(BindingInfo);
+
+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(ExternalTexture, metadata, plane0, plane1);
+};
+
+/// Ensure that all the fields of ExternalTexture are reflected.
+TINT_ASSERT_ALL_FIELDS_REFLECTED(ExternalTexture);
+
+}  // 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 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{};
+    /// Mapping of BindingPoint to new Access
+    std::unordered_map<BindingPoint, tint::core::Access> access_controls;
+    /// The binding points that will be ignored by the rebustness transform.
+    std::vector<BindingPoint> ignored_by_robustness_transform;
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(Bindings,
+                 uniform,
+                 storage,
+                 texture,
+                 storage_texture,
+                 sampler,
+                 external_texture,
+                 access_controls,
+                 ignored_by_robustness_transform);
+};
+
+/// Ensure that all the fields of Bindings are reflected.
+TINT_ASSERT_ALL_FIELDS_REFLECTED(Bindings);
+
 /// kMaxInterStageLocations == D3D11_PS_INPUT_REGISTER_COUNT - 2
 /// D3D11_PS_INPUT_REGISTER_COUNT == D3D12_PS_INPUT_REGISTER_COUNT
 constexpr uint32_t kMaxInterStageLocations = 30;
@@ -92,17 +193,8 @@
     /// The binding point to use for information passed via root constants.
     std::optional<BindingPoint> root_constant_binding_point;
 
-    /// Options used in the binding mappings for external textures
-    ExternalTextureOptions external_texture_options = {};
-
-    /// Options used in the bindings remapper
-    BindingRemapperOptions binding_remapper_options = {};
-
-    /// The binding points that will be ignored in the rebustness transform.
-    std::vector<BindingPoint> binding_points_ignored_in_robustness_transform;
-
-    /// AccessControls is a map of old binding point to new access control
-    std::unordered_map<BindingPoint, core::Access> access_controls;
+    /// The bindings
+    Bindings bindings;
 
     /// Options used to deal with pixel local storage variables
     PixelLocalOptions pixel_local_options = {};
@@ -119,10 +211,7 @@
                  array_length_from_uniform,
                  interstage_locations,
                  root_constant_binding_point,
-                 external_texture_options,
-                 binding_remapper_options,
-                 binding_points_ignored_in_robustness_transform,
-                 access_controls,
+                 bindings,
                  pixel_local_options);
 };
 
diff --git a/src/tint/lang/hlsl/writer/helpers/BUILD.bazel b/src/tint/lang/hlsl/writer/helpers/BUILD.bazel
new file mode 100644
index 0000000..24a3b69
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/helpers/BUILD.bazel
@@ -0,0 +1,76 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# 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/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/hlsl/writer/helpers/BUILD.cmake b/src/tint/lang/hlsl/writer/helpers/BUILD.cmake
new file mode 100644
index 0000000..6332974
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/helpers/BUILD.cmake
@@ -0,0 +1,71 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# 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_hlsl_writer_helpers
+# Kind:      lib
+################################################################################
+tint_add_target(tint_lang_hlsl_writer_helpers lib
+  lang/hlsl/writer/helpers/generate_bindings.cc
+  lang/hlsl/writer/helpers/generate_bindings.h
+)
+
+tint_target_add_dependencies(tint_lang_hlsl_writer_helpers lib
+  tint_api_common
+  tint_api_options
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_hlsl_writer_common
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_features
+  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
+)
diff --git a/src/tint/lang/hlsl/writer/helpers/BUILD.gn b/src/tint/lang/hlsl/writer/helpers/BUILD.gn
new file mode 100644
index 0000000..8583f77
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/helpers/BUILD.gn
@@ -0,0 +1,72 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# 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}/api/options",
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/lang/hlsl/writer/common",
+    "${tint_src_dir}/lang/wgsl",
+    "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/features",
+    "${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",
+  ]
+}
diff --git a/src/tint/lang/hlsl/writer/helpers/generate_bindings.cc b/src/tint/lang/hlsl/writer/helpers/generate_bindings.cc
new file mode 100644
index 0000000..b10d2f6
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/helpers/generate_bindings.cc
@@ -0,0 +1,121 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/hlsl/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/containers/hashmap.h"
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::hlsl::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{};
+
+    // Collect next valid binding number per group
+    Hashmap<uint32_t, uint32_t, 4> group_to_next_binding_number;
+    Vector<tint::BindingPoint, 4> 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->Attributes().binding_point) {
+                if (auto val = group_to_next_binding_number.Get(bp->group)) {
+                    *val = std::max(*val, bp->binding + 1);
+                } else {
+                    group_to_next_binding_number.Add(bp->group, 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.Push(*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.GetOrAddZero(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::hlsl::writer
diff --git a/src/tint/lang/hlsl/writer/helpers/generate_bindings.h b/src/tint/lang/hlsl/writer/helpers/generate_bindings.h
new file mode 100644
index 0000000..249075a
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/helpers/generate_bindings.h
@@ -0,0 +1,44 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_HLSL_WRITER_HELPERS_GENERATE_BINDINGS_H_
+#define SRC_TINT_LANG_HLSL_WRITER_HELPERS_GENERATE_BINDINGS_H_
+
+#include "src/tint/lang/hlsl/writer/common/options.h"
+
+// Forward declarations
+namespace tint {
+class Program;
+}
+
+namespace tint::hlsl::writer {
+
+Bindings GenerateBindings(const Program& program);
+
+}  // namespace tint::hlsl::writer
+
+#endif  // SRC_TINT_LANG_HLSL_WRITER_HELPERS_GENERATE_BINDINGS_H_
diff --git a/src/tint/lang/msl/writer/common/option_helpers.cc b/src/tint/lang/msl/writer/common/option_helpers.cc
index 433f256..d72d984 100644
--- a/src/tint/lang/msl/writer/common/option_helpers.cc
+++ b/src/tint/lang/msl/writer/common/option_helpers.cc
@@ -164,7 +164,7 @@
     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;
+            const binding::BindingInfo& dst_binding_point = it.second;
 
             // Bindings which go to the same slot in MSL do not need to be re-bound.
             if (src_binding_point.group == 0 &&
@@ -210,19 +210,3 @@
 }
 
 }  // namespace tint::msl::writer
-
-namespace std {
-
-/// Custom std::hash specialization for tint::msl::writer::binding::BindingInfo so
-/// they can be used as keys for std::unordered_map and std::unordered_set.
-template <>
-class hash<tint::msl::writer::binding::BindingInfo> {
-  public:
-    /// @param info the binding to create a hash for
-    /// @return the hash value
-    inline std::size_t operator()(const tint::msl::writer::binding::BindingInfo& info) const {
-        return tint::Hash(info.binding);
-    }
-};
-
-}  // namespace std
diff --git a/src/tint/lang/msl/writer/common/options.h b/src/tint/lang/msl/writer/common/options.h
index 3b359ab..2f5e770 100644
--- a/src/tint/lang/msl/writer/common/options.h
+++ b/src/tint/lang/msl/writer/common/options.h
@@ -52,6 +52,9 @@
     /// @returns true if this BindingInfo is not equal to `rhs`
     inline bool operator!=(const BindingInfo& rhs) const { return !(*this == rhs); }
 
+    /// @returns the hash code of the BindingInfo
+    size_t HashCode() const { return Hash(binding); }
+
     /// Reflect the fields of this class so taht it can be used by tint::ForeachField()
     TINT_REFLECT(BindingInfo, binding);
 };
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 346f46d..d76a591 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -109,10 +109,7 @@
     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>(
         remapper_data, std::unordered_map<BindingPoint, core::Access>{},
         /* allow_collisions */ false);
diff --git a/src/tint/lang/spirv/writer/common/option_helper.cc b/src/tint/lang/spirv/writer/common/option_helper.cc
index 93873c3..37f2377 100644
--- a/src/tint/lang/spirv/writer/common/option_helper.cc
+++ b/src/tint/lang/spirv/writer/common/option_helper.cc
@@ -187,7 +187,7 @@
     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;
+            const binding::BindingInfo& 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 &&
@@ -233,19 +233,3 @@
 }
 
 }  // 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/options.h b/src/tint/lang/spirv/writer/common/options.h
index 4fcbafe..0fd6f8c 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -54,6 +54,9 @@
     /// @returns true if this BindingInfo is not equal to `rhs`
     inline bool operator!=(const BindingInfo& rhs) const { return !(*this == rhs); }
 
+    /// @returns the hash code of the BindingInfo
+    size_t HashCode() const { return Hash(group, binding); }
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(BindingInfo, group, binding);
 };