Reland: Enable YCbCr sampler support on platforms other than Android

Previously YCbCr Vulkan samplers were supported only on Android for
external images, while Vulkan requires YCbCr sampler for I420 YUV image
formats such as VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM,
VK_FORMAT_G8_B8R8_2PLANE_420_UNORM.
This CL:
 - Adds VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM and
   VK_FORMAT_G8_B8R8_2PLANE_420_UNORM as supported Vulkan formats
 - Updates GrVkYcbcrConversionInfo to add fFormat field and allow
   fExternalFormat=0.
 - Removes assertions format=VK_FORMAT_UNDEFINED for all images that
   have ycbcr info.

Bug: chromium:981022
Change-Id: Id4d81b20d9fda4d9ad0831f77e6025eed3db2bfd
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/233776
Commit-Queue: Greg Daniel <egdaniel@google.com>
Reviewed-by: Greg Daniel <egdaniel@google.com>
Auto-Submit: Sergey Ulanov <sergeyu@chromium.org>
diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 4784038..98892fd 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -21,3 +21,6 @@
  * Modify GrBackendFormat getters to not return internal pointers. Use an enum class for GL formats.
 
  * Expose GrContext::dump() when SK_ENABLE_DUMP_GPU is defined.
+
+ * Vulkan backend now supports YCbCr sampler for I420 Vulkan images that are not
+   backed by external images.
diff --git a/gn/tests.gni b/gn/tests.gni
index 5815c04..951a04e 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -303,6 +303,7 @@
   "$_tests/VkPriorityExtensionTest.cpp",
   "$_tests/VkProtectedContextTest.cpp",
   "$_tests/VkWrapTests.cpp",
+  "$_tests/VkYcbcrSamplerTest.cpp",
   "$_tests/VptrTest.cpp",
   "$_tests/WindowRectanglesTest.cpp",
   "$_tests/WritePixelsTest.cpp",
diff --git a/include/gpu/GrBackendSurface.h b/include/gpu/GrBackendSurface.h
index 1695f4c..e98f6af 100644
--- a/include/gpu/GrBackendSurface.h
+++ b/include/gpu/GrBackendSurface.h
@@ -69,9 +69,6 @@
         return GrBackendFormat(format, GrVkYcbcrConversionInfo());
     }
 
-    // This is used for external textures and the VkFormat is assumed to be VK_FORMAT_UNDEFINED.
-    // This call is only supported on Android since the GrVkYcbcrConvesionInfo contains an android
-    // external format.
     static GrBackendFormat MakeVk(const GrVkYcbcrConversionInfo& ycbcrInfo);
 
 #ifdef SK_DAWN
diff --git a/include/gpu/vk/GrVkTypes.h b/include/gpu/vk/GrVkTypes.h
index c227998..9abf14d 100644
--- a/include/gpu/vk/GrVkTypes.h
+++ b/include/gpu/vk/GrVkTypes.h
@@ -67,14 +67,37 @@
 // object for an VkExternalFormatANDROID.
 struct GrVkYcbcrConversionInfo {
     GrVkYcbcrConversionInfo()
-            : fYcbcrModel(VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY)
+            : fFormat(VK_FORMAT_UNDEFINED)
+            , fExternalFormat(0)
+            , fYcbcrModel(VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY)
             , fYcbcrRange(VK_SAMPLER_YCBCR_RANGE_ITU_FULL)
             , fXChromaOffset(VK_CHROMA_LOCATION_COSITED_EVEN)
             , fYChromaOffset(VK_CHROMA_LOCATION_COSITED_EVEN)
             , fChromaFilter(VK_FILTER_NEAREST)
-            , fForceExplicitReconstruction(false)
-            , fExternalFormat(0)
-            , fExternalFormatFeatures(0) {}
+            , fForceExplicitReconstruction(false) {}
+
+    GrVkYcbcrConversionInfo(VkFormat format,
+                            int64_t externalFormat,
+                            VkSamplerYcbcrModelConversion ycbcrModel,
+                            VkSamplerYcbcrRange ycbcrRange,
+                            VkChromaLocation xChromaOffset,
+                            VkChromaLocation yChromaOffset,
+                            VkFilter chromaFilter,
+                            VkBool32 forceExplicitReconstruction,
+                            VkFormatFeatureFlags formatFeatures)
+            : fFormat(format)
+            , fExternalFormat(externalFormat)
+            , fYcbcrModel(ycbcrModel)
+            , fYcbcrRange(ycbcrRange)
+            , fXChromaOffset(xChromaOffset)
+            , fYChromaOffset(yChromaOffset)
+            , fChromaFilter(chromaFilter)
+            , fForceExplicitReconstruction(forceExplicitReconstruction)
+            , fFormatFeatures(formatFeatures) {
+        SkASSERT(fYcbcrModel != VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY);
+        // Either format or externalFormat must be specified.
+        SkASSERT((fFormat != VK_FORMAT_UNDEFINED) ^ (externalFormat != 0));
+    }
 
     GrVkYcbcrConversionInfo(VkSamplerYcbcrModelConversion ycbcrModel,
                             VkSamplerYcbcrRange ycbcrRange,
@@ -84,35 +107,35 @@
                             VkBool32 forceExplicitReconstruction,
                             uint64_t externalFormat,
                             VkFormatFeatureFlags externalFormatFeatures)
-            : fYcbcrModel(ycbcrModel)
-            , fYcbcrRange(ycbcrRange)
-            , fXChromaOffset(xChromaOffset)
-            , fYChromaOffset(yChromaOffset)
-            , fChromaFilter(chromaFilter)
-            , fForceExplicitReconstruction(forceExplicitReconstruction)
-            , fExternalFormat(externalFormat)
-            , fExternalFormatFeatures(externalFormatFeatures) {
-        SkASSERT(fExternalFormat);
-    }
+            : GrVkYcbcrConversionInfo(VK_FORMAT_UNDEFINED, externalFormat, ycbcrModel, ycbcrRange,
+                                      xChromaOffset, yChromaOffset, chromaFilter,
+                                      forceExplicitReconstruction, externalFormatFeatures) {}
 
     bool operator==(const GrVkYcbcrConversionInfo& that) const {
-        // Invalid objects are not required to have all other fields intialized or matching.
+        // Invalid objects are not required to have all other fields initialized or matching.
         if (!this->isValid() && !that.isValid()) {
             return true;
         }
-        return this->fYcbcrModel == that.fYcbcrModel &&
+        return this->fFormat == that.fFormat &&
+               this->fExternalFormat == that.fExternalFormat &&
+               this->fYcbcrModel == that.fYcbcrModel &&
                this->fYcbcrRange == that.fYcbcrRange &&
                this->fXChromaOffset == that.fXChromaOffset &&
                this->fYChromaOffset == that.fYChromaOffset &&
                this->fChromaFilter == that.fChromaFilter &&
-               this->fForceExplicitReconstruction == that.fForceExplicitReconstruction &&
-               this->fExternalFormat == that.fExternalFormat;
-        // We don't check fExternalFormatFeatures here since all matching external formats must have
-        // the same format features at least in terms of how they effect ycbcr sampler conversion.
+               this->fForceExplicitReconstruction == that.fForceExplicitReconstruction;
     }
     bool operator!=(const GrVkYcbcrConversionInfo& that) const { return !(*this == that); }
 
-    bool isValid() const { return fExternalFormat != 0; }
+    bool isValid() const { return fYcbcrModel != VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY; }
+
+    // Format of the source image. Must be set to VK_FORMAT_UNDEFINED for external images or
+    // a valid image format otherwise.
+    VkFormat                         fFormat;
+
+    // The external format. Must be non-zero for external images, zero otherwise.
+    // Should be compatible to be used in a VkExternalFormatANDROID struct.
+    uint64_t                         fExternalFormat;
 
     VkSamplerYcbcrModelConversion    fYcbcrModel;
     VkSamplerYcbcrRange              fYcbcrRange;
@@ -120,11 +143,10 @@
     VkChromaLocation                 fYChromaOffset;
     VkFilter                         fChromaFilter;
     VkBool32                         fForceExplicitReconstruction;
-    // The external format should be compatible to be used in a VkExternalFormatANDROID struct
-    uint64_t                         fExternalFormat;
-    // The format features here should be those returned by a call to
+
+    // For external images format features here should be those returned by a call to
     // vkAndroidHardwareBufferFormatPropertiesANDROID
-    VkFormatFeatureFlags             fExternalFormatFeatures;
+    VkFormatFeatureFlags             fFormatFeatures;
 };
 
 struct GrVkImageInfo {
diff --git a/src/gpu/GrAHardwareBufferUtils.cpp b/src/gpu/GrAHardwareBufferUtils.cpp
index 47899e8..77a5ed0 100644
--- a/src/gpu/GrAHardwareBufferUtils.cpp
+++ b/src/gpu/GrAHardwareBufferUtils.cpp
@@ -138,7 +138,7 @@
                     ycbcrConversion.fYChromaOffset = hwbFormatProps.suggestedYChromaOffset;
                     ycbcrConversion.fForceExplicitReconstruction = VK_FALSE;
                     ycbcrConversion.fExternalFormat = hwbFormatProps.externalFormat;
-                    ycbcrConversion.fExternalFormatFeatures = hwbFormatProps.formatFeatures;
+                    ycbcrConversion.fFormatFeatures = hwbFormatProps.formatFeatures;
                     if (VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT &
                         hwbFormatProps.formatFeatures) {
                         ycbcrConversion.fChromaFilter = VK_FILTER_LINEAR;
diff --git a/src/gpu/GrBackendSurface.cpp b/src/gpu/GrBackendSurface.cpp
index 412d8da..fa95520 100644
--- a/src/gpu/GrBackendSurface.cpp
+++ b/src/gpu/GrBackendSurface.cpp
@@ -92,11 +92,8 @@
 }
 
 GrBackendFormat GrBackendFormat::MakeVk(const GrVkYcbcrConversionInfo& ycbcrInfo) {
-#ifdef SK_BUILD_FOR_ANDROID
-    return GrBackendFormat(VK_FORMAT_UNDEFINED, ycbcrInfo);
-#else
-    return GrBackendFormat();
-#endif
+    SkASSERT(ycbcrInfo.isValid());
+    return GrBackendFormat(ycbcrInfo.fFormat, ycbcrInfo);
 }
 
 GrBackendFormat::GrBackendFormat(VkFormat vkFormat, const GrVkYcbcrConversionInfo& ycbcrInfo)
@@ -109,7 +106,7 @@
         , fTextureType(GrTextureType::k2D) {
     fVk.fFormat = vkFormat;
     fVk.fYcbcrConversionInfo = ycbcrInfo;
-    if (fVk.fYcbcrConversionInfo.isValid()) {
+    if (fVk.fYcbcrConversionInfo.isValid() && fVk.fYcbcrConversionInfo.fExternalFormat) {
         fTextureType = GrTextureType::kExternal;
     }
 }
@@ -564,7 +561,7 @@
         case GrBackendApi::kVulkan: {
             auto info = fVkInfo.snapImageInfo();
             if (info.fYcbcrConversionInfo.isValid()) {
-                SkASSERT(info.fFormat == VK_FORMAT_UNDEFINED);
+                SkASSERT(info.fFormat == info.fYcbcrConversionInfo.fFormat);
                 return GrBackendFormat::MakeVk(info.fYcbcrConversionInfo);
             }
             return GrBackendFormat::MakeVk(info.fFormat);
@@ -848,7 +845,7 @@
         case GrBackendApi::kVulkan: {
             auto info = fVkInfo.snapImageInfo();
             if (info.fYcbcrConversionInfo.isValid()) {
-                SkASSERT(info.fFormat == VK_FORMAT_UNDEFINED);
+                SkASSERT(info.fFormat == info.fYcbcrConversionInfo.fFormat);
                 return GrBackendFormat::MakeVk(info.fYcbcrConversionInfo);
             }
             return GrBackendFormat::MakeVk(info.fFormat);
diff --git a/src/gpu/GrPrimitiveProcessor.cpp b/src/gpu/GrPrimitiveProcessor.cpp
index 32790b9..becab18 100644
--- a/src/gpu/GrPrimitiveProcessor.cpp
+++ b/src/gpu/GrPrimitiveProcessor.cpp
@@ -81,7 +81,6 @@
     fTextureType = textureType;
     fConfig = config;
     fExtraSamplerKey = extraSamplerKey;
-    SkASSERT(!fExtraSamplerKey || textureType == GrTextureType::kExternal);
 }
 
 void GrPrimitiveProcessor::TextureSampler::reset(GrTextureType textureType,
diff --git a/src/gpu/GrProgramDesc.cpp b/src/gpu/GrProgramDesc.cpp
index b826125..d20d6e1 100644
--- a/src/gpu/GrProgramDesc.cpp
+++ b/src/gpu/GrProgramDesc.cpp
@@ -71,7 +71,6 @@
         uint32_t extraSamplerKey = gpu->getExtraSamplerKeyForProgram(
                 sampler.samplerState(), sampler.proxy()->backendFormat());
         if (extraSamplerKey) {
-            SkASSERT(sampler.proxy()->textureType() == GrTextureType::kExternal);
             // We first mark the normal sampler key with last bit to flag that it has an extra
             // sampler key. We then add both keys.
             SkASSERT((samplerKey & (1 << 31)) == 0);
@@ -95,7 +94,6 @@
                 sampler.textureType(), sampler.swizzle(), sampler.config(), caps);
         uint32_t extraSamplerKey = sampler.extraSamplerKey();
         if (extraSamplerKey) {
-            SkASSERT(sampler.textureType() == GrTextureType::kExternal);
             // We first mark the normal sampler key with last bit to flag that it has an extra
             // sampler key. We then add both keys.
             SkASSERT((samplerKey & (1 << 31)) == 0);
diff --git a/src/gpu/vk/GrVkCaps.cpp b/src/gpu/vk/GrVkCaps.cpp
index 15e7f16..d1181a0 100644
--- a/src/gpu/vk/GrVkCaps.cpp
+++ b/src/gpu/vk/GrVkCaps.cpp
@@ -333,18 +333,15 @@
 
     auto ycbcrFeatures =
             get_extension_feature_struct<VkPhysicalDeviceSamplerYcbcrConversionFeatures>(
-                    features,
-                    VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES);
+                    features, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES);
     if (ycbcrFeatures && ycbcrFeatures->samplerYcbcrConversion &&
-        fSupportsAndroidHWBExternalMemory &&
         (physicalDeviceVersion >= VK_MAKE_VERSION(1, 1, 0) ||
          (extensions.hasExtension(VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME, 1) &&
-          this->supportsMaintenance1() &&
-          this->supportsBindMemory2() &&
-          this->supportsMemoryRequirements2() &&
-          this->supportsPhysicalDeviceProperties2()))) {
+          this->supportsMaintenance1() && this->supportsBindMemory2() &&
+          this->supportsMemoryRequirements2() && this->supportsPhysicalDeviceProperties2()))) {
         fSupportsYcbcrConversion = true;
     }
+
     // We always push back the default GrVkYcbcrConversionInfo so that the case of no conversion
     // will return a key of 0.
     fYcbcrInfos.push_back(GrVkYcbcrConversionInfo());
@@ -648,6 +645,8 @@
     VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK,
     VK_FORMAT_R16_UNORM,
     VK_FORMAT_R16G16_UNORM,
+    VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM,
+    VK_FORMAT_G8_B8R8_2PLANE_420_UNORM,
     // Experimental (for Y416 and mutant P016/P010)
     VK_FORMAT_R16G16B16A16_UNORM,
     VK_FORMAT_R16G16_SFLOAT,
@@ -674,18 +673,13 @@
                                const VkPhysicalDeviceProperties& properties) {
     static_assert(SK_ARRAY_COUNT(kVkFormats) == GrVkCaps::kNumVkFormats,
                   "Size of VkFormats array must match static value in header");
-    for (size_t i = 0; i < SK_ARRAY_COUNT(kVkFormats); ++i) {
-        VkFormat format = kVkFormats[i];
-        if (!format_is_srgb(format) || fSRGBSupport) {
-            fFormatTable[i].init(interface, physDev, properties, format);
-        }
-    }
 
     // Go through all the formats and init their support surface and data GrColorTypes.
 
     // Format: VK_FORMAT_R8G8B8A8_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R8G8B8A8_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R8G8B8A8_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 2;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -709,6 +703,7 @@
     // Format: VK_FORMAT_R8_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R8_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R8_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 2;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -733,6 +728,7 @@
     // Format: VK_FORMAT_B8G8R8A8_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_B8G8R8A8_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_B8G8R8A8_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -748,6 +744,7 @@
     // Format: VK_FORMAT_R5G6B5_UNORM_PACK16
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R5G6B5_UNORM_PACK16);
+        info.init(interface, physDev, properties, VK_FORMAT_R5G6B5_UNORM_PACK16);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -763,6 +760,7 @@
     // Format: VK_FORMAT_R16G16B16A16_SFLOAT
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R16G16B16A16_SFLOAT);
+        info.init(interface, physDev, properties, VK_FORMAT_R16G16B16A16_SFLOAT);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 2;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -784,6 +782,7 @@
     // Format: VK_FORMAT_R16_SFLOAT
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R16_SFLOAT);
+        info.init(interface, physDev, properties, VK_FORMAT_R16_SFLOAT);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -801,6 +800,7 @@
     // Format: VK_FORMAT_R8G8B8_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R8G8B8_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R8G8B8_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -816,6 +816,7 @@
     // Format: VK_FORMAT_R8G8_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R8G8_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R8G8_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -831,6 +832,7 @@
     // Format: VK_FORMAT_A2B10G10R10_UNORM_PACK32
     {
         auto& info = this->getFormatInfo(VK_FORMAT_A2B10G10R10_UNORM_PACK32);
+        info.init(interface, physDev, properties, VK_FORMAT_A2B10G10R10_UNORM_PACK32);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -846,6 +848,7 @@
     // Format: VK_FORMAT_B4G4R4A4_UNORM_PACK16
     {
         auto& info = this->getFormatInfo(VK_FORMAT_B4G4R4A4_UNORM_PACK16);
+        info.init(interface, physDev, properties, VK_FORMAT_B4G4R4A4_UNORM_PACK16);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -863,6 +866,7 @@
     // Format: VK_FORMAT_R4G4B4A4_UNORM_PACK16
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R4G4B4A4_UNORM_PACK16);
+        info.init(interface, physDev, properties, VK_FORMAT_R4G4B4A4_UNORM_PACK16);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -878,6 +882,7 @@
     // Format: VK_FORMAT_R32G32B32A32_SFLOAT
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R32G32B32A32_SFLOAT);
+        info.init(interface, physDev, properties, VK_FORMAT_R32G32B32A32_SFLOAT);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -893,6 +898,9 @@
     // Format: VK_FORMAT_R8G8B8A8_SRGB
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R8G8B8A8_SRGB);
+        if (fSRGBSupport) {
+            info.init(interface, physDev, properties, VK_FORMAT_R8G8B8A8_SRGB);
+        }
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -908,6 +916,7 @@
     // Format: VK_FORMAT_R16_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R16_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R16_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -923,6 +932,7 @@
     // Format: VK_FORMAT_R16G16_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R16G16_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R16G16_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -938,6 +948,7 @@
     // Format: VK_FORMAT_R16G16B16A16_UNORM
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R16G16B16A16_UNORM);
+        info.init(interface, physDev, properties, VK_FORMAT_R16G16B16A16_UNORM);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -953,6 +964,7 @@
     // Format: VK_FORMAT_R16G16_SFLOAT
     {
         auto& info = this->getFormatInfo(VK_FORMAT_R16G16_SFLOAT);
+        info.init(interface, physDev, properties, VK_FORMAT_R16G16_SFLOAT);
         if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
             info.fColorTypeInfoCount = 1;
             info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
@@ -965,8 +977,46 @@
             }
         }
     }
+    // Format: VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM
+    {
+        auto& info = this->getFormatInfo(VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM);
+        if (fSupportsYcbcrConversion) {
+            info.init(interface, physDev, properties, VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM);
+        }
+        if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
+            info.fColorTypeInfoCount = 1;
+            info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
+            int ctIdx = 0;
+            // Format: VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM, Surface: kRGB_888x
+            {
+                auto& ctInfo = info.fColorTypeInfos[ctIdx++];
+                ctInfo.fColorType = GrColorType::kRGB_888x;
+                ctInfo.fFlags = ColorTypeInfo::kUploadData_Flag;
+            }
+        }
+    }
+    // Format: VK_FORMAT_G8_B8R8_2PLANE_420_UNORM
+    {
+        auto& info = this->getFormatInfo(VK_FORMAT_G8_B8R8_2PLANE_420_UNORM);
+        if (fSupportsYcbcrConversion) {
+            info.init(interface, physDev, properties, VK_FORMAT_G8_B8R8_2PLANE_420_UNORM);
+        }
+        if (SkToBool(info.fOptimalFlags & FormatInfo::kTextureable_Flag)) {
+            info.fColorTypeInfoCount = 1;
+            info.fColorTypeInfos.reset(new ColorTypeInfo[info.fColorTypeInfoCount]());
+            int ctIdx = 0;
+            // Format: VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, Surface: kRGB_888x
+            {
+                auto& ctInfo = info.fColorTypeInfos[ctIdx++];
+                ctInfo.fColorType = GrColorType::kRGB_888x;
+                ctInfo.fFlags = ColorTypeInfo::kUploadData_Flag;
+            }
+        }
+    }
     // Format: VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK
     {
+        auto& info = this->getFormatInfo(VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK);
+        info.init(interface, physDev, properties, VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK);
         // No supported GrColorTypes.
     }
 }
@@ -1201,6 +1251,11 @@
         return {GrColorType::kUnknown, 0};
     }
 
+
+    if (GrVkFormatNeedsYcbcrSampler(vkFormat)) {
+        return {GrColorType::kUnknown, 0};
+    }
+
     // The VkBufferImageCopy bufferOffset field must be both a multiple of 4 and of a single texel.
     size_t offsetAlignment = align_to_4(GrVkBytesPerFormat(vkFormat));
 
@@ -1248,21 +1303,23 @@
 }
 
 static GrPixelConfig validate_image_info(VkFormat format, GrColorType ct, bool hasYcbcrConversion) {
-    if (format == VK_FORMAT_UNDEFINED) {
-        // If the format is undefined then it is only valid as an external image which requires that
-        // we have a valid VkYcbcrConversion.
-        if (hasYcbcrConversion) {
+    if (hasYcbcrConversion) {
+        if (GrVkFormatNeedsYcbcrSampler(format)) {
+            return kRGB_888X_GrPixelConfig;
+        }
+
+        // Format may be undefined for external images, which are required to have YCbCr conversion.
+        if (VK_FORMAT_UNDEFINED == format) {
             // We don't actually care what the color type or config are since we won't use those
             // values for external textures. However, for read pixels we will draw to a non ycbcr
             // texture of this config so we set RGBA here for that.
             return kRGBA_8888_GrPixelConfig;
-        } else {
-            return kUnknown_GrPixelConfig;
         }
+
+        return kUnknown_GrPixelConfig;
     }
 
-    if (hasYcbcrConversion) {
-        // We only support having a ycbcr conversion for external images.
+    if (VK_FORMAT_UNDEFINED == format) {
         return kUnknown_GrPixelConfig;
     }
 
@@ -1482,6 +1539,10 @@
         return {GrColorType::kUnknown, 0};
     }
 
+    if (GrVkFormatNeedsYcbcrSampler(vkFormat)) {
+        return {GrColorType::kUnknown, 0};
+    }
+
     // The VkBufferImageCopy bufferOffset field must be both a multiple of 4 and of a single texel.
     size_t offsetAlignment = align_to_4(GrVkBytesPerFormat(vkFormat));
 
diff --git a/src/gpu/vk/GrVkCaps.h b/src/gpu/vk/GrVkCaps.h
index 938eff5..2cc1083 100644
--- a/src/gpu/vk/GrVkCaps.h
+++ b/src/gpu/vk/GrVkCaps.h
@@ -253,7 +253,7 @@
         std::unique_ptr<ColorTypeInfo[]> fColorTypeInfos;
         int fColorTypeInfoCount = 0;
     };
-    static const size_t kNumVkFormats = 18;
+    static const size_t kNumVkFormats = 20;
     FormatInfo fFormatTable[kNumVkFormats];
 
     FormatInfo& getFormatInfo(VkFormat);
diff --git a/src/gpu/vk/GrVkGpu.cpp b/src/gpu/vk/GrVkGpu.cpp
index b2b3c4b..5cc1331 100644
--- a/src/gpu/vk/GrVkGpu.cpp
+++ b/src/gpu/vk/GrVkGpu.cpp
@@ -1136,7 +1136,7 @@
     }
 
     if (info.fYcbcrConversionInfo.isValid()) {
-        if (!caps.supportsYcbcrConversion() || info.fFormat != VK_NULL_HANDLE) {
+        if (!caps.supportsYcbcrConversion()) {
             return false;
         }
     }
@@ -1874,6 +1874,11 @@
         return GrBackendTexture();
     }
 
+    if (GrVkFormatNeedsYcbcrSampler(vkFormat)) {
+        SkDebugf("Can't create BackendTexture that requires Ycbcb sampler.\n");
+        return GrBackendTexture();
+    }
+
     GrVkImageInfo info;
     if (!this->createVkImageForBackendSurface(vkFormat, w, h, true,
                                               GrRenderable::kYes == renderable, mipMapped, srcData,
@@ -2629,7 +2634,11 @@
     const GrVkSampler* sampler = this->resourceProvider().findOrCreateCompatibleSampler(
             samplerState, *ycbcrInfo);
 
-    return sampler->uniqueID();
+    uint32_t result = sampler->uniqueID();
+
+    sampler->unref(this);
+
+    return result;
 }
 
 void GrVkGpu::storeVkPipelineCacheData() {
diff --git a/src/gpu/vk/GrVkImage.h b/src/gpu/vk/GrVkImage.h
index 222fdd5..8f44863 100644
--- a/src/gpu/vk/GrVkImage.h
+++ b/src/gpu/vk/GrVkImage.h
@@ -57,7 +57,7 @@
     VkFormat imageFormat() const { return fInfo.fFormat; }
     GrBackendFormat getBackendFormat() const {
         if (fResource && this->ycbcrConversionInfo().isValid()) {
-            SkASSERT(this->imageFormat() == VK_FORMAT_UNDEFINED);
+            SkASSERT(this->imageFormat() == this->ycbcrConversionInfo().fFormat);
             return GrBackendFormat::MakeVk(this->ycbcrConversionInfo());
         }
         SkASSERT(this->imageFormat() != VK_FORMAT_UNDEFINED);
diff --git a/src/gpu/vk/GrVkImageView.cpp b/src/gpu/vk/GrVkImageView.cpp
index 73b54e3..ce7b1c5 100644
--- a/src/gpu/vk/GrVkImageView.cpp
+++ b/src/gpu/vk/GrVkImageView.cpp
@@ -19,7 +19,7 @@
     GrVkSamplerYcbcrConversion* ycbcrConversion = nullptr;
 
     if (ycbcrInfo.isValid()) {
-        SkASSERT(gpu->vkCaps().supportsYcbcrConversion() && format == VK_FORMAT_UNDEFINED);
+        SkASSERT(gpu->vkCaps().supportsYcbcrConversion() && format == ycbcrInfo.fFormat);
 
         ycbcrConversion =
                 gpu->resourceProvider().findOrCreateCompatibleSamplerYcbcrConversion(ycbcrInfo);
diff --git a/src/gpu/vk/GrVkResourceProvider.cpp b/src/gpu/vk/GrVkResourceProvider.cpp
index 8befc5b..cb21719 100644
--- a/src/gpu/vk/GrVkResourceProvider.cpp
+++ b/src/gpu/vk/GrVkResourceProvider.cpp
@@ -388,12 +388,16 @@
     fExternalRenderPasses.reset();
 
     // Iterate through all store GrVkSamplers and unref them before resetting the hash.
-    SkTDynamicHash<GrVkSampler, GrVkSampler::Key>::Iter iter(&fSamplers);
-    for (; !iter.done(); ++iter) {
+    for (decltype(fSamplers)::Iter iter(&fSamplers); !iter.done(); ++iter) {
         (*iter).unref(fGpu);
     }
     fSamplers.reset();
 
+    for (decltype(fYcbcrConversions)::Iter iter(&fYcbcrConversions); !iter.done(); ++iter) {
+        (*iter).unref(fGpu);
+    }
+    fYcbcrConversions.reset();
+
     fPipelineStateCache->release();
 
     GR_VK_CALL(fGpu->vkInterface(), DestroyPipelineCache(fGpu->device(), fPipelineCache, nullptr));
@@ -462,6 +466,11 @@
     }
     fSamplers.reset();
 
+    for (decltype(fYcbcrConversions)::Iter iter(&fYcbcrConversions); !iter.done(); ++iter) {
+        (*iter).unrefAndAbandon();
+    }
+    fYcbcrConversions.reset();
+
     fPipelineStateCache->abandon();
 
     fPipelineCache = VK_NULL_HANDLE;
diff --git a/src/gpu/vk/GrVkSampler.cpp b/src/gpu/vk/GrVkSampler.cpp
index 15336b0..f6242c3 100644
--- a/src/gpu/vk/GrVkSampler.cpp
+++ b/src/gpu/vk/GrVkSampler.cpp
@@ -82,8 +82,7 @@
 
         createInfo.pNext = &conversionInfo;
 
-        const VkFormatFeatureFlags& flags = ycbcrInfo.fExternalFormatFeatures;
-
+        VkFormatFeatureFlags flags = ycbcrInfo.fFormatFeatures;
         if (!SkToBool(flags & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT)) {
             createInfo.magFilter = VK_FILTER_NEAREST;
             createInfo.minFilter = VK_FILTER_NEAREST;
diff --git a/src/gpu/vk/GrVkSamplerYcbcrConversion.cpp b/src/gpu/vk/GrVkSamplerYcbcrConversion.cpp
index 8757ac5..f33f521 100644
--- a/src/gpu/vk/GrVkSamplerYcbcrConversion.cpp
+++ b/src/gpu/vk/GrVkSamplerYcbcrConversion.cpp
@@ -14,11 +14,9 @@
     if (!gpu->vkCaps().supportsYcbcrConversion()) {
         return nullptr;
     }
-    // We only support creating ycbcr conversion for external formats;
-    SkASSERT(info.fExternalFormat);
 
 #ifdef SK_DEBUG
-    const VkFormatFeatureFlags& featureFlags = info.fExternalFormatFeatures;
+    const VkFormatFeatureFlags& featureFlags = info.fFormatFeatures;
     if (info.fXChromaOffset == VK_CHROMA_LOCATION_MIDPOINT ||
         info.fYChromaOffset == VK_CHROMA_LOCATION_MIDPOINT) {
         SkASSERT(featureFlags & VK_FORMAT_FEATURE_MIDPOINT_CHROMA_SAMPLES_BIT);
@@ -36,36 +34,50 @@
     }
 #endif
 
-#ifdef SK_BUILD_FOR_ANDROID
-    VkExternalFormatANDROID externalFormat;
-    externalFormat.sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID;
-    externalFormat.pNext = nullptr;
-    externalFormat.externalFormat = info.fExternalFormat;
 
     VkSamplerYcbcrConversionCreateInfo ycbcrCreateInfo;
-    memset(&ycbcrCreateInfo, 0, sizeof(VkSamplerYcbcrConversionCreateInfo));
     ycbcrCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
-    ycbcrCreateInfo.pNext = &externalFormat;
-    ycbcrCreateInfo.format = VK_FORMAT_UNDEFINED;
+    ycbcrCreateInfo.pNext = nullptr;
+    ycbcrCreateInfo.format = info.fFormat;
     ycbcrCreateInfo.ycbcrModel = info.fYcbcrModel;
     ycbcrCreateInfo.ycbcrRange = info.fYcbcrRange;
-    // Componets is ignored for external format conversions;
-    // ycbcrCreateInfo.components = {0, 0, 0, 0};
+
+    // Components is ignored for external format conversions. For all other formats identity swizzle
+    // is used. It can be added to GrVkYcbcrConversionInfo if necessary.
+    ycbcrCreateInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
+                                  VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
     ycbcrCreateInfo.xChromaOffset = info.fXChromaOffset;
     ycbcrCreateInfo.yChromaOffset = info.fYChromaOffset;
     ycbcrCreateInfo.chromaFilter = info.fChromaFilter;
     ycbcrCreateInfo.forceExplicitReconstruction = info.fForceExplicitReconstruction;
 
+#ifdef SK_BUILD_FOR_ANDROID
+    VkExternalFormatANDROID externalFormat;
+    if (info.fExternalFormat) {
+        // Format must not be specified for external images.
+        SkASSERT(info.fFormat == VK_FORMAT_UNDEFINED);
+        externalFormat.sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID;
+        externalFormat.pNext = nullptr;
+        externalFormat.externalFormat = info.fExternalFormat;
+        ycbcrCreateInfo.pNext = &externalFormat;
+    }
+#else
+    // External images are supported only on Android;
+    SkASSERT(!info.fExternalFormat);
+#endif
+
+    if (!info.fExternalFormat) {
+        SkASSERT(info.fFormat != VK_FORMAT_UNDEFINED);
+    }
+
     VkSamplerYcbcrConversion conversion;
     GR_VK_CALL(gpu->vkInterface(), CreateSamplerYcbcrConversion(gpu->device(), &ycbcrCreateInfo,
                                                                 nullptr, &conversion));
     if (conversion == VK_NULL_HANDLE) {
         return nullptr;
     }
+
     return new GrVkSamplerYcbcrConversion(conversion, GenerateKey(info));
-#else
-    return nullptr;
-#endif
 }
 
 void GrVkSamplerYcbcrConversion::freeGPUData(GrVkGpu* gpu) const {
@@ -96,6 +108,5 @@
     ycbcrKey |= (static_cast<uint8_t>(ycbcrInfo.fChromaFilter) << kChromaFilterShift);
     ycbcrKey |= (static_cast<uint8_t>(ycbcrInfo.fForceExplicitReconstruction) << kReconShift);
 
-    return {ycbcrInfo.fExternalFormat, ycbcrKey};
+    return Key{ycbcrInfo.fFormat, ycbcrInfo.fExternalFormat, ycbcrKey};
 }
-
diff --git a/src/gpu/vk/GrVkSamplerYcbcrConversion.h b/src/gpu/vk/GrVkSamplerYcbcrConversion.h
index 1b57772..cf7a2c5 100644
--- a/src/gpu/vk/GrVkSamplerYcbcrConversion.h
+++ b/src/gpu/vk/GrVkSamplerYcbcrConversion.h
@@ -22,18 +22,21 @@
     VkSamplerYcbcrConversion ycbcrConversion() const { return fYcbcrConversion; }
 
     struct Key {
-        Key() : fExternalFormat(0), fConversionKey(0) {}
-        Key(uint64_t externalFormat, uint8_t conversionKey) {
+        Key() : fVkFormat(VK_FORMAT_UNDEFINED), fExternalFormat(0), fConversionKey(0) {}
+        Key(VkFormat vkFormat, uint64_t externalFormat, uint8_t conversionKey) {
             memset(this, 0, sizeof(Key));
+            fVkFormat = vkFormat;
             fExternalFormat = externalFormat;
             fConversionKey = conversionKey;
         }
 
+        VkFormat fVkFormat;
         uint64_t fExternalFormat;
         uint8_t  fConversionKey;
 
         bool operator==(const Key& that) const {
-            return this->fExternalFormat == that.fExternalFormat &&
+            return this->fVkFormat == that.fVkFormat &&
+                   this->fExternalFormat == that.fExternalFormat &&
                    this->fConversionKey == that.fConversionKey;
         }
     };
diff --git a/src/gpu/vk/GrVkUniformHandler.cpp b/src/gpu/vk/GrVkUniformHandler.cpp
index bfd9e77..f1b62b0 100644
--- a/src/gpu/vk/GrVkUniformHandler.cpp
+++ b/src/gpu/vk/GrVkUniformHandler.cpp
@@ -202,6 +202,16 @@
     }
 }
 
+GrVkUniformHandler::~GrVkUniformHandler() {
+    GrVkGpu* gpu = static_cast<GrVkPipelineStateBuilder*>(fProgramBuilder)->gpu();
+    for (decltype(fSamplers)::Iter iter(&fSamplers); iter.next();) {
+        if (iter->fImmutableSampler) {
+            iter->fImmutableSampler->unref(gpu);
+            iter->fImmutableSampler = nullptr;
+        }
+    }
+}
+
 GrGLSLUniformHandler::UniformHandle GrVkUniformHandler::internalAddUniformArray(
                                                                             uint32_t visibility,
                                                                             GrSLType type,
@@ -280,10 +290,9 @@
     info.fVisibility = kFragment_GrShaderFlag;
     info.fUBOffset = 0;
 
-    // Check if we are dealing with an external texture and store the needed information if so
+    // Check if we are dealing with an external texture and store the needed information if so.
     const GrVkTexture* vkTexture = static_cast<const GrVkTexture*>(texture);
     if (vkTexture->ycbcrConversionInfo().isValid()) {
-        SkASSERT(type == GrTextureType::kExternal);
         GrVkGpu* gpu = static_cast<GrVkPipelineStateBuilder*>(fProgramBuilder)->gpu();
         info.fImmutableSampler = gpu->resourceProvider().findOrCreateCompatibleSampler(
                 state, vkTexture->ycbcrConversionInfo());
diff --git a/src/gpu/vk/GrVkUniformHandler.h b/src/gpu/vk/GrVkUniformHandler.h
index 8d80f20..f280970 100644
--- a/src/gpu/vk/GrVkUniformHandler.h
+++ b/src/gpu/vk/GrVkUniformHandler.h
@@ -39,12 +39,13 @@
         uint32_t                fVisibility;
         // fUBOffset is only valid if the GrSLType of the fVariable is not a sampler
         uint32_t                fUBOffset;
-        // The SamplerState, maxMipLevel, and ycbcrInfo are only valid if the GrSLType is a sampler
-        // and that sampler is used for sampling an external image with a ycbcr conversion.
+        // fImmutableSampler is used for sampling an image with a ycbcr conversion.
         const GrVkSampler*      fImmutableSampler = nullptr;
     };
     typedef GrTAllocator<UniformInfo> UniformInfoArray;
 
+    ~GrVkUniformHandler() override;
+
     const GrShaderVar& getUniformVariable(UniformHandle u) const override {
         return fUniforms[u.toIndex()].fVariable;
     }
diff --git a/src/gpu/vk/GrVkUtil.cpp b/src/gpu/vk/GrVkUtil.cpp
index 382aefd..e7164ff 100644
--- a/src/gpu/vk/GrVkUtil.cpp
+++ b/src/gpu/vk/GrVkUtil.cpp
@@ -100,30 +100,32 @@
 #ifdef SK_DEBUG
 bool GrVkFormatColorTypePairIsValid(VkFormat format, GrColorType colorType) {
     switch (format) {
-        case VK_FORMAT_R8G8B8A8_UNORM:           return GrColorType::kRGBA_8888 == colorType ||
-                                                        GrColorType::kRGB_888x == colorType;
-        case VK_FORMAT_B8G8R8A8_UNORM:           return GrColorType::kBGRA_8888 == colorType;
-        case VK_FORMAT_R8G8B8A8_SRGB:            return GrColorType::kRGBA_8888_SRGB == colorType;
-        case VK_FORMAT_R8G8B8_UNORM:             return GrColorType::kRGB_888x == colorType;
-        case VK_FORMAT_R8G8_UNORM:               return GrColorType::kRG_88 == colorType;
-        case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return GrColorType::kRGBA_1010102 == colorType;
-        case VK_FORMAT_R5G6B5_UNORM_PACK16:      return GrColorType::kBGR_565 == colorType;
+        case VK_FORMAT_R8G8B8A8_UNORM:            return GrColorType::kRGBA_8888 == colorType ||
+                                                         GrColorType::kRGB_888x == colorType;
+        case VK_FORMAT_B8G8R8A8_UNORM:            return GrColorType::kBGRA_8888 == colorType;
+        case VK_FORMAT_R8G8B8A8_SRGB:             return GrColorType::kRGBA_8888_SRGB == colorType;
+        case VK_FORMAT_R8G8B8_UNORM:              return GrColorType::kRGB_888x == colorType;
+        case VK_FORMAT_R8G8_UNORM:                return GrColorType::kRG_88 == colorType;
+        case VK_FORMAT_A2B10G10R10_UNORM_PACK32:  return GrColorType::kRGBA_1010102 == colorType;
+        case VK_FORMAT_R5G6B5_UNORM_PACK16:       return GrColorType::kBGR_565 == colorType;
         // R4G4B4A4 is not required to be supported so we actually
         // store RGBA_4444 data as B4G4R4A4.
-        case VK_FORMAT_B4G4R4A4_UNORM_PACK16:    return GrColorType::kABGR_4444 == colorType;
-        case VK_FORMAT_R4G4B4A4_UNORM_PACK16:    return GrColorType::kABGR_4444 == colorType;
+        case VK_FORMAT_B4G4R4A4_UNORM_PACK16:     return GrColorType::kABGR_4444 == colorType;
+        case VK_FORMAT_R4G4B4A4_UNORM_PACK16:     return GrColorType::kABGR_4444 == colorType;
         case VK_FORMAT_R8_UNORM:                 return GrColorType::kAlpha_8 == colorType ||
-                                                        GrColorType::kGray_8 == colorType;
-        case VK_FORMAT_R32G32B32A32_SFLOAT:      return GrColorType::kRGBA_F32 == colorType;
-        case VK_FORMAT_R16G16B16A16_SFLOAT:      return GrColorType::kRGBA_F16 == colorType ||
-                                                        GrColorType::kRGBA_F16_Clamped == colorType;
-        case VK_FORMAT_R16_SFLOAT:               return GrColorType::kAlpha_F16 == colorType;
-        case VK_FORMAT_R16_UNORM:                return GrColorType::kR_16 == colorType;
-        case VK_FORMAT_R16G16_UNORM:             return GrColorType::kRG_1616 == colorType;
+                                                         GrColorType::kGray_8 == colorType;
+        case VK_FORMAT_R32G32B32A32_SFLOAT:       return GrColorType::kRGBA_F32 == colorType;
+        case VK_FORMAT_R16G16B16A16_SFLOAT:       return GrColorType::kRGBA_F16 == colorType ||
+                                                         GrColorType::kRGBA_F16_Clamped == colorType;
+        case VK_FORMAT_R16_SFLOAT:                return GrColorType::kAlpha_F16 == colorType;
+        case VK_FORMAT_R16_UNORM:                 return GrColorType::kR_16 == colorType;
+        case VK_FORMAT_R16G16_UNORM:              return GrColorType::kRG_1616 == colorType;
+        case VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM: return GrColorType::kRGB_888x == colorType;
+        case VK_FORMAT_G8_B8R8_2PLANE_420_UNORM:  return GrColorType::kRGB_888x == colorType;
         // Experimental (for Y416 and mutant P016/P010)
-        case VK_FORMAT_R16G16B16A16_UNORM:       return GrColorType::kRGBA_16161616 == colorType;
-        case VK_FORMAT_R16G16_SFLOAT:            return GrColorType::kRG_F16 == colorType;
-        default:                                 return false;
+        case VK_FORMAT_R16G16B16A16_UNORM:        return GrColorType::kRGBA_16161616 == colorType;
+        case VK_FORMAT_R16G16_SFLOAT:             return GrColorType::kRG_F16 == colorType;
+        default:                                  return false;
     }
 
     SkUNREACHABLE;
@@ -148,6 +150,8 @@
         case VK_FORMAT_R16_SFLOAT:
         case VK_FORMAT_R16_UNORM:
         case VK_FORMAT_R16G16_UNORM:
+        case VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM:
+        case VK_FORMAT_G8_B8R8_2PLANE_420_UNORM:
         // Experimental (for Y416 and mutant P016/P010)
         case VK_FORMAT_R16G16B16A16_UNORM:
         case VK_FORMAT_R16G16_SFLOAT:
@@ -157,6 +161,11 @@
     }
 }
 
+bool GrVkFormatNeedsYcbcrSampler(VkFormat format) {
+    return format == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM ||
+           format == VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM;
+}
+
 bool GrSampleCountToVkSampleCount(uint32_t samples, VkSampleCountFlagBits* vkSamples) {
     SkASSERT(samples >= 1);
     switch (samples) {
diff --git a/src/gpu/vk/GrVkUtil.h b/src/gpu/vk/GrVkUtil.h
index d58f585..dded319 100644
--- a/src/gpu/vk/GrVkUtil.h
+++ b/src/gpu/vk/GrVkUtil.h
@@ -36,6 +36,8 @@
 
 bool GrVkFormatIsSupported(VkFormat);
 
+bool GrVkFormatNeedsYcbcrSampler(VkFormat format);
+
 #ifdef SK_DEBUG
 /**
  * Returns true if the passed in VkFormat and GrColorType are compatible with each other.
diff --git a/tests/VkYcbcrSamplerTest.cpp b/tests/VkYcbcrSamplerTest.cpp
new file mode 100644
index 0000000..19efbec
--- /dev/null
+++ b/tests/VkYcbcrSamplerTest.cpp
@@ -0,0 +1,430 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkTypes.h"
+
+#if SK_SUPPORT_GPU && defined(SK_VULKAN)
+
+#include "include/core/SkImage.h"
+#include "include/core/SkSurface.h"
+#include "include/gpu/GrContext.h"
+#include "include/gpu/vk/GrVkBackendContext.h"
+#include "include/gpu/vk/GrVkExtensions.h"
+#include "tests/Test.h"
+#include "tools/gpu/vk/VkTestUtils.h"
+
+const size_t kImageWidth = 8;
+const size_t kImageHeight = 8;
+
+static int getY(size_t x, size_t y) {
+    return 16 + (x + y) * 219 / (kImageWidth + kImageHeight - 2);
+}
+static int getU(size_t x, size_t y) { return 16 + x * 224 / (kImageWidth - 1); }
+static int getV(size_t x, size_t y) { return 16 + y * 224 / (kImageHeight - 1); }
+
+#define DECLARE_VK_PROC(name) PFN_vk##name fVk##name
+
+#define ACQUIRE_INST_VK_PROC(name)                                                           \
+    fVk##name = reinterpret_cast<PFN_vk##name>(getProc("vk" #name, fBackendContext.fInstance,\
+                                                       VK_NULL_HANDLE));                     \
+    if (fVk##name == nullptr) {                                                              \
+        ERRORF(reporter, "Function ptr for vk%s could not be acquired\n", #name);            \
+        return false;                                                                        \
+    }
+
+#define ACQUIRE_DEVICE_VK_PROC(name)                                                          \
+    fVk##name = reinterpret_cast<PFN_vk##name>(getProc("vk" #name, VK_NULL_HANDLE, fDevice)); \
+    if (fVk##name == nullptr) {                                                               \
+        ERRORF(reporter, "Function ptr for vk%s could not be acquired\n", #name);             \
+        return false;                                                                         \
+    }
+
+class VkYcbcrSamplerTestHelper {
+public:
+    VkYcbcrSamplerTestHelper() {}
+    ~VkYcbcrSamplerTestHelper();
+
+    bool init(skiatest::Reporter* reporter);
+
+    sk_sp<SkImage> createI420Image(skiatest::Reporter* reporter);
+
+    GrContext* getGrContext() { return fGrContext.get(); }
+
+private:
+    GrVkExtensions fExtensions;
+    VkPhysicalDeviceFeatures2 fFeatures = {};
+    VkDebugReportCallbackEXT fDebugCallback = VK_NULL_HANDLE;
+
+    DECLARE_VK_PROC(DestroyInstance);
+    DECLARE_VK_PROC(DeviceWaitIdle);
+    DECLARE_VK_PROC(DestroyDevice);
+
+    DECLARE_VK_PROC(GetPhysicalDeviceFormatProperties);
+    DECLARE_VK_PROC(GetPhysicalDeviceMemoryProperties);
+
+    DECLARE_VK_PROC(CreateImage);
+    DECLARE_VK_PROC(DestroyImage);
+    DECLARE_VK_PROC(GetImageMemoryRequirements);
+    DECLARE_VK_PROC(AllocateMemory);
+    DECLARE_VK_PROC(FreeMemory);
+    DECLARE_VK_PROC(BindImageMemory);
+    DECLARE_VK_PROC(MapMemory);
+    DECLARE_VK_PROC(UnmapMemory);
+    DECLARE_VK_PROC(FlushMappedMemoryRanges);
+    DECLARE_VK_PROC(GetImageSubresourceLayout);
+
+    VkDevice fDevice = VK_NULL_HANDLE;
+
+    PFN_vkDestroyDebugReportCallbackEXT fDestroyDebugCallback = nullptr;
+
+    GrVkBackendContext fBackendContext;
+    sk_sp<GrContext> fGrContext;
+
+    VkImage fImage = VK_NULL_HANDLE;
+    VkDeviceMemory fImageMemory = VK_NULL_HANDLE;
+    GrBackendTexture texture;
+};
+
+VkYcbcrSamplerTestHelper::~VkYcbcrSamplerTestHelper() {
+    fGrContext.reset();
+
+    if (fImage != VK_NULL_HANDLE) {
+        fVkDestroyImage(fDevice, fImage, nullptr);
+        fImage = VK_NULL_HANDLE;
+    }
+    if (fImageMemory != VK_NULL_HANDLE) {
+        fVkFreeMemory(fDevice, fImageMemory, nullptr);
+        fImageMemory = VK_NULL_HANDLE;
+    }
+
+    fBackendContext.fMemoryAllocator.reset();
+    if (fDevice != VK_NULL_HANDLE) {
+        fVkDeviceWaitIdle(fDevice);
+        fVkDestroyDevice(fDevice, nullptr);
+        fDevice = VK_NULL_HANDLE;
+    }
+    if (fDebugCallback != VK_NULL_HANDLE) {
+        fDestroyDebugCallback(fBackendContext.fInstance, fDebugCallback, nullptr);
+    }
+    if (fBackendContext.fInstance != VK_NULL_HANDLE) {
+        fVkDestroyInstance(fBackendContext.fInstance, nullptr);
+        fBackendContext.fInstance = VK_NULL_HANDLE;
+    }
+
+    sk_gpu_test::FreeVulkanFeaturesStructs(&fFeatures);
+}
+
+bool VkYcbcrSamplerTestHelper::init(skiatest::Reporter* reporter) {
+    PFN_vkGetInstanceProcAddr instProc;
+    PFN_vkGetDeviceProcAddr devProc;
+    if (!sk_gpu_test::LoadVkLibraryAndGetProcAddrFuncs(&instProc, &devProc)) {
+        ERRORF(reporter, "Failed to load Vulkan");
+        return false;
+    }
+    auto getProc = [&instProc, &devProc](const char* proc_name,
+                                         VkInstance instance, VkDevice device) {
+        if (device != VK_NULL_HANDLE) {
+            return devProc(device, proc_name);
+        }
+        return instProc(instance, proc_name);
+    };
+
+    fFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
+    fFeatures.pNext = nullptr;
+
+    fBackendContext.fInstance = VK_NULL_HANDLE;
+    fBackendContext.fDevice = VK_NULL_HANDLE;
+
+    if (!sk_gpu_test::CreateVkBackendContext(getProc, &fBackendContext, &fExtensions, &fFeatures,
+                                             &fDebugCallback, nullptr, sk_gpu_test::CanPresentFn(),
+                                             false)) {
+        return false;
+    }
+    fDevice = fBackendContext.fDevice;
+
+    if (fDebugCallback != VK_NULL_HANDLE) {
+        fDestroyDebugCallback = reinterpret_cast<PFN_vkDestroyDebugReportCallbackEXT>(
+                instProc(fBackendContext.fInstance, "vkDestroyDebugReportCallbackEXT"));
+    }
+    ACQUIRE_INST_VK_PROC(DestroyInstance)
+    ACQUIRE_INST_VK_PROC(DeviceWaitIdle)
+    ACQUIRE_INST_VK_PROC(DestroyDevice)
+
+    ACQUIRE_INST_VK_PROC(GetPhysicalDeviceFormatProperties)
+    ACQUIRE_INST_VK_PROC(GetPhysicalDeviceMemoryProperties)
+
+    ACQUIRE_DEVICE_VK_PROC(CreateImage)
+    ACQUIRE_DEVICE_VK_PROC(DestroyImage)
+    ACQUIRE_DEVICE_VK_PROC(GetImageMemoryRequirements)
+    ACQUIRE_DEVICE_VK_PROC(AllocateMemory)
+    ACQUIRE_DEVICE_VK_PROC(FreeMemory)
+    ACQUIRE_DEVICE_VK_PROC(BindImageMemory)
+    ACQUIRE_DEVICE_VK_PROC(MapMemory)
+    ACQUIRE_DEVICE_VK_PROC(UnmapMemory)
+    ACQUIRE_DEVICE_VK_PROC(FlushMappedMemoryRanges)
+    ACQUIRE_DEVICE_VK_PROC(GetImageSubresourceLayout)
+
+    bool ycbcrSupported = false;
+    VkBaseOutStructure* feature = reinterpret_cast<VkBaseOutStructure*>(fFeatures.pNext);
+    while (feature) {
+        if (feature->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES) {
+            VkPhysicalDeviceSamplerYcbcrConversionFeatures* ycbcrFeatures =
+                    reinterpret_cast<VkPhysicalDeviceSamplerYcbcrConversionFeatures*>(feature);
+            ycbcrSupported = ycbcrFeatures->samplerYcbcrConversion;
+            break;
+        }
+        feature = feature->pNext;
+    }
+    if (!ycbcrSupported) {
+        return false;
+    }
+
+    fGrContext = GrContext::MakeVulkan(fBackendContext);
+    if (!fGrContext) {
+        return false;
+    }
+
+    return true;
+}
+
+sk_sp<SkImage> VkYcbcrSamplerTestHelper::createI420Image(skiatest::Reporter* reporter) {
+    // Verify that the image format is supported.
+    VkFormatProperties formatProperties;
+    fVkGetPhysicalDeviceFormatProperties(fBackendContext.fPhysicalDevice,
+                                         VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, &formatProperties);
+    if (!(formatProperties.linearTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) {
+        // VK_FORMAT_G8_B8R8_2PLANE_420_UNORM is not supported
+        return nullptr;
+    }
+
+    // Create YCbCr image.
+    VkImageCreateInfo vkImageInfo = {};
+    vkImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+    vkImageInfo.imageType = VK_IMAGE_TYPE_2D;
+    vkImageInfo.format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
+    vkImageInfo.extent = VkExtent3D{kImageWidth, kImageHeight, 1};
+    vkImageInfo.mipLevels = 1;
+    vkImageInfo.arrayLayers = 1;
+    vkImageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
+    vkImageInfo.tiling = VK_IMAGE_TILING_LINEAR;
+    vkImageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
+    vkImageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+    vkImageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+
+    REPORTER_ASSERT(reporter, fImage == VK_NULL_HANDLE);
+    if (fVkCreateImage(fDevice, &vkImageInfo, nullptr, &fImage) != VK_SUCCESS) {
+        ERRORF(reporter, "Failed to allocate I420 image");
+        return nullptr;
+    }
+
+    VkMemoryRequirements requirements;
+    fVkGetImageMemoryRequirements(fDevice, fImage, &requirements);
+
+    uint32_t memoryTypeIndex = 0;
+    bool foundHeap = false;
+    VkPhysicalDeviceMemoryProperties phyDevMemProps;
+    fVkGetPhysicalDeviceMemoryProperties(fBackendContext.fPhysicalDevice, &phyDevMemProps);
+    for (uint32_t i = 0; i < phyDevMemProps.memoryTypeCount && !foundHeap; ++i) {
+        if (requirements.memoryTypeBits & (1 << i)) {
+            // Map host-visible memory.
+            if (phyDevMemProps.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) {
+                memoryTypeIndex = i;
+                foundHeap = true;
+            }
+        }
+    }
+    if (!foundHeap) {
+        ERRORF(reporter, "Failed to find valid heap for imported memory");
+        return nullptr;
+    }
+
+    VkMemoryAllocateInfo allocInfo = {};
+    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+    allocInfo.allocationSize = requirements.size;
+    allocInfo.memoryTypeIndex = memoryTypeIndex;
+
+    REPORTER_ASSERT(reporter, fImageMemory == VK_NULL_HANDLE);
+    if (fVkAllocateMemory(fDevice, &allocInfo, nullptr, &fImageMemory) != VK_SUCCESS) {
+        ERRORF(reporter, "Failed to allocate VkDeviceMemory.");
+        return nullptr;
+    }
+
+    void* mappedBuffer;
+    if (fVkMapMemory(fDevice, fImageMemory, 0u, requirements.size, 0u, &mappedBuffer) !=
+        VK_SUCCESS) {
+        ERRORF(reporter, "Failed to map Vulkan memory.");
+        return nullptr;
+    }
+
+    // Write Y channel.
+    VkImageSubresource subresource;
+    subresource.aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT;
+    subresource.mipLevel = 0;
+    subresource.arrayLayer = 0;
+
+    VkSubresourceLayout yLayout;
+    fVkGetImageSubresourceLayout(fDevice, fImage, &subresource, &yLayout);
+    uint8_t* bufferData = reinterpret_cast<uint8_t*>(mappedBuffer) + yLayout.offset;
+    for (size_t y = 0; y < kImageHeight; ++y) {
+        for (size_t x = 0; x < kImageWidth; ++x) {
+            bufferData[y * yLayout.rowPitch + x] = getY(x, y);
+        }
+    }
+
+    // Write UV channels.
+    subresource.aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT;
+    VkSubresourceLayout uvLayout;
+    fVkGetImageSubresourceLayout(fDevice, fImage, &subresource, &uvLayout);
+    bufferData = reinterpret_cast<uint8_t*>(mappedBuffer) + uvLayout.offset;
+    for (size_t y = 0; y < kImageHeight / 2; ++y) {
+        for (size_t x = 0; x < kImageWidth / 2; ++x) {
+            bufferData[y * uvLayout.rowPitch + x * 2] = getU(x * 2, y * 2);
+            bufferData[y * uvLayout.rowPitch + x * 2 + 1] = getV(x * 2, y * 2);
+        }
+    }
+
+    VkMappedMemoryRange flushRange;
+    flushRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
+    flushRange.pNext = nullptr;
+    flushRange.memory = fImageMemory;
+    flushRange.offset = 0;
+    flushRange.size = VK_WHOLE_SIZE;
+    if (fVkFlushMappedMemoryRanges(fDevice, 1, &flushRange) != VK_SUCCESS) {
+        ERRORF(reporter, "Failed to flush buffer memory.");
+        return nullptr;
+    }
+    fVkUnmapMemory(fDevice, fImageMemory);
+
+    // Bind image memory.
+    if (fVkBindImageMemory(fDevice, fImage, fImageMemory, 0u) != VK_SUCCESS) {
+        ERRORF(reporter, "Failed to bind VkImage memory.");
+        return nullptr;
+    }
+
+    // Wrap the image into SkImage.
+    GrVkYcbcrConversionInfo ycbcrInfo(vkImageInfo.format,
+                                      /*externalFormat=*/0,
+                                      VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709,
+                                      VK_SAMPLER_YCBCR_RANGE_ITU_NARROW,
+                                      VK_CHROMA_LOCATION_COSITED_EVEN,
+                                      VK_CHROMA_LOCATION_COSITED_EVEN,
+                                      VK_FILTER_LINEAR,
+                                      false,
+                                      formatProperties.linearTilingFeatures);
+    GrVkAlloc alloc(fImageMemory, 0 /* offset */, requirements.size, 0 /* flags */);
+    GrVkImageInfo imageInfo(fImage, alloc, VK_IMAGE_TILING_LINEAR, VK_IMAGE_LAYOUT_UNDEFINED,
+                            vkImageInfo.format, 1 /* levelCount */, VK_QUEUE_FAMILY_IGNORED,
+                            GrProtected::kNo, ycbcrInfo);
+
+    texture = GrBackendTexture(kImageWidth, kImageHeight, imageInfo);
+    sk_sp<SkImage> image = SkImage::MakeFromTexture(fGrContext.get(),
+                                                    texture,
+                                                    kTopLeft_GrSurfaceOrigin,
+                                                    kRGB_888x_SkColorType,
+                                                    kPremul_SkAlphaType,
+                                                    nullptr);
+
+    if (!image) {
+        ERRORF(reporter, "Failed to wrap VkImage with SkImage");
+        return nullptr;
+    }
+
+    return image;
+}
+
+static int round_and_clamp(float x) {
+    int r = static_cast<int>(round(x));
+    if (r > 255) return 255;
+    if (r < 0) return 0;
+    return r;
+}
+
+DEF_GPUTEST(VkYCbcrSampler_DrawImageWithYcbcrSampler, reporter, options) {
+    VkYcbcrSamplerTestHelper helper;
+    if (!helper.init(reporter)) {
+        return;
+    }
+
+    sk_sp<SkImage> srcImage = helper.createI420Image(reporter);
+    if (!srcImage) {
+        return;
+    }
+
+    sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(
+            helper.getGrContext(), SkBudgeted::kNo,
+            SkImageInfo::Make(kImageWidth, kImageHeight, kN32_SkColorType, kPremul_SkAlphaType));
+    if (!surface) {
+        ERRORF(reporter, "Failed to create target SkSurface");
+        return;
+    }
+    surface->getCanvas()->drawImage(srcImage, 0, 0);
+    surface->flush();
+
+    std::vector<uint8_t> readbackData(kImageWidth * kImageHeight * 4);
+    if (!surface->readPixels(SkImageInfo::Make(kImageWidth, kImageHeight, kRGBA_8888_SkColorType,
+                                               kOpaque_SkAlphaType),
+                             readbackData.data(), kImageWidth * 4, 0, 0)) {
+        ERRORF(reporter, "Readback failed");
+        return;
+    }
+
+    // Allow resulting color to be off by 1 in each channel as some Vulkan implementations do not
+    // round YCbCr sampler result properly.
+    const int kColorTolerance = 1;
+
+    // Verify results only for pixels with even coordinates, since others use
+    // interpolated U & V channels.
+    for (size_t y = 0; y < kImageHeight; y += 2) {
+        for (size_t x = 0; x < kImageWidth; x += 2) {
+            // createI420Image() initializes the image with VK_SAMPLER_YCBCR_RANGE_ITU_NARROW.
+            float yChannel = (static_cast<float>(getY(x, y)) - 16.0) / 219.0;
+            float uChannel = (static_cast<float>(getU(x, y)) - 128.0) / 224.0;
+            float vChannel = (static_cast<float>(getV(x, y)) - 128.0) / 224.0;
+
+            // BR.709 conversion as specified in
+            // https://www.khronos.org/registry/DataFormat/specs/1.2/dataformat.1.2.html#MODEL_YUV
+            int expectedR = round_and_clamp((yChannel + 1.5748f * vChannel) * 255.0);
+            int expectedG = round_and_clamp((yChannel - 0.13397432f / 0.7152f * uChannel -
+                                             0.33480248f / 0.7152f * vChannel) *
+                                            255.0);
+            int expectedB = round_and_clamp((yChannel + 1.8556f * uChannel) * 255.0);
+
+            int r = readbackData[(y * kImageWidth + x) * 4];
+            if (abs(r - expectedR) > kColorTolerance) {
+                ERRORF(reporter, "R should be %d, but is %d at (%d, %d)", expectedR, r, x, y);
+            }
+
+            int g = readbackData[(y * kImageWidth + x) * 4 + 1];
+            if (abs(g - expectedG) > kColorTolerance) {
+                ERRORF(reporter, "G should be %d, but is %d at (%d, %d)", expectedG, g, x, y);
+            }
+
+            int b = readbackData[(y * kImageWidth + x) * 4 + 2];
+            if (abs(b - expectedB) > kColorTolerance) {
+                ERRORF(reporter, "B should be %d, but is %d at (%d, %d)", expectedB, b, x, y);
+            }
+        }
+    }
+}
+
+// Verifies that it's not possible to allocate Ycbcr texture directly.
+DEF_GPUTEST(VkYCbcrSampler_NoYcbcrSurface, reporter, options) {
+    VkYcbcrSamplerTestHelper helper;
+    if (!helper.init(reporter)) {
+        return;
+    }
+
+    GrBackendTexture texture = helper.getGrContext()->createBackendTexture(
+            kImageWidth, kImageHeight, GrBackendFormat::MakeVk(VK_FORMAT_G8_B8R8_2PLANE_420_UNORM),
+            GrMipMapped::kNo, GrRenderable::kNo, GrProtected::kNo);
+    if (texture.isValid()) {
+        ERRORF(reporter,
+               "GrContext::createBackendTexture() didn't fail as expected for Ycbcr format.");
+    }
+}
+
+#endif  // SK_SUPPORT_GPU && defined(SK_VULKAN)
diff --git a/tools/gpu/vk/VkTestUtils.cpp b/tools/gpu/vk/VkTestUtils.cpp
index 1a8066f..5b7e8c2 100644
--- a/tools/gpu/vk/VkTestUtils.cpp
+++ b/tools/gpu/vk/VkTestUtils.cpp
@@ -381,6 +381,7 @@
                 sizeof(VkPhysicalDeviceSamplerYcbcrConversionFeatures));
         ycbcrFeature->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES;
         ycbcrFeature->pNext = nullptr;
+        ycbcrFeature->samplerYcbcrConversion = VK_TRUE;
         *tailPNext = ycbcrFeature;
         tailPNext = &ycbcrFeature->pNext;
     }