[YCbCr Samplers] Add support for TextureViewDescriptor

Add YCbCrVulkanDescriptor to TextureViewDescriptor and necessary
support needed for creating a vkImageView out of it. Also add some
validation and tests.

Change-Id: I38a5019febdcc9c6cd9c38832719fa0f7eaed6a5
Bug: dawn:2476
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/184621
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Saifuddin Hitawala <hitawala@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/include/dawn/native/VulkanBackend.h b/include/dawn/native/VulkanBackend.h
index d5f31b4..141e4b5 100644
--- a/include/dawn/native/VulkanBackend.h
+++ b/include/dawn/native/VulkanBackend.h
@@ -82,7 +82,7 @@
     using ExternalImageExportInfo::ExternalImageExportInfo;
 };
 
-// Can be chained in WGPUSamplerDescriptor
+// Can be chained in WGPUSamplerDescriptor and WGPUTextureViewDescriptor
 struct DAWN_NATIVE_EXPORT YCbCrVulkanDescriptor : wgpu::ChainedStruct {
     YCbCrVulkanDescriptor();
 
diff --git a/src/dawn/native/ChainUtilsImpl.inl b/src/dawn/native/ChainUtilsImpl.inl
index 6850ad3..c94d4da 100644
--- a/src/dawn/native/ChainUtilsImpl.inl
+++ b/src/dawn/native/ChainUtilsImpl.inl
@@ -113,6 +113,12 @@
         AdditionalExtensionsList<const vulkan::YCbCrVulkanDescriptor*>;
 };
 
+template <>
+struct AdditionalExtensions<TextureViewDescriptor> {
+    using List =
+        AdditionalExtensionsList<const vulkan::YCbCrVulkanDescriptor*>;
+};
+
 }  // namespace detail
 }  // namespace dawn::native
 
diff --git a/src/dawn/native/CommandBufferStateTracker.cpp b/src/dawn/native/CommandBufferStateTracker.cpp
index 4ee8e11..cbc445c 100644
--- a/src/dawn/native/CommandBufferStateTracker.cpp
+++ b/src/dawn/native/CommandBufferStateTracker.cpp
@@ -613,6 +613,8 @@
     }
 
     if (aspects[VALIDATION_ASPECT_BIND_GROUPS]) {
+        // TODO(crbug.com/dawn/2476): Validate TextureViewDescriptor YCbCrInfo matches with that in
+        // SamplerDescriptor.
         for (BindGroupIndex i : IterateBitSet(mLastPipelineLayout->GetBindGroupLayoutsMask())) {
             DAWN_ASSERT(HasPipeline());
 
diff --git a/src/dawn/native/Subresource.cpp b/src/dawn/native/Subresource.cpp
index 974cde3..2429029 100644
--- a/src/dawn/native/Subresource.cpp
+++ b/src/dawn/native/Subresource.cpp
@@ -82,6 +82,8 @@
 }
 
 Aspect SelectFormatAspects(const Format& format, wgpu::TextureAspect aspect) {
+    // TODO(crbug.com/dawn/2476): Return Aspect::Color for TextureFormat::External if aspect is
+    // present else None.
     switch (aspect) {
         case wgpu::TextureAspect::All:
             return format.aspects;
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index 76fb4af..c3ce8af 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -46,6 +46,11 @@
 #include "dawn/native/ValidationUtils_autogen.h"
 
 namespace dawn::native {
+
+namespace vulkan {
+struct YCbCrVulkanDescriptor;
+}
+
 namespace {
 
 MaybeError ValidateTextureViewFormatCompatibility(const DeviceBase* device,
@@ -589,13 +594,12 @@
 MaybeError ValidateTextureViewDescriptor(const DeviceBase* device,
                                          const TextureBase* texture,
                                          const UnpackedPtr<TextureViewDescriptor>& descriptor) {
-    DAWN_INVALID_IF(descriptor->nextInChain != nullptr, "nextInChain must be nullptr.");
-
     // Parent texture should have been already validated.
     DAWN_ASSERT(texture);
     DAWN_ASSERT(!texture->IsError());
 
     DAWN_TRY(ValidateTextureViewDimension(descriptor->dimension));
+    // TODO(crbug.com/dawn/2476): Add necessary validation for TextureFormat::External.
     DAWN_TRY(ValidateTextureFormat(descriptor->format));
     DAWN_TRY(ValidateTextureAspect(descriptor->aspect));
 
@@ -626,9 +630,16 @@
         "texture's mip level count (%u).",
         descriptor->baseMipLevel, descriptor->mipLevelCount, texture->GetNumMipLevels());
 
+    // TODO(crbug.com/dawn/2476): Add necessary validations to CanViewTextureAs for
+    // TextureFormat::External.
     DAWN_TRY(ValidateCanViewTextureAs(device, texture, *viewFormat, descriptor->aspect));
     DAWN_TRY(ValidateTextureViewDimensionCompatibility(device, texture, descriptor));
 
+    if (descriptor.Get<vulkan::YCbCrVulkanDescriptor>()) {
+        DAWN_INVALID_IF(!device->HasFeature(Feature::YCbCrVulkanSamplers), "%s is not enabled.",
+                        wgpu::FeatureName::YCbCrVulkanSamplers);
+    }
+
     return {};
 }
 
@@ -667,6 +678,7 @@
         }
     }
 
+    // TODO(crbug.com/dawn/2476): Add TextureFormat::External validation.
     if (desc.format == wgpu::TextureFormat::Undefined) {
         const Format& format = texture->GetFormat();
 
diff --git a/src/dawn/native/vulkan/SamplerVk.cpp b/src/dawn/native/vulkan/SamplerVk.cpp
index 4f3e334..f6c3c2e 100644
--- a/src/dawn/native/vulkan/SamplerVk.cpp
+++ b/src/dawn/native/vulkan/SamplerVk.cpp
@@ -125,22 +125,7 @@
     if (auto* vulkanYCbCrDescriptor = Unpack(descriptor).Get<vulkan::YCbCrVulkanDescriptor>()) {
         const VkSamplerYcbcrConversionCreateInfo& vulkanYCbCrInfo =
             vulkanYCbCrDescriptor->vulkanYCbCrInfo;
-#if DAWN_PLATFORM_IS(ANDROID)
-        const VkBaseInStructure* chain =
-            static_cast<const VkBaseInStructure*>(vulkanYCbCrInfo.pNext);
-        while (chain != nullptr) {
-            if (chain->sType == VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID) {
-                const VkExternalFormatANDROID* vkExternalFormat =
-                    reinterpret_cast<const VkExternalFormatANDROID*>(chain);
-                DAWN_INVALID_IF((vkExternalFormat->externalFormat == 0 &&
-                                 vulkanYCbCrInfo.format == VK_FORMAT_UNDEFINED),
-                                "Both VkFormat and VkExternalFormatANDROID are undefined.");
-                break;
-            }
-            chain = chain->pNext;
-        }
-#endif  // DAWN_PLATFORM_IS(ANDROID)
-
+        DAWN_TRY(ValidateCanCreateSamplerYCbCrConversion(vulkanYCbCrInfo));
         DAWN_TRY(CheckVkSuccess(
             device->fn.CreateSamplerYcbcrConversion(device->GetVkDevice(), &vulkanYCbCrInfo,
                                                     nullptr, &*mSamplerYCbCrConversion),
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index df558e5..5a11158 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -1760,6 +1760,24 @@
     usageInfo.usage = VulkanImageUsage(usage, GetFormat());
     createInfo.pNext = &usageInfo;
 
+    VkSamplerYcbcrConversionInfo samplerYCbCrInfo = {};
+    if (auto* vulkanYCbCrDescriptor = descriptor.Get<vulkan::YCbCrVulkanDescriptor>()) {
+        // TODO(crbug.com/dawn/2476): Validate mSamplerYcbcrConversionCreateInfo matches with that
+        // in SamplerDescriptor.
+        mSamplerYcbcrConversionCreateInfo = vulkanYCbCrDescriptor->vulkanYCbCrInfo;
+        DAWN_TRY(ValidateCanCreateSamplerYCbCrConversion(mSamplerYcbcrConversionCreateInfo));
+        DAWN_TRY(CheckVkSuccess(device->fn.CreateSamplerYcbcrConversion(
+                                    device->GetVkDevice(), &mSamplerYcbcrConversionCreateInfo,
+                                    nullptr, &*mSamplerYCbCrConversion),
+                                "CreateSamplerYcbcrConversion for vkImageView"));
+
+        samplerYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO;
+        samplerYCbCrInfo.pNext = nullptr;
+        samplerYCbCrInfo.conversion = mSamplerYCbCrConversion;
+
+        createInfo.pNext = &samplerYCbCrInfo;
+    }
+
     DAWN_TRY(CheckVkSuccess(
         device->fn.CreateImageView(device->GetVkDevice(), &createInfo, nullptr, &*mHandle),
         "CreateImageView"));
@@ -1784,6 +1802,11 @@
 void TextureView::DestroyImpl() {
     Device* device = ToBackend(GetTexture()->GetDevice());
 
+    if (mSamplerYCbCrConversion != VK_NULL_HANDLE) {
+        device->GetFencedDeleter()->DeleteWhenUnused(mSamplerYCbCrConversion);
+        mSamplerYCbCrConversion = VK_NULL_HANDLE;
+    }
+
     if (mHandle != VK_NULL_HANDLE) {
         device->GetFencedDeleter()->DeleteWhenUnused(mHandle);
         mHandle = VK_NULL_HANDLE;
diff --git a/src/dawn/native/vulkan/TextureVk.h b/src/dawn/native/vulkan/TextureVk.h
index f38fd7d..9923e86 100644
--- a/src/dawn/native/vulkan/TextureVk.h
+++ b/src/dawn/native/vulkan/TextureVk.h
@@ -257,6 +257,8 @@
 
     VkImageView mHandle = VK_NULL_HANDLE;
     VkImageView mHandleForBGRA8UnormStorage = VK_NULL_HANDLE;
+    VkSamplerYcbcrConversion mSamplerYCbCrConversion = VK_NULL_HANDLE;
+    VkSamplerYcbcrConversionCreateInfo mSamplerYcbcrConversionCreateInfo;
     std::vector<VkImageView> mHandlesFor2DViewOn3D;
 };
 
diff --git a/src/dawn/native/vulkan/UtilsVulkan.cpp b/src/dawn/native/vulkan/UtilsVulkan.cpp
index e8c1533..5dc630c 100644
--- a/src/dawn/native/vulkan/UtilsVulkan.cpp
+++ b/src/dawn/native/vulkan/UtilsVulkan.cpp
@@ -327,4 +327,23 @@
     return DAWN_VALIDATION_ERROR("DRM format modifier %u not supported.", modifier);
 }
 
+MaybeError ValidateCanCreateSamplerYCbCrConversion(
+    const VkSamplerYcbcrConversionCreateInfo& vulkanYCbCrInfo) {
+#if DAWN_PLATFORM_IS(ANDROID)
+    const VkBaseInStructure* chain = static_cast<const VkBaseInStructure*>(vulkanYCbCrInfo.pNext);
+    while (chain != nullptr) {
+        if (chain->sType == VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID) {
+            const VkExternalFormatANDROID* vkExternalFormat =
+                reinterpret_cast<const VkExternalFormatANDROID*>(chain);
+            DAWN_INVALID_IF((vkExternalFormat->externalFormat == 0 &&
+                             vulkanYCbCrInfo.format == VK_FORMAT_UNDEFINED),
+                            "Both VkFormat and VkExternalFormatANDROID are undefined.");
+            break;
+        }
+        chain = chain->pNext;
+    }
+#endif  // DAWN_PLATFORM_IS(ANDROID)
+    return {};
+}
+
 }  // namespace dawn::native::vulkan
diff --git a/src/dawn/native/vulkan/UtilsVulkan.h b/src/dawn/native/vulkan/UtilsVulkan.h
index 8bd149a..6e06e199c 100644
--- a/src/dawn/native/vulkan/UtilsVulkan.h
+++ b/src/dawn/native/vulkan/UtilsVulkan.h
@@ -178,6 +178,10 @@
     VkPhysicalDevice vkPhysicalDevice,
     VkFormat format,
     uint64_t modifier);
+
+MaybeError ValidateCanCreateSamplerYCbCrConversion(
+    const VkSamplerYcbcrConversionCreateInfo& vulkanYCbCrInfo);
+
 }  // namespace dawn::native::vulkan
 
 #endif  // SRC_DAWN_NATIVE_VULKAN_UTILSVULKAN_H_
diff --git a/src/dawn/tests/end2end/YCbCrSamplerTests.cpp b/src/dawn/tests/end2end/YCbCrSamplerTests.cpp
index 795b3f1..0707640 100644
--- a/src/dawn/tests/end2end/YCbCrSamplerTests.cpp
+++ b/src/dawn/tests/end2end/YCbCrSamplerTests.cpp
@@ -37,7 +37,40 @@
 namespace dawn {
 namespace {
 
-class YCbCrSamplerTest : public DawnTest {
+constexpr uint32_t kWidth = 32u;
+constexpr uint32_t kHeight = 32u;
+constexpr uint32_t kDefaultMipLevels = 6u;
+constexpr uint32_t kDefaultLayerCount = 1u;
+constexpr uint32_t kDefaultSampleCount = 1u;
+constexpr wgpu::TextureFormat kDefaultTextureFormat = wgpu::TextureFormat::RGBA8Unorm;
+
+wgpu::Texture Create2DTexture(wgpu::Device& device) {
+    wgpu::TextureDescriptor descriptor;
+    descriptor.dimension = wgpu::TextureDimension::e2D;
+    descriptor.size.width = kWidth;
+    descriptor.size.height = kHeight;
+    descriptor.size.depthOrArrayLayers = kDefaultLayerCount;
+    descriptor.sampleCount = kDefaultSampleCount;
+    descriptor.format = kDefaultTextureFormat;
+    descriptor.mipLevelCount = kDefaultMipLevels;
+    descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::RenderAttachment;
+    return device.CreateTexture(&descriptor);
+}
+
+wgpu::TextureViewDescriptor CreateDefaultViewDescriptor(wgpu::TextureViewDimension dimension) {
+    wgpu::TextureViewDescriptor descriptor;
+    descriptor.format = kDefaultTextureFormat;
+    descriptor.dimension = dimension;
+    descriptor.baseMipLevel = 0;
+    if (dimension != wgpu::TextureViewDimension::e1D) {
+        descriptor.mipLevelCount = kDefaultMipLevels;
+    }
+    descriptor.baseArrayLayer = 0;
+    descriptor.arrayLayerCount = kDefaultLayerCount;
+    return descriptor;
+}
+
+class YCbCrInfoTest : public DawnTest {
   protected:
     void SetUp() override {
         DawnTest::SetUp();
@@ -56,27 +89,27 @@
     }
 };
 
-// Test that it is possible to create the sampler with ycbcr sampler descriptor.
-TEST_P(YCbCrSamplerTest, YCbCrSamplerValidWhenFeatureEnabled) {
+// Test that it is possible to create the sampler with ycbcr vulkan descriptor.
+TEST_P(YCbCrInfoTest, YCbCrSamplerValidWhenFeatureEnabled) {
     wgpu::SamplerDescriptor samplerDesc = {};
-    native::vulkan::YCbCrVulkanDescriptor samplerYCbCrDesc = {};
-    samplerYCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
-    samplerYCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
 
-    samplerDesc.nextInChain = &samplerYCbCrDesc;
+    samplerDesc.nextInChain = &yCbCrDesc;
 
     device.CreateSampler(&samplerDesc);
 }
 
-// Test that it is possible to create the sampler with ycbcr sampler descriptor with only vulkan
+// Test that it is possible to create the sampler with ycbcr vulkan descriptor with only vulkan
 // format set.
-TEST_P(YCbCrSamplerTest, YCbCrSamplerValidWithOnlyVkFormat) {
+TEST_P(YCbCrInfoTest, YCbCrSamplerValidWithOnlyVkFormat) {
     wgpu::SamplerDescriptor samplerDesc = {};
-    native::vulkan::YCbCrVulkanDescriptor samplerYCbCrDesc = {};
-    samplerYCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
-    samplerYCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
 
 #if DAWN_PLATFORM_IS(ANDROID)
     VkExternalFormatANDROID vulkanExternalFormat = {};
@@ -85,23 +118,23 @@
     // format is set as VK_FORMAT.
     vulkanExternalFormat.externalFormat = 0;
 
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
 #endif  // DAWN_PLATFORM_IS(ANDROID)
 
-    samplerDesc.nextInChain = &samplerYCbCrDesc;
+    samplerDesc.nextInChain = &yCbCrDesc;
 
     device.CreateSampler(&samplerDesc);
 }
 
-// Test that it is possible to create the sampler with ycbcr sampler descriptor with only external
+// Test that it is possible to create the sampler with ycbcr vulkan descriptor with only external
 // format set.
-TEST_P(YCbCrSamplerTest, YCbCrSamplerValidWithOnlyExternalFormat) {
+TEST_P(YCbCrInfoTest, YCbCrSamplerValidWithOnlyExternalFormat) {
     wgpu::SamplerDescriptor samplerDesc = {};
-    native::vulkan::YCbCrVulkanDescriptor samplerYCbCrDesc = {};
-    samplerYCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
     // format is set as externalFormat.
-    samplerYCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_UNDEFINED;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_UNDEFINED;
 
 #if DAWN_PLATFORM_IS(ANDROID)
     VkExternalFormatANDROID vulkanExternalFormat = {};
@@ -109,22 +142,22 @@
     vulkanExternalFormat.pNext = nullptr;
     vulkanExternalFormat.externalFormat = 5;
 
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
 #endif  // DAWN_PLATFORM_IS(ANDROID)
 
-    samplerDesc.nextInChain = &samplerYCbCrDesc;
+    samplerDesc.nextInChain = &yCbCrDesc;
 
     device.CreateSampler(&samplerDesc);
 }
 
-// Test that it is not possible to create the sampler with ycbcr sampler descriptor and no format
+// Test that it is NOT possible to create the sampler with ycbcr vulkan descriptor and no format
 // set.
-TEST_P(YCbCrSamplerTest, YCbCrSamplerInvalidWithNoFormat) {
+TEST_P(YCbCrInfoTest, YCbCrSamplerInvalidWithNoFormat) {
     wgpu::SamplerDescriptor samplerDesc = {};
-    native::vulkan::YCbCrVulkanDescriptor samplerYCbCrDesc = {};
-    samplerYCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
-    samplerYCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_UNDEFINED;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_UNDEFINED;
 
 #if DAWN_PLATFORM_IS(ANDROID)
     VkExternalFormatANDROID vulkanExternalFormat = {};
@@ -132,15 +165,119 @@
     vulkanExternalFormat.pNext = nullptr;
     vulkanExternalFormat.externalFormat = 0;
 
-    samplerYCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
 #endif  // DAWN_PLATFORM_IS(ANDROID)
 
-    samplerDesc.nextInChain = &samplerYCbCrDesc;
+    samplerDesc.nextInChain = &yCbCrDesc;
 
     ASSERT_DEVICE_ERROR(device.CreateSampler(&samplerDesc));
 }
 
-DAWN_INSTANTIATE_TEST(YCbCrSamplerTest, VulkanBackend());
+// Test that it is possible to create texture view with ycbcr vulkan descriptor.
+TEST_P(YCbCrInfoTest, YCbCrTextureViewValidWhenFeatureEnabled) {
+    wgpu::Texture texture = Create2DTexture(device);
+
+    wgpu::TextureViewDescriptor descriptor =
+        CreateDefaultViewDescriptor(wgpu::TextureViewDimension::e2D);
+    descriptor.arrayLayerCount = 1;
+
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
+
+    descriptor.nextInChain = &yCbCrDesc;
+
+    texture.CreateView(&descriptor);
+}
+
+// Test that it is possible to create texture view with ycbcr vulkan descriptor with only vulkan
+// format set.
+TEST_P(YCbCrInfoTest, YCbCrTextureViewValidWithOnlyVkFormat) {
+    wgpu::Texture texture = Create2DTexture(device);
+
+    wgpu::TextureViewDescriptor descriptor =
+        CreateDefaultViewDescriptor(wgpu::TextureViewDimension::e2D);
+    descriptor.arrayLayerCount = 1;
+
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
+
+#if DAWN_PLATFORM_IS(ANDROID)
+    VkExternalFormatANDROID vulkanExternalFormat = {};
+    vulkanExternalFormat.sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID;
+    vulkanExternalFormat.pNext = nullptr;
+    // format is set as VK_FORMAT.
+    vulkanExternalFormat.externalFormat = 0;
+
+    yCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
+#endif  // DAWN_PLATFORM_IS(ANDROID)
+
+    descriptor.nextInChain = &yCbCrDesc;
+
+    texture.CreateView(&descriptor);
+}
+
+// Test that it is possible to create texture view with ycbcr vulkan descriptor with only external
+// format set.
+TEST_P(YCbCrInfoTest, YCbCrTextureViewValidWithOnlyExternalFormat) {
+    wgpu::Texture texture = Create2DTexture(device);
+
+    wgpu::TextureViewDescriptor descriptor =
+        CreateDefaultViewDescriptor(wgpu::TextureViewDimension::e2D);
+    descriptor.arrayLayerCount = 1;
+
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    // format is set as externalFormat.
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_UNDEFINED;
+
+#if DAWN_PLATFORM_IS(ANDROID)
+    VkExternalFormatANDROID vulkanExternalFormat = {};
+    vulkanExternalFormat.sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID;
+    vulkanExternalFormat.pNext = nullptr;
+    vulkanExternalFormat.externalFormat = 5;
+
+    yCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
+#endif  // DAWN_PLATFORM_IS(ANDROID)
+
+    descriptor.nextInChain = &yCbCrDesc;
+
+    texture.CreateView(&descriptor);
+}
+
+// Test that it is NOT possible to create texture view with ycbcr vulkan descriptor and no format
+// set.
+TEST_P(YCbCrInfoTest, YCbCrTextureViewInvalidWithNoFormat) {
+    wgpu::Texture texture = Create2DTexture(device);
+
+    wgpu::TextureViewDescriptor descriptor =
+        CreateDefaultViewDescriptor(wgpu::TextureViewDimension::e2D);
+    descriptor.arrayLayerCount = 1;
+
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    yCbCrDesc.vulkanYCbCrInfo.pNext = nullptr;
+    yCbCrDesc.vulkanYCbCrInfo.format = VK_FORMAT_UNDEFINED;
+
+#if DAWN_PLATFORM_IS(ANDROID)
+    VkExternalFormatANDROID vulkanExternalFormat = {};
+    vulkanExternalFormat.sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID;
+    vulkanExternalFormat.pNext = nullptr;
+    vulkanExternalFormat.externalFormat = 0;
+
+    yCbCrDesc.vulkanYCbCrInfo.pNext = &vulkanExternalFormat;
+#endif  // DAWN_PLATFORM_IS(ANDROID)
+
+    descriptor.nextInChain = &yCbCrDesc;
+
+    ASSERT_DEVICE_ERROR(texture.CreateView(&descriptor));
+}
+
+DAWN_INSTANTIATE_TEST(YCbCrInfoTest, VulkanBackend());
 
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/unittests/ChainUtilsTests.cpp b/src/dawn/tests/unittests/ChainUtilsTests.cpp
index 9a0ed6e..b78d21f 100644
--- a/src/dawn/tests/unittests/ChainUtilsTests.cpp
+++ b/src/dawn/tests/unittests/ChainUtilsTests.cpp
@@ -40,11 +40,9 @@
 // values should be nullptr.
 TEST(ChainUtilsTests, ValidateAndUnpackEmpty) {
     {
-        // TextureViewDescriptor (as of when this test was written) does not have any valid chains
-        // in the JSON nor via additional extensions.
+        // TextureViewDescriptor has at least 1 valid chain extension..
         TextureViewDescriptor desc;
         auto unpacked = ValidateAndUnpack(&desc).AcquireSuccess();
-        static_assert(std::tuple_size_v<decltype(unpacked)::TupleType> == 0);
         EXPECT_TRUE(unpacked.Empty());
     }
     {
diff --git a/src/dawn/tests/unittests/validation/YCbCrSamplerValidationTests.cpp b/src/dawn/tests/unittests/validation/YCbCrSamplerValidationTests.cpp
index 8711f4c..43f384e 100644
--- a/src/dawn/tests/unittests/validation/YCbCrSamplerValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/YCbCrSamplerValidationTests.cpp
@@ -35,24 +35,71 @@
 namespace dawn {
 namespace {
 
-class YCbCrSamplerWithoutFeatureValidationTest : public ValidationTest {
+constexpr uint32_t kWidth = 32u;
+constexpr uint32_t kHeight = 32u;
+constexpr uint32_t kDefaultMipLevels = 1u;
+constexpr uint32_t kDefaultLayerCount = 1u;
+constexpr uint32_t kDefaultSampleCount = 1u;
+constexpr wgpu::TextureFormat kDefaultTextureFormat = wgpu::TextureFormat::RGBA8Unorm;
+
+wgpu::Texture Create2DTexture(wgpu::Device& device) {
+    wgpu::TextureDescriptor descriptor;
+    descriptor.dimension = wgpu::TextureDimension::e2D;
+    descriptor.size.width = kWidth;
+    descriptor.size.height = kHeight;
+    descriptor.size.depthOrArrayLayers = kDefaultLayerCount;
+    descriptor.sampleCount = kDefaultSampleCount;
+    descriptor.format = kDefaultTextureFormat;
+    descriptor.mipLevelCount = kDefaultMipLevels;
+    descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::RenderAttachment;
+    return device.CreateTexture(&descriptor);
+}
+
+wgpu::TextureViewDescriptor CreateDefaultViewDescriptor(wgpu::TextureViewDimension dimension) {
+    wgpu::TextureViewDescriptor descriptor;
+    descriptor.format = kDefaultTextureFormat;
+    descriptor.dimension = dimension;
+    descriptor.baseMipLevel = 0;
+    if (dimension != wgpu::TextureViewDimension::e1D) {
+        descriptor.mipLevelCount = kDefaultMipLevels;
+    }
+    descriptor.baseArrayLayer = 0;
+    descriptor.arrayLayerCount = kDefaultLayerCount;
+    return descriptor;
+}
+
+class YCbCrInfoWithoutFeatureValidationTest : public ValidationTest {
     void SetUp() override {
         ValidationTest::SetUp();
         DAWN_SKIP_TEST_IF(UsesWire());
     }
 };
 
-// Tests that creating a sampler with a valid ycbcr sampler descriptor raises an error
+// Tests that creating a sampler with a valid ycbcr vulkan descriptor raises an error
 // if the required feature is not enabled.
-TEST_F(YCbCrSamplerWithoutFeatureValidationTest, YCbCrSamplerNotSupported) {
+TEST_F(YCbCrInfoWithoutFeatureValidationTest, YCbCrSamplerNotSupported) {
     wgpu::SamplerDescriptor samplerDesc = {};
-    native::vulkan::YCbCrVulkanDescriptor samplerYCbCrDesc = {};
-    samplerDesc.nextInChain = &samplerYCbCrDesc;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    samplerDesc.nextInChain = &yCbCrDesc;
 
     ASSERT_DEVICE_ERROR(device.CreateSampler(&samplerDesc));
 }
 
-class YCbCrSamplerWithFeatureValidationTest : public YCbCrSamplerWithoutFeatureValidationTest {
+// Tests that creating a texture view with a valid ycbcr vulkan descriptor raises an error
+// if the required feature is not enabled.
+TEST_F(YCbCrInfoWithoutFeatureValidationTest, YCbCrTextureViewNotSupported) {
+    wgpu::Texture texture = Create2DTexture(device);
+
+    wgpu::TextureViewDescriptor descriptor =
+        CreateDefaultViewDescriptor(wgpu::TextureViewDimension::e2D);
+    descriptor.arrayLayerCount = 1;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    descriptor.nextInChain = &yCbCrDesc;
+
+    ASSERT_DEVICE_ERROR(texture.CreateView(&descriptor));
+}
+
+class YCbCrInfoWithFeatureValidationTest : public YCbCrInfoWithoutFeatureValidationTest {
     WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
                                 wgpu::DeviceDescriptor descriptor) override {
         wgpu::FeatureName requiredFeatures[2] = {wgpu::FeatureName::StaticSamplers,
@@ -63,20 +110,20 @@
     }
 };
 
-// Tests that creating a sampler with a valid ycbcr sampler descriptor succeeds if the
+// Tests that creating a sampler with a valid ycbcr vulkan descriptor succeeds if the
 // required feature is enabled.
-TEST_F(YCbCrSamplerWithFeatureValidationTest, YCbCrSamplerSupported) {
+TEST_F(YCbCrInfoWithFeatureValidationTest, YCbCrSamplerSupported) {
     wgpu::SamplerDescriptor samplerDesc = {};
-    native::vulkan::YCbCrVulkanDescriptor samplerYCbCrDesc = {};
-    samplerYCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
-    samplerDesc.nextInChain = &samplerYCbCrDesc;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    samplerDesc.nextInChain = &yCbCrDesc;
 
     device.CreateSampler(&samplerDesc);
 }
 
 // Tests that creating a bind group layout with a valid static sampler succeeds if the
 // required feature is enabled.
-TEST_F(YCbCrSamplerWithFeatureValidationTest, CreateBindGroupWithYCbCrSamplerSupported) {
+TEST_F(YCbCrInfoWithFeatureValidationTest, CreateBindGroupWithYCbCrSamplerSupported) {
     wgpu::BindGroupLayoutEntry binding = {};
     binding.binding = 0;
     wgpu::StaticSamplerBindingLayout staticSamplerBinding = {};
@@ -104,7 +151,7 @@
 
 // Verifies that creation of a correctly-specified bind group for a layout that
 // has a sampler and a static sampler succeeds.
-TEST_F(YCbCrSamplerWithFeatureValidationTest, CreateBindGroupWithSamplerAndStaticSamplerSupported) {
+TEST_F(YCbCrInfoWithFeatureValidationTest, CreateBindGroupWithSamplerAndStaticSamplerSupported) {
     std::vector<wgpu::BindGroupLayoutEntry> entries;
 
     wgpu::BindGroupLayoutEntry binding0 = {};
@@ -139,7 +186,7 @@
 // Verifies that creation of a bind group with the correct number of entries for a layout that has a
 // sampler and a static sampler raises an error if the entry is specified at the
 // index of the static sampler rather than that of the sampler.
-TEST_F(YCbCrSamplerWithFeatureValidationTest, BindGroupCreationForSamplerBindingTypeCausesError) {
+TEST_F(YCbCrInfoWithFeatureValidationTest, BindGroupCreationForSamplerBindingTypeCausesError) {
     std::vector<wgpu::BindGroupLayoutEntry> entries;
 
     wgpu::BindGroupLayoutEntry binding0 = {};
@@ -172,5 +219,33 @@
         utils::MakeBindGroup(device, layout, {{1, device.CreateSampler(&samplerDesc0)}}));
 }
 
+// Tests that creating a texture view with a valid ycbcr vulkan descriptor succeeds if the
+// required feature is enabled.
+TEST_F(YCbCrInfoWithFeatureValidationTest, YCbCrTextureViewSupported) {
+    wgpu::Texture texture = Create2DTexture(device);
+
+    wgpu::TextureViewDescriptor descriptor =
+        CreateDefaultViewDescriptor(wgpu::TextureViewDimension::e2D);
+    descriptor.arrayLayerCount = 1;
+    native::vulkan::YCbCrVulkanDescriptor yCbCrDesc = {};
+    yCbCrDesc.vulkanYCbCrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
+    descriptor.nextInChain = &yCbCrDesc;
+
+    texture.CreateView(&descriptor);
+}
+
+// TODO(crbug.com/dawn/2476): Add test validating binding fails if sampler or texture view is not
+// YCbCr
+// TODO(crbug.com/dawn/2476): Add test validating binding passes if sampler and texture view is
+// YCbCr
+// TODO(crbug.com/dawn/2476): Add test validating binding fails if texture view ycbcr info is
+// different from that on sampler
+// TODO(crbug.com/dawn/2476): Add test validating binding passes if texture view ycbcr info is same
+// as that on sampler
+// TODO(crbug.com/dawn/2476): Add validation that mipLevel, arrayLayers are always 1 along with 2D
+// view dimension (see
+// https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCreateInfo.html) with
+// YCbCr and tests for it.
+
 }  // anonymous namespace
 }  // namespace dawn