skia2/tests/VkYcbcrSamplerTest.cpp
Sergey Ulanov 2739fd2f09 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>
2019-08-12 14:21:01 +00:00

431 lines
17 KiB
C++

/*
* 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)