Implement External Texture Binding Functionality

Adds functionality to BindGroupLayout and BindGroup to allow
GPUExternalTexture bindings.

Bug: dawn:728
Change-Id: I651b28606dceda15f0a944711ddba639df77c1a3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/47381
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/dawn.json b/dawn.json
index 41ed56b..7029aad 100644
--- a/dawn.json
+++ b/dawn.json
@@ -65,7 +65,7 @@
     },
     "bind group entry": {
         "category": "structure",
-        "extensible": false,
+        "extensible": true,
         "members": [
             {"name": "binding", "type": "uint32_t"},
             {"name": "buffer", "type": "buffer", "optional": true},
@@ -146,6 +146,20 @@
         ]
     },
 
+    "external texture binding entry": {
+        "category": "structure",
+        "chained": true,
+        "members": [
+            {"name": "external texture", "type": "external texture"}
+        ]
+    },
+
+    "external texture binding layout": {
+        "category": "structure",
+        "chained": true,
+        "members": []
+    },
+
     "storage texture access": {
         "category": "enum",
         "values": [
@@ -1787,7 +1801,9 @@
             {"value": 5, "name": "shader module SPIRV descriptor"},
             {"value": 6, "name": "shader module WGSL descriptor"},
             {"value": 7, "name": "primitive depth clamping state"},
-            {"value": 8, "name": "surface descriptor from windows core window"}
+            {"value": 8, "name": "surface descriptor from windows core window"},
+            {"value": 9, "name": "external texture binding entry"},
+            {"value": 10, "name": "external texture binding layout"}
         ]
     },
     "texture": {
diff --git a/src/common/Constants.h b/src/common/Constants.h
index 1ca0a66..f92c6b7 100644
--- a/src/common/Constants.h
+++ b/src/common/Constants.h
@@ -68,4 +68,10 @@
 // * 1024 / 8.
 static constexpr uint32_t kMaxQueryCount = 8192u;
 
+// An external texture occupies multiple binding slots. These are the per-external-texture bindings
+// needed.
+static constexpr uint8_t kSampledTexturesPerExternalTexture = 3u;
+static constexpr uint8_t kSamplersPerExternalTexture = 1u;
+static constexpr uint8_t kUniformsPerExternalTexture = 1u;
+
 #endif  // COMMON_CONSTANTS_H_
diff --git a/src/dawn_native/BindGroup.cpp b/src/dawn_native/BindGroup.cpp
index c84d578..6078eaa 100644
--- a/src/dawn_native/BindGroup.cpp
+++ b/src/dawn_native/BindGroup.cpp
@@ -19,7 +19,9 @@
 #include "common/ityp_bitset.h"
 #include "dawn_native/BindGroupLayout.h"
 #include "dawn_native/Buffer.h"
+#include "dawn_native/ChainUtils_autogen.h"
 #include "dawn_native/Device.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/Sampler.h"
 #include "dawn_native/Texture.h"
 
@@ -33,7 +35,7 @@
                                          const BindGroupEntry& entry,
                                          const BindingInfo& bindingInfo) {
             if (entry.buffer == nullptr || entry.sampler != nullptr ||
-                entry.textureView != nullptr) {
+                entry.textureView != nullptr || entry.nextInChain != nullptr) {
                 return DAWN_VALIDATION_ERROR("Expected buffer binding");
             }
             DAWN_TRY(device->ValidateObject(entry.buffer));
@@ -111,7 +113,7 @@
                                           const BindGroupEntry& entry,
                                           const BindingInfo& bindingInfo) {
             if (entry.textureView == nullptr || entry.sampler != nullptr ||
-                entry.buffer != nullptr) {
+                entry.buffer != nullptr || entry.nextInChain != nullptr) {
                 return DAWN_VALIDATION_ERROR("Expected texture binding");
             }
             DAWN_TRY(device->ValidateObject(entry.textureView));
@@ -176,7 +178,7 @@
                                           const BindGroupEntry& entry,
                                           const BindingInfo& bindingInfo) {
             if (entry.sampler == nullptr || entry.textureView != nullptr ||
-                entry.buffer != nullptr) {
+                entry.buffer != nullptr || entry.nextInChain != nullptr) {
                 return DAWN_VALIDATION_ERROR("Expected sampler binding");
             }
             DAWN_TRY(device->ValidateObject(entry.sampler));
@@ -203,6 +205,25 @@
             return {};
         }
 
+        MaybeError ValidateExternalTextureBinding(const DeviceBase* device,
+                                                  const BindGroupEntry& entry,
+                                                  const BindingInfo& bindingInfo) {
+            const ExternalTextureBindingEntry* externalTextureBindingEntry = nullptr;
+            FindInChain(entry.nextInChain, &externalTextureBindingEntry);
+
+            if (entry.sampler != nullptr || entry.textureView != nullptr ||
+                entry.buffer != nullptr || externalTextureBindingEntry == nullptr) {
+                return DAWN_VALIDATION_ERROR("Expected external texture binding");
+            }
+
+            DAWN_TRY(ValidateSingleSType(externalTextureBindingEntry->nextInChain,
+                                         wgpu::SType::ExternalTextureBindingEntry));
+
+            DAWN_TRY(device->ValidateObject(externalTextureBindingEntry->externalTexture));
+
+            return {};
+        }
+
     }  // anonymous namespace
 
     MaybeError ValidateBindGroupDescriptor(DeviceBase* device,
@@ -250,6 +271,9 @@
                 case BindingInfoType::Sampler:
                     DAWN_TRY(ValidateSamplerBinding(device, entry, bindingInfo));
                     break;
+                case BindingInfoType::ExternalTexture:
+                    DAWN_TRY(ValidateExternalTextureBinding(device, entry, bindingInfo));
+                    break;
             }
         }
 
@@ -309,6 +333,14 @@
                 mBindingData.bindings[bindingIndex] = entry.sampler;
                 continue;
             }
+
+            const ExternalTextureBindingEntry* externalTextureBindingEntry = nullptr;
+            FindInChain(entry.nextInChain, &externalTextureBindingEntry);
+            if (externalTextureBindingEntry != nullptr) {
+                ASSERT(mBindingData.bindings[bindingIndex] == nullptr);
+                mBindingData.bindings[bindingIndex] = externalTextureBindingEntry->externalTexture;
+                continue;
+            }
         }
 
         uint32_t packedIdx = 0;
@@ -388,4 +420,12 @@
         return static_cast<TextureViewBase*>(mBindingData.bindings[bindingIndex].Get());
     }
 
+    ExternalTextureBase* BindGroupBase::GetBindingAsExternalTexture(BindingIndex bindingIndex) {
+        ASSERT(!IsError());
+        ASSERT(bindingIndex < mLayout->GetBindingCount());
+        ASSERT(mLayout->GetBindingInfo(bindingIndex).bindingType ==
+               BindingInfoType::ExternalTexture);
+        return static_cast<ExternalTextureBase*>(mBindingData.bindings[bindingIndex].Get());
+    }
+
 }  // namespace dawn_native
diff --git a/src/dawn_native/BindGroup.h b/src/dawn_native/BindGroup.h
index c29bbeb..a636fe8 100644
--- a/src/dawn_native/BindGroup.h
+++ b/src/dawn_native/BindGroup.h
@@ -49,6 +49,7 @@
         SamplerBase* GetBindingAsSampler(BindingIndex bindingIndex) const;
         TextureViewBase* GetBindingAsTextureView(BindingIndex bindingIndex);
         const ityp::span<uint32_t, uint64_t>& GetUnverifiedBufferSizes() const;
+        ExternalTextureBase* GetBindingAsExternalTexture(BindingIndex bindingIndex);
 
       protected:
         // To save memory, the size of a bind group is dynamically determined and the bind group is
diff --git a/src/dawn_native/BindGroupLayout.cpp b/src/dawn_native/BindGroupLayout.cpp
index e07656a..0f322af 100644
--- a/src/dawn_native/BindGroupLayout.cpp
+++ b/src/dawn_native/BindGroupLayout.cpp
@@ -15,6 +15,8 @@
 #include "dawn_native/BindGroupLayout.h"
 
 #include "common/BitSetIterator.h"
+
+#include "dawn_native/ChainUtils_autogen.h"
 #include "dawn_native/Device.h"
 #include "dawn_native/ObjectContentHasher.h"
 #include "dawn_native/PerStage.h"
@@ -147,10 +149,16 @@
                 }
             }
 
+            const ExternalTextureBindingLayout* externalTextureBindingLayout = nullptr;
+            FindInChain(entry.nextInChain, &externalTextureBindingLayout);
+            if (externalTextureBindingLayout != nullptr) {
+                bindingMemberCount++;
+            }
+
             if (bindingMemberCount != 1) {
                 return DAWN_VALIDATION_ERROR(
-                    "Exactly one of buffer, sampler, texture, or storageTexture must be set for "
-                    "each BindGroupLayoutEntry");
+                    "Exactly one of buffer, sampler, texture, storageTexture, or externalTexture "
+                    "must be set for each BindGroupLayoutEntry");
             }
 
             if (!IsSubset(entry.visibility, allowedStages)) {
@@ -190,6 +198,8 @@
                     return a.storageTexture.access != b.storageTexture.access ||
                            a.storageTexture.viewDimension != b.storageTexture.viewDimension ||
                            a.storageTexture.format != b.storageTexture.format;
+                case BindingInfoType::ExternalTexture:
+                    return false;
             }
         }
 
@@ -229,6 +239,12 @@
                 if (binding.storageTexture.viewDimension == wgpu::TextureViewDimension::Undefined) {
                     bindingInfo.storageTexture.viewDimension = wgpu::TextureViewDimension::e2D;
                 }
+            } else {
+                const ExternalTextureBindingLayout* externalTextureBindingLayout = nullptr;
+                FindInChain(binding.nextInChain, &externalTextureBindingLayout);
+                if (externalTextureBindingLayout != nullptr) {
+                    bindingInfo.bindingType = BindingInfoType::ExternalTexture;
+                }
             }
 
             return bindingInfo;
@@ -309,6 +325,8 @@
                         return aInfo.storageTexture.format < bInfo.storageTexture.format;
                     }
                     break;
+                case BindingInfoType::ExternalTexture:
+                    break;
             }
             return false;
         }
diff --git a/src/dawn_native/BindingInfo.cpp b/src/dawn_native/BindingInfo.cpp
index 3b13a33..e2facc4 100644
--- a/src/dawn_native/BindingInfo.cpp
+++ b/src/dawn_native/BindingInfo.cpp
@@ -14,6 +14,8 @@
 
 #include "dawn_native/BindingInfo.h"
 
+#include "dawn_native/ChainUtils_autogen.h"
+
 namespace dawn_native {
 
     void IncrementBindingCounts(BindingCounts* bindingCounts, const BindGroupLayoutEntry& entry) {
@@ -56,6 +58,12 @@
             perStageBindingCountMember = &PerStageBindingCounts::sampledTextureCount;
         } else if (entry.storageTexture.access != wgpu::StorageTextureAccess::Undefined) {
             perStageBindingCountMember = &PerStageBindingCounts::storageTextureCount;
+        } else {
+            const ExternalTextureBindingLayout* externalTextureBindingLayout;
+            FindInChain(entry.nextInChain, &externalTextureBindingLayout);
+            if (externalTextureBindingLayout != nullptr) {
+                perStageBindingCountMember = &PerStageBindingCounts::externalTextureCount;
+            }
         }
 
         ASSERT(perStageBindingCountMember != nullptr);
@@ -81,6 +89,8 @@
                 rhs.perStage[stage].storageTextureCount;
             bindingCounts->perStage[stage].uniformBufferCount +=
                 rhs.perStage[stage].uniformBufferCount;
+            bindingCounts->perStage[stage].externalTextureCount +=
+                rhs.perStage[stage].externalTextureCount;
         }
     }
 
@@ -104,25 +114,65 @@
                     "The number of sampled textures exceeds the maximum "
                     "per-stage limit.");
             }
+
+            // The per-stage number of external textures is bound by the maximum sampled textures
+            // per stage.
+            if (bindingCounts.perStage[stage].externalTextureCount >
+                kMaxSampledTexturesPerShaderStage / kSampledTexturesPerExternalTexture) {
+                return DAWN_VALIDATION_ERROR(
+                    "The number of external textures exceeds the maximum "
+                    "per-stage limit.");
+            }
+
+            if (bindingCounts.perStage[stage].sampledTextureCount +
+                    (bindingCounts.perStage[stage].externalTextureCount *
+                     kSampledTexturesPerExternalTexture) >
+                kMaxSampledTexturesPerShaderStage) {
+                return DAWN_VALIDATION_ERROR(
+                    "The combination of sampled textures and external textures exceeds the maximum "
+                    "per-stage limit.");
+            }
+
             if (bindingCounts.perStage[stage].samplerCount > kMaxSamplersPerShaderStage) {
                 return DAWN_VALIDATION_ERROR(
                     "The number of samplers exceeds the maximum per-stage limit.");
             }
+
+            if (bindingCounts.perStage[stage].samplerCount +
+                    (bindingCounts.perStage[stage].externalTextureCount *
+                     kSamplersPerExternalTexture) >
+                kMaxSamplersPerShaderStage) {
+                return DAWN_VALIDATION_ERROR(
+                    "The combination of samplers and external textures exceeds the maximum "
+                    "per-stage limit.");
+            }
+
             if (bindingCounts.perStage[stage].storageBufferCount >
                 kMaxStorageBuffersPerShaderStage) {
                 return DAWN_VALIDATION_ERROR(
                     "The number of storage buffers exceeds the maximum per-stage limit.");
             }
+
             if (bindingCounts.perStage[stage].storageTextureCount >
                 kMaxStorageTexturesPerShaderStage) {
                 return DAWN_VALIDATION_ERROR(
                     "The number of storage textures exceeds the maximum per-stage limit.");
             }
+
             if (bindingCounts.perStage[stage].uniformBufferCount >
                 kMaxUniformBuffersPerShaderStage) {
                 return DAWN_VALIDATION_ERROR(
                     "The number of uniform buffers exceeds the maximum per-stage limit.");
             }
+
+            if (bindingCounts.perStage[stage].uniformBufferCount +
+                    (bindingCounts.perStage[stage].externalTextureCount *
+                     kUniformsPerExternalTexture) >
+                kMaxUniformBuffersPerShaderStage) {
+                return DAWN_VALIDATION_ERROR(
+                    "The combination of uniform buffers and external textures exceeds the maximum "
+                    "per-stage limit.");
+            }
         }
 
         return {};
diff --git a/src/dawn_native/BindingInfo.h b/src/dawn_native/BindingInfo.h
index ef3c7ac..f4a1730 100644
--- a/src/dawn_native/BindingInfo.h
+++ b/src/dawn_native/BindingInfo.h
@@ -48,12 +48,7 @@
     // TODO(enga): Figure out a good number for this.
     static constexpr uint32_t kMaxOptimalBindingsPerGroup = 32;
 
-    enum class BindingInfoType {
-        Buffer,
-        Sampler,
-        Texture,
-        StorageTexture,
-    };
+    enum class BindingInfoType { Buffer, Sampler, Texture, StorageTexture, ExternalTexture };
 
     struct BindingInfo {
         BindingNumber binding;
@@ -74,6 +69,7 @@
         uint32_t storageBufferCount;
         uint32_t storageTextureCount;
         uint32_t uniformBufferCount;
+        uint32_t externalTextureCount;
     };
 
     struct BindingCounts {
diff --git a/src/dawn_native/ExternalTexture.cpp b/src/dawn_native/ExternalTexture.cpp
index 76d06c5..6b1cee8 100644
--- a/src/dawn_native/ExternalTexture.cpp
+++ b/src/dawn_native/ExternalTexture.cpp
@@ -85,7 +85,7 @@
 
     ExternalTextureBase::ExternalTextureBase(DeviceBase* device,
                                              const ExternalTextureDescriptor* descriptor)
-        : ObjectBase(device) {
+        : ObjectBase(device), mState(ExternalTextureState::Alive) {
         textureViews[0] = descriptor->plane0;
     }
 
@@ -93,15 +93,24 @@
         : ObjectBase(device, tag) {
     }
 
-    std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat> ExternalTextureBase::GetTextureViews()
-        const {
+    const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>&
+    ExternalTextureBase::GetTextureViews() const {
         return textureViews;
     }
 
+    MaybeError ExternalTextureBase::ValidateCanUseInSubmitNow() const {
+        ASSERT(!IsError());
+        if (mState == ExternalTextureState::Destroyed) {
+            return DAWN_VALIDATION_ERROR("Destroyed external texture used in a submit");
+        }
+        return {};
+    }
+
     void ExternalTextureBase::APIDestroy() {
         if (GetDevice()->ConsumedError(GetDevice()->ValidateObject(this))) {
             return;
         }
+        mState = ExternalTextureState::Destroyed;
         ASSERT(!IsError());
     }
 
diff --git a/src/dawn_native/ExternalTexture.h b/src/dawn_native/ExternalTexture.h
index 7bc81e2..6dc5402 100644
--- a/src/dawn_native/ExternalTexture.h
+++ b/src/dawn_native/ExternalTexture.h
@@ -31,11 +31,14 @@
 
     class ExternalTextureBase : public ObjectBase {
       public:
+        enum class ExternalTextureState { Alive, Destroyed };
         static ResultOrError<Ref<ExternalTextureBase>> Create(
             DeviceBase* device,
             const ExternalTextureDescriptor* descriptor);
 
-        std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat> GetTextureViews() const;
+        const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>& GetTextureViews() const;
+
+        MaybeError ValidateCanUseInSubmitNow() const;
 
         static ExternalTextureBase* MakeError(DeviceBase* device);
 
@@ -45,6 +48,7 @@
         ExternalTextureBase(DeviceBase* device, const ExternalTextureDescriptor* descriptor);
         ExternalTextureBase(DeviceBase* device, ObjectBase::ErrorTag tag);
         std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat> textureViews;
+        ExternalTextureState mState;
     };
 }  // namespace dawn_native
 
diff --git a/src/dawn_native/Forward.h b/src/dawn_native/Forward.h
index 66e07e4..9ee495d 100644
--- a/src/dawn_native/Forward.h
+++ b/src/dawn_native/Forward.h
@@ -30,6 +30,7 @@
     class CommandBufferBase;
     class CommandEncoder;
     class ComputePassEncoder;
+    class ExternalTextureBase;
     class Fence;
     class InstanceBase;
     class PipelineBase;
diff --git a/src/dawn_native/PassResourceUsage.h b/src/dawn_native/PassResourceUsage.h
index 3168c39..555eb0f 100644
--- a/src/dawn_native/PassResourceUsage.h
+++ b/src/dawn_native/PassResourceUsage.h
@@ -44,6 +44,8 @@
 
         std::vector<TextureBase*> textures;
         std::vector<TextureSubresourceUsage> textureUsages;
+
+        std::vector<ExternalTextureBase*> externalTextures;
     };
 
     // Contains all the resource usage data for a compute pass.
@@ -64,6 +66,7 @@
         // All the resources referenced by this compute pass for validation in Queue::Submit.
         std::set<BufferBase*> referencedBuffers;
         std::set<TextureBase*> referencedTextures;
+        std::set<ExternalTextureBase*> referencedExternalTextures;
     };
 
     // Contains all the resource usage data for a render pass.
diff --git a/src/dawn_native/PassResourceUsageTracker.cpp b/src/dawn_native/PassResourceUsageTracker.cpp
index 75db456..18fd319 100644
--- a/src/dawn_native/PassResourceUsageTracker.cpp
+++ b/src/dawn_native/PassResourceUsageTracker.cpp
@@ -17,6 +17,7 @@
 #include "dawn_native/BindGroup.h"
 #include "dawn_native/Buffer.h"
 #include "dawn_native/EnumMaskIterator.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/Format.h"
 #include "dawn_native/QuerySet.h"
 #include "dawn_native/Texture.h"
@@ -109,6 +110,23 @@
                     break;
                 }
 
+                case BindingInfoType::ExternalTexture: {
+                    ExternalTextureBase* externalTexture =
+                        group->GetBindingAsExternalTexture(bindingIndex);
+
+                    const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>& textureViews =
+                        externalTexture->GetTextureViews();
+
+                    // Only single-plane formats are supported right now, so assert only one
+                    // view exists.
+                    ASSERT(textureViews[1].Get() == nullptr);
+                    ASSERT(textureViews[2].Get() == nullptr);
+
+                    mExternalTextureUsages.insert(externalTexture);
+                    TextureViewUsedAs(textureViews[0].Get(), wgpu::TextureUsage::Sampled);
+                    break;
+                }
+
                 case BindingInfoType::Sampler:
                     break;
             }
@@ -132,8 +150,13 @@
             result.textureUsages.push_back(std::move(it.second));
         }
 
+        for (auto& it : mExternalTextureUsages) {
+            result.externalTextures.push_back(it);
+        }
+
         mBufferUsages.clear();
         mTextureUsages.clear();
+        mExternalTextureUsages.clear();
 
         return result;
     }
@@ -162,6 +185,22 @@
                     break;
                 }
 
+                case BindingInfoType::ExternalTexture: {
+                    ExternalTextureBase* externalTexture =
+                        group->GetBindingAsExternalTexture(index);
+                    const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>& textureViews =
+                        externalTexture->GetTextureViews();
+
+                    // Only single-plane formats are supported right now, so assert only one
+                    // view exists.
+                    ASSERT(textureViews[1].Get() == nullptr);
+                    ASSERT(textureViews[2].Get() == nullptr);
+
+                    mUsage.referencedExternalTextures.insert(externalTexture);
+                    mUsage.referencedTextures.insert(textureViews[0].Get()->GetTexture());
+                    break;
+                }
+
                 case BindingInfoType::StorageTexture:
                 case BindingInfoType::Sampler:
                     break;
diff --git a/src/dawn_native/PassResourceUsageTracker.h b/src/dawn_native/PassResourceUsageTracker.h
index 56b5854..d4de8fa 100644
--- a/src/dawn_native/PassResourceUsageTracker.h
+++ b/src/dawn_native/PassResourceUsageTracker.h
@@ -25,6 +25,7 @@
 
     class BindGroupBase;
     class BufferBase;
+    class ExternalTextureBase;
     class QuerySetBase;
     class TextureBase;
 
@@ -46,6 +47,7 @@
       private:
         std::map<BufferBase*, wgpu::BufferUsage> mBufferUsages;
         std::map<TextureBase*, TextureSubresourceUsage> mTextureUsages;
+        std::set<ExternalTextureBase*> mExternalTextureUsages;
     };
 
     // Helper class to build ComputePassResourceUsages
diff --git a/src/dawn_native/PipelineLayout.cpp b/src/dawn_native/PipelineLayout.cpp
index bc659ee..52e2008 100644
--- a/src/dawn_native/PipelineLayout.cpp
+++ b/src/dawn_native/PipelineLayout.cpp
@@ -147,6 +147,15 @@
                 case BindingInfoType::StorageTexture:
                     entry.storageTexture = shaderBinding.storageTexture;
                     break;
+                case BindingInfoType::ExternalTexture:
+                    // TODO(dawn:728) On backend configurations that use SPIRV-Cross to reflect
+                    // shader info - the shader must have been already transformed prior to
+                    // reflecting the shader. During transformation, all instances of
+                    // texture_external are changed to texture_2d<f32>. This means that when
+                    // extracting shader info, external textures will be seen as sampled 2d
+                    // textures. In the future when Dawn no longer uses SPIRV-Cross, we should
+                    // handle external textures here.
+                    break;
             }
             return entry;
         };
diff --git a/src/dawn_native/Queue.cpp b/src/dawn_native/Queue.cpp
index fc3d9b3..05e76a0 100644
--- a/src/dawn_native/Queue.cpp
+++ b/src/dawn_native/Queue.cpp
@@ -23,6 +23,7 @@
 #include "dawn_native/CopyTextureForBrowserHelper.h"
 #include "dawn_native/Device.h"
 #include "dawn_native/DynamicUploader.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/QuerySet.h"
 #include "dawn_native/RenderPassEncoder.h"
 #include "dawn_native/RenderPipeline.h"
@@ -382,6 +383,10 @@
                 for (const TextureBase* texture : scope.textures) {
                     DAWN_TRY(texture->ValidateCanUseInSubmitNow());
                 }
+
+                for (const ExternalTextureBase* externalTexture : scope.externalTextures) {
+                    DAWN_TRY(externalTexture->ValidateCanUseInSubmitNow());
+                }
             }
 
             for (const ComputePassResourceUsage& pass : usages.computePasses) {
@@ -391,6 +396,9 @@
                 for (const TextureBase* texture : pass.referencedTextures) {
                     DAWN_TRY(texture->ValidateCanUseInSubmitNow());
                 }
+                for (const ExternalTextureBase* externalTexture : pass.referencedExternalTextures) {
+                    DAWN_TRY(externalTexture->ValidateCanUseInSubmitNow());
+                }
             }
 
             for (const BufferBase* buffer : usages.topLevelBuffers) {
diff --git a/src/dawn_native/ShaderModule.cpp b/src/dawn_native/ShaderModule.cpp
index b191636..4bff6d6 100644
--- a/src/dawn_native/ShaderModule.cpp
+++ b/src/dawn_native/ShaderModule.cpp
@@ -159,6 +159,8 @@
                 case tint::inspector::ResourceBinding::ResourceType::kReadOnlyStorageTexture:
                 case tint::inspector::ResourceBinding::ResourceType::kWriteOnlyStorageTexture:
                     return BindingInfoType::StorageTexture;
+                case tint::inspector::ResourceBinding::ResourceType::kExternalTexture:
+                    return BindingInfoType::ExternalTexture;
 
                 default:
                     UNREACHABLE();
@@ -503,9 +505,19 @@
                 const BindingInfo& layoutInfo = layout->GetBindingInfo(bindingIndex);
 
                 if (layoutInfo.bindingType != shaderInfo.bindingType) {
-                    return DAWN_VALIDATION_ERROR(
-                        "The binding type of the bind group layout entry conflicts " +
-                        GetShaderDeclarationString(group, bindingNumber));
+                    // TODO(dawn:728) On backend configurations that use SPIRV-Cross to reflect
+                    // shader info - the shader must have been already transformed prior to
+                    // reflecting the shader. During transformation, all instances of
+                    // texture_external are changed to texture_2d<f32>. This means that when
+                    // extracting shader info, external textures will be seen as sampled 2d
+                    // textures. In the future when Dawn no longer uses SPIRV-Cross, the
+                    // if-statement below should be removed.
+                    if (layoutInfo.bindingType != BindingInfoType::ExternalTexture ||
+                        shaderInfo.bindingType != BindingInfoType::Texture) {
+                        return DAWN_VALIDATION_ERROR(
+                            "The binding type of the bind group layout entry conflicts " +
+                            GetShaderDeclarationString(group, bindingNumber));
+                    }
                 }
 
                 if ((layoutInfo.visibility & StageBit(entryPoint.stage)) == 0) {
@@ -567,6 +579,17 @@
                         break;
                     }
 
+                    case BindingInfoType::ExternalTexture: {
+                        // TODO(dawn:728) On backend configurations that use SPIRV-Cross to reflect
+                        // shader info - the shader must have been already transformed prior to
+                        // reflecting the shader. During transformation, all instances of
+                        // texture_external are changed to texture_2d<f32>. This means that when
+                        // extracting shader info, external textures will be seen as sampled 2d
+                        // textures. In the future when Dawn no longer uses SPIRV-Cross, we should
+                        // handle external textures here.
+                        break;
+                    }
+
                     case BindingInfoType::Buffer: {
                         // Binding mismatch between shader and bind group is invalid. For example, a
                         // writable binding in the shader with a readonly storage buffer in the bind
@@ -755,6 +778,11 @@
                         }
                         case BindingInfoType::Sampler: {
                             info->sampler.type = wgpu::SamplerBindingType::Filtering;
+                            break;
+                        }
+                        case BindingInfoType::ExternalTexture: {
+                            return DAWN_VALIDATION_ERROR("External textures are not supported.");
+                            break;
                         }
                     }
                 }
@@ -994,6 +1022,8 @@
                                 TintTextureDimensionToTextureViewDimension(resource.dim);
 
                             break;
+                        case BindingInfoType::ExternalTexture:
+                            break;
                         default:
                             return DAWN_VALIDATION_ERROR("Unknown binding type in Shader");
                     }
diff --git a/src/dawn_native/d3d12/BindGroupD3D12.cpp b/src/dawn_native/d3d12/BindGroupD3D12.cpp
index 75eb734..a1b095b 100644
--- a/src/dawn_native/d3d12/BindGroupD3D12.cpp
+++ b/src/dawn_native/d3d12/BindGroupD3D12.cpp
@@ -15,6 +15,7 @@
 #include "dawn_native/d3d12/BindGroupD3D12.h"
 
 #include "common/BitSetIterator.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/d3d12/BindGroupLayoutD3D12.h"
 #include "dawn_native/d3d12/BufferD3D12.h"
 #include "dawn_native/d3d12/DeviceD3D12.h"
@@ -185,6 +186,26 @@
                     break;
                 }
 
+                case BindingInfoType::ExternalTexture: {
+                    const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>& views =
+                        GetBindingAsExternalTexture(bindingIndex)->GetTextureViews();
+
+                    // Only single-plane formats are supported right now, so assert only one view
+                    // exists.
+                    ASSERT(views[1].Get() == nullptr);
+                    ASSERT(views[2].Get() == nullptr);
+
+                    auto& srv = ToBackend(views[0])->GetSRVDescriptor();
+
+                    ID3D12Resource* resource =
+                        ToBackend(views[0]->GetTexture())->GetD3D12Resource();
+
+                    d3d12Device->CreateShaderResourceView(
+                        resource, &srv,
+                        viewAllocation.OffsetFrom(viewSizeIncrement, bindingOffsets[bindingIndex]));
+                    break;
+                }
+
                 case BindingInfoType::Sampler: {
                     // No-op as samplers will be later initialized by CreateSamplers().
                     break;
diff --git a/src/dawn_native/d3d12/BindGroupLayoutD3D12.cpp b/src/dawn_native/d3d12/BindGroupLayoutD3D12.cpp
index c515166..8557b44 100644
--- a/src/dawn_native/d3d12/BindGroupLayoutD3D12.cpp
+++ b/src/dawn_native/d3d12/BindGroupLayoutD3D12.cpp
@@ -41,6 +41,7 @@
                     return BindGroupLayout::DescriptorType::Sampler;
 
                 case BindingInfoType::Texture:
+                case BindingInfoType::ExternalTexture:
                     return BindGroupLayout::DescriptorType::SRV;
 
                 case BindingInfoType::StorageTexture:
@@ -77,6 +78,8 @@
             // dynamic resources in calculating the size of the descriptor heap.
             ASSERT(!bindingInfo.buffer.hasDynamicOffset);
 
+            // TODO(dawn:728) In the future, special handling will be needed for external textures
+            // here because they encompass multiple views.
             DescriptorType descriptorType = WGPUBindingInfoToDescriptorType(bindingInfo);
             mBindingOffsets[bindingIndex] = mDescriptorCounts[descriptorType]++;
         }
@@ -135,6 +138,8 @@
             }
 
             // TODO(shaobo.yan@intel.com): Implement dynamic buffer offset.
+            // TODO(dawn:728) In the future, special handling will be needed here for external
+            // textures because they encompass multiple views.
             DescriptorType descriptorType = WGPUBindingInfoToDescriptorType(bindingInfo);
             mBindingOffsets[bindingIndex] += descriptorOffsets[descriptorType];
         }
diff --git a/src/dawn_native/metal/CommandBufferMTL.mm b/src/dawn_native/metal/CommandBufferMTL.mm
index 04cb72d..e7da839 100644
--- a/src/dawn_native/metal/CommandBufferMTL.mm
+++ b/src/dawn_native/metal/CommandBufferMTL.mm
@@ -17,6 +17,7 @@
 #include "dawn_native/BindGroupTracker.h"
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/RenderBundle.h"
 #include "dawn_native/metal/BindGroupMTL.h"
 #include "dawn_native/metal/BufferMTL.h"
@@ -467,6 +468,32 @@
                             }
                             break;
                         }
+
+                        case BindingInfoType::ExternalTexture: {
+                            const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>& views =
+                                group->GetBindingAsExternalTexture(bindingIndex)->GetTextureViews();
+
+                            // Only single-plane formats are supported right now, so assert only one
+                            // view exists.
+                            ASSERT(views[1].Get() == nullptr);
+                            ASSERT(views[2].Get() == nullptr);
+
+                            TextureView* textureView = ToBackend(views[0].Get());
+
+                            if (hasVertStage) {
+                                [render setVertexTexture:textureView->GetMTLTexture()
+                                                 atIndex:vertIndex];
+                            }
+                            if (hasFragStage) {
+                                [render setFragmentTexture:textureView->GetMTLTexture()
+                                                   atIndex:fragIndex];
+                            }
+                            if (hasComputeStage) {
+                                [compute setTexture:textureView->GetMTLTexture()
+                                            atIndex:computeIndex];
+                            }
+                            break;
+                        }
                     }
                 }
             }
diff --git a/src/dawn_native/metal/PipelineLayoutMTL.mm b/src/dawn_native/metal/PipelineLayoutMTL.mm
index 34ddc44..4faf5db 100644
--- a/src/dawn_native/metal/PipelineLayoutMTL.mm
+++ b/src/dawn_native/metal/PipelineLayoutMTL.mm
@@ -58,6 +58,7 @@
 
                         case BindingInfoType::Texture:
                         case BindingInfoType::StorageTexture:
+                        case BindingInfoType::ExternalTexture:
                             mIndexInfo[stage][group][bindingIndex] = textureIndex;
                             textureIndex++;
                             break;
diff --git a/src/dawn_native/opengl/CommandBufferGL.cpp b/src/dawn_native/opengl/CommandBufferGL.cpp
index 1fbbbba..0d8599f 100644
--- a/src/dawn_native/opengl/CommandBufferGL.cpp
+++ b/src/dawn_native/opengl/CommandBufferGL.cpp
@@ -19,6 +19,7 @@
 #include "dawn_native/BindGroupTracker.h"
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/RenderBundle.h"
 #include "dawn_native/opengl/BufferGL.h"
 #include "dawn_native/opengl/ComputePipelineGL.h"
@@ -362,6 +363,29 @@
                                                 texture->GetGLFormat().internalFormat);
                             break;
                         }
+
+                        case BindingInfoType::ExternalTexture: {
+                            const std::array<Ref<TextureViewBase>, kMaxPlanesPerFormat>&
+                                textureViews = mBindGroups[index]
+                                                   ->GetBindingAsExternalTexture(bindingIndex)
+                                                   ->GetTextureViews();
+
+                            // Only single-plane formats are supported right now, so assert only one
+                            // view exists.
+                            ASSERT(textureViews[1].Get() == nullptr);
+                            ASSERT(textureViews[2].Get() == nullptr);
+
+                            TextureView* view = ToBackend(textureViews[0].Get());
+                            GLuint handle = view->GetHandle();
+                            GLenum target = view->GetGLTarget();
+                            GLuint viewIndex = indices[bindingIndex];
+
+                            for (auto unit : mPipeline->GetTextureUnitsForTextureView(viewIndex)) {
+                                gl.ActiveTexture(GL_TEXTURE0 + unit);
+                                gl.BindTexture(target, handle);
+                            }
+                            break;
+                        }
                     }
                 }
             }
diff --git a/src/dawn_native/opengl/PipelineLayoutGL.cpp b/src/dawn_native/opengl/PipelineLayoutGL.cpp
index 088eaf3..ef2cf7c 100644
--- a/src/dawn_native/opengl/PipelineLayoutGL.cpp
+++ b/src/dawn_native/opengl/PipelineLayoutGL.cpp
@@ -58,6 +58,7 @@
                         break;
 
                     case BindingInfoType::Texture:
+                    case BindingInfoType::ExternalTexture:
                         mIndexInfo[group][bindingIndex] = sampledTextureIndex;
                         sampledTextureIndex++;
                         break;
diff --git a/src/dawn_native/vulkan/BindGroupLayoutVk.cpp b/src/dawn_native/vulkan/BindGroupLayoutVk.cpp
index 78f7a7a..6ee1d49 100644
--- a/src/dawn_native/vulkan/BindGroupLayoutVk.cpp
+++ b/src/dawn_native/vulkan/BindGroupLayoutVk.cpp
@@ -67,6 +67,7 @@
             case BindingInfoType::Sampler:
                 return VK_DESCRIPTOR_TYPE_SAMPLER;
             case BindingInfoType::Texture:
+            case BindingInfoType::ExternalTexture:
                 return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
             case BindingInfoType::StorageTexture:
                 return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
@@ -99,6 +100,8 @@
             VkDescriptorSetLayoutBinding vkBinding;
             vkBinding.binding = useBindingIndex ? static_cast<uint32_t>(bindingIndex)
                                                 : static_cast<uint32_t>(bindingNumber);
+            // TODO(dawn:728) In the future, special handling will be needed for external textures
+            // here because they encompass multiple views.
             vkBinding.descriptorType = VulkanDescriptorType(bindingInfo);
             vkBinding.descriptorCount = 1;
             vkBinding.stageFlags = VulkanShaderStageFlags(bindingInfo.visibility);
@@ -123,6 +126,8 @@
         std::map<VkDescriptorType, uint32_t> descriptorCountPerType;
 
         for (BindingIndex bindingIndex{0}; bindingIndex < GetBindingCount(); ++bindingIndex) {
+            // TODO(dawn:728) In the future, special handling will be needed for external textures
+            // here because they encompass multiple views.
             VkDescriptorType vulkanType = VulkanDescriptorType(GetBindingInfo(bindingIndex));
 
             // map::operator[] will return 0 if the key doesn't exist.
diff --git a/src/dawn_native/vulkan/BindGroupVk.cpp b/src/dawn_native/vulkan/BindGroupVk.cpp
index b2334d1..251e4ff 100644
--- a/src/dawn_native/vulkan/BindGroupVk.cpp
+++ b/src/dawn_native/vulkan/BindGroupVk.cpp
@@ -16,6 +16,7 @@
 
 #include "common/BitSetIterator.h"
 #include "common/ityp_stack_vec.h"
+#include "dawn_native/ExternalTexture.h"
 #include "dawn_native/vulkan/BindGroupLayoutVk.h"
 #include "dawn_native/vulkan/BufferVk.h"
 #include "dawn_native/vulkan/DeviceVk.h"
@@ -113,6 +114,25 @@
                     write.pImageInfo = &writeImageInfo[numWrites];
                     break;
                 }
+
+                case BindingInfoType::ExternalTexture: {
+                    const std::array<Ref<dawn_native::TextureViewBase>, kMaxPlanesPerFormat>&
+                        textureViews = GetBindingAsExternalTexture(bindingIndex)->GetTextureViews();
+
+                    // Only single-plane formats are supported right now, so ensure only one view
+                    // exists.
+                    ASSERT(textureViews[1].Get() == nullptr);
+                    ASSERT(textureViews[2].Get() == nullptr);
+
+                    TextureView* view = ToBackend(textureViews[0].Get());
+
+                    writeImageInfo[numWrites].imageView = view->GetHandle();
+                    writeImageInfo[numWrites].imageLayout = VulkanImageLayout(
+                        ToBackend(view->GetTexture()), wgpu::TextureUsage::Sampled);
+
+                    write.pImageInfo = &writeImageInfo[numWrites];
+                    break;
+                }
             }
 
             numWrites++;
diff --git a/src/tests/end2end/BindGroupTests.cpp b/src/tests/end2end/BindGroupTests.cpp
index 8408609..155bd25 100644
--- a/src/tests/end2end/BindGroupTests.cpp
+++ b/src/tests/end2end/BindGroupTests.cpp
@@ -1183,12 +1183,12 @@
     for (uint32_t i = 0; i < kMaxSampledTexturesPerShaderStage; ++i) {
         wgpu::Texture texture = CreateTextureWithRedData(
             wgpu::TextureFormat::R8Unorm, expectedValue, wgpu::TextureUsage::Sampled);
-        bgEntries.push_back({binding, nullptr, 0, 0, nullptr, texture.CreateView()});
+        bgEntries.push_back({nullptr, binding, nullptr, 0, 0, nullptr, texture.CreateView()});
 
         interface << "[[group(0), binding(" << binding++ << ")]] "
                   << "var tex" << i << " : texture_2d<f32>;\n";
 
-        bgEntries.push_back({binding, nullptr, 0, 0, device.CreateSampler(), nullptr});
+        bgEntries.push_back({nullptr, binding, nullptr, 0, 0, device.CreateSampler(), nullptr});
 
         interface << "[[group(0), binding(" << binding++ << ")]]"
                   << "var samp" << i << " : sampler;\n";
@@ -1202,7 +1202,7 @@
     for (uint32_t i = 0; i < kMaxStorageTexturesPerShaderStage; ++i) {
         wgpu::Texture texture = CreateTextureWithRedData(
             wgpu::TextureFormat::R32Uint, expectedValue, wgpu::TextureUsage::Storage);
-        bgEntries.push_back({binding, nullptr, 0, 0, nullptr, texture.CreateView()});
+        bgEntries.push_back({nullptr, binding, nullptr, 0, 0, nullptr, texture.CreateView()});
 
         interface << "[[group(0), binding(" << binding++ << ")]] "
                   << "var image" << i << " : [[access(read)]] texture_storage_2d<r32uint>;\n";
@@ -1216,7 +1216,7 @@
     for (uint32_t i = 0; i < kMaxUniformBuffersPerShaderStage; ++i) {
         wgpu::Buffer buffer = utils::CreateBufferFromData<uint32_t>(
             device, wgpu::BufferUsage::Uniform, {expectedValue, 0, 0, 0});
-        bgEntries.push_back({binding, buffer, 0, 4 * sizeof(uint32_t), nullptr, nullptr});
+        bgEntries.push_back({nullptr, binding, buffer, 0, 4 * sizeof(uint32_t), nullptr, nullptr});
 
         interface << "[[block]] struct UniformBuffer" << i << R"({
                 value : u32;
@@ -1233,7 +1233,7 @@
     for (uint32_t i = 0; i < kMaxStorageBuffersPerShaderStage - 1; ++i) {
         wgpu::Buffer buffer = utils::CreateBufferFromData<uint32_t>(
             device, wgpu::BufferUsage::Storage, {expectedValue});
-        bgEntries.push_back({binding, buffer, 0, sizeof(uint32_t), nullptr, nullptr});
+        bgEntries.push_back({nullptr, binding, buffer, 0, sizeof(uint32_t), nullptr, nullptr});
 
         interface << "[[block]] struct ReadOnlyStorageBuffer" << i << R"({
                 value : u32;
@@ -1250,7 +1250,7 @@
 
     wgpu::Buffer result = utils::CreateBufferFromData<uint32_t>(
         device, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc, {0});
-    bgEntries.push_back({binding, result, 0, sizeof(uint32_t), nullptr, nullptr});
+    bgEntries.push_back({nullptr, binding, result, 0, sizeof(uint32_t), nullptr, nullptr});
 
     interface << R"([[block]] struct ReadWriteStorageBuffer{
             value : u32;
diff --git a/src/tests/end2end/ExternalTextureTests.cpp b/src/tests/end2end/ExternalTextureTests.cpp
index 3060815..99fa9bf 100644
--- a/src/tests/end2end/ExternalTextureTests.cpp
+++ b/src/tests/end2end/ExternalTextureTests.cpp
@@ -13,6 +13,8 @@
 // limitations under the License.
 
 #include "tests/DawnTest.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/WGPUHelpers.h"
 
 namespace {
 
@@ -43,8 +45,6 @@
 }  // anonymous namespace
 
 TEST_P(ExternalTextureTests, CreateExternalTextureSuccess) {
-    DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-
     wgpu::Texture texture = Create2DTexture(device, kWidth, kHeight, kFormat, kSampledUsage);
 
     // Create a texture view for the external texture
@@ -61,6 +61,94 @@
     ASSERT_NE(externalTexture.Get(), nullptr);
 }
 
+TEST_P(ExternalTextureTests, SampleExternalTexture) {
+    wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
+        [[stage(vertex)]] fn main([[builtin(vertex_idx)]] VertexIndex : u32) -> [[builtin(position)]] vec4<f32> {
+
+            let positions : array<vec4<f32>, 3> = array<vec4<f32>, 3>(
+                vec4<f32>(-1.0, 1.0, 0.0, 1.0),
+                vec4<f32>(-1.0, -1.0, 0.0, 1.0),
+                vec4<f32>(1.0, 1.0, 0.0, 1.0)
+            );
+
+            return positions[VertexIndex];
+        })");
+
+    const wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
+        [[group(0), binding(0)]] var s : sampler;
+        [[group(0), binding(1)]] var t : texture_external;
+
+        [[stage(fragment)]] fn main([[builtin(position)]] FragCoord : vec4<f32>)
+                                 -> [[location(0)]] vec4<f32> {
+            return textureSampleLevel(t, s, FragCoord.xy / vec2<f32>(4.0, 4.0));
+        })");
+
+    wgpu::Texture sampledTexture =
+        Create2DTexture(device, kWidth, kHeight, kFormat,
+                        wgpu::TextureUsage::Sampled | wgpu::TextureUsage::RenderAttachment);
+    wgpu::Texture renderTexture =
+        Create2DTexture(device, kWidth, kHeight, kFormat,
+                        wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment);
+
+    // Create a texture view for the external texture
+    wgpu::TextureView externalView = sampledTexture.CreateView();
+
+    // Initialize texture with green to ensure it is sampled from later.
+    {
+        utils::ComboRenderPassDescriptor renderPass({externalView}, nullptr);
+        renderPass.cColorAttachments[0].clearColor = {0.0f, 1.0f, 0.0f, 1.0f};
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+        pass.EndPass();
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    // Create an ExternalTextureDescriptor from the texture view
+    wgpu::ExternalTextureDescriptor externalDesc;
+    externalDesc.plane0 = externalView;
+    externalDesc.format = kFormat;
+
+    // Import the external texture
+    wgpu::ExternalTexture externalTexture = device.CreateExternalTexture(&externalDesc);
+
+    // Create a sampler and bind group
+    wgpu::Sampler sampler = device.CreateSampler();
+
+    wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
+        device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering},
+                 {1, wgpu::ShaderStage::Fragment, &utils::kExternalTextureBindingLayout}});
+    wgpu::BindGroup bindGroup =
+        utils::MakeBindGroup(device, bgl, {{0, sampler}, {1, externalTexture}});
+
+    // Pipeline Creation
+    utils::ComboRenderPipelineDescriptor descriptor;
+    descriptor.layout = utils::MakeBasicPipelineLayout(device, &bgl);
+    descriptor.vertex.module = vsModule;
+    descriptor.cFragment.module = fsModule;
+    descriptor.cTargets[0].format = kFormat;
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&descriptor);
+
+    // Run the shader, which should sample from the external texture and draw a triangle into the
+    // upper left corner of the render texture.
+    wgpu::TextureView renderView = renderTexture.CreateView();
+    utils::ComboRenderPassDescriptor renderPass({renderView}, nullptr);
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+    {
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bindGroup);
+        pass.Draw(3);
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kGreen, renderTexture, 0, 0);
+}
+
 DAWN_INSTANTIATE_TEST(ExternalTextureTests,
                       D3D12Backend(),
                       MetalBackend(),
diff --git a/src/tests/unittests/validation/BindGroupValidationTests.cpp b/src/tests/unittests/validation/BindGroupValidationTests.cpp
index 601f421..33c656f 100644
--- a/src/tests/unittests/validation/BindGroupValidationTests.cpp
+++ b/src/tests/unittests/validation/BindGroupValidationTests.cpp
@@ -53,9 +53,14 @@
         }
         { mSampler = device.CreateSampler(); }
         {
-            mSampledTexture =
-                CreateTexture(wgpu::TextureUsage::Sampled, wgpu::TextureFormat::RGBA8Unorm, 1);
+            mSampledTexture = CreateTexture(wgpu::TextureUsage::Sampled, kDefaultTextureFormat, 1);
             mSampledTextureView = mSampledTexture.CreateView();
+
+            wgpu::ExternalTextureDescriptor externalTextureDesc;
+            externalTextureDesc.format = kDefaultTextureFormat;
+            externalTextureDesc.plane0 = mSampledTextureView;
+            mExternalTexture = device.CreateExternalTexture(&externalTextureDesc);
+            mExternalTextureBindingEntry.externalTexture = mExternalTexture;
         }
     }
 
@@ -65,6 +70,12 @@
     wgpu::Sampler mSampler;
     wgpu::Texture mSampledTexture;
     wgpu::TextureView mSampledTextureView;
+    wgpu::ExternalTextureBindingEntry mExternalTextureBindingEntry;
+
+    static constexpr wgpu::TextureFormat kDefaultTextureFormat = wgpu::TextureFormat::RGBA8Unorm;
+
+  private:
+    wgpu::ExternalTexture mExternalTexture;
 };
 
 // Test the validation of BindGroupDescriptor::nextInChain
@@ -159,6 +170,11 @@
     ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
     binding.buffer = nullptr;
 
+    // Setting the external texture view as well is an error
+    binding.nextInChain = &mExternalTextureBindingEntry;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    binding.nextInChain = nullptr;
+
     // Setting the sampler to an error sampler is an error.
     {
         wgpu::SamplerDescriptor samplerDesc;
@@ -208,10 +224,15 @@
     ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
     binding.buffer = nullptr;
 
+    // Setting the external texture view as well is an error
+    binding.nextInChain = &mExternalTextureBindingEntry;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    binding.nextInChain = nullptr;
+
     // Setting the texture view to an error texture view is an error.
     {
         wgpu::TextureViewDescriptor viewDesc;
-        viewDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+        viewDesc.format = kDefaultTextureFormat;
         viewDesc.dimension = wgpu::TextureViewDimension::e2D;
         viewDesc.baseMipLevel = 0;
         viewDesc.mipLevelCount = 0;
@@ -262,6 +283,11 @@
     ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
     binding.sampler = nullptr;
 
+    // Setting the external texture view as well is an error
+    binding.nextInChain = &mExternalTextureBindingEntry;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    binding.nextInChain = nullptr;
+
     // Setting the buffer to an error buffer is an error.
     {
         wgpu::BufferDescriptor bufferDesc;
@@ -277,6 +303,91 @@
     }
 }
 
+// Check that an external texture binding must contain exactly an external texture
+TEST_F(BindGroupValidationTest, ExternalTextureBindingType) {
+    // Create an external texture
+    wgpu::Texture texture = CreateTexture(wgpu::TextureUsage::Sampled, kDefaultTextureFormat, 1);
+    wgpu::ExternalTextureDescriptor externalDesc;
+    externalDesc.plane0 = texture.CreateView();
+    externalDesc.format = kDefaultTextureFormat;
+    wgpu::ExternalTexture externalTexture = device.CreateExternalTexture(&externalDesc);
+
+    // Create a bind group layout for a single external texture
+    wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
+        device, {{0, wgpu::ShaderStage::Fragment, &utils::kExternalTextureBindingLayout}});
+
+    wgpu::BindGroupEntry binding;
+    binding.binding = 0;
+    binding.sampler = nullptr;
+    binding.textureView = nullptr;
+    binding.buffer = nullptr;
+    binding.offset = 0;
+    binding.size = 0;
+
+    wgpu::BindGroupDescriptor descriptor;
+    descriptor.layout = layout;
+    descriptor.entryCount = 1;
+    descriptor.entries = &binding;
+
+    // Not setting anything fails
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+
+    // Control case: setting just the external texture works
+    wgpu::ExternalTextureBindingEntry externalBindingEntry;
+    externalBindingEntry.externalTexture = externalTexture;
+    binding.nextInChain = &externalBindingEntry;
+    device.CreateBindGroup(&descriptor);
+
+    // Setting the texture view as well is an error
+    binding.textureView = mSampledTextureView;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    binding.textureView = nullptr;
+
+    // Setting the sampler as well is an error
+    binding.sampler = mSampler;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    binding.sampler = nullptr;
+
+    // Setting the buffer as well is an error
+    binding.buffer = mUBO;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    binding.buffer = nullptr;
+
+    // Setting the external texture to an error external texture is an error.
+    {
+        wgpu::ExternalTextureDescriptor errorExternalDesciptor;
+        errorExternalDesciptor.plane0 = texture.CreateView();
+        errorExternalDesciptor.format = wgpu::TextureFormat::R8Uint;
+
+        wgpu::ExternalTexture errorExternalTexture;
+        ASSERT_DEVICE_ERROR(errorExternalTexture =
+                                device.CreateExternalTexture(&errorExternalDesciptor));
+
+        wgpu::ExternalTextureBindingEntry errorExternalBindingEntry;
+        errorExternalBindingEntry.externalTexture = errorExternalTexture;
+        binding.nextInChain = &errorExternalBindingEntry;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+        binding.nextInChain = nullptr;
+    }
+
+    // Setting an external texture with another external texture chained is an error.
+    {
+        wgpu::ExternalTexture externalTexture2 = device.CreateExternalTexture(&externalDesc);
+        wgpu::ExternalTextureBindingEntry externalBindingEntry2;
+        externalBindingEntry2.externalTexture = externalTexture2;
+        externalBindingEntry.nextInChain = &externalBindingEntry2;
+
+        ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    }
+
+    // Chaining a struct that isn't an external texture binding entry is an error.
+    {
+        wgpu::ExternalTextureBindingLayout externalBindingLayout;
+        binding.nextInChain = &externalBindingLayout;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
+    }
+}
+
 // Check that a texture must have the correct usage
 TEST_F(BindGroupValidationTest, TextureUsage) {
     wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
@@ -746,7 +857,7 @@
         wgpu::BindGroupLayoutEntry otherEntry;
     };
 
-    std::array<TestInfo, 7> kTestInfos = {
+    std::array<TestInfo, 8> kTestInfos = {
         TestInfo{kMaxSampledTexturesPerShaderStage, BGLEntryType(wgpu::TextureSampleType::Float),
                  BGLEntryType(wgpu::BufferBindingType::Uniform)},
         TestInfo{kMaxSamplersPerShaderStage, BGLEntryType(wgpu::SamplerBindingType::Filtering),
@@ -765,7 +876,12 @@
             BGLEntryType(wgpu::BufferBindingType::Uniform)},
         TestInfo{kMaxUniformBuffersPerShaderStage, BGLEntryType(wgpu::BufferBindingType::Uniform),
                  BGLEntryType(wgpu::TextureSampleType::Float)},
-    };
+        // External textures use multiple bindings (3 sampled textures, 1 sampler, 1 uniform buffer)
+        // that count towards the per stage binding limits. The number of external textures are
+        // currently restricted by the maximum number of sampled textures.
+        TestInfo{kMaxSampledTexturesPerShaderStage / kSampledTexturesPerExternalTexture,
+                 BGLEntryType(&utils::kExternalTextureBindingLayout),
+                 BGLEntryType(wgpu::BufferBindingType::Uniform)}};
 
     for (TestInfo info : kTestInfos) {
         wgpu::BindGroupLayout bgl[2];
@@ -829,6 +945,98 @@
     }
 }
 
+// External textures require multiple binding slots (3 sampled texture, 1 uniform buffer, 1
+// sampler), so ensure that these count towards the limit when combined non-external texture
+// bindings.
+TEST_F(BindGroupLayoutValidationTest, PerStageLimitsWithExternalTexture) {
+    struct TestInfo {
+        uint32_t maxCount;
+        uint32_t bindingsPerExternalTexture;
+        wgpu::BindGroupLayoutEntry entry;
+        wgpu::BindGroupLayoutEntry otherEntry;
+    };
+
+    std::array<TestInfo, 3> kTestInfos = {
+        TestInfo{kMaxSampledTexturesPerShaderStage, kSampledTexturesPerExternalTexture,
+                 BGLEntryType(wgpu::TextureSampleType::Float),
+                 BGLEntryType(wgpu::BufferBindingType::Uniform)},
+        TestInfo{kMaxSamplersPerShaderStage, kSamplersPerExternalTexture,
+                 BGLEntryType(wgpu::SamplerBindingType::Filtering),
+                 BGLEntryType(wgpu::BufferBindingType::Uniform)},
+        TestInfo{kMaxUniformBuffersPerShaderStage, kUniformsPerExternalTexture,
+                 BGLEntryType(wgpu::BufferBindingType::Uniform),
+                 BGLEntryType(wgpu::TextureSampleType::Float)},
+    };
+
+    for (TestInfo info : kTestInfos) {
+        wgpu::BindGroupLayout bgl[2];
+        std::vector<utils::BindingLayoutEntryInitializationHelper> maxBindings;
+
+        // Create an external texture binding layout entry
+        wgpu::BindGroupLayoutEntry entry = BGLEntryType(&utils::kExternalTextureBindingLayout);
+        entry.binding = 0;
+        maxBindings.push_back(entry);
+
+        // Create the other bindings such that we reach the max bindings per stage when including
+        // the external texture.
+        for (uint32_t i = 1; i <= info.maxCount - info.bindingsPerExternalTexture; ++i) {
+            wgpu::BindGroupLayoutEntry entry = info.entry;
+            entry.binding = i;
+            maxBindings.push_back(entry);
+        }
+
+        // Ensure that creation without the external texture works.
+        bgl[0] = MakeBindGroupLayout(maxBindings.data(), maxBindings.size());
+
+        // Adding an extra binding of a different type works.
+        {
+            std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
+            wgpu::BindGroupLayoutEntry entry = info.otherEntry;
+            entry.binding = info.maxCount;
+            bindings.push_back(entry);
+            MakeBindGroupLayout(bindings.data(), bindings.size());
+        }
+
+        // Adding an extra binding of the maxed type in a different stage works
+        {
+            std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
+            wgpu::BindGroupLayoutEntry entry = info.entry;
+            entry.binding = info.maxCount;
+            entry.visibility = wgpu::ShaderStage::Fragment;
+            bindings.push_back(entry);
+            MakeBindGroupLayout(bindings.data(), bindings.size());
+        }
+
+        // Adding an extra binding of the maxed type and stage exceeds the per stage limit.
+        {
+            std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
+            wgpu::BindGroupLayoutEntry entry = info.entry;
+            entry.binding = info.maxCount;
+            bindings.push_back(entry);
+            ASSERT_DEVICE_ERROR(MakeBindGroupLayout(bindings.data(), bindings.size()));
+        }
+
+        // Creating a pipeline layout from the valid BGL works.
+        TestCreatePipelineLayout(bgl, 1, true);
+
+        // Adding an extra binding of a different type in a different BGL works
+        bgl[1] = utils::MakeBindGroupLayout(device, {info.otherEntry});
+        TestCreatePipelineLayout(bgl, 2, true);
+
+        {
+            // Adding an extra binding of the maxed type in a different stage works
+            wgpu::BindGroupLayoutEntry entry = info.entry;
+            entry.visibility = wgpu::ShaderStage::Fragment;
+            bgl[1] = utils::MakeBindGroupLayout(device, {entry});
+            TestCreatePipelineLayout(bgl, 2, true);
+        }
+
+        // Adding an extra binding of the maxed type in a different BGL exceeds the per stage limit.
+        bgl[1] = utils::MakeBindGroupLayout(device, {info.entry});
+        TestCreatePipelineLayout(bgl, 2, false);
+    }
+}
+
 // Check that dynamic buffer numbers exceed maximum value in one bind group layout.
 TEST_F(BindGroupLayoutValidationTest, DynamicBufferNumberLimit) {
     wgpu::BindGroupLayout bgl[2];
@@ -1847,6 +2055,28 @@
                                       wgpu::TextureViewDimension::e2D}})}));
 }
 
+// TODO(dawn:728) Enable this test when Dawn no longer relies on SPIRV-Cross to extract shader info.
+TEST_F(BindGroupLayoutCompatibilityTest, DISABLED_ExternalTextureBindGroupLayoutCompatibility) {
+    wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
+        device, {{0, wgpu::ShaderStage::Fragment, &utils::kExternalTextureBindingLayout}});
+
+    // Test that an external texture binding works with a texture_external in the shader.
+    CreateFSRenderPipeline(R"(
+            [[group(0), binding(0)]] var myExternalTexture: texture_external;
+            [[stage(fragment)]] fn main() {
+                textureDimensions(myExternalTexture);
+            })",
+                           {bgl});
+
+    // Test that an external texture binding doesn't work with a texture_2d<f32> in the shader.
+    ASSERT_DEVICE_ERROR(CreateFSRenderPipeline(R"(
+            [[group(0), binding(0)]] var myTexture: texture_2d<f32>;
+            [[stage(fragment)]] fn main() {
+                textureDimensions(myTexture);
+            })",
+                                               {bgl}));
+}
+
 class BindingsValidationTest : public BindGroupLayoutCompatibilityTest {
   public:
     void TestRenderPassBindings(const wgpu::BindGroup* bg,
diff --git a/src/tests/unittests/validation/ExternalTextureTests.cpp b/src/tests/unittests/validation/ExternalTextureTests.cpp
index 1095312..57ec556 100644
--- a/src/tests/unittests/validation/ExternalTextureTests.cpp
+++ b/src/tests/unittests/validation/ExternalTextureTests.cpp
@@ -14,6 +14,9 @@
 
 #include "tests/unittests/validation/ValidationTest.h"
 
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/WGPUHelpers.h"
+
 namespace {
     class ExternalTextureTest : public ValidationTest {
       public:
@@ -26,11 +29,41 @@
             descriptor.sampleCount = kDefaultSampleCount;
             descriptor.dimension = wgpu::TextureDimension::e2D;
             descriptor.format = kDefaultTextureFormat;
-            descriptor.usage = wgpu::TextureUsage::Sampled;
+            descriptor.usage = wgpu::TextureUsage::Sampled | wgpu::TextureUsage::RenderAttachment;
             return descriptor;
         }
 
       protected:
+        void SetUp() override {
+            ValidationTest::SetUp();
+
+            queue = device.GetQueue();
+        }
+
+        wgpu::RenderPipeline CreateBasicRenderPipeline(wgpu::ExternalTexture externalTexture) {
+            wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
+                device, {{0, wgpu::ShaderStage::Fragment, &utils::kExternalTextureBindingLayout}});
+
+            bindGroup = utils::MakeBindGroup(device, bgl, {{0, externalTexture}});
+
+            wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
+            [[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
+                return vec4<f32>();
+            })");
+            wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
+            [[group(0), binding(0)]] var myExternalTexture: texture_external;
+            [[stage(fragment)]] fn main() {
+                textureDimensions(myExternalTexture);
+            })");
+
+            utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+            pipelineDescriptor.vertex.module = vsModule;
+            pipelineDescriptor.cFragment.module = fsModule;
+            wgpu::PipelineLayout pipelineLayout = utils::MakeBasicPipelineLayout(device, &bgl);
+            pipelineDescriptor.layout = pipelineLayout;
+            return device.CreateRenderPipeline(&pipelineDescriptor);
+        }
+
         static constexpr uint32_t kWidth = 32;
         static constexpr uint32_t kHeight = 32;
         static constexpr uint32_t kDefaultDepth = 1;
@@ -39,6 +72,10 @@
 
         static constexpr wgpu::TextureFormat kDefaultTextureFormat =
             wgpu::TextureFormat::RGBA8Unorm;
+
+        wgpu::Queue queue;
+        wgpu::RenderPipeline renderPipeline;
+        wgpu::BindGroup bindGroup;
     };
 
     TEST_F(ExternalTextureTest, CreateExternalTextureValidation) {
@@ -112,4 +149,110 @@
         }
     }
 
+    // Test that submitting a command encoder that contains a destroyed external texture results in
+    // an error.
+    TEST_F(ExternalTextureTest, SubmitDestroyedExternalTexture) {
+        wgpu::TextureDescriptor textureDescriptor = CreateDefaultTextureDescriptor();
+        wgpu::Texture texture = device.CreateTexture(&textureDescriptor);
+
+        wgpu::ExternalTextureDescriptor externalDesc;
+        externalDesc.format = kDefaultTextureFormat;
+        externalDesc.plane0 = texture.CreateView();
+        wgpu::ExternalTexture externalTexture = device.CreateExternalTexture(&externalDesc);
+
+        wgpu::RenderPipeline pipeline = CreateBasicRenderPipeline(externalTexture);
+
+        // Create another texture to use as a color attachment.
+        wgpu::TextureDescriptor renderTextureDescriptor = CreateDefaultTextureDescriptor();
+        wgpu::Texture renderTexture = device.CreateTexture(&renderTextureDescriptor);
+        wgpu::TextureView renderView = renderTexture.CreateView();
+
+        utils::ComboRenderPassDescriptor renderPass({renderView}, nullptr);
+
+        // Control case should succeed.
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+            {
+                pass.SetPipeline(pipeline);
+                pass.SetBindGroup(0, bindGroup);
+                pass.Draw(1);
+                pass.EndPass();
+            }
+
+            wgpu::CommandBuffer commands = encoder.Finish();
+
+            queue.Submit(1, &commands);
+        }
+
+        // Destroying the external texture should result in an error.
+        {
+            externalTexture.Destroy();
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+            {
+                pass.SetPipeline(pipeline);
+                pass.SetBindGroup(0, bindGroup);
+                pass.Draw(1);
+                pass.EndPass();
+            }
+
+            wgpu::CommandBuffer commands = encoder.Finish();
+            ASSERT_DEVICE_ERROR(queue.Submit(1, &commands));
+        }
+    }
+
+    // Test that submitting a command encoder that contains a destroyed external texture plane
+    // results in an error.
+    TEST_F(ExternalTextureTest, SubmitDestroyedExternalTexturePlane) {
+        wgpu::TextureDescriptor textureDescriptor = CreateDefaultTextureDescriptor();
+        wgpu::Texture texture = device.CreateTexture(&textureDescriptor);
+
+        wgpu::ExternalTextureDescriptor externalDesc;
+        externalDesc.format = kDefaultTextureFormat;
+        externalDesc.plane0 = texture.CreateView();
+        wgpu::ExternalTexture externalTexture = device.CreateExternalTexture(&externalDesc);
+
+        wgpu::RenderPipeline pipeline = CreateBasicRenderPipeline(externalTexture);
+
+        // Create another texture to use as a color attachment.
+        wgpu::TextureDescriptor renderTextureDescriptor = CreateDefaultTextureDescriptor();
+        wgpu::Texture renderTexture = device.CreateTexture(&renderTextureDescriptor);
+        wgpu::TextureView renderView = renderTexture.CreateView();
+
+        utils::ComboRenderPassDescriptor renderPass({renderView}, nullptr);
+
+        // Control case should succeed.
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+            {
+                pass.SetPipeline(pipeline);
+                pass.SetBindGroup(0, bindGroup);
+                pass.Draw(1);
+                pass.EndPass();
+            }
+
+            wgpu::CommandBuffer commands = encoder.Finish();
+
+            queue.Submit(1, &commands);
+        }
+
+        // Destroying an external texture underlying plane should result in an error.
+        {
+            texture.Destroy();
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+            {
+                pass.SetPipeline(pipeline);
+                pass.SetBindGroup(0, bindGroup);
+                pass.Draw(1);
+                pass.EndPass();
+            }
+
+            wgpu::CommandBuffer commands = encoder.Finish();
+            ASSERT_DEVICE_ERROR(queue.Submit(1, &commands));
+        }
+    }
+
 }  // namespace
\ No newline at end of file
diff --git a/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp b/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp
index f673148..de024b0 100644
--- a/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp
+++ b/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp
@@ -274,6 +274,33 @@
     }
 }
 
+// Test that an external texture binding type matches a shader using texture_external.
+// TODO(dawn:728) Enable this test once Dawn no longer relies on SPIRV-Cross to extract shader info.
+// Consider combining with the similar test above.
+TEST_F(GetBindGroupLayoutTests, DISABLED_ExternalTextureBindingType) {
+    // This test works assuming Dawn Native's object deduplication.
+    // Getting the same pointer to equivalent bind group layouts is an implementation detail of Dawn
+    // Native.
+    DAWN_SKIP_TEST_IF(UsesWire());
+
+    wgpu::BindGroupLayoutEntry binding = {};
+    binding.binding = 0;
+    binding.visibility = wgpu::ShaderStage::Fragment;
+
+    wgpu::BindGroupLayoutDescriptor desc = {};
+    desc.entryCount = 1;
+    desc.entries = &binding;
+
+    binding.nextInChain = &utils::kExternalTextureBindingLayout;
+    wgpu::RenderPipeline pipeline = RenderPipelineFromFragmentShader(R"(
+            [[group(0), binding(0)]] var myExternalTexture: texture_external;
+
+            [[stage(fragment)]] fn main() {
+               textureDimensions(myExternalTexture);
+            })");
+    EXPECT_EQ(device.CreateBindGroupLayout(&desc).Get(), pipeline.GetBindGroupLayout(0).Get());
+}
+
 // Test that texture view dimension matches the shader.
 TEST_F(GetBindGroupLayoutTests, ViewDimension) {
     // This test works assuming Dawn Native's object deduplication.
diff --git a/src/tests/unittests/wire/WireOptionalTests.cpp b/src/tests/unittests/wire/WireOptionalTests.cpp
index 012f01d..c52c5ac 100644
--- a/src/tests/unittests/wire/WireOptionalTests.cpp
+++ b/src/tests/unittests/wire/WireOptionalTests.cpp
@@ -40,6 +40,7 @@
     entry.sampler = nullptr;
     entry.textureView = nullptr;
     entry.buffer = nullptr;
+    entry.nextInChain = nullptr;
 
     WGPUBindGroupDescriptor bgDesc = {};
     bgDesc.layout = bgl;
diff --git a/src/utils/WGPUHelpers.cpp b/src/utils/WGPUHelpers.cpp
index 7f29e62..61277e2 100644
--- a/src/utils/WGPUHelpers.cpp
+++ b/src/utils/WGPUHelpers.cpp
@@ -26,7 +26,6 @@
 #include <sstream>
 
 namespace utils {
-
     wgpu::ShaderModule CreateShaderModuleFromASM(const wgpu::Device& device, const char* source) {
         // Use SPIRV-Tools's C API to assemble the SPIR-V assembly text to binary. Because the types
         // aren't RAII, we don't return directly on success and instead always go through the code
@@ -296,6 +295,19 @@
         storageTexture.viewDimension = textureViewDimension;
     }
 
+    // ExternalTextureBindingLayout never contains data, so just make one that can be reused instead
+    // of declaring a new one every time it's needed.
+    wgpu::ExternalTextureBindingLayout kExternalTextureBindingLayout = {};
+
+    BindingLayoutEntryInitializationHelper::BindingLayoutEntryInitializationHelper(
+        uint32_t entryBinding,
+        wgpu::ShaderStage entryVisibility,
+        wgpu::ExternalTextureBindingLayout* bindingLayout) {
+        binding = entryBinding;
+        visibility = entryVisibility;
+        nextInChain = bindingLayout;
+    }
+
     BindingLayoutEntryInitializationHelper::BindingLayoutEntryInitializationHelper(
         const wgpu::BindGroupLayoutEntry& entry)
         : wgpu::BindGroupLayoutEntry(entry) {
@@ -311,6 +323,13 @@
         : binding(binding), textureView(textureView) {
     }
 
+    BindingInitializationHelper::BindingInitializationHelper(
+        uint32_t binding,
+        const wgpu::ExternalTexture& externalTexture)
+        : binding(binding) {
+        externalTextureBindingEntry.externalTexture = externalTexture;
+    }
+
     BindingInitializationHelper::BindingInitializationHelper(uint32_t binding,
                                                              const wgpu::Buffer& buffer,
                                                              uint64_t offset,
@@ -327,6 +346,9 @@
         result.buffer = buffer;
         result.offset = offset;
         result.size = size;
+        if (externalTextureBindingEntry.externalTexture != nullptr) {
+            result.nextInChain = &externalTextureBindingEntry;
+        }
 
         return result;
     }
diff --git a/src/utils/WGPUHelpers.h b/src/utils/WGPUHelpers.h
index 1ae082e..09070ca 100644
--- a/src/utils/WGPUHelpers.h
+++ b/src/utils/WGPUHelpers.h
@@ -97,6 +97,8 @@
     wgpu::PipelineLayout MakePipelineLayout(const wgpu::Device& device,
                                             std::vector<wgpu::BindGroupLayout> bgls);
 
+    extern wgpu::ExternalTextureBindingLayout kExternalTextureBindingLayout;
+
     // Helpers to make creating bind group layouts look nicer:
     //
     //   utils::MakeBindGroupLayout(device, {
@@ -126,6 +128,9 @@
             wgpu::StorageTextureAccess storageTextureAccess,
             wgpu::TextureFormat format,
             wgpu::TextureViewDimension viewDimension = wgpu::TextureViewDimension::e2D);
+        BindingLayoutEntryInitializationHelper(uint32_t entryBinding,
+                                               wgpu::ShaderStage entryVisibility,
+                                               wgpu::ExternalTextureBindingLayout* bindingLayout);
 
         BindingLayoutEntryInitializationHelper(const wgpu::BindGroupLayoutEntry& entry);
     };
@@ -147,6 +152,7 @@
     struct BindingInitializationHelper {
         BindingInitializationHelper(uint32_t binding, const wgpu::Sampler& sampler);
         BindingInitializationHelper(uint32_t binding, const wgpu::TextureView& textureView);
+        BindingInitializationHelper(uint32_t binding, const wgpu::ExternalTexture& externalTexture);
         BindingInitializationHelper(uint32_t binding,
                                     const wgpu::Buffer& buffer,
                                     uint64_t offset = 0,
@@ -158,6 +164,7 @@
         wgpu::Sampler sampler;
         wgpu::TextureView textureView;
         wgpu::Buffer buffer;
+        wgpu::ExternalTextureBindingEntry externalTextureBindingEntry;
         uint64_t offset = 0;
         uint64_t size = 0;
     };