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