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