D3D12: Support per plane views with NV12 textures

Adds support for NV12 texture format and per plane view aspects.
Only allows planar sampling of imported DX11 textures. See usage
tests for examples and formats.h for rules.

Bug: dawn:551
Change-Id: I44b89d2c07bb9969638e77ce7c756ef367167f0c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/38781
Commit-Queue: Bryan Bernhart <bryan.bernhart@intel.com>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/dawn.json b/dawn.json
index 050af44..9eacbf9 100644
--- a/dawn.json
+++ b/dawn.json
@@ -817,7 +817,8 @@
             {"name": "texture compression BC", "type": "bool", "default": "false"},
             {"name": "shader float16", "type": "bool", "default": "false"},
             {"name": "pipeline statistics query", "type": "bool", "default": "false"},
-            {"name": "timestamp query", "type": "bool", "default": "false"}
+            {"name": "timestamp query", "type": "bool", "default": "false"},
+            {"name": "multi planar formats", "type": "bool", "default": "false"}
         ]
     },
     "depth stencil state descriptor": {
@@ -1709,7 +1710,9 @@
         "values": [
             {"value": 0, "name": "all"},
             {"value": 1, "name": "stencil only"},
-            {"value": 2, "name": "depth only"}
+            {"value": 2, "name": "depth only"},
+            {"value": 3, "name": "plane 0 only"},
+            {"value": 4, "name": "plane 1 only"}
         ]
     },
     "texture component type": {
@@ -1823,7 +1826,8 @@
             {"value": 50, "name": "BC6H RGB ufloat"},
             {"value": 51, "name": "BC6H RGB float"},
             {"value": 52, "name": "BC7 RGBA unorm"},
-            {"value": 53, "name": "BC7 RGBA unorm srgb"}
+            {"value": 53, "name": "BC7 RGBA unorm srgb"},
+            {"value": 54, "name": "R8 BG8 Biplanar 420 unorm"}
         ]
     },
     "texture usage": {
diff --git a/src/dawn_native/CommandValidation.cpp b/src/dawn_native/CommandValidation.cpp
index 04dfb22..174510b 100644
--- a/src/dawn_native/CommandValidation.cpp
+++ b/src/dawn_native/CommandValidation.cpp
@@ -355,7 +355,8 @@
         return {};
     }
 
-    // Always returns a single aspect (color, stencil, or depth).
+    // Always returns a single aspect (color, stencil, depth, or ith plane for multi-planar
+    // formats).
     ResultOrError<Aspect> SingleAspectUsedByTextureCopyView(const TextureCopyView& view) {
         const Format& format = view.texture->GetFormat();
         switch (view.aspect) {
@@ -375,6 +376,9 @@
             case wgpu::TextureAspect::StencilOnly:
                 ASSERT(format.aspects & Aspect::Stencil);
                 return Aspect::Stencil;
+            case wgpu::TextureAspect::Plane0Only:
+            case wgpu::TextureAspect::Plane1Only:
+                UNREACHABLE();
         }
     }
 
diff --git a/src/dawn_native/Extensions.cpp b/src/dawn_native/Extensions.cpp
index d356616..98d4a61 100644
--- a/src/dawn_native/Extensions.cpp
+++ b/src/dawn_native/Extensions.cpp
@@ -47,7 +47,12 @@
              {Extension::TimestampQuery,
               {"timestamp_query", "Support Timestamp Query",
                "https://bugs.chromium.org/p/dawn/issues/detail?id=434"},
-              &WGPUDeviceProperties::timestampQuery}}};
+              &WGPUDeviceProperties::timestampQuery},
+             {Extension::MultiPlanarFormats,
+              {"multiplanar_formats",
+               "Import and use multi-planar texture formats with per plane views",
+               "https://bugs.chromium.org/p/dawn/issues/detail?id=551"},
+              &WGPUDeviceProperties::multiPlanarFormats}}};
 
     }  // anonymous namespace
 
diff --git a/src/dawn_native/Extensions.h b/src/dawn_native/Extensions.h
index ba32ee1..08689ca 100644
--- a/src/dawn_native/Extensions.h
+++ b/src/dawn_native/Extensions.h
@@ -28,6 +28,7 @@
         ShaderFloat16,
         PipelineStatisticsQuery,
         TimestampQuery,
+        MultiPlanarFormats,
 
         EnumCount,
         InvalidEnum = EnumCount,
diff --git a/src/dawn_native/Format.cpp b/src/dawn_native/Format.cpp
index f1d7d16..9a137ba 100644
--- a/src/dawn_native/Format.cpp
+++ b/src/dawn_native/Format.cpp
@@ -27,6 +27,15 @@
         static const AspectInfo kStencil8AspectInfo = {{1, 1, 1},
                                                        wgpu::TextureComponentType::Uint,
                                                        ComponentTypeBit::Uint};
+
+        // R8BG8Biplanar420Unorm must be specialized since it represents planar data and cannot be
+        // used without a per plane format. In particular, the component type is float since
+        // Dawn does not allow texture format reinterpretion (ex. using R8BG82plane420 with Uint or
+        // Unorm). Block size is always zero since the format is not renderable or copyable.
+        static const AspectInfo kR8BG8Biplanar420UnormAspectInfo = {
+            {0, 0, 0},
+            wgpu::TextureComponentType::Float,
+            ComponentTypeBit::Float};
     }
 
     // Format
@@ -99,6 +108,10 @@
         return (aspects & (Aspect::Depth | Aspect::Stencil)) != 0;
     }
 
+    bool Format::IsMultiPlanar() const {
+        return (aspects & (Aspect::Plane0 | Aspect::Plane1)) != 0;
+    }
+
     const AspectInfo& Format::GetAspectInfo(wgpu::TextureAspect aspect) const {
         return GetAspectInfo(ConvertAspect(*this, aspect));
     }
@@ -111,6 +124,12 @@
         // same aspect information, special case it to return a constant AspectInfo.
         if (aspect == Aspect::Stencil) {
             return kStencil8AspectInfo;
+            // multi-planar formats are specified per plane aspect. Since it does not support
+            // non-planar access, it can always be the same aspect information, special cased to
+            // return a constant AspectInfo.
+            // TODO(dawn:551): Refactor and remove GetAspectFormat.
+        } else if (format == wgpu::TextureFormat::R8BG8Biplanar420Unorm) {
+            return kR8BG8Biplanar420UnormAspectInfo;
         } else {
             return firstAspect;
         }
@@ -120,6 +139,24 @@
         return ComputeFormatIndex(format);
     }
 
+    wgpu::TextureFormat Format::GetAspectFormat(wgpu::TextureAspect aspect) const {
+        switch (format) {
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                switch (aspect) {
+                    case wgpu::TextureAspect::Plane0Only:
+                        return wgpu::TextureFormat::R8Unorm;
+                    case wgpu::TextureAspect::Plane1Only:
+                        return wgpu::TextureFormat::RG8Unorm;
+                    default:
+                        return wgpu::TextureFormat::Undefined;
+                }
+                break;
+            // TODO(dawn:551): Consider using for depth-stencil formats.
+            default:
+                return format;
+        }
+    }
+
     // Implementation details of the format table of the DeviceBase
 
     // For the enum for formats are packed but this might change when we have a broader extension
@@ -148,7 +185,9 @@
 
             // Vulkan describes bytesPerRow in units of texels. If there's any format for which this
             // ASSERT isn't true, then additional validation on bytesPerRow must be added.
-            ASSERT((kTextureBytesPerRowAlignment % format.firstAspect.block.byteSize) == 0);
+            // Multi-planar formats are not copyable and have no first aspect.
+            ASSERT(format.IsMultiPlanar() ||
+                   (kTextureBytesPerRowAlignment % format.firstAspect.block.byteSize) == 0);
 
             table[index] = format;
             formatsSet.set(index);
@@ -207,6 +246,18 @@
             AddFormat(internalFormat);
         };
 
+        auto AddMultiPlanarFormat = [&AddFormat](wgpu::TextureFormat format, Aspect aspects,
+                                                 bool isSupported) {
+            Format internalFormat;
+            internalFormat.format = format;
+            internalFormat.isRenderable = false;
+            internalFormat.isCompressed = false;
+            internalFormat.isSupported = isSupported;
+            internalFormat.supportsStorageUsage = false;
+            internalFormat.aspects = aspects;
+            AddFormat(internalFormat);
+        };
+
         // clang-format off
 
         // 1 byte color formats
@@ -281,6 +332,10 @@
         AddCompressedFormat(wgpu::TextureFormat::BC7RGBAUnorm, 16, 4, 4, isBCFormatSupported);
         AddCompressedFormat(wgpu::TextureFormat::BC7RGBAUnormSrgb, 16, 4, 4, isBCFormatSupported);
 
+        // multi-planar formats
+        const bool isMultiPlanarFormatSupported = device->IsExtensionEnabled(Extension::MultiPlanarFormats);
+        AddMultiPlanarFormat(wgpu::TextureFormat::R8BG8Biplanar420Unorm, Aspect::Plane0 | Aspect::Plane1, isMultiPlanarFormatSupported);
+
         // clang-format on
 
         // This checks that each format is set at least once, the second part of checking that all
diff --git a/src/dawn_native/Format.h b/src/dawn_native/Format.h
index fe0c41f..b02d493 100644
--- a/src/dawn_native/Format.h
+++ b/src/dawn_native/Format.h
@@ -23,6 +23,22 @@
 
 #include <array>
 
+// About multi-planar formats.
+//
+// Dawn supports additional multi-planar formats when the multiplanar_formats extension is enabled.
+// When enabled, Dawn treats planar data as sub-resources (ie. 1 sub-resource == 1 view == 1 plane).
+// A multi-planar format name encodes the channel mapping and order of planes. For example,
+// R8BG8Biplanar420Unorm is YUV 4:2:0 where Plane 0 = R8, and Plane 1 = BG8.
+//
+// Requirements:
+// * Plane aspects cannot be combined with color, depth, or stencil aspects.
+// * Only compatible multi-planar formats of planes can be used with multi-planar texture
+// formats.
+// * Can't access multiple planes without creating per plane views (no color conversion).
+// * Multi-planar format cannot be written or read without a per plane view.
+//
+// TODO(dawn:551): Consider moving this comment.
+
 namespace dawn_native {
 
     enum class Aspect : uint8_t;
@@ -56,7 +72,7 @@
 
     // The number of formats Dawn knows about. Asserts in BuildFormatTable ensure that this is the
     // exact number of known format.
-    static constexpr size_t kKnownFormatCount = 53;
+    static constexpr size_t kKnownFormatCount = 54;
 
     struct Format;
     using FormatTable = std::array<Format, kKnownFormatCount>;
@@ -76,6 +92,10 @@
         bool HasStencil() const;
         bool HasDepthOrStencil() const;
 
+        // IsMultiPlanar() returns true if the format allows selecting a plane index. This is only
+        // allowed by multi-planar formats (ex. NV12).
+        bool IsMultiPlanar() const;
+
         const AspectInfo& GetAspectInfo(wgpu::TextureAspect aspect) const;
         const AspectInfo& GetAspectInfo(Aspect aspect) const;
 
@@ -83,6 +103,10 @@
         // in [0, kKnownFormatCount)
         size_t GetIndex() const;
 
+        // Used to lookup the compatible view format using an aspect which corresponds to the
+        // plane index. Returns Undefined if the wrong plane aspect is requested.
+        wgpu::TextureFormat GetAspectFormat(wgpu::TextureAspect aspect) const;
+
       private:
         // The most common aspect: the color aspect for color texture, the depth aspect for
         // depth[-stencil] textures.
diff --git a/src/dawn_native/Subresource.cpp b/src/dawn_native/Subresource.cpp
index ad9e0ee..1114bae 100644
--- a/src/dawn_native/Subresource.cpp
+++ b/src/dawn_native/Subresource.cpp
@@ -31,6 +31,21 @@
         return aspectMask;
     }
 
+    Aspect ConvertViewAspect(const Format& format, wgpu::TextureAspect aspect) {
+        // Color view |format| must be treated as the same plane |aspect|.
+        if (format.aspects == Aspect::Color) {
+            switch (aspect) {
+                case wgpu::TextureAspect::Plane0Only:
+                    return Aspect::Plane0;
+                case wgpu::TextureAspect::Plane1Only:
+                    return Aspect::Plane1;
+                default:
+                    break;
+            }
+        }
+        return ConvertAspect(format, aspect);
+    }
+
     Aspect SelectFormatAspects(const Format& format, wgpu::TextureAspect aspect) {
         switch (aspect) {
             case wgpu::TextureAspect::All:
@@ -39,6 +54,10 @@
                 return format.aspects & Aspect::Depth;
             case wgpu::TextureAspect::StencilOnly:
                 return format.aspects & Aspect::Stencil;
+            case wgpu::TextureAspect::Plane0Only:
+                return format.aspects & Aspect::Plane0;
+            case wgpu::TextureAspect::Plane1Only:
+                return format.aspects & Aspect::Plane1;
         }
     }
 
@@ -47,8 +66,10 @@
         switch (aspect) {
             case Aspect::Color:
             case Aspect::Depth:
+            case Aspect::Plane0:
             case Aspect::CombinedDepthStencil:
                 return 0;
+            case Aspect::Plane1:
             case Aspect::Stencil:
                 return 1;
             default:
@@ -63,6 +84,8 @@
         if (aspects == Aspect::Color || aspects == Aspect::Depth ||
             aspects == Aspect::CombinedDepthStencil) {
             return 1;
+        } else if (aspects == (Aspect::Plane0 | Aspect::Plane1)) {
+            return 2;
         } else {
             ASSERT(aspects == (Aspect::Depth | Aspect::Stencil));
             return 2;
diff --git a/src/dawn_native/Subresource.h b/src/dawn_native/Subresource.h
index 1cf439c..ce9b3f3 100644
--- a/src/dawn_native/Subresource.h
+++ b/src/dawn_native/Subresource.h
@@ -29,14 +29,18 @@
         Depth = 0x2,
         Stencil = 0x4,
 
+        // Aspects used to select individual planes in a multi-planar format.
+        Plane0 = 0x8,
+        Plane1 = 0x10,
+
         // An aspect for that represents the combination of both the depth and stencil aspects. It
         // can be ignored outside of the Vulkan backend.
-        CombinedDepthStencil = 0x8,
+        CombinedDepthStencil = 0x20,
     };
 
     template <>
     struct EnumBitmaskSize<Aspect> {
-        static constexpr unsigned value = 4;
+        static constexpr unsigned value = 6;
     };
 
     // Convert the TextureAspect to an Aspect mask for the format. ASSERTs if the aspect
@@ -53,6 +57,10 @@
     // selected aspects.
     Aspect SelectFormatAspects(const Format& format, wgpu::TextureAspect aspect);
 
+    // Convert TextureAspect to the aspect which corresponds to the view format. This
+    // special cases per plane view formats before calling ConvertAspect.
+    Aspect ConvertViewAspect(const Format& format, wgpu::TextureAspect aspect);
+
     // Helper struct to make it clear that what the parameters of a range mean.
     template <typename T>
     struct FirstAndCountRange {
diff --git a/src/dawn_native/Texture.cpp b/src/dawn_native/Texture.cpp
index 127dc39..8bfe2c5 100644
--- a/src/dawn_native/Texture.cpp
+++ b/src/dawn_native/Texture.cpp
@@ -29,7 +29,7 @@
         // TODO(jiawei.shao@intel.com): implement texture view format compatibility rule
         MaybeError ValidateTextureViewFormatCompatibility(const TextureBase* texture,
                                                           const TextureViewDescriptor* descriptor) {
-            if (texture->GetFormat().format != descriptor->format) {
+            if (texture->GetFormat().GetAspectFormat(descriptor->aspect) != descriptor->format) {
                 return DAWN_VALIDATION_ERROR(
                     "The format of texture view is not compatible to the original texture");
             }
@@ -227,6 +227,11 @@
                 return DAWN_VALIDATION_ERROR("Format cannot be used in storage textures");
             }
 
+            constexpr wgpu::TextureUsage kValidMultiPlanarUsages = wgpu::TextureUsage::Sampled;
+            if (format->IsMultiPlanar() && !IsSubset(descriptor->usage, kValidMultiPlanarUsages)) {
+                return DAWN_VALIDATION_ERROR("Multi-planar format doesn't have valid usage.");
+            }
+
             return {};
         }
 
@@ -352,7 +357,7 @@
         }
 
         if (desc.format == wgpu::TextureFormat::Undefined) {
-            desc.format = texture->GetFormat().format;
+            desc.format = texture->GetFormat().GetAspectFormat(desc.aspect);
         }
         if (desc.arrayLayerCount == 0) {
             desc.arrayLayerCount = texture->GetArrayLayers() - desc.baseArrayLayer;
@@ -608,7 +613,7 @@
           mTexture(texture),
           mFormat(GetDevice()->GetValidInternalFormat(descriptor->format)),
           mDimension(descriptor->dimension),
-          mRange({ConvertAspect(mFormat, descriptor->aspect),
+          mRange({ConvertViewAspect(mFormat, descriptor->aspect),
                   {descriptor->baseArrayLayer, descriptor->arrayLayerCount},
                   {descriptor->baseMipLevel, descriptor->mipLevelCount}}) {
     }
diff --git a/src/dawn_native/d3d12/AdapterD3D12.cpp b/src/dawn_native/d3d12/AdapterD3D12.cpp
index 3d9f50b..8b31f39 100644
--- a/src/dawn_native/d3d12/AdapterD3D12.cpp
+++ b/src/dawn_native/d3d12/AdapterD3D12.cpp
@@ -122,6 +122,7 @@
         if (mDeviceInfo.supportsShaderFloat16 && GetBackend()->GetFunctions()->IsDXCAvailable()) {
             mSupportedExtensions.EnableExtension(Extension::ShaderFloat16);
         }
+        mSupportedExtensions.EnableExtension(Extension::MultiPlanarFormats);
     }
 
     MaybeError Adapter::InitializeDebugLayerFilters() {
diff --git a/src/dawn_native/d3d12/D3D12Info.cpp b/src/dawn_native/d3d12/D3D12Info.cpp
index b270693..d7a0f5e 100644
--- a/src/dawn_native/d3d12/D3D12Info.cpp
+++ b/src/dawn_native/d3d12/D3D12Info.cpp
@@ -59,6 +59,20 @@
             }
         }
 
+        // Used to share resources cross-API. If we query CheckFeatureSupport for
+        // D3D12_FEATURE_D3D12_OPTIONS4 successfully, then we can use cross-API sharing.
+        info.supportsSharedResourceCapabilityTier1 = false;
+        D3D12_FEATURE_DATA_D3D12_OPTIONS4 featureOptions4 = {};
+        if (SUCCEEDED(adapter.GetDevice()->CheckFeatureSupport(
+                D3D12_FEATURE_D3D12_OPTIONS4, &featureOptions4, sizeof(featureOptions4)))) {
+            // Tier 1 support additionally enables the NV12 format. Since only the NV12 format
+            // is used by Dawn, check for Tier 1.
+            if (featureOptions4.SharedResourceCompatibilityTier >=
+                D3D12_SHARED_RESOURCE_COMPATIBILITY_TIER_1) {
+                info.supportsSharedResourceCapabilityTier1 = true;
+            }
+        }
+
         D3D12_FEATURE_DATA_SHADER_MODEL knownShaderModels[] = {{D3D_SHADER_MODEL_6_2},
                                                                {D3D_SHADER_MODEL_6_1},
                                                                {D3D_SHADER_MODEL_6_0},
diff --git a/src/dawn_native/d3d12/D3D12Info.h b/src/dawn_native/d3d12/D3D12Info.h
index 46b2d09..a719595 100644
--- a/src/dawn_native/d3d12/D3D12Info.h
+++ b/src/dawn_native/d3d12/D3D12Info.h
@@ -32,6 +32,7 @@
         // indicates that current driver supports the maximum shader model is shader model 6.2.
         uint32_t shaderModel;
         PerStage<std::wstring> shaderProfiles;
+        bool supportsSharedResourceCapabilityTier1;
     };
 
     ResultOrError<D3D12DeviceInfo> GatherDeviceInfo(const Adapter& adapter);
diff --git a/src/dawn_native/d3d12/TextureD3D12.cpp b/src/dawn_native/d3d12/TextureD3D12.cpp
index f7d5d04..a136c4c 100644
--- a/src/dawn_native/d3d12/TextureD3D12.cpp
+++ b/src/dawn_native/d3d12/TextureD3D12.cpp
@@ -202,6 +202,7 @@
                 case wgpu::TextureFormat::BC7RGBAUnormSrgb:
                     return DXGI_FORMAT_BC7_TYPELESS;
 
+                case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
                 case wgpu::TextureFormat::Undefined:
                     UNREACHABLE();
             }
@@ -324,6 +325,9 @@
             case wgpu::TextureFormat::BC7RGBAUnormSrgb:
                 return DXGI_FORMAT_BC7_UNORM_SRGB;
 
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                return DXGI_FORMAT_NV12;
+
             case wgpu::TextureFormat::Undefined:
                 UNREACHABLE();
         }
@@ -379,11 +383,34 @@
         return {};
     }
 
+    // https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_shared_resource_compatibility_tier
+    MaybeError ValidateD3D12VideoTextureCanBeShared(Device* device, DXGI_FORMAT textureFormat) {
+        const bool supportsSharedResourceCapabilityTier1 =
+            device->GetDeviceInfo().supportsSharedResourceCapabilityTier1;
+        switch (textureFormat) {
+            // MSDN docs are not correct, NV12 requires at-least tier 1.
+            case DXGI_FORMAT_NV12:
+                if (supportsSharedResourceCapabilityTier1) {
+                    return {};
+                }
+                break;
+            default:
+                break;
+        }
+
+        return DAWN_VALIDATION_ERROR("DXGI format does not support cross-API sharing.");
+    }
+
     // static
     ResultOrError<Ref<Texture>> Texture::Create(Device* device,
                                                 const TextureDescriptor* descriptor) {
         Ref<Texture> dawnTexture =
             AcquireRef(new Texture(device, descriptor, TextureState::OwnedInternal));
+
+        if (dawnTexture->GetFormat().IsMultiPlanar()) {
+            return DAWN_VALIDATION_ERROR("Cannot create a multi-planar formatted texture directly");
+        }
+
         DAWN_TRY(dawnTexture->InitializeAsInternalTexture());
         return std::move(dawnTexture);
     }
@@ -401,6 +428,14 @@
             AcquireRef(new Texture(device, textureDescriptor, TextureState::OwnedExternal));
         DAWN_TRY(dawnTexture->InitializeAsExternalTexture(textureDescriptor, sharedHandle,
                                                           acquireMutexKey, isSwapChainTexture));
+
+        // Importing a multi-planar format must be initialized. This is required because
+        // a shared multi-planar format cannot be initialized by Dawn.
+        if (!descriptor->isInitialized && dawnTexture->GetFormat().IsMultiPlanar()) {
+            return DAWN_VALIDATION_ERROR(
+                "Cannot create a multi-planar formatted texture without being initialized");
+        }
+
         dawnTexture->SetIsSubresourceContentInitialized(descriptor->isInitialized,
                                                         dawnTexture->GetAllSubresources());
         return std::move(dawnTexture);
@@ -431,6 +466,13 @@
 
         DAWN_TRY(ValidateD3D12TextureCanBeWrapped(d3d12Resource.Get(), descriptor));
 
+        // Shared handle is assumed to support resource sharing capability. The resource
+        // shared capability tier must agree to share resources between D3D devices.
+        if (GetFormat().IsMultiPlanar()) {
+            DAWN_TRY(ValidateD3D12VideoTextureCanBeShared(ToBackend(GetDevice()),
+                                                          D3D12TextureFormat(descriptor->format)));
+        }
+
         ComPtr<IDXGIKeyedMutex> dxgiKeyedMutex;
         DAWN_TRY_ASSIGN(dxgiKeyedMutex,
                         dawnDevice->CreateKeyedMutexForTexture(d3d12Resource.Get()));
@@ -1039,6 +1081,12 @@
                             // sampled.
                             mSrvDesc.Format = DXGI_FORMAT_UNKNOWN;
                             break;
+
+                        // Depth formats cannot use plane aspects.
+                        case wgpu::TextureAspect::Plane0Only:
+                        case wgpu::TextureAspect::Plane1Only:
+                            UNREACHABLE();
+                            break;
                     }
                     break;
                 default:
@@ -1047,6 +1095,12 @@
             }
         }
 
+        // Per plane view formats must have the plane slice number be the index of the plane in the
+        // array of textures.
+        if (texture->GetFormat().IsMultiPlanar()) {
+            planeSlice = GetAspectIndex(ConvertViewAspect(GetFormat(), descriptor->aspect));
+        }
+
         // Currently we always use D3D12_TEX2D_ARRAY_SRV because we cannot specify base array layer
         // and layer count in D3D12_TEX2D_SRV. For 2D texture views, we treat them as 1-layer 2D
         // array textures.
diff --git a/src/dawn_native/opengl/CommandBufferGL.cpp b/src/dawn_native/opengl/CommandBufferGL.cpp
index 6edc768..c1d7cfe 100644
--- a/src/dawn_native/opengl/CommandBufferGL.cpp
+++ b/src/dawn_native/opengl/CommandBufferGL.cpp
@@ -307,6 +307,8 @@
                                         case Aspect::None:
                                         case Aspect::Color:
                                         case Aspect::CombinedDepthStencil:
+                                        case Aspect::Plane0:
+                                        case Aspect::Plane1:
                                             UNREACHABLE();
                                         case Aspect::Depth:
                                             gl.TexParameteri(target, GL_DEPTH_STENCIL_TEXTURE_MODE,
@@ -483,6 +485,8 @@
                             break;
                         case Aspect::CombinedDepthStencil:
                         case Aspect::None:
+                        case Aspect::Plane0:
+                        case Aspect::Plane1:
                             UNREACHABLE();
                     }
                     if (srcTexture->GetArrayLayers() == 1) {
@@ -795,6 +799,8 @@
 
                         case Aspect::CombinedDepthStencil:
                         case Aspect::None:
+                        case Aspect::Plane0:
+                        case Aspect::Plane1:
                             UNREACHABLE();
                     }
 
diff --git a/src/dawn_native/vulkan/TextureVk.cpp b/src/dawn_native/vulkan/TextureVk.cpp
index a60efc3..bd80f39 100644
--- a/src/dawn_native/vulkan/TextureVk.cpp
+++ b/src/dawn_native/vulkan/TextureVk.cpp
@@ -338,7 +338,7 @@
                 return VK_FORMAT_BC7_UNORM_BLOCK;
             case wgpu::TextureFormat::BC7RGBAUnormSrgb:
                 return VK_FORMAT_BC7_SRGB_BLOCK;
-
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
             case wgpu::TextureFormat::Undefined:
                 UNREACHABLE();
         }
@@ -757,6 +757,9 @@
             case wgpu::TextureAspect::StencilOnly:
                 ASSERT(GetFormat().aspects & Aspect::Stencil);
                 return VulkanAspectMask(Aspect::Stencil);
+            case wgpu::TextureAspect::Plane0Only:
+            case wgpu::TextureAspect::Plane1Only:
+                UNREACHABLE();
         }
     }
 
diff --git a/src/dawn_native/vulkan/UtilsVulkan.cpp b/src/dawn_native/vulkan/UtilsVulkan.cpp
index b379e25..7e58319 100644
--- a/src/dawn_native/vulkan/UtilsVulkan.cpp
+++ b/src/dawn_native/vulkan/UtilsVulkan.cpp
@@ -65,6 +65,8 @@
                     flags |= VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
                     break;
 
+                case Aspect::Plane0:
+                case Aspect::Plane1:
                 case Aspect::None:
                     UNREACHABLE();
             }
diff --git a/src/tests/BUILD.gn b/src/tests/BUILD.gn
index a5da848..ea06fa4 100644
--- a/src/tests/BUILD.gn
+++ b/src/tests/BUILD.gn
@@ -215,6 +215,7 @@
     "unittests/validation/ValidationTest.h",
     "unittests/validation/VertexBufferValidationTests.cpp",
     "unittests/validation/VertexStateValidationTests.cpp",
+    "unittests/validation/VideoViewsValidationTests.cpp",
     "unittests/wire/WireArgumentTests.cpp",
     "unittests/wire/WireBasicTests.cpp",
     "unittests/wire/WireBufferMappingTests.cpp",
@@ -345,6 +346,7 @@
     sources += [
       "end2end/D3D12CachingTests.cpp",
       "end2end/D3D12ResourceWrappingTests.cpp",
+      "end2end/D3D12VideoViewsTests.cpp",
     ]
     libs += [
       "d3d11.lib",
diff --git a/src/tests/end2end/D3D12VideoViewsTests.cpp b/src/tests/end2end/D3D12VideoViewsTests.cpp
new file mode 100644
index 0000000..3ba3519
--- /dev/null
+++ b/src/tests/end2end/D3D12VideoViewsTests.cpp
@@ -0,0 +1,432 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/DawnTest.h"
+
+#include <d3d11.h>
+#include <d3d12.h>
+#include <dxgi1_4.h>
+#include <wrl/client.h>
+
+#include "dawn_native/D3D12Backend.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/WGPUHelpers.h"
+
+using Microsoft::WRL::ComPtr;
+
+namespace {
+    class D3D12VideoViewsTests : public DawnTest {
+      protected:
+        void SetUp() override {
+            DawnTest::SetUp();
+            DAWN_SKIP_TEST_IF(UsesWire());
+            DAWN_SKIP_TEST_IF(!IsMultiPlanarFormatsSupported());
+
+            // Create the D3D11 device/contexts that will be used in subsequent tests
+            ComPtr<ID3D12Device> d3d12Device = dawn_native::d3d12::GetD3D12Device(device.Get());
+
+            const LUID adapterLuid = d3d12Device->GetAdapterLuid();
+
+            ComPtr<IDXGIFactory4> dxgiFactory;
+            HRESULT hr = ::CreateDXGIFactory2(0, IID_PPV_ARGS(&dxgiFactory));
+            ASSERT_EQ(hr, S_OK);
+
+            ComPtr<IDXGIAdapter> dxgiAdapter;
+            hr = dxgiFactory->EnumAdapterByLuid(adapterLuid, IID_PPV_ARGS(&dxgiAdapter));
+            ASSERT_EQ(hr, S_OK);
+
+            ComPtr<ID3D11Device> d3d11Device;
+            D3D_FEATURE_LEVEL d3dFeatureLevel;
+            ComPtr<ID3D11DeviceContext> d3d11DeviceContext;
+            hr = ::D3D11CreateDevice(dxgiAdapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0,
+                                     nullptr, 0, D3D11_SDK_VERSION, &d3d11Device, &d3dFeatureLevel,
+                                     &d3d11DeviceContext);
+            ASSERT_EQ(hr, S_OK);
+
+            // Runtime of the created texture (D3D11 device) and OpenSharedHandle runtime (Dawn's
+            // D3D12 device) must agree on resource sharing capability. For NV12 formats, D3D11
+            // requires at-least D3D11_SHARED_RESOURCE_TIER_2 support.
+            // https://docs.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_shared_resource_tier
+            D3D11_FEATURE_DATA_D3D11_OPTIONS5 featureOptions5{};
+            hr = d3d11Device->CheckFeatureSupport(D3D11_FEATURE_D3D11_OPTIONS5, &featureOptions5,
+                                                  sizeof(featureOptions5));
+            ASSERT_EQ(hr, S_OK);
+
+            ASSERT_GE(featureOptions5.SharedResourceTier, D3D11_SHARED_RESOURCE_TIER_2);
+
+            mD3d11Device = std::move(d3d11Device);
+        }
+
+        std::vector<const char*> GetRequiredExtensions() override {
+            mIsMultiPlanarFormatsSupported = SupportsExtensions({"multiplanar_formats"});
+            if (!mIsMultiPlanarFormatsSupported) {
+                return {};
+            }
+
+            return {"multiplanar_formats"};
+        }
+
+        bool IsMultiPlanarFormatsSupported() const {
+            return mIsMultiPlanarFormatsSupported;
+        }
+
+        static DXGI_FORMAT GetDXGITextureFormat(wgpu::TextureFormat format) {
+            switch (format) {
+                case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                    return DXGI_FORMAT_NV12;
+                default:
+                    UNREACHABLE();
+                    return DXGI_FORMAT_UNKNOWN;
+            }
+        }
+
+        // Returns a pre-prepared multi-planar formatted texture
+        // The encoded texture data represents a 4x4 converted image. When |isCheckerboard| is true,
+        // the upper left and bottom right fill a 2x2 grey block, from RGB(128, 128, 128), while the
+        // upper right and bottom left fill a 2x2 white block, from RGB(255, 255, 255). When
+        // |isCheckerboard| is false, the image is converted from a solid grey 4x4 block.
+        static std::vector<uint8_t> GetTestTextureData(wgpu::TextureFormat format,
+                                                       bool isCheckerboard) {
+            constexpr uint8_t Y1 = kGreyYUVColor[kYUVLumaPlaneIndex].r;
+            constexpr uint8_t U1 = kGreyYUVColor[kYUVChromaPlaneIndex].r;
+            constexpr uint8_t V1 = kGreyYUVColor[kYUVChromaPlaneIndex].g;
+
+            switch (format) {
+                // The first 16 bytes is the luma plane (Y), followed by the chroma plane (UV) which
+                // is half the number of bytes (subsampled by 2) but same bytes per line as luma
+                // plane.
+                case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                    if (isCheckerboard) {
+                        constexpr uint8_t Y2 = kWhiteYUVColor[kYUVLumaPlaneIndex].r;
+                        constexpr uint8_t U2 = kWhiteYUVColor[kYUVChromaPlaneIndex].r;
+                        constexpr uint8_t V2 = kWhiteYUVColor[kYUVChromaPlaneIndex].g;
+                        // clang-format off
+                        return {
+                            Y2, Y2, Y1, Y1, // plane 0, start + 0
+                            Y2, Y2, Y1, Y1,
+                            Y1, Y1, Y2, Y2,
+                            Y1, Y1, Y2, Y2,
+                            U1, V1, U2, V2, // plane 1, start + 16
+                            U2, V2, U1, V1,
+                        };
+                        // clang-format on
+                    } else {
+                        // clang-format off
+                        return {
+                            Y1, Y1, Y1, Y1,  // plane 0, start + 0
+                            Y1, Y1, Y1, Y1,
+                            Y1, Y1, Y1, Y1,
+                            Y1, Y1, Y1, Y1,
+                            U1, V1, U1, V1,  // plane 1, start + 16
+                            U1, V1, U1, V1,
+                        };
+                        // clang-format on
+                    }
+                default:
+                    UNREACHABLE();
+                    return {};
+            }
+        }
+
+        wgpu::Texture CreateVideoTextureForTest(wgpu::TextureFormat format,
+                                                wgpu::TextureUsage usage,
+                                                bool isCheckerboard = false) {
+            wgpu::TextureDescriptor textureDesc;
+            textureDesc.format = format;
+            textureDesc.dimension = wgpu::TextureDimension::e2D;
+            textureDesc.usage = usage;
+            textureDesc.size = {kYUVImageDataWidthInTexels, kYUVImageDataHeightInTexels, 1};
+
+            // Create a DX11 texture with data then wrap it in a shared handle.
+            D3D11_TEXTURE2D_DESC d3dDescriptor;
+            d3dDescriptor.Width = kYUVImageDataWidthInTexels;
+            d3dDescriptor.Height = kYUVImageDataHeightInTexels;
+            d3dDescriptor.MipLevels = 1;
+            d3dDescriptor.ArraySize = 1;
+            d3dDescriptor.Format = GetDXGITextureFormat(format);
+            d3dDescriptor.SampleDesc.Count = 1;
+            d3dDescriptor.SampleDesc.Quality = 0;
+            d3dDescriptor.Usage = D3D11_USAGE_DEFAULT;
+            d3dDescriptor.BindFlags = D3D11_BIND_SHADER_RESOURCE;
+            d3dDescriptor.CPUAccessFlags = 0;
+            d3dDescriptor.MiscFlags =
+                D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
+
+            std::vector<uint8_t> initialData = GetTestTextureData(format, isCheckerboard);
+
+            D3D11_SUBRESOURCE_DATA subres;
+            subres.pSysMem = initialData.data();
+            subres.SysMemPitch = kYUVImageDataWidthInTexels;
+
+            ComPtr<ID3D11Texture2D> d3d11Texture;
+            HRESULT hr = mD3d11Device->CreateTexture2D(&d3dDescriptor, &subres, &d3d11Texture);
+            EXPECT_EQ(hr, S_OK);
+
+            ComPtr<IDXGIResource1> dxgiResource;
+            hr = d3d11Texture.As(&dxgiResource);
+            EXPECT_EQ(hr, S_OK);
+
+            HANDLE sharedHandle;
+            hr = dxgiResource->CreateSharedHandle(
+                nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, nullptr,
+                &sharedHandle);
+            EXPECT_EQ(hr, S_OK);
+
+            dawn_native::d3d12::ExternalImageDescriptorDXGISharedHandle externDesc;
+            externDesc.cTextureDescriptor =
+                reinterpret_cast<const WGPUTextureDescriptor*>(&textureDesc);
+            externDesc.sharedHandle = sharedHandle;
+            externDesc.acquireMutexKey = 1;
+            externDesc.isInitialized = true;
+
+            // DX11 texture should be initialized upon CreateTexture2D. However, if we do not
+            // acquire/release the keyed mutex before using the wrapped WebGPU texture, the WebGPU
+            // texture is left uninitialized. This is required for D3D11 and D3D12 interop.
+            ComPtr<IDXGIKeyedMutex> dxgiKeyedMutex;
+            hr = d3d11Texture.As(&dxgiKeyedMutex);
+            EXPECT_EQ(hr, S_OK);
+
+            hr = dxgiKeyedMutex->AcquireSync(0, INFINITE);
+            EXPECT_EQ(hr, S_OK);
+
+            hr = dxgiKeyedMutex->ReleaseSync(1);
+            EXPECT_EQ(hr, S_OK);
+
+            // Open the DX11 texture in Dawn from the shared handle and return it as a WebGPU
+            // texture.
+            wgpu::Texture wgpuTexture = wgpu::Texture::Acquire(
+                dawn_native::d3d12::WrapSharedHandle(device.Get(), &externDesc));
+
+            // Handle is no longer needed once resources are created.
+            ::CloseHandle(sharedHandle);
+
+            return wgpuTexture;
+        }
+
+        // Vertex shader used to render a sampled texture into a quad.
+        wgpu::ShaderModule GetTestVertexShaderModule() const {
+            return utils::CreateShaderModuleFromWGSL(device, R"(
+                [[builtin(position)]] var<out> Position : vec4<f32>;
+                [[location(0)]] var<out> texCoord : vec2 <f32>;
+
+                [[builtin(vertex_index)]] var<in> VertexIndex : u32;
+
+                [[stage(vertex)]] fn main() -> void {
+                    const pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+                        vec2<f32>(-1.0, 1.0),
+                        vec2<f32>(-1.0, -1.0),
+                        vec2<f32>(1.0, -1.0),
+                        vec2<f32>(-1.0, 1.0),
+                        vec2<f32>(1.0, -1.0),
+                        vec2<f32>(1.0, 1.0)
+                    );
+                    Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+                    texCoord = vec2<f32>(Position.xy * 0.5) + vec2<f32>(0.5, 0.5);
+            })");
+        }
+
+        // The width and height in texels are 4 for all YUV formats.
+        static constexpr uint32_t kYUVImageDataWidthInTexels = 4;
+        static constexpr uint32_t kYUVImageDataHeightInTexels = 4;
+
+        static constexpr size_t kYUVLumaPlaneIndex = 0;
+        static constexpr size_t kYUVChromaPlaneIndex = 1;
+
+        // RGB colors converted into YUV (per plane), for testing.
+        static constexpr std::array<RGBA8, 2> kGreyYUVColor = {RGBA8{126, 0, 0, 0xFF},     // Y
+                                                               RGBA8{128, 128, 0, 0xFF}};  // UV
+
+        static constexpr std::array<RGBA8, 2> kWhiteYUVColor = {RGBA8{235, 0, 0, 0xFF},     // Y
+                                                                RGBA8{128, 128, 0, 0xFF}};  // UV
+
+        ComPtr<ID3D11Device> mD3d11Device;
+
+        bool mIsMultiPlanarFormatsSupported = false;
+    };
+}  // namespace
+
+// Samples the luminance (Y) plane from an imported NV12 texture into a single channel of an RGBA
+// output attachment and checks for the expected pixel value in the rendered quad.
+TEST_P(D3D12VideoViewsTests, NV12SampleYtoR) {
+    wgpu::Texture wgpuTexture = CreateVideoTextureForTest(
+        wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+    wgpu::TextureViewDescriptor viewDesc;
+    viewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+    wgpu::TextureView textureView = wgpuTexture.CreateView(&viewDesc);
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDescriptor(device);
+    renderPipelineDescriptor.vertexStage.module = GetTestVertexShaderModule();
+
+    renderPipelineDescriptor.cFragmentStage.module = utils::CreateShaderModuleFromWGSL(device, R"(
+            [[set(0), binding(0)]] var<uniform_constant> sampler0 : sampler;
+            [[set(0), binding(1)]] var<uniform_constant> texture : texture_2d<f32>;
+
+            [[location(0)]] var<in> texCoord : vec2<f32>;
+            [[location(0)]] var<out> fragColor : vec4<f32>;
+
+            [[stage(fragment)]] fn main() -> void {
+               var y : f32 = textureSample(texture, sampler0, texCoord).r;
+               fragColor = vec4<f32>(y, 0.0, 0.0, 1.0);
+            })");
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(
+        device, kYUVImageDataWidthInTexels, kYUVImageDataHeightInTexels);
+    renderPipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+    renderPipelineDescriptor.primitiveTopology = wgpu::PrimitiveTopology::TriangleList;
+
+    wgpu::RenderPipeline renderPipeline = device.CreateRenderPipeline(&renderPipelineDescriptor);
+
+    wgpu::Sampler sampler = device.CreateSampler();
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(renderPipeline);
+        pass.SetBindGroup(0, utils::MakeBindGroup(device, renderPipeline.GetBindGroupLayout(0),
+                                                  {{0, sampler}, {1, textureView}}));
+        pass.Draw(6);
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    // Test the luma plane in the top left corner of grey RGB image.
+    EXPECT_PIXEL_RGBA8_EQ(kGreyYUVColor[kYUVLumaPlaneIndex], renderPass.color, 0, 0);
+}
+
+// Samples the chrominance (UV) plane from an imported texture into two channels of an RGBA output
+// attachment and checks for the expected pixel value in the rendered quad.
+TEST_P(D3D12VideoViewsTests, NV12SampleUVtoRG) {
+    wgpu::Texture wgpuTexture = CreateVideoTextureForTest(
+        wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+    wgpu::TextureViewDescriptor viewDesc;
+    viewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+    wgpu::TextureView textureView = wgpuTexture.CreateView(&viewDesc);
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDescriptor(device);
+    renderPipelineDescriptor.vertexStage.module = GetTestVertexShaderModule();
+
+    renderPipelineDescriptor.cFragmentStage.module = utils::CreateShaderModuleFromWGSL(device, R"(
+            [[set(0), binding(0)]] var<uniform_constant> sampler0 : sampler;
+            [[set(0), binding(1)]] var<uniform_constant> texture : texture_2d<f32>;
+
+            [[location(0)]] var<in> texCoord : vec2<f32>;
+            [[location(0)]] var<out> fragColor : vec4<f32>;
+
+            [[stage(fragment)]] fn main() -> void {
+               var u : f32 = textureSample(texture, sampler0, texCoord).r;
+               var v : f32 = textureSample(texture, sampler0, texCoord).g;
+               fragColor = vec4<f32>(u, v, 0.0, 1.0);
+            })");
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(
+        device, kYUVImageDataWidthInTexels, kYUVImageDataHeightInTexels);
+    renderPipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+    renderPipelineDescriptor.primitiveTopology = wgpu::PrimitiveTopology::TriangleList;
+
+    wgpu::RenderPipeline renderPipeline = device.CreateRenderPipeline(&renderPipelineDescriptor);
+
+    wgpu::Sampler sampler = device.CreateSampler();
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(renderPipeline);
+        pass.SetBindGroup(0, utils::MakeBindGroup(device, renderPipeline.GetBindGroupLayout(0),
+                                                  {{0, sampler}, {1, textureView}}));
+        pass.Draw(6);
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    // Test the chroma plane in the top left corner of grey RGB image.
+    EXPECT_PIXEL_RGBA8_EQ(kGreyYUVColor[kYUVChromaPlaneIndex], renderPass.color, 0, 0);
+}
+
+// Renders a NV12 "checkerboard" texture into a RGB quad then checks the color at specific
+// points to ensure the image has not been flipped.
+TEST_P(D3D12VideoViewsTests, NV12SampleYUVtoRGB) {
+    wgpu::Texture wgpuTexture = CreateVideoTextureForTest(
+        wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled, true);
+
+    wgpu::TextureViewDescriptor lumaViewDesc;
+    lumaViewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+    wgpu::TextureView lumaTextureView = wgpuTexture.CreateView(&lumaViewDesc);
+
+    wgpu::TextureViewDescriptor chromaViewDesc;
+    chromaViewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+    wgpu::TextureView chromaTextureView = wgpuTexture.CreateView(&chromaViewDesc);
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDescriptor(device);
+    renderPipelineDescriptor.vertexStage.module = GetTestVertexShaderModule();
+
+    renderPipelineDescriptor.cFragmentStage.module = utils::CreateShaderModuleFromWGSL(device, R"(
+            [[set(0), binding(0)]] var<uniform_constant> sampler0 : sampler;
+            [[set(0), binding(1)]] var<uniform_constant> lumaTexture : texture_2d<f32>;
+            [[set(0), binding(2)]] var<uniform_constant> chromaTexture : texture_2d<f32>;
+
+            [[location(0)]] var<in> texCoord : vec2<f32>;
+            [[location(0)]] var<out> fragColor : vec4<f32>;
+
+            [[stage(fragment)]] fn main() -> void {
+               var y : f32 = textureSample(lumaTexture, sampler0, texCoord).r;
+               var u : f32 = textureSample(chromaTexture, sampler0, texCoord).r;
+               var v : f32 = textureSample(chromaTexture, sampler0, texCoord).g;
+               fragColor = vec4<f32>(y, u, v, 1.0);
+            })");
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(
+        device, kYUVImageDataWidthInTexels, kYUVImageDataHeightInTexels);
+    renderPipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+
+    wgpu::RenderPipeline renderPipeline = device.CreateRenderPipeline(&renderPipelineDescriptor);
+
+    wgpu::Sampler sampler = device.CreateSampler();
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(renderPipeline);
+        pass.SetBindGroup(
+            0, utils::MakeBindGroup(device, renderPipeline.GetBindGroupLayout(0),
+                                    {{0, sampler}, {1, lumaTextureView}, {2, chromaTextureView}}));
+        pass.Draw(6);
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    // Test four corners of the grey-white checkerboard image (YUV color space).
+    RGBA8 greyYUV(kGreyYUVColor[kYUVLumaPlaneIndex].r, kGreyYUVColor[kYUVChromaPlaneIndex].r,
+                  kGreyYUVColor[kYUVChromaPlaneIndex].g, 0xFF);
+    EXPECT_PIXEL_RGBA8_EQ(greyYUV, renderPass.color, 0, 0);  // top left
+    EXPECT_PIXEL_RGBA8_EQ(greyYUV, renderPass.color, kYUVImageDataWidthInTexels - 1,
+                          kYUVImageDataHeightInTexels - 1);  // bottom right
+
+    RGBA8 whiteYUV(kWhiteYUVColor[kYUVLumaPlaneIndex].r, kWhiteYUVColor[kYUVChromaPlaneIndex].r,
+                   kWhiteYUVColor[kYUVChromaPlaneIndex].g, 0xFF);
+
+    EXPECT_PIXEL_RGBA8_EQ(whiteYUV, renderPass.color, kYUVImageDataWidthInTexels - 1,
+                          0);  // top right
+    EXPECT_PIXEL_RGBA8_EQ(whiteYUV, renderPass.color, 0,
+                          kYUVImageDataHeightInTexels - 1);  // bottom left
+}
+
+DAWN_INSTANTIATE_TEST(D3D12VideoViewsTests, D3D12Backend());
\ No newline at end of file
diff --git a/src/tests/unittests/validation/VideoViewsValidationTests.cpp b/src/tests/unittests/validation/VideoViewsValidationTests.cpp
new file mode 100644
index 0000000..9eae1a4
--- /dev/null
+++ b/src/tests/unittests/validation/VideoViewsValidationTests.cpp
@@ -0,0 +1,341 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/unittests/validation/ValidationTest.h"
+
+#include "utils/WGPUHelpers.h"
+
+namespace {
+
+    class VideoViewsValidation : public ValidationTest {
+      protected:
+        WGPUDevice CreateTestDevice() override {
+            dawn_native::DeviceDescriptor descriptor;
+            descriptor.requiredExtensions = {"multiplanar_formats"};
+            return adapter.CreateDevice(&descriptor);
+        }
+
+        wgpu::Texture CreateVideoTextureForTest(wgpu::TextureFormat format,
+                                                wgpu::TextureUsage usage) {
+            wgpu::TextureDescriptor descriptor;
+            descriptor.dimension = wgpu::TextureDimension::e2D;
+            descriptor.size.width = 1;
+            descriptor.size.height = 1;
+            descriptor.format = format;
+            descriptor.usage = usage;
+            return device.CreateTexture(&descriptor);
+        }
+    };
+
+    // Test texture views compatibility rules.
+    TEST_F(VideoViewsValidation, CreateViewFails) {
+        wgpu::Texture videoTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::None);
+
+        // Create a default view with no plane selected.
+        ASSERT_DEVICE_ERROR(videoTexture.CreateView());
+
+        wgpu::TextureViewDescriptor viewDesc = {};
+
+        // Correct plane index but incompatible view format.
+        viewDesc.format = wgpu::TextureFormat::R8Uint;
+        viewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+        ASSERT_DEVICE_ERROR(videoTexture.CreateView(&viewDesc));
+
+        // Compatible view format but wrong plane index.
+        viewDesc.format = wgpu::TextureFormat::R8Unorm;
+        viewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+        ASSERT_DEVICE_ERROR(videoTexture.CreateView(&viewDesc));
+
+        // Compatible view format but wrong aspect.
+        viewDesc.format = wgpu::TextureFormat::R8Unorm;
+        viewDesc.aspect = wgpu::TextureAspect::All;
+        ASSERT_DEVICE_ERROR(videoTexture.CreateView(&viewDesc));
+
+        // Create a single plane texture.
+        wgpu::TextureDescriptor desc;
+        desc.format = wgpu::TextureFormat::RGBA8Unorm;
+        desc.dimension = wgpu::TextureDimension::e2D;
+        desc.usage = wgpu::TextureUsage::None;
+        desc.size = {1, 1, 1};
+
+        wgpu::Texture texture = device.CreateTexture(&desc);
+
+        // Plane aspect specified with non-planar texture.
+        viewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+        ASSERT_DEVICE_ERROR(texture.CreateView(&viewDesc));
+
+        viewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+        ASSERT_DEVICE_ERROR(texture.CreateView(&viewDesc));
+
+        // Planar views with non-planar texture.
+        viewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+        viewDesc.format = wgpu::TextureFormat::R8Unorm;
+        ASSERT_DEVICE_ERROR(texture.CreateView(&viewDesc));
+
+        viewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+        viewDesc.format = wgpu::TextureFormat::RG8Unorm;
+        ASSERT_DEVICE_ERROR(texture.CreateView(&viewDesc));
+    }
+
+    // Test texture views compatibility rules.
+    TEST_F(VideoViewsValidation, CreateViewSucceeds) {
+        wgpu::Texture yuvTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::None);
+
+        // Per plane view formats unspecified.
+        wgpu::TextureViewDescriptor planeViewDesc = {};
+        planeViewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+        wgpu::TextureView plane0View = yuvTexture.CreateView(&planeViewDesc);
+
+        planeViewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+        wgpu::TextureView plane1View = yuvTexture.CreateView(&planeViewDesc);
+
+        ASSERT_NE(plane0View.Get(), nullptr);
+        ASSERT_NE(plane1View.Get(), nullptr);
+
+        // Per plane view formats specified.
+        planeViewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+        planeViewDesc.format = wgpu::TextureFormat::R8Unorm;
+        plane0View = yuvTexture.CreateView(&planeViewDesc);
+
+        planeViewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+        planeViewDesc.format = wgpu::TextureFormat::RG8Unorm;
+        plane1View = yuvTexture.CreateView(&planeViewDesc);
+
+        ASSERT_NE(plane0View.Get(), nullptr);
+        ASSERT_NE(plane1View.Get(), nullptr);
+    }
+
+    // Test copying from one multi-planar format into another fails.
+    TEST_F(VideoViewsValidation, T2TCopyAllAspectsFails) {
+        wgpu::Texture srcTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::Texture dstTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::TextureCopyView srcCopyView = utils::CreateTextureCopyView(srcTexture, 0, {0, 0, 0});
+
+        wgpu::TextureCopyView dstCopyView = utils::CreateTextureCopyView(dstTexture, 0, {0, 0, 0});
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToTexture(&srcCopyView, &dstCopyView, &copySize);
+        ASSERT_DEVICE_ERROR(encoder.Finish());
+    }
+
+    // Test copying from one multi-planar format into another per plane fails.
+    TEST_F(VideoViewsValidation, T2TCopyPlaneAspectFails) {
+        wgpu::Texture srcTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::Texture dstTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::TextureCopyView srcCopyView =
+            utils::CreateTextureCopyView(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane0Only);
+
+        wgpu::TextureCopyView dstCopyView =
+            utils::CreateTextureCopyView(dstTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane1Only);
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyTextureToTexture(&srcCopyView, &dstCopyView, &copySize);
+            ASSERT_DEVICE_ERROR(encoder.Finish());
+        }
+
+        srcCopyView =
+            utils::CreateTextureCopyView(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane1Only);
+
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyTextureToTexture(&srcCopyView, &dstCopyView, &copySize);
+            ASSERT_DEVICE_ERROR(encoder.Finish());
+        }
+    }
+
+    // Test copying from a multi-planar format to a buffer fails.
+    TEST_F(VideoViewsValidation, T2BCopyAllAspectsFails) {
+        wgpu::Texture srcTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::BufferDescriptor bufferDescriptor;
+        bufferDescriptor.size = 1;
+        bufferDescriptor.usage = wgpu::BufferUsage::CopyDst;
+        wgpu::Buffer dstBuffer = device.CreateBuffer(&bufferDescriptor);
+
+        wgpu::TextureCopyView srcCopyView = utils::CreateTextureCopyView(srcTexture, 0, {0, 0, 0});
+
+        wgpu::BufferCopyView dstCopyView = utils::CreateBufferCopyView(dstBuffer, 0, 4);
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToBuffer(&srcCopyView, &dstCopyView, &copySize);
+        ASSERT_DEVICE_ERROR(encoder.Finish());
+    }
+
+    // Test copying from multi-planar format per plane to a buffer fails.
+    TEST_F(VideoViewsValidation, T2BCopyPlaneAspectsFails) {
+        wgpu::Texture srcTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::BufferDescriptor bufferDescriptor;
+        bufferDescriptor.size = 1;
+        bufferDescriptor.usage = wgpu::BufferUsage::CopyDst;
+        wgpu::Buffer dstBuffer = device.CreateBuffer(&bufferDescriptor);
+
+        wgpu::TextureCopyView srcCopyView =
+            utils::CreateTextureCopyView(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane0Only);
+
+        wgpu::BufferCopyView dstCopyView = utils::CreateBufferCopyView(dstBuffer, 0, 4);
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyTextureToBuffer(&srcCopyView, &dstCopyView, &copySize);
+            ASSERT_DEVICE_ERROR(encoder.Finish());
+        }
+
+        srcCopyView =
+            utils::CreateTextureCopyView(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane1Only);
+
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyTextureToBuffer(&srcCopyView, &dstCopyView, &copySize);
+            ASSERT_DEVICE_ERROR(encoder.Finish());
+        }
+    }
+
+    // Test copying from a buffer to a multi-planar format fails.
+    TEST_F(VideoViewsValidation, B2TCopyAllAspectsFails) {
+        std::vector<uint8_t> dummyData(4, 0);
+
+        wgpu::Buffer srcBuffer = utils::CreateBufferFromData(
+            device, dummyData.data(), dummyData.size(), wgpu::BufferUsage::CopySrc);
+
+        wgpu::Texture dstTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::BufferCopyView srcCopyView = utils::CreateBufferCopyView(srcBuffer, 0, 12, 4);
+
+        wgpu::TextureCopyView dstCopyView = utils::CreateTextureCopyView(dstTexture, 0, {0, 0, 0});
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyBufferToTexture(&srcCopyView, &dstCopyView, &copySize);
+        ASSERT_DEVICE_ERROR(encoder.Finish());
+    }
+
+    // Test copying from a buffer to a multi-planar format per plane fails.
+    TEST_F(VideoViewsValidation, B2TCopyPlaneAspectsFails) {
+        std::vector<uint8_t> dummyData(4, 0);
+
+        wgpu::Buffer srcBuffer = utils::CreateBufferFromData(
+            device, dummyData.data(), dummyData.size(), wgpu::BufferUsage::CopySrc);
+
+        wgpu::Texture dstTexture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::BufferCopyView srcCopyView = utils::CreateBufferCopyView(srcBuffer, 0, 12, 4);
+
+        wgpu::TextureCopyView dstCopyView =
+            utils::CreateTextureCopyView(dstTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane0Only);
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyBufferToTexture(&srcCopyView, &dstCopyView, &copySize);
+            ASSERT_DEVICE_ERROR(encoder.Finish());
+        }
+
+        dstCopyView =
+            utils::CreateTextureCopyView(dstTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane1Only);
+
+        {
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyBufferToTexture(&srcCopyView, &dstCopyView, &copySize);
+            ASSERT_DEVICE_ERROR(encoder.Finish());
+        }
+    }
+
+    // Tests which multi-planar formats are allowed to be sampled.
+    TEST_F(VideoViewsValidation, SamplingMultiPlanarTexture) {
+        wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
+            device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
+
+        // R8BG8Biplanar420Unorm is allowed to be sampled, if plane 0 or plane 1 is selected.
+        wgpu::Texture texture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::TextureViewDescriptor desc = {};
+
+        desc.aspect = wgpu::TextureAspect::Plane0Only;
+        utils::MakeBindGroup(device, layout, {{0, texture.CreateView(&desc)}});
+
+        desc.aspect = wgpu::TextureAspect::Plane1Only;
+        utils::MakeBindGroup(device, layout, {{0, texture.CreateView(&desc)}});
+    }
+
+    // Tests creating a texture with a multi-plane format.
+    TEST_F(VideoViewsValidation, CreateTextureFails) {
+        // multi-planar formats are NOT allowed to be renderable.
+        ASSERT_DEVICE_ERROR(CreateVideoTextureForTest(wgpu::TextureFormat::R8BG8Biplanar420Unorm,
+                                                      wgpu::TextureUsage::RenderAttachment));
+    }
+
+    // Tests writing into a multi-planar format fails.
+    TEST_F(VideoViewsValidation, WriteTextureAllAspectsFails) {
+        wgpu::Texture texture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::TextureDataLayout textureDataLayout = utils::CreateTextureDataLayout(0, 4, 4);
+
+        wgpu::TextureCopyView textureCopyView = utils::CreateTextureCopyView(texture, 0, {0, 0, 0});
+
+        std::vector<uint8_t> dummyData(4, 0);
+        wgpu::Extent3D writeSize = {1, 1, 1};
+
+        wgpu::Queue queue = device.GetQueue();
+
+        ASSERT_DEVICE_ERROR(queue.WriteTexture(&textureCopyView, dummyData.data(), dummyData.size(),
+                                               &textureDataLayout, &writeSize));
+    }
+
+    // Tests writing into a multi-planar format per plane fails.
+    TEST_F(VideoViewsValidation, WriteTexturePlaneAspectsFails) {
+        wgpu::Texture texture = CreateVideoTextureForTest(
+            wgpu::TextureFormat::R8BG8Biplanar420Unorm, wgpu::TextureUsage::Sampled);
+
+        wgpu::TextureDataLayout textureDataLayout = utils::CreateTextureDataLayout(0, 12, 4);
+        wgpu::TextureCopyView textureCopyView =
+            utils::CreateTextureCopyView(texture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane0Only);
+
+        std::vector<uint8_t> dummmyData(4, 0);
+        wgpu::Extent3D writeSize = {1, 1, 1};
+
+        wgpu::Queue queue = device.GetQueue();
+
+        ASSERT_DEVICE_ERROR(queue.WriteTexture(&textureCopyView, dummmyData.data(),
+                                               dummmyData.size(), &textureDataLayout, &writeSize));
+    }
+
+}  // anonymous namespace
\ No newline at end of file
diff --git a/src/utils/TextureFormatUtils.cpp b/src/utils/TextureFormatUtils.cpp
index 73a22ab..6881da5 100644
--- a/src/utils/TextureFormatUtils.cpp
+++ b/src/utils/TextureFormatUtils.cpp
@@ -109,6 +109,10 @@
 
             case wgpu::TextureFormat::Depth24Plus:
             case wgpu::TextureFormat::Depth24PlusStencil8:
+
+            // Block size of a multi-planar format depends on aspect.
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+
             case wgpu::TextureFormat::Undefined:
                 UNREACHABLE();
         }
@@ -173,6 +177,9 @@
             case wgpu::TextureFormat::BC7RGBAUnormSrgb:
                 return 4u;
 
+            // Block size of a multi-planar format depends on aspect.
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+
             case wgpu::TextureFormat::Undefined:
                 UNREACHABLE();
         }
@@ -237,6 +244,9 @@
             case wgpu::TextureFormat::BC7RGBAUnormSrgb:
                 return 4u;
 
+            // Block size of a multi-planar format depends on aspect.
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+
             case wgpu::TextureFormat::Undefined:
                 UNREACHABLE();
         }