Added MultiPlanarFormatExtendedUsages feature.

This feature allows:
- Creating multiplanar textures without importing external image.
- Including CopyDst as texture's usage.
- Copy per-plane data between texture and buffer.

In future this feature would allow:
- Using the texture as render target.
- Copy per-plane data between textures.

This is useful for Chrome to be able to use a multiplanar shared image
with SwiftShader. Chrome emulates the support by creating "shadow"
WGPUTexture to copy/from the shared image. The WGPUTexture needs to be
multiplanar formatted.

Bug: dawn:1923
Bug: chromium:1467566
Change-Id: I00674c41a59cb3b39a39194520c470662efd56ae
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/148000
Reviewed-by: Austin Eng <enga@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Quyen Le <lehoangquyen@chromium.org>
diff --git a/dawn.json b/dawn.json
index afa734e..378a3e9 100644
--- a/dawn.json
+++ b/dawn.json
@@ -1862,6 +1862,7 @@
             {"value": 1017, "name": "pixel local storage coherent", "tags": ["dawn"]},
             {"value": 1018, "name": "pixel local storage non coherent", "tags": ["dawn"]},
             {"value": 1019, "name": "norm16 texture formats", "tags": ["dawn"]},
+            {"value": 1020, "name": "multi planar format extended usages", "tags": ["dawn"]},
 
             {"value": 1100, "name": "shared texture memory vk dedicated allocation", "tags": ["dawn", "native"]},
             {"value": 1101, "name": "shared texture memory a hardware buffer", "tags": ["dawn", "native"]},
diff --git a/src/dawn/native/CommandValidation.cpp b/src/dawn/native/CommandValidation.cpp
index 7d30e42..26e7899 100644
--- a/src/dawn/native/CommandValidation.cpp
+++ b/src/dawn/native/CommandValidation.cpp
@@ -400,8 +400,9 @@
             ASSERT(format.aspects & Aspect::Stencil);
             return Aspect::Stencil;
         case wgpu::TextureAspect::Plane0Only:
+            return Aspect::Plane0;
         case wgpu::TextureAspect::Plane1Only:
-            break;
+            return Aspect::Plane1;
     }
     UNREACHABLE();
 }
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index b57bf19..d441d66 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -1525,7 +1525,13 @@
 }
 
 void DeviceBase::APIValidateTextureDescriptor(const TextureDescriptor* desc) {
-    DAWN_UNUSED(ConsumedError(ValidateTextureDescriptor(this, desc)));
+    AllowMultiPlanarTextureFormat allowMultiPlanar;
+    if (HasFeature(Feature::MultiPlanarFormatExtendedUsages)) {
+        allowMultiPlanar = AllowMultiPlanarTextureFormat::Yes;
+    } else {
+        allowMultiPlanar = AllowMultiPlanarTextureFormat::No;
+    }
+    DAWN_UNUSED(ConsumedError(ValidateTextureDescriptor(this, desc, allowMultiPlanar)));
 }
 
 QueueBase* DeviceBase::GetQueue() const {
@@ -1806,7 +1812,14 @@
 ResultOrError<Ref<TextureBase>> DeviceBase::CreateTexture(const TextureDescriptor* descriptor) {
     DAWN_TRY(ValidateIsAlive());
     if (IsValidationEnabled()) {
-        DAWN_TRY_CONTEXT(ValidateTextureDescriptor(this, descriptor), "validating %s.", descriptor);
+        AllowMultiPlanarTextureFormat allowMultiPlanar;
+        if (HasFeature(Feature::MultiPlanarFormatExtendedUsages)) {
+            allowMultiPlanar = AllowMultiPlanarTextureFormat::Yes;
+        } else {
+            allowMultiPlanar = AllowMultiPlanarTextureFormat::No;
+        }
+        DAWN_TRY_CONTEXT(ValidateTextureDescriptor(this, descriptor, allowMultiPlanar),
+                         "validating %s.", descriptor);
     }
     return CreateTextureImpl(descriptor);
 }
diff --git a/src/dawn/native/Features.cpp b/src/dawn/native/Features.cpp
index 323d9e5..77b396f 100644
--- a/src/dawn/native/Features.cpp
+++ b/src/dawn/native/Features.cpp
@@ -108,6 +108,11 @@
     {Feature::DawnMultiPlanarFormats,
      {"Import and use multi-planar texture formats with per plane views",
       "https://bugs.chromium.org/p/dawn/issues/detail?id=551", FeatureInfo::FeatureState::Stable}},
+    {Feature::MultiPlanarFormatExtendedUsages,
+     {"Enable creating multi-planar formatted textures directly without importing. Also allows "
+      "including CopyDst as texture's usage and per plane copies between a texture and a buffer.",
+      "https://bugs.chromium.org/p/dawn/issues/detail?id=551",
+      FeatureInfo::FeatureState::Experimental}},
     {Feature::DawnNative,
      {"WebGPU is running on top of dawn_native.",
       "https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/dawn/features/"
diff --git a/src/dawn/native/Format.cpp b/src/dawn/native/Format.cpp
index 9489d24..065e939 100644
--- a/src/dawn/native/Format.cpp
+++ b/src/dawn/native/Format.cpp
@@ -139,6 +139,40 @@
     return aspectInfo[aspectIndex];
 }
 
+Extent3D Format::GetAspectSize(Aspect aspect, const Extent3D& textureSize) const {
+    switch (aspect) {
+        case Aspect::Color:
+        case Aspect::Depth:
+        case Aspect::Stencil:
+        case Aspect::CombinedDepthStencil:
+            return textureSize;
+        case Aspect::Plane0:
+            ASSERT(IsMultiPlanar());
+            return textureSize;
+        case Aspect::Plane1: {
+            ASSERT(IsMultiPlanar());
+            auto planeSize = textureSize;
+            switch (format) {
+                case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                    if (planeSize.width > 1) {
+                        planeSize.width >>= 1;
+                    }
+                    if (planeSize.height > 1) {
+                        planeSize.height >>= 1;
+                    }
+                    break;
+                default:
+                    UNREACHABLE();
+            }
+            return planeSize;
+        }
+        case Aspect::None:
+            break;
+    }
+
+    UNREACHABLE();
+}
+
 FormatIndex Format::GetIndex() const {
     return ComputeFormatIndex(format);
 }
diff --git a/src/dawn/native/Format.h b/src/dawn/native/Format.h
index 96ff348..d571c2f 100644
--- a/src/dawn/native/Format.h
+++ b/src/dawn/native/Format.h
@@ -153,6 +153,9 @@
     // Currently means they differ only in sRGB-ness.
     bool ViewCompatibleWith(const Format& otherFormat) const;
 
+    // Returns the aspect's size given the texture's size.
+    Extent3D GetAspectSize(Aspect aspect, const Extent3D& textureSize) const;
+
   private:
     // Used to store the aspectInfo for one or more planes. For single plane "color" formats,
     // only the first aspect info or aspectInfo[0] is valid. For depth-stencil, the first aspect
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index 0de3436..b560856 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -361,9 +361,12 @@
         // Legacy path
         // TODO(crbug.com/dawn/1795): Remove after migrating all old usages.
         // Only allows simple readonly texture usages.
-        constexpr wgpu::TextureUsage kValidMultiPlanarUsages =
+        wgpu::TextureUsage validMultiPlanarUsages =
             wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc;
-        DAWN_INVALID_IF(format->IsMultiPlanar() && !IsSubset(usage, kValidMultiPlanarUsages),
+        if (device->HasFeature(Feature::MultiPlanarFormatExtendedUsages)) {
+            validMultiPlanarUsages |= wgpu::TextureUsage::CopyDst;
+        }
+        DAWN_INVALID_IF(format->IsMultiPlanar() && !IsSubset(usage, validMultiPlanarUsages),
                         "The texture usage (%s) is incompatible with the multi-planar format (%s).",
                         usage, format->format);
     } else {
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index fae26ae..ea7fd26 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -317,6 +317,7 @@
 
     if (allMultiplanarFormatsSupported) {
         EnableFeature(Feature::DawnMultiPlanarFormats);
+        EnableFeature(Feature::MultiPlanarFormatExtendedUsages);
     }
 
     EnableFeature(Feature::SurfaceCapabilities);
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index 588bc1a..09cd9c5 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -709,7 +709,7 @@
 
     VkImageFormatListCreateInfo imageFormatListInfo = {};
     std::vector<VkFormat> viewFormats;
-    bool requiresCreateMutableFormatBit = GetViewFormats().any();
+    bool requiresViewFormatsList = GetViewFormats().any();
     // As current SPIR-V SPEC doesn't support 'bgra8' as a valid image format, to support the
     // STORAGE usage of BGRA8Unorm we have to create an RGBA8Unorm image view on the BGRA8Unorm
     // storage texture and polyfill it as RGBA8Unorm in Tint. See http://crbug.com/dawn/1641 for
@@ -717,10 +717,19 @@
     if (createInfo.format == VK_FORMAT_B8G8R8A8_UNORM &&
         createInfo.usage & VK_IMAGE_USAGE_STORAGE_BIT) {
         viewFormats.push_back(VK_FORMAT_R8G8B8A8_UNORM);
-        requiresCreateMutableFormatBit = true;
+        requiresViewFormatsList = true;
     }
-    if (requiresCreateMutableFormatBit) {
+    if (GetFormat().IsMultiPlanar() || requiresViewFormatsList) {
+        // Multi-planar image needs to have VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT in order to be able
+        // to create per-plane view. See
+        // https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCreateFlagBits.html
+        //
+        // Note: we cannot include R8 & RG8 in the viewFormats list of
+        // G8_B8R8_2PLANE_420_UNORM. The Vulkan validation layer will disallow that.
         createInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
+    }
+
+    if (requiresViewFormatsList) {
         if (device->GetDeviceInfo().HasExt(DeviceExt::ImageFormatList)) {
             createInfoChain.Add(&imageFormatListInfo,
                                 VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO);
@@ -1272,15 +1281,17 @@
     imageRange.levelCount = 1;
     imageRange.layerCount = 1;
 
-    if (GetFormat().isCompressed) {
+    if (GetFormat().isCompressed || GetFormat().IsMultiPlanar()) {
         if (range.aspects == Aspect::None) {
             return {};
         }
         // need to clear the texture with a copy from buffer
-        ASSERT(range.aspects == Aspect::Color);
+        ASSERT(range.aspects == Aspect::Color || range.aspects == Aspect::Plane0 ||
+               range.aspects == Aspect::Plane1);
         const TexelBlockInfo& blockInfo = GetFormat().GetAspectInfo(range.aspects).block;
 
         Extent3D largestMipSize = GetMipLevelSingleSubresourcePhysicalSize(range.baseMipLevel);
+        largestMipSize = GetFormat().GetAspectSize(range.aspects, largestMipSize);
 
         uint32_t bytesPerRow = Align((largestMipSize.width / blockInfo.width) * blockInfo.byteSize,
                                      device->GetOptimalBytesPerRowAlignment());
@@ -1297,6 +1308,7 @@
         for (uint32_t level = range.baseMipLevel; level < range.baseMipLevel + range.levelCount;
              ++level) {
             Extent3D copySize = GetMipLevelSingleSubresourcePhysicalSize(level);
+            copySize = GetFormat().GetAspectSize(range.aspects, copySize);
             imageRange.baseMipLevel = level;
             for (uint32_t layer = range.baseArrayLayer;
                  layer < range.baseArrayLayer + range.layerCount; ++layer) {
diff --git a/src/dawn/tests/end2end/VideoViewsTests.cpp b/src/dawn/tests/end2end/VideoViewsTests.cpp
index 97942e5..0e8771f 100644
--- a/src/dawn/tests/end2end/VideoViewsTests.cpp
+++ b/src/dawn/tests/end2end/VideoViewsTests.cpp
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "dawn/tests/end2end/VideoViewsTests.h"
+
 #include <utility>
 
-#include "dawn/tests/end2end/VideoViewsTests.h"
+#include "dawn/common/Math.h"
 #include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/TestUtils.h"
+#include "dawn/utils/TextureUtils.h"
 #include "dawn/utils/WGPUHelpers.h"
 
 namespace dawn {
@@ -26,28 +30,18 @@
 
 VideoViewsTestBackend::~VideoViewsTestBackend() = default;
 
-constexpr std::array<utils::RGBA8, 2> VideoViewsTests::kYellowYUVColor;
-constexpr std::array<utils::RGBA8, 2> VideoViewsTests::kWhiteYUVColor;
-constexpr std::array<utils::RGBA8, 2> VideoViewsTests::kBlueYUVColor;
-constexpr std::array<utils::RGBA8, 2> VideoViewsTests::kRedYUVColor;
+constexpr std::array<utils::RGBA8, 2> VideoViewsTestsBase::kYellowYUVColor;
+constexpr std::array<utils::RGBA8, 2> VideoViewsTestsBase::kWhiteYUVColor;
+constexpr std::array<utils::RGBA8, 2> VideoViewsTestsBase::kBlueYUVColor;
+constexpr std::array<utils::RGBA8, 2> VideoViewsTestsBase::kRedYUVColor;
 
-void VideoViewsTests::SetUp() {
+void VideoViewsTestsBase::SetUp() {
     DawnTest::SetUp();
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     DAWN_TEST_UNSUPPORTED_IF(!IsMultiPlanarFormatsSupported());
-
-    mBackend = VideoViewsTestBackend::Create();
-    mBackend->OnSetUp(device.Get());
 }
 
-void VideoViewsTests::TearDown() {
-    if (!UsesWire() && IsMultiPlanarFormatsSupported()) {
-        mBackend->OnTearDown();
-    }
-    DawnTest::TearDown();
-}
-
-std::vector<wgpu::FeatureName> VideoViewsTests::GetRequiredFeatures() {
+std::vector<wgpu::FeatureName> VideoViewsTestsBase::GetRequiredFeatures() {
     std::vector<wgpu::FeatureName> requiredFeatures = {};
     mIsMultiPlanarFormatsSupported = SupportsFeatures({wgpu::FeatureName::DawnMultiPlanarFormats});
     if (mIsMultiPlanarFormatsSupported) {
@@ -57,7 +51,7 @@
     return requiredFeatures;
 }
 
-bool VideoViewsTests::IsMultiPlanarFormatsSupported() const {
+bool VideoViewsTestsBase::IsMultiPlanarFormatsSupported() const {
     return mIsMultiPlanarFormatsSupported;
 }
 
@@ -67,8 +61,8 @@
 // blue block, and bottom left is a 2x2 white block. When |isCheckerboard| is false, the
 // image is converted from a solid yellow 4x4 block.
 // static
-std::vector<uint8_t> VideoViewsTests::GetTestTextureData(wgpu::TextureFormat format,
-                                                         bool isCheckerboard) {
+std::vector<uint8_t> VideoViewsTestsBase::GetTestTextureData(wgpu::TextureFormat format,
+                                                             bool isCheckerboard) {
     constexpr uint8_t Yy = kYellowYUVColor[kYUVLumaPlaneIndex].r;
     constexpr uint8_t Yu = kYellowYUVColor[kYUVChromaPlaneIndex].r;
     constexpr uint8_t Yv = kYellowYUVColor[kYUVChromaPlaneIndex].g;
@@ -132,7 +126,7 @@
     }
 }
 
-uint32_t VideoViewsTests::NumPlanes(wgpu::TextureFormat format) {
+uint32_t VideoViewsTestsBase::NumPlanes(wgpu::TextureFormat format) {
     switch (format) {
         case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
             return 2;
@@ -141,11 +135,11 @@
             return 0;
     }
 }
-std::vector<uint8_t> VideoViewsTests::GetTestTextureDataWithPlaneIndex(size_t planeIndex,
-                                                                       size_t bytesPerRow,
-                                                                       size_t height,
-                                                                       bool isCheckerboard) {
-    std::vector<uint8_t> texelData = VideoViewsTests::GetTestTextureData(
+std::vector<uint8_t> VideoViewsTestsBase::GetTestTextureDataWithPlaneIndex(size_t planeIndex,
+                                                                           size_t bytesPerRow,
+                                                                           size_t height,
+                                                                           bool isCheckerboard) {
+    std::vector<uint8_t> texelData = VideoViewsTestsBase::GetTestTextureData(
         wgpu::TextureFormat::R8BG8Biplanar420Unorm, isCheckerboard);
     const uint32_t texelDataRowBytes = kYUVImageDataWidthInTexels;
     const uint32_t texelDataHeight =
@@ -155,7 +149,7 @@
     uint32_t plane_first_texel_offset = 0;
     // The size of the test video frame is 4 x 4
     switch (planeIndex) {
-        case VideoViewsTests::kYUVLumaPlaneIndex:
+        case VideoViewsTestsBase::kYUVLumaPlaneIndex:
             for (uint32_t i = 0; i < texelDataHeight; ++i) {
                 if (i < texelDataHeight) {
                     for (uint32_t j = 0; j < texelDataRowBytes; ++j) {
@@ -165,7 +159,7 @@
                 }
             }
             return texels;
-        case VideoViewsTests::kYUVChromaPlaneIndex:
+        case VideoViewsTestsBase::kYUVChromaPlaneIndex:
             // TexelData is 4 * 6 size, first 4 * 4 is Y plane, UV plane started
             // at index 16.
             plane_first_texel_offset = 16;
@@ -185,7 +179,7 @@
 }
 
 // Vertex shader used to render a sampled texture into a quad.
-wgpu::ShaderModule VideoViewsTests::GetTestVertexShaderModule() const {
+wgpu::ShaderModule VideoViewsTestsBase::GetTestVertexShaderModule() const {
     return utils::CreateShaderModule(device, R"(
                 struct VertexOut {
                     @location(0) texCoord : vec2 <f32>,
@@ -209,6 +203,28 @@
             })");
 }
 
+class VideoViewsTests : public VideoViewsTestsBase {
+  protected:
+    void SetUp() override {
+        VideoViewsTestsBase::SetUp();
+        DAWN_TEST_UNSUPPORTED_IF(UsesWire());
+        DAWN_TEST_UNSUPPORTED_IF(!IsMultiPlanarFormatsSupported());
+
+        mBackend = VideoViewsTestBackend::Create();
+        mBackend->OnSetUp(device.Get());
+    }
+
+    void TearDown() override {
+        if (!UsesWire() && IsMultiPlanarFormatsSupported()) {
+            mBackend->OnTearDown();
+        }
+        VideoViewsTestsBase::TearDown();
+    }
+    std::unique_ptr<VideoViewsTestBackend> mBackend;
+};
+
+namespace {
+
 // Create video texture uninitialized.
 TEST_P(VideoViewsTests, CreateVideoTextureWithoutInitializedData) {
     ASSERT_DEVICE_ERROR(
@@ -220,8 +236,6 @@
     mBackend->DestroyVideoTextureForTest(std::move(platformTexture));
 }
 
-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(VideoViewsTests, NV12SampleYtoR) {
@@ -887,8 +901,245 @@
     mBackend->DestroyVideoTextureForTest(std::move(platformTexture));
 }
 
+class VideoViewsExtendedUsagesTests : public VideoViewsTestsBase {
+  protected:
+    void SetUp() override {
+        VideoViewsTestsBase::SetUp();
+
+        DAWN_TEST_UNSUPPORTED_IF(!IsMultiPlanarFormatsSupported());
+
+        DAWN_TEST_UNSUPPORTED_IF(
+            !device.HasFeature(wgpu::FeatureName::MultiPlanarFormatExtendedUsages));
+    }
+
+    std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+        std::vector<wgpu::FeatureName> requiredFeatures =
+            VideoViewsTestsBase::GetRequiredFeatures();
+        if (SupportsFeatures({wgpu::FeatureName::MultiPlanarFormatExtendedUsages})) {
+            requiredFeatures.push_back(wgpu::FeatureName::MultiPlanarFormatExtendedUsages);
+        }
+        return requiredFeatures;
+    }
+
+    wgpu::Texture CreateMultiPlanarTexture(wgpu::TextureFormat format,
+                                           wgpu::TextureUsage usage,
+                                           bool isCheckerboard = false,
+                                           bool initialized = true) {
+        wgpu::TextureDescriptor desc;
+        desc.format = format;
+        desc.size = {VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                     VideoViewsTestsBase::kYUVImageDataHeightInTexels, 1};
+        desc.usage = usage;
+
+        wgpu::DawnTextureInternalUsageDescriptor internalDesc;
+        internalDesc.internalUsage = wgpu::TextureUsage::CopyDst;
+        desc.nextInChain = &internalDesc;
+
+        auto texture = device.CreateTexture(&desc);
+        if (texture == nullptr) {
+            return nullptr;
+        }
+
+        if (initialized) {
+            size_t numPlanes = VideoViewsTestsBase::NumPlanes(format);
+
+            wgpu::DawnEncoderInternalUsageDescriptor encoderInternalDesc;
+            encoderInternalDesc.useInternalUsages = true;
+            wgpu::CommandEncoderDescriptor encoderDesc;
+            encoderDesc.nextInChain = &encoderInternalDesc;
+
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder(&encoderDesc);
+
+            for (size_t plane = 0; plane < numPlanes; ++plane) {
+                size_t bytesPerRow = VideoViewsTestsBase::kYUVImageDataWidthInTexels;
+                bytesPerRow = Align(bytesPerRow, 256);
+
+                wgpu::ImageCopyTexture copyDst =
+                    utils::CreateImageCopyTexture(texture, 0, {0, 0, 0});
+
+                wgpu::Extent3D copySize{VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                                        VideoViewsTestsBase::kYUVImageDataHeightInTexels, 1};
+                switch (plane) {
+                    case VideoViewsTestsBase::kYUVLumaPlaneIndex:
+                        copyDst.aspect = wgpu::TextureAspect::Plane0Only;
+                        break;
+                    case VideoViewsTestsBase::kYUVChromaPlaneIndex:
+                        copyDst.aspect = wgpu::TextureAspect::Plane1Only;
+                        copySize.width /= 2;
+                        copySize.height /= 2;
+                        break;
+                    default:
+                        UNREACHABLE();
+                }
+
+                // Staging buffer.
+                wgpu::BufferDescriptor bufferDesc;
+                bufferDesc.size = bytesPerRow * copySize.height;
+                bufferDesc.mappedAtCreation = true;
+                bufferDesc.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::MapWrite;
+
+                auto buffer = device.CreateBuffer(&bufferDesc);
+
+                std::vector<uint8_t> data = VideoViewsTestsBase::GetTestTextureDataWithPlaneIndex(
+                    plane, bytesPerRow, VideoViewsTestsBase::kYUVImageDataHeightInTexels,
+                    isCheckerboard);
+
+                memcpy(buffer.GetMappedRange(), data.data(), bufferDesc.size);
+                buffer.Unmap();
+
+                wgpu::ImageCopyBuffer copySrc =
+                    utils::CreateImageCopyBuffer(buffer, 0, bytesPerRow);
+
+                encoder.CopyBufferToTexture(&copySrc, &copyDst, &copySize);
+            }  // for plane
+
+            auto cmdBuffer = encoder.Finish();
+            device.GetQueue().Submit(1, &cmdBuffer);
+        }  // initialized
+
+        return texture;
+    }
+};
+
+// Test that creating multi-planar texture should success if device is created with
+// MultiPlanarFormatExtendedUsages feature enabled.
+TEST_P(VideoViewsExtendedUsagesTests, CreateTextureSucceeds) {
+    auto texture = CreateMultiPlanarTexture(wgpu::TextureFormat::R8BG8Biplanar420Unorm,
+                                            wgpu::TextureUsage::TextureBinding);
+    EXPECT_NE(texture, nullptr);
+}
+
+// Tests sampling a multi-planar texture.
+TEST_P(VideoViewsExtendedUsagesTests, SamplingMultiPlanarTexture) {
+    // TODO(crbug.com/dawn/1998): Failure on Intel's Vulkan device.
+    DAWN_SUPPRESS_TEST_IF(IsWindows() && IsVulkan() && IsIntel());
+
+    auto texture = CreateMultiPlanarTexture(wgpu::TextureFormat::R8BG8Biplanar420Unorm,
+                                            wgpu::TextureUsage::TextureBinding,
+                                            /*isCheckerboard*/ true,
+                                            /*initialized*/ true);
+    EXPECT_NE(texture, nullptr);
+
+    wgpu::TextureViewDescriptor lumaViewDesc;
+    lumaViewDesc.format = wgpu::TextureFormat::R8Unorm;
+    lumaViewDesc.aspect = wgpu::TextureAspect::Plane0Only;
+    wgpu::TextureView lumaTextureView = texture.CreateView(&lumaViewDesc);
+
+    wgpu::TextureViewDescriptor chromaViewDesc;
+    chromaViewDesc.format = wgpu::TextureFormat::RG8Unorm;
+    chromaViewDesc.aspect = wgpu::TextureAspect::Plane1Only;
+    wgpu::TextureView chromaTextureView = texture.CreateView(&chromaViewDesc);
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDescriptor;
+    renderPipelineDescriptor.vertex.module = GetTestVertexShaderModule();
+
+    renderPipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+            @group(0) @binding(0) var sampler0 : sampler;
+            @group(0) @binding(1) var lumaTexture : texture_2d<f32>;
+            @group(0) @binding(2) var chromaTexture : texture_2d<f32>;
+
+            @fragment
+            fn main(@location(0) texCoord : vec2f) -> @location(0) vec4f {
+               let y : f32 = textureSample(lumaTexture, sampler0, texCoord).r;
+               let u : f32 = textureSample(chromaTexture, sampler0, texCoord).r;
+               let v : f32 = textureSample(chromaTexture, sampler0, texCoord).g;
+               return vec4f(y, u, v, 1.0);
+            })");
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(
+        device, kYUVImageDataWidthInTexels, kYUVImageDataHeightInTexels);
+    renderPipelineDescriptor.cTargets[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.End();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    std::vector<uint8_t> expectedData =
+        GetTestTextureData(wgpu::TextureFormat::RGBA8Unorm, /*isCheckerboard*/ true);
+    std::vector<utils::RGBA8> expectedRGBA;
+    for (uint8_t i = 0; i < expectedData.size(); i += 3) {
+        expectedRGBA.push_back({expectedData[i], expectedData[i + 1], expectedData[i + 2], 0xFF});
+    }
+
+    EXPECT_TEXTURE_EQ(expectedRGBA.data(), renderPass.color, {0, 0},
+                      {kYUVImageDataWidthInTexels, kYUVImageDataHeightInTexels});
+}
+
+// Test copying from multi-planar format per plane to a buffer succeeds.
+TEST_P(VideoViewsExtendedUsagesTests, T2BCopyPlaneAspectsSucceeds) {
+    const std::vector<uint8_t> expectedData =
+        GetTestTextureData(wgpu::TextureFormat::RGBA8Unorm, /*isCheckerboard*/ false);
+
+    auto srcTexture =
+        CreateMultiPlanarTexture(wgpu::TextureFormat::R8BG8Biplanar420Unorm,
+                                 wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc,
+                                 /*isCheckerboard*/ false,
+                                 /*initialized*/ true);
+    EXPECT_NE(srcTexture, nullptr);
+
+    wgpu::BufferDescriptor bufferDescriptor;
+    bufferDescriptor.size = 256;
+    bufferDescriptor.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
+    wgpu::Buffer dstBuffer = device.CreateBuffer(&bufferDescriptor);
+
+    wgpu::ImageCopyBuffer copyDst = utils::CreateImageCopyBuffer(dstBuffer, 0, 256);
+
+    wgpu::Extent3D copySize = {1, 1, 1};
+
+    // Plane0
+    wgpu::ImageCopyTexture copySrc =
+        utils::CreateImageCopyTexture(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane0Only);
+
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
+
+        auto cmdBuffer = encoder.Finish();
+        device.GetQueue().Submit(1, &cmdBuffer);
+
+        EXPECT_BUFFER_U8_EQ(expectedData[0], dstBuffer, 0);
+    }
+
+    // Plane1
+    copySrc =
+        utils::CreateImageCopyTexture(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::Plane1Only);
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
+
+        auto cmdBuffer = encoder.Finish();
+        device.GetQueue().Submit(1, &cmdBuffer);
+
+        uint16_t expectedUVData;
+        memcpy(&expectedUVData, expectedData.data() + 1, sizeof(expectedUVData));
+        EXPECT_BUFFER_U16_EQ(expectedUVData, dstBuffer, 0);
+    }
+}
+
 DAWN_INSTANTIATE_TEST_V(VideoViewsTests, VideoViewsTestBackend::Backends());
 DAWN_INSTANTIATE_TEST_V(VideoViewsValidationTests, VideoViewsTestBackend::Backends());
 
+DAWN_INSTANTIATE_TEST(VideoViewsExtendedUsagesTests,
+                      D3D11Backend(),
+                      D3D12Backend(),
+                      MetalBackend(),
+                      OpenGLBackend(),
+                      OpenGLESBackend(),
+                      VulkanBackend());
+
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/end2end/VideoViewsTests.h b/src/dawn/tests/end2end/VideoViewsTests.h
index 0e1b894..bfc24ad 100644
--- a/src/dawn/tests/end2end/VideoViewsTests.h
+++ b/src/dawn/tests/end2end/VideoViewsTests.h
@@ -53,7 +53,7 @@
     virtual void DestroyVideoTextureForTest(std::unique_ptr<PlatformTexture>&& platformTexture) = 0;
 };
 
-class VideoViewsTests : public DawnTest {
+class VideoViewsTestsBase : public DawnTest {
   public:
     // The width and height in texels are 4 for all YUV formats.
     static constexpr uint32_t kYUVImageDataWidthInTexels = 4;
@@ -90,12 +90,10 @@
 
   protected:
     void SetUp() override;
-    void TearDown() override;
     std::vector<wgpu::FeatureName> GetRequiredFeatures() override;
     bool IsMultiPlanarFormatsSupported() const;
     wgpu::ShaderModule GetTestVertexShaderModule() const;
 
-    std::unique_ptr<VideoViewsTestBackend> mBackend;
     bool mIsMultiPlanarFormatsSupported = false;
 };
 
diff --git a/src/dawn/tests/end2end/VideoViewsTests_gbm.cpp b/src/dawn/tests/end2end/VideoViewsTests_gbm.cpp
index 8de4141..08461c5 100644
--- a/src/dawn/tests/end2end/VideoViewsTests_gbm.cpp
+++ b/src/dawn/tests/end2end/VideoViewsTests_gbm.cpp
@@ -143,8 +143,8 @@
             // of I915_FORMAT_MOD_Y_TILED.
             flags |= GBM_BO_USE_SW_WRITE_RARELY;
         }
-        gbm_bo* gbmBo = gbm_bo_create(mGbmDevice, VideoViewsTests::kYUVImageDataWidthInTexels,
-                                      VideoViewsTests::kYUVImageDataHeightInTexels,
+        gbm_bo* gbmBo = gbm_bo_create(mGbmDevice, VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                                      VideoViewsTestsBase::kYUVImageDataHeightInTexels,
                                       GetGbmBoFormat(format), flags);
         if (gbmBo == nullptr) {
             return nullptr;
@@ -153,18 +153,18 @@
         if (initialized) {
             void* mapHandle = nullptr;
             uint32_t strideBytes = 0;
-            void* addr = gbm_bo_map(gbmBo, 0, 0, VideoViewsTests::kYUVImageDataWidthInTexels,
-                                    VideoViewsTests::kYUVImageDataHeightInTexels,
+            void* addr = gbm_bo_map(gbmBo, 0, 0, VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                                    VideoViewsTestsBase::kYUVImageDataHeightInTexels,
                                     GBM_BO_TRANSFER_WRITE, &strideBytes, &mapHandle);
             EXPECT_NE(addr, nullptr);
             std::vector<uint8_t> initialData =
-                VideoViewsTests::GetTestTextureData(format, isCheckerboard);
+                VideoViewsTestsBase::GetTestTextureData(format, isCheckerboard);
             uint8_t* srcBegin = initialData.data();
             uint8_t* srcEnd = srcBegin + initialData.size();
             uint8_t* dstBegin = static_cast<uint8_t*>(addr);
-            for (; srcBegin < srcEnd;
-                 srcBegin += VideoViewsTests::kYUVImageDataWidthInTexels, dstBegin += strideBytes) {
-                std::memcpy(dstBegin, srcBegin, VideoViewsTests::kYUVImageDataWidthInTexels);
+            for (; srcBegin < srcEnd; srcBegin += VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                                      dstBegin += strideBytes) {
+                std::memcpy(dstBegin, srcBegin, VideoViewsTestsBase::kYUVImageDataWidthInTexels);
             }
 
             gbm_bo_unmap(gbmBo, mapHandle);
@@ -174,8 +174,8 @@
         textureDesc.format = format;
         textureDesc.dimension = wgpu::TextureDimension::e2D;
         textureDesc.usage = usage;
-        textureDesc.size = {VideoViewsTests::kYUVImageDataWidthInTexels,
-                            VideoViewsTests::kYUVImageDataHeightInTexels, 1};
+        textureDesc.size = {VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                            VideoViewsTestsBase::kYUVImageDataHeightInTexels, 1};
 
         wgpu::DawnTextureInternalUsageDescriptor internalDesc;
         internalDesc.internalUsage = wgpu::TextureUsage::CopySrc;
diff --git a/src/dawn/tests/end2end/VideoViewsTests_mac.cpp b/src/dawn/tests/end2end/VideoViewsTests_mac.cpp
index b1eb583..1d58acd 100644
--- a/src/dawn/tests/end2end/VideoViewsTests_mac.cpp
+++ b/src/dawn/tests/end2end/VideoViewsTests_mac.cpp
@@ -66,7 +66,7 @@
     size_t GetSubSamplingFactorPerPlane(wgpu::TextureFormat format, size_t plane) {
         switch (format) {
             case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
-                return plane == VideoViewsTests::kYUVLumaPlaneIndex ? 1 : 2;
+                return plane == VideoViewsTestsBase::kYUVLumaPlaneIndex ? 1 : 2;
             default:
                 UNREACHABLE();
                 return 0;
@@ -76,7 +76,7 @@
     size_t BytesPerElement(wgpu::TextureFormat format, size_t plane) {
         switch (format) {
             case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
-                return plane == VideoViewsTests::kYUVLumaPlaneIndex ? 1 : 2;
+                return plane == VideoViewsTestsBase::kYUVLumaPlaneIndex ? 1 : 2;
             default:
                 UNREACHABLE();
                 return 0;
@@ -91,19 +91,19 @@
         CFMutableDictionaryRef dict(CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
                                                               &kCFTypeDictionaryKeyCallBacks,
                                                               &kCFTypeDictionaryValueCallBacks));
-        AddIntegerValue(dict, kIOSurfaceWidth, VideoViewsTests::kYUVImageDataWidthInTexels);
-        AddIntegerValue(dict, kIOSurfaceHeight, VideoViewsTests::kYUVImageDataHeightInTexels);
+        AddIntegerValue(dict, kIOSurfaceWidth, VideoViewsTestsBase::kYUVImageDataWidthInTexels);
+        AddIntegerValue(dict, kIOSurfaceHeight, VideoViewsTestsBase::kYUVImageDataHeightInTexels);
         AddIntegerValue(dict, kIOSurfacePixelFormat, ToCVFormat(format));
 
-        size_t num_planes = VideoViewsTests::NumPlanes(format);
+        size_t num_planes = VideoViewsTestsBase::NumPlanes(format);
 
         CFMutableArrayRef planes(
             CFArrayCreateMutable(kCFAllocatorDefault, num_planes, &kCFTypeArrayCallBacks));
         size_t total_bytes_alloc = 0;
         for (size_t plane = 0; plane < num_planes; ++plane) {
             const size_t factor = GetSubSamplingFactorPerPlane(format, plane);
-            const size_t plane_width = VideoViewsTests::kYUVImageDataWidthInTexels / factor;
-            const size_t plane_height = VideoViewsTests::kYUVImageDataHeightInTexels / factor;
+            const size_t plane_width = VideoViewsTestsBase::kYUVImageDataWidthInTexels / factor;
+            const size_t plane_height = VideoViewsTestsBase::kYUVImageDataHeightInTexels / factor;
             const size_t plane_bytes_per_element = BytesPerElement(format, plane);
             const size_t plane_bytes_per_row = IOSurfaceAlignProperty(
                 kIOSurfacePlaneBytesPerRow, plane_width * plane_bytes_per_element);
@@ -138,7 +138,7 @@
         if (initialized) {
             IOSurfaceLock(surface, 0, nullptr);
             for (size_t plane = 0; plane < num_planes; ++plane) {
-                std::vector<uint8_t> data = VideoViewsTests::GetTestTextureDataWithPlaneIndex(
+                std::vector<uint8_t> data = VideoViewsTestsBase::GetTestTextureDataWithPlaneIndex(
                     plane, IOSurfaceGetBytesPerRowOfPlane(surface, plane),
                     IOSurfaceGetHeightOfPlane(surface, plane), isCheckerboard);
                 void* pointer = IOSurfaceGetBaseAddressOfPlane(surface, plane);
@@ -151,8 +151,8 @@
         textureDesc.format = format;
         textureDesc.dimension = wgpu::TextureDimension::e2D;
         textureDesc.usage = usage;
-        textureDesc.size = {VideoViewsTests::kYUVImageDataWidthInTexels,
-                            VideoViewsTests::kYUVImageDataHeightInTexels, 1};
+        textureDesc.size = {VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                            VideoViewsTestsBase::kYUVImageDataHeightInTexels, 1};
 
         wgpu::DawnTextureInternalUsageDescriptor internalDesc;
         internalDesc.internalUsage = wgpu::TextureUsage::CopySrc;
diff --git a/src/dawn/tests/end2end/VideoViewsTests_win.cpp b/src/dawn/tests/end2end/VideoViewsTests_win.cpp
index f505716..c30235c 100644
--- a/src/dawn/tests/end2end/VideoViewsTests_win.cpp
+++ b/src/dawn/tests/end2end/VideoViewsTests_win.cpp
@@ -99,13 +99,13 @@
         textureDesc.format = format;
         textureDesc.dimension = wgpu::TextureDimension::e2D;
         textureDesc.usage = usage;
-        textureDesc.size = {VideoViewsTests::kYUVImageDataWidthInTexels,
-                            VideoViewsTests::kYUVImageDataHeightInTexels, 1};
+        textureDesc.size = {VideoViewsTestsBase::kYUVImageDataWidthInTexels,
+                            VideoViewsTestsBase::kYUVImageDataHeightInTexels, 1};
 
         // Create a DX11 texture with data then wrap it in a shared handle.
         D3D11_TEXTURE2D_DESC d3dDescriptor;
-        d3dDescriptor.Width = VideoViewsTests::kYUVImageDataWidthInTexels;
-        d3dDescriptor.Height = VideoViewsTests::kYUVImageDataHeightInTexels;
+        d3dDescriptor.Width = VideoViewsTestsBase::kYUVImageDataWidthInTexels;
+        d3dDescriptor.Height = VideoViewsTestsBase::kYUVImageDataHeightInTexels;
         d3dDescriptor.MipLevels = 1;
         d3dDescriptor.ArraySize = 1;
         d3dDescriptor.Format = GetDXGITextureFormat(format);
@@ -117,11 +117,11 @@
         d3dDescriptor.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED;
 
         std::vector<uint8_t> initialData =
-            VideoViewsTests::GetTestTextureData(format, isCheckerboard);
+            VideoViewsTestsBase::GetTestTextureData(format, isCheckerboard);
 
         D3D11_SUBRESOURCE_DATA subres;
         subres.pSysMem = initialData.data();
-        subres.SysMemPitch = VideoViewsTests::kYUVImageDataWidthInTexels;
+        subres.SysMemPitch = VideoViewsTestsBase::kYUVImageDataWidthInTexels;
 
         ComPtr<ID3D11Texture2D> d3d11Texture;
         HRESULT hr = mD3d11Device->CreateTexture2D(
diff --git a/src/dawn/wire/SupportedFeatures.cpp b/src/dawn/wire/SupportedFeatures.cpp
index f5fbeed..bbbb8bb 100644
--- a/src/dawn/wire/SupportedFeatures.cpp
+++ b/src/dawn/wire/SupportedFeatures.cpp
@@ -52,6 +52,7 @@
         case WGPUFeatureName_DepthClipControl:
         case WGPUFeatureName_DawnInternalUsages:
         case WGPUFeatureName_DawnMultiPlanarFormats:
+        case WGPUFeatureName_MultiPlanarFormatExtendedUsages:
         case WGPUFeatureName_ChromiumExperimentalDp4a:
         case WGPUFeatureName_ShaderF16:
         case WGPUFeatureName_RG11B10UfloatRenderable: