#include "mocktail/graphics/vulkan_etc2_emulation.h" #include "mocktail/graphics/etc2_decoder.h" #include "mocktail/graphics/texture_override.h" #include #include #include #include #include #include namespace mocktail::graphics { namespace { void ExpectMapping(VkFormat format, VkFormat host, EtcFormat etc) { Etc2EmulatedFormat mapped; ASSERT_TRUE(LookupEmulatedEtc2Format(format, &mapped)) << format; EXPECT_EQ(mapped.host_format, host) << format; EXPECT_EQ(mapped.etc_format, etc) << format; } TEST(VulkanEtc2EmulationTest, MapsEveryEtc2AndEacFormat) { ExpectMapping(VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgb8); ExpectMapping(VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgb8); ExpectMapping(VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgb8A1); ExpectMapping(VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgb8A1); ExpectMapping(VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgba8); ExpectMapping(VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgba8); ExpectMapping(VK_FORMAT_EAC_R11_UNORM_BLOCK, VK_FORMAT_R16_UNORM, EtcFormat::kEacR11); ExpectMapping(VK_FORMAT_EAC_R11_SNORM_BLOCK, VK_FORMAT_R16_SNORM, EtcFormat::kEacR11Signed); ExpectMapping(VK_FORMAT_EAC_R11G11_UNORM_BLOCK, VK_FORMAT_R16G16_UNORM, EtcFormat::kEacRg11); ExpectMapping(VK_FORMAT_EAC_R11G11_SNORM_BLOCK, VK_FORMAT_R16G16_SNORM, EtcFormat::kEacRg11Signed); } TEST(VulkanEtc2EmulationTest, AdvertisesEtc2UnlessSwitchedOff) { EXPECT_TRUE(Etc2SupportAdvertised(nullptr)); EXPECT_TRUE(Etc2SupportAdvertised("")); EXPECT_TRUE(Etc2SupportAdvertised("1")); EXPECT_FALSE(Etc2SupportAdvertised("0")); } TEST(VulkanEtc2EmulationTest, ParsesSmallTextureUpscale) { EXPECT_EQ(SmallTextureUpscale(nullptr), 4u); EXPECT_EQ(SmallTextureUpscale(""), 4u); EXPECT_EQ(SmallTextureUpscale("abc"), 4u); EXPECT_EQ(SmallTextureUpscale("0"), 1u); EXPECT_EQ(SmallTextureUpscale("1"), 1u); EXPECT_EQ(SmallTextureUpscale("2"), 2u); EXPECT_EQ(SmallTextureUpscale("99"), 8u); } TEST(VulkanEtc2EmulationTest, IgnoresOtherFormats) { Etc2EmulatedFormat mapped; EXPECT_FALSE(LookupEmulatedEtc2Format(VK_FORMAT_R8G8B8A8_UNORM, &mapped)); EXPECT_FALSE(LookupEmulatedEtc2Format(VK_FORMAT_BC7_UNORM_BLOCK, &mapped)); EXPECT_FALSE( LookupEmulatedEtc2Format(VK_FORMAT_ASTC_4x4_UNORM_BLOCK, &mapped)); } TEST(VulkanEtc2EmulationTest, KeepsOnlySamplingAndTransferFeatures) { const VkFormatFeatureFlags host = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; EXPECT_EQ(EmulatedEtc2FormatFeatures(host), static_cast( VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT)); EXPECT_EQ(EmulatedEtc2FormatFeatures(VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT), 0U); } TEST(VulkanEtc2EmulationTest, CountsUploadBytesPerRegion) { VkDeviceSize compressed = 0; VkDeviceSize decoded = 0; ASSERT_TRUE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgba8, {5, 3, 1}, 2, &compressed, &decoded)); EXPECT_EQ(compressed, 64U); EXPECT_EQ(decoded, 120U); ASSERT_TRUE(Etc2UploadByteCounts(EtcFormat::kEacR11, {1, 1, 1}, 1, &compressed, &decoded)); EXPECT_EQ(compressed, 8U); EXPECT_EQ(decoded, 2U); } TEST(VulkanEtc2EmulationTest, RejectsUnsupportedRegions) { VkDeviceSize compressed = 0; VkDeviceSize decoded = 0; EXPECT_FALSE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgb8, {4, 4, 2}, 1, &compressed, &decoded)); EXPECT_FALSE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgb8, {0, 4, 1}, 1, &compressed, &decoded)); EXPECT_FALSE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgb8, {4, 4, 1}, 0, &compressed, &decoded)); } template Handle FakeHandle(std::uintptr_t value) { return reinterpret_cast(value); } constexpr std::uintptr_t kSourceMemory = 0x300; constexpr std::uintptr_t kStagingBuffer = 0x500; constexpr std::uintptr_t kStagingMemory = 0x600; std::array g_source{}; // Every staging allocation maps here. Staging blocks are larger, but these // tests write only the first few kilobytes of any block. std::array g_staging{}; VkExtent3D g_created_extent{}; VkExtent3D g_copied_extent{}; VkDeviceSize g_created_buffer_size = 0; std::uintptr_t g_next_image = 0x100; std::uint32_t g_copy_image_calls = 0; std::uint32_t g_create_buffer_calls = 0; std::uint32_t g_allocate_memory_calls = 0; std::uint32_t g_destroy_buffer_calls = 0; std::uint32_t g_free_memory_calls = 0; VkImageBlit g_blit{}; VkFilter g_blit_filter = VK_FILTER_NEAREST; VKAPI_ATTR VkResult VKAPI_CALL FakeCreateImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { g_created_extent = info->extent; *image = FakeHandle(g_next_image++); return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL FakeCopyImage(VkCommandBuffer, VkImage, VkImageLayout, VkImage, VkImageLayout, std::uint32_t, const VkImageCopy*) { ++g_copy_image_calls; } VKAPI_ATTR void VKAPI_CALL FakeBlitImage(VkCommandBuffer, VkImage, VkImageLayout, VkImage, VkImageLayout, std::uint32_t count, const VkImageBlit* blits, VkFilter filter) { g_blit = count > 0 ? blits[0] : VkImageBlit{}; g_blit_filter = filter; } VKAPI_ATTR VkResult VKAPI_CALL FakeCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) { g_created_buffer_size = info->size; *buffer = FakeHandle(kStagingBuffer + g_create_buffer_calls); ++g_create_buffer_calls; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL FakeBufferRequirements( VkDevice, VkBuffer, VkMemoryRequirements* requirements) { requirements->size = g_created_buffer_size; requirements->memoryTypeBits = 1; } VKAPI_ATTR VkResult VKAPI_CALL FakeAllocateMemory( VkDevice, const VkMemoryAllocateInfo*, const VkAllocationCallbacks*, VkDeviceMemory* memory) { *memory = FakeHandle(kStagingMemory); ++g_allocate_memory_calls; return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL FakeBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL FakeMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize, VkMemoryMapFlags, void** data) { *data = memory == FakeHandle(kSourceMemory) ? g_source.data() + offset : g_staging.data() + offset; return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL FakeUnmapMemory(VkDevice, VkDeviceMemory) {} VKAPI_ATTR void VKAPI_CALL FakeDestroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) { ++g_destroy_buffer_calls; } VKAPI_ATTR void VKAPI_CALL FakeFreeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) { ++g_free_memory_calls; } // Records the host-write barrier that must precede each staged copy, so the // decode's writes are visible to the device even though they land after the // submit that reads them. std::uint32_t g_barriers_before_copy = 0; VkPipelineStageFlags g_barrier_src_stage = 0; VkPipelineStageFlags g_barrier_dst_stage = 0; VkAccessFlags g_barrier_src_access = 0; VkAccessFlags g_barrier_dst_access = 0; std::uint32_t g_pending_barriers = 0; VKAPI_ATTR void VKAPI_CALL FakePipelineBarrier( VkCommandBuffer, VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage, VkDependencyFlags, std::uint32_t count, const VkMemoryBarrier* barriers, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) { g_barrier_src_stage = src_stage; g_barrier_dst_stage = dst_stage; if (count > 0) { g_barrier_src_access = barriers[0].srcAccessMask; g_barrier_dst_access = barriers[0].dstAccessMask; } ++g_pending_barriers; } // Staging buffer and offset of each recorded copy's first region. struct StagedCopy { VkBuffer buffer = VK_NULL_HANDLE; VkDeviceSize offset = 0; }; std::vector g_staged_copies; VKAPI_ATTR void VKAPI_CALL FakeCopyBufferToImage( VkCommandBuffer, VkBuffer buffer, VkImage, VkImageLayout, std::uint32_t count, const VkBufferImageCopy* regions) { g_copied_extent = count > 0 ? regions[0].imageExtent : VkExtent3D{}; g_barriers_before_copy = g_pending_barriers; if (count > 0) { g_staged_copies.push_back({buffer, regions[0].bufferOffset}); } } constexpr std::uintptr_t kTimelineSemaphore = 0x900; std::mutex g_signal_mutex; std::vector g_signalled; VKAPI_ATTR VkResult VKAPI_CALL FakeCreateSemaphore( VkDevice, const VkSemaphoreCreateInfo*, const VkAllocationCallbacks*, VkSemaphore* semaphore) { *semaphore = FakeHandle(kTimelineSemaphore); return VK_SUCCESS; } VKAPI_ATTR VkResult VKAPI_CALL FakeSignalSemaphore(VkDevice, const VkSemaphoreSignalInfo* info) { std::lock_guard lock(g_signal_mutex); g_signalled.push_back(info->value); return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL FakeDestroySemaphore(VkDevice, VkSemaphore, const VkAllocationCallbacks*) {} std::vector SignalledValues() { std::lock_guard lock(g_signal_mutex); return g_signalled; } void ResetSignalledValues() { std::lock_guard lock(g_signal_mutex); g_signalled.clear(); } VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr( VkDevice, const char* name) { const auto is = [name](const char* candidate) { return std::strcmp(name, candidate) == 0; }; if (is("vkCreateSemaphore")) return reinterpret_cast(FakeCreateSemaphore); if (is("vkSignalSemaphore")) return reinterpret_cast(FakeSignalSemaphore); if (is("vkDestroySemaphore")) return reinterpret_cast(FakeDestroySemaphore); if (is("vkCreateImage")) return reinterpret_cast(FakeCreateImage); if (is("vkCreateBuffer")) return reinterpret_cast(FakeCreateBuffer); if (is("vkGetBufferMemoryRequirements")) return reinterpret_cast(FakeBufferRequirements); if (is("vkAllocateMemory")) return reinterpret_cast(FakeAllocateMemory); if (is("vkBindBufferMemory")) return reinterpret_cast(FakeBindBufferMemory); if (is("vkMapMemory")) return reinterpret_cast(FakeMapMemory); if (is("vkUnmapMemory")) return reinterpret_cast(FakeUnmapMemory); if (is("vkDestroyBuffer")) return reinterpret_cast(FakeDestroyBuffer); if (is("vkFreeMemory")) return reinterpret_cast(FakeFreeMemory); if (is("vkCmdCopyBufferToImage")) return reinterpret_cast(FakeCopyBufferToImage); if (is("vkCmdPipelineBarrier")) return reinterpret_cast(FakePipelineBarrier); if (is("vkCmdCopyImage")) return reinterpret_cast(FakeCopyImage); if (is("vkCmdBlitImage")) return reinterpret_cast(FakeBlitImage); return nullptr; } TEST(VulkanEtc2EmulationTest, SkipsUploadOutsideTheMappedWindow) { const VkDevice device = FakeHandle(0x10); const VkBuffer source = FakeHandle(0x200); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x700); g_source.fill(0); g_staging.fill(0xAA); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x20), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.extent = {4, 4, 1}; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); // The 8-byte block sits at memory offset 16, but only [0, 8) is mapped. ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 16), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, 8, 0, &mapped), VK_SUCCESS); VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = {4, 4, 1}; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); emulation.PrepareSubmit(&command_buffer, 1); EXPECT_EQ(g_staging[0], 0xAA); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, DecodesAgainOnEveryResubmit) { const VkDevice device = FakeHandle(0x11); const VkBuffer source = FakeHandle(0x201); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x701); g_source.fill(0); g_staging.fill(0xAA); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x21), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.extent = {4, 4, 1}; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = {4, 4, 1}; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); // Differential block: every texel decodes to 134. const std::array first = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; std::memcpy(g_source.data(), first.data(), first.size()); emulation.PrepareSubmit(&command_buffer, 1); EXPECT_EQ(g_staging[0], 134); // Individual block: every texel decodes to 138. const std::array second = {0x88, 0x88, 0x88, 0x00, 0, 0, 0, 0}; std::memcpy(g_source.data(), second.data(), second.size()); emulation.PrepareSubmit(&command_buffer, 1); EXPECT_EQ(g_staging[0], 138); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, UpscalesSmallTexturesAndTheirUploads) { const VkDevice device = FakeHandle(0x12); const VkBuffer source = FakeHandle(0x202); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x702); g_source.fill(0); g_staging.fill(0xAA); ASSERT_EQ(setenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE", "2", 1), 0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; ASSERT_EQ(unsetenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE"), 0); emulation.RegisterDevice(device, FakeHandle(0x22), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {4, 4, 1}; image_info.mipLevels = 1; image_info.arrayLayers = 1; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); EXPECT_EQ(g_created_extent.width, 8u); EXPECT_EQ(g_created_extent.height, 8u); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = {4, 4, 1}; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); EXPECT_EQ(g_copied_extent.width, 8u); EXPECT_EQ(g_copied_extent.height, 8u); const std::array block = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; std::memcpy(g_source.data(), block.data(), block.size()); emulation.PrepareSubmit(&command_buffer, 1); // The host region is 8x8: every 4x4 texel repeated 2x2. std::array expected{}; ASSERT_TRUE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, block.data(), block.size(), 4, 4, expected.data(), expected.size())); for (std::size_t y = 0; y < 8; ++y) { for (std::size_t x = 0; x < 8; ++x) { for (std::size_t channel = 0; channel < 4; ++channel) { ASSERT_EQ(g_staging[(y * 8 + x) * 4 + channel], expected[((y / 2) * 4 + x / 2) * 4 + channel]) << x << "," << y; } } } EXPECT_EQ(g_staging[8 * 8 * 4], 0xAA); emulation.ReleaseCommandBuffer(command_buffer); } // Block-rounded uploads (a 1x1 mip sent as 4x4) must not scale past the // host mip; the host region snaps to the host mip's edge instead. TEST(VulkanEtc2EmulationTest, ClampsScaledUploadsToTheHostMip) { const VkDevice device = FakeHandle(0x14); const VkBuffer source = FakeHandle(0x204); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x704); g_source.fill(0); g_staging.fill(0xAA); ASSERT_EQ(setenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE", "2", 1), 0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; ASSERT_EQ(unsetenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE"), 0); emulation.RegisterDevice(device, FakeHandle(0x24), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {4, 4, 1}; image_info.mipLevels = 3; image_info.arrayLayers = 1; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); // Mip 2 is 1x1 for the application and 2x2 on the host. VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.mipLevel = 2; region.imageSubresource.layerCount = 1; region.imageExtent = {4, 4, 1}; g_copied_extent = {}; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); EXPECT_EQ(g_copied_extent.width, 2u); EXPECT_EQ(g_copied_extent.height, 2u); // Differential block: every texel decodes to 134. const std::array block = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; std::memcpy(g_source.data(), block.data(), block.size()); emulation.PrepareSubmit(&command_buffer, 1); for (std::size_t index = 0; index < 2 * 2 * 4; ++index) { ASSERT_EQ(g_staging[index], index % 4 == 3 ? 255 : 134) << index; } EXPECT_EQ(g_staging[2 * 2 * 4], 0xAA); emulation.ReleaseCommandBuffer(command_buffer); } // Drawn at its own size, a scaled texture samples a smaller host mip, so // every host mip carries level-0 detail rather than the application's mip. TEST(VulkanEtc2EmulationTest, FillsScaledMipsFromLevelZero) { const VkDevice device = FakeHandle(0x15); const VkBuffer source = FakeHandle(0x205); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x705); g_source.fill(0); g_staging.fill(0xAA); ASSERT_EQ(setenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE", "2", 1), 0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; ASSERT_EQ(unsetenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE"), 0); emulation.RegisterDevice(device, FakeHandle(0x25), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {4, 4, 1}; image_info.mipLevels = 3; image_info.arrayLayers = 1; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); // Level 0 decodes to 134 everywhere, level 1 (at byte 8) to 138. const std::array level0 = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; const std::array level1 = {0x88, 0x88, 0x88, 0x00, 0, 0, 0, 0}; std::memcpy(g_source.data(), level0.data(), level0.size()); std::memcpy(g_source.data() + 8, level1.data(), level1.size()); std::array regions{}; regions[0].imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; regions[0].imageSubresource.layerCount = 1; regions[0].imageExtent = {4, 4, 1}; regions[1] = regions[0]; regions[1].bufferOffset = 8; regions[1].imageSubresource.mipLevel = 1; regions[1].imageExtent = {2, 2, 1}; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, regions.data()); emulation.PrepareSubmit(&command_buffer, 1); // Host level 0 is 8x8 (256 bytes); host level 1 is 4x4 and follows it, // holding level 0 itself rather than the 138 texels of level 1. RgbaImage level0_texels; level0_texels.width = 4; level0_texels.height = 4; level0_texels.pixels.resize(4 * 4 * 4); ASSERT_TRUE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, level0.data(), level0.size(), 4, 4, level0_texels.pixels.data(), level0_texels.pixels.size())); std::array expected{}; ResampleRgba(level0_texels, 4, 4, expected.data()); for (std::size_t index = 0; index < expected.size(); ++index) { ASSERT_EQ(g_staging[256 + index], expected[index]) << index; } emulation.ReleaseCommandBuffer(command_buffer); } // Level 0 carries the texels every scaled mip is built from, so a mip // recorded ahead of it must still resolve against it. TEST(VulkanEtc2EmulationTest, FillsScaledMipsFromLevelZeroRecordedLast) { const VkDevice device = FakeHandle(0x16); const VkBuffer source = FakeHandle(0x206); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x706); g_source.fill(0); g_staging.fill(0xAA); ASSERT_EQ(setenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE", "2", 1), 0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; ASSERT_EQ(unsetenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE"), 0); emulation.RegisterDevice(device, FakeHandle(0x26), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {4, 4, 1}; image_info.mipLevels = 3; image_info.arrayLayers = 1; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); const std::array level0 = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; const std::array level1 = {0x88, 0x88, 0x88, 0x00, 0, 0, 0, 0}; std::memcpy(g_source.data(), level0.data(), level0.size()); std::memcpy(g_source.data() + 8, level1.data(), level1.size()); // Level 1 is recorded first, so its staging bytes lead the batch. std::array regions{}; regions[0].imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; regions[0].imageSubresource.layerCount = 1; regions[0].bufferOffset = 8; regions[0].imageSubresource.mipLevel = 1; regions[0].imageExtent = {2, 2, 1}; regions[1] = regions[0]; regions[1].bufferOffset = 0; regions[1].imageSubresource.mipLevel = 0; regions[1].imageExtent = {4, 4, 1}; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, regions.data()); emulation.PrepareSubmit(&command_buffer, 1); RgbaImage level0_texels; level0_texels.width = 4; level0_texels.height = 4; level0_texels.pixels.resize(4 * 4 * 4); ASSERT_TRUE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, level0.data(), level0.size(), 4, 4, level0_texels.pixels.data(), level0_texels.pixels.size())); std::array expected{}; ResampleRgba(level0_texels, 4, 4, expected.data()); for (std::size_t index = 0; index < expected.size(); ++index) { ASSERT_EQ(g_staging[index], expected[index]) << index; } emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, ReusesStagingBuffersAcrossCommandBuffers) { const VkDevice device = FakeHandle(0x17); const VkBuffer source = FakeHandle(0x207); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); g_source.fill(0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x27), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.extent = {4, 4, 1}; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); g_create_buffer_calls = 0; g_allocate_memory_calls = 0; g_destroy_buffer_calls = 0; g_free_memory_calls = 0; VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = {4, 4, 1}; for (std::uintptr_t frame = 0; frame < 8; ++frame) { const VkCommandBuffer command_buffer = FakeHandle(0x780 + frame); emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); emulation.PrepareSubmit(&command_buffer, 1); emulation.ReleaseCommandBuffer(command_buffer); } EXPECT_EQ(g_create_buffer_calls, 1u); EXPECT_EQ(g_allocate_memory_calls, 1u); EXPECT_EQ(g_destroy_buffer_calls, 0u); emulation.DestroyDevice(device); EXPECT_EQ(g_destroy_buffer_calls, 1u); EXPECT_EQ(g_free_memory_calls, 1u); } // Registers `device` and creates an EAC R11 image of `extent` whose uploads // read from `source`. EAC is never upscaled, so staging bytes are the // decoded size: two bytes per texel. VkImage SetUpStagingTest(VulkanEtc2Emulation& emulation, VkDevice device, VkBuffer source, VkExtent3D extent) { VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; emulation.RegisterDevice(device, FakeHandle(0x28), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_EAC_R11_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = extent; image_info.mipLevels = 1; image_info.arrayLayers = 1; VkImage image = VK_NULL_HANDLE; EXPECT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); EXPECT_EQ(emulation.BindBufferMemory( device, source, FakeHandle(kSourceMemory), 0), VK_SUCCESS); g_create_buffer_calls = 0; g_destroy_buffer_calls = 0; g_staged_copies.clear(); return image; } void RecordUpload(VulkanEtc2Emulation& emulation, VkDevice device, VkCommandBuffer command_buffer, VkBuffer source, VkImage image, VkExtent3D extent) { VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = extent; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); } // One buffer and allocation per upload costs the render thread a driver call // on each side of the upload's life, so uploads share large blocks. TEST(VulkanEtc2EmulationTest, SubAllocatesUploadsFromOneStagingBlock) { const VkDevice device = FakeHandle(0x18); const VkBuffer source = FakeHandle(0x208); VulkanEtc2Emulation emulation; const VkImage image = SetUpStagingTest(emulation, device, source, {4, 4, 1}); std::vector command_buffers; for (std::uintptr_t index = 0; index < 8; ++index) { command_buffers.push_back(FakeHandle(0x790 + index)); RecordUpload(emulation, device, command_buffers.back(), source, image, {4, 4, 1}); } EXPECT_EQ(g_create_buffer_calls, 1u); ASSERT_EQ(g_staged_copies.size(), 8u); for (std::size_t index = 0; index < g_staged_copies.size(); ++index) { EXPECT_EQ(g_staged_copies[index].buffer, g_staged_copies[0].buffer); EXPECT_EQ(g_staged_copies[index].offset % 4, 0u) << index; if (index > 0) { // Each upload takes 4 x 4 texels of 2 bytes, clear of the one before. EXPECT_GE(g_staged_copies[index].offset, g_staged_copies[index - 1].offset + 4 * 4 * 2) << index; } } for (VkCommandBuffer command_buffer : command_buffers) { emulation.ReleaseCommandBuffer(command_buffer); } EXPECT_EQ(g_destroy_buffer_calls, 0u); emulation.DestroyDevice(device); EXPECT_EQ(g_destroy_buffer_calls, 1u); } TEST(VulkanEtc2EmulationTest, StartsANewStagingBlockWhenTheCurrentIsFull) { const VkDevice device = FakeHandle(0x19); const VkBuffer source = FakeHandle(0x209); VulkanEtc2Emulation emulation(1024); // 16 x 16 texels of 2 bytes: two uploads fill a block. const VkImage image = SetUpStagingTest(emulation, device, source, {16, 16, 1}); const VkCommandBuffer command_buffer = FakeHandle(0x7a0); for (int upload = 0; upload < 3; ++upload) { RecordUpload(emulation, device, command_buffer, source, image, {16, 16, 1}); } EXPECT_EQ(g_create_buffer_calls, 2u); ASSERT_EQ(g_staged_copies.size(), 3u); EXPECT_EQ(g_staged_copies[1].buffer, g_staged_copies[0].buffer); EXPECT_NE(g_staged_copies[2].buffer, g_staged_copies[0].buffer); EXPECT_EQ(g_staged_copies[2].offset, 0u); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, GivesAnUploadLargerThanABlockItsOwnBlock) { const VkDevice device = FakeHandle(0x1a); const VkBuffer source = FakeHandle(0x20a); VulkanEtc2Emulation emulation(1024); const VkImage image = SetUpStagingTest(emulation, device, source, {32, 32, 1}); const VkCommandBuffer command_buffer = FakeHandle(0x7a1); RecordUpload(emulation, device, command_buffer, source, image, {32, 32, 1}); EXPECT_EQ(g_create_buffer_calls, 1u); EXPECT_GE(g_created_buffer_size, 32u * 32u * 2u); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, RefillsAStagingBlockOnceItsCopiesAreReleased) { const VkDevice device = FakeHandle(0x1b); const VkBuffer source = FakeHandle(0x20b); VulkanEtc2Emulation emulation(1024); const VkImage image = SetUpStagingTest(emulation, device, source, {16, 16, 1}); const VkCommandBuffer first = FakeHandle(0x7a2); const VkCommandBuffer second = FakeHandle(0x7a3); RecordUpload(emulation, device, first, source, image, {16, 16, 1}); RecordUpload(emulation, device, first, source, image, {16, 16, 1}); emulation.ReleaseCommandBuffer(first); RecordUpload(emulation, device, second, source, image, {16, 16, 1}); EXPECT_EQ(g_create_buffer_calls, 1u); ASSERT_EQ(g_staged_copies.size(), 3u); EXPECT_EQ(g_staged_copies[2].buffer, g_staged_copies[0].buffer); EXPECT_EQ(g_staged_copies[2].offset, 0u); emulation.ReleaseCommandBuffer(second); } TEST(VulkanEtc2EmulationTest, KeepsFourIdleStagingBlocksAndDestroysTheRest) { const VkDevice device = FakeHandle(0x1c); const VkBuffer source = FakeHandle(0x20c); VulkanEtc2Emulation emulation(1024); const VkImage image = SetUpStagingTest(emulation, device, source, {16, 16, 1}); // Six command buffers each fill a block of their own. std::vector command_buffers; for (std::uintptr_t index = 0; index < 6; ++index) { command_buffers.push_back(FakeHandle(0x7b0 + index)); RecordUpload(emulation, device, command_buffers.back(), source, image, {16, 16, 1}); RecordUpload(emulation, device, command_buffers.back(), source, image, {16, 16, 1}); } ASSERT_EQ(g_create_buffer_calls, 6u); for (VkCommandBuffer command_buffer : command_buffers) { emulation.ReleaseCommandBuffer(command_buffer); } EXPECT_EQ(g_destroy_buffer_calls, 2u); const VkCommandBuffer again = FakeHandle(0x7c0); RecordUpload(emulation, device, again, source, image, {16, 16, 1}); EXPECT_EQ(g_create_buffer_calls, 6u); emulation.ReleaseCommandBuffer(again); } // Copies out of a scaled image cover its host bounds and shrink back onto // an unscaled destination, which a plain copy cannot do. TEST(VulkanEtc2EmulationTest, BlitsCopiesThatTouchScaledImages) { const VkDevice device = FakeHandle(0x16); const VkCommandBuffer command_buffer = FakeHandle(0x706); ASSERT_EQ(setenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE", "2", 1), 0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; ASSERT_EQ(unsetenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE"), 0); emulation.RegisterDevice(device, FakeHandle(0x26), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {4, 4, 1}; image_info.mipLevels = 3; image_info.arrayLayers = 1; VkImage small = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &small), VK_SUCCESS); image_info.extent = {128, 128, 1}; image_info.mipLevels = 8; VkImage large = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &large), VK_SUCCESS); // Mip 1 of the small image (2x2, block-rounded to 4x4) lands in mip 6 of // the large one, 2x2 at offset (1, 1). VkImageCopy region{}; region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.srcSubresource.mipLevel = 1; region.srcSubresource.layerCount = 1; region.dstSubresource = region.srcSubresource; region.dstSubresource.mipLevel = 6; region.dstOffset = {1, 1, 0}; region.extent = {4, 4, 1}; g_copy_image_calls = 0; g_blit = {}; emulation.CmdCopyImage(device, command_buffer, small, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, large, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); EXPECT_EQ(g_copy_image_calls, 0u); EXPECT_EQ(g_blit.srcSubresource.mipLevel, 1u); EXPECT_EQ(g_blit.srcOffsets[1].x, 4); EXPECT_EQ(g_blit.srcOffsets[1].y, 4); EXPECT_EQ(g_blit.dstSubresource.mipLevel, 6u); EXPECT_EQ(g_blit.dstOffsets[0].x, 1); EXPECT_EQ(g_blit.dstOffsets[1].x, 5); EXPECT_EQ(g_blit_filter, VK_FILTER_LINEAR); // Copies between unscaled images pass through untouched. emulation.CmdCopyImage(device, command_buffer, large, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, large, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); EXPECT_EQ(g_copy_image_calls, 1u); } // bufferRowLength counts texels of source pitch, not of the copied region, so // a padded row holds blocks the region does not cover. TEST(VulkanEtc2EmulationTest, ReadsCompressedRowsAtTheRequestedStride) { const VkDevice device = FakeHandle(0x18); const VkBuffer source = FakeHandle(0x208); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x708); g_source.fill(0); g_staging.fill(0xAA); ASSERT_EQ(setenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE", "1", 1), 0); VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VulkanEtc2Emulation emulation; ASSERT_EQ(unsetenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE"), 0); emulation.RegisterDevice(device, FakeHandle(0x28), true, memory, FakeGetDeviceProcAddr); VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {8, 8, 1}; image_info.mipLevels = 1; image_info.arrayLayers = 1; VkImage image = VK_NULL_HANDLE; ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); ASSERT_EQ(g_created_extent.width, 8u); ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); // Differential block: every texel decodes to 134. const std::array covered = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; // Individual block: every texel decodes to 138. const std::array padding = {0x88, 0x88, 0x88, 0x00, 0, 0, 0, 0}; // 16 texels of pitch is 4 blocks per row; the 8x8 region covers the first 2. const std::array layout = { covered.data(), covered.data(), padding.data(), padding.data(), covered.data(), covered.data()}; for (std::size_t block = 0; block < layout.size(); ++block) { std::memcpy(g_source.data() + block * 8, layout[block], 8); } VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = {8, 8, 1}; region.bufferRowLength = 16; emulation.CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); emulation.PrepareSubmit(&command_buffer, 1); std::array packed{}; for (std::size_t block = 0; block < 4; ++block) { std::memcpy(packed.data() + block * 8, covered.data(), covered.size()); } std::array expected{}; ASSERT_TRUE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, packed.data(), packed.size(), 8, 8, expected.data(), expected.size())); for (std::size_t index = 0; index < expected.size(); ++index) { ASSERT_EQ(g_staging[index], expected[index]) << "byte " << index; } emulation.ReleaseCommandBuffer(command_buffer); } // Records one 4x4 ETC2 upload whose every texel decodes to 134, and returns // the image it targets. VkImage RecordDecodeOf134(VulkanEtc2Emulation* emulation, VkDevice device, VkBuffer source, VkDeviceMemory source_memory, VkCommandBuffer command_buffer) { VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; image_info.extent = {4, 4, 1}; VkImage image = VK_NULL_HANDLE; EXPECT_EQ(emulation->CreateImage(device, &image_info, nullptr, &image), VK_SUCCESS); EXPECT_EQ(emulation->BindBufferMemory(device, source, source_memory, 0), VK_SUCCESS); void* mapped = nullptr; EXPECT_EQ(emulation->MapMemory(device, source_memory, 0, g_source.size(), 0, &mapped), VK_SUCCESS); const std::array block = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; std::memcpy(g_source.data(), block.data(), block.size()); VkBufferImageCopy region{}; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; region.imageExtent = {4, 4, 1}; emulation->CmdCopyBufferToImage(device, command_buffer, source, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); return image; } VkPhysicalDeviceMemoryProperties HostVisibleMemory() { VkPhysicalDeviceMemoryProperties memory{}; memory.memoryTypeCount = 1; memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; return memory; } TEST(VulkanEtc2EmulationTest, DecodesOffTheCallingThreadWithATimeline) { const VkDevice device = FakeHandle(0x18); const VkBuffer source = FakeHandle(0x208); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x708); g_source.fill(0); g_staging.fill(0xAA); ResetSignalledValues(); VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x28), true, HostVisibleMemory(), FakeGetDeviceProcAddr, true); RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer); const Etc2SubmitWait wait = emulation.PrepareSubmit(&command_buffer, 1, true); EXPECT_EQ(wait.semaphore, FakeHandle(kTimelineSemaphore)); EXPECT_GT(wait.value, 0U); // Releasing the command buffer waits for the decode that writes its staging. emulation.ReleaseCommandBuffer(command_buffer); EXPECT_EQ(g_staging[0], 134); EXPECT_EQ(SignalledValues(), std::vector{wait.value}); } TEST(VulkanEtc2EmulationTest, DecodesOnTheCallerWithoutATimeline) { const VkDevice device = FakeHandle(0x19); const VkBuffer source = FakeHandle(0x209); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x709); g_source.fill(0); g_staging.fill(0xAA); ResetSignalledValues(); VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x29), true, HostVisibleMemory(), FakeGetDeviceProcAddr, false); RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer); const Etc2SubmitWait wait = emulation.PrepareSubmit(&command_buffer, 1, true); EXPECT_EQ(wait.semaphore, VK_NULL_HANDLE); EXPECT_EQ(wait.value, 0U); // Decoded before the call returned, so no wait is owed. EXPECT_EQ(g_staging[0], 134); EXPECT_TRUE(SignalledValues().empty()); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, UnmapWaitsForDecodesReadingThatMemory) { const VkDevice device = FakeHandle(0x1C); const VkBuffer source = FakeHandle(0x20C); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x70D); g_source.fill(0); g_staging.fill(0xAA); ResetSignalledValues(); VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x2C), true, HostVisibleMemory(), FakeGetDeviceProcAddr, true); RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer); const Etc2SubmitWait wait = emulation.PrepareSubmit(&command_buffer, 1, true); ASSERT_NE(wait.semaphore, VK_NULL_HANDLE); // A deferred gather reads the application's mapping on a worker, so the // unmap must not return while a decode can still dereference it. emulation.UnmapMemory(device, source_memory); const std::vector signalled = SignalledValues(); ASSERT_FALSE(signalled.empty()) << "unmap returned with a decode in flight"; EXPECT_GE(signalled.back(), wait.value); EXPECT_EQ(g_staging[0], 134); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, FreeMemoryWaitsForDecodesReadingThatMemory) { const VkDevice device = FakeHandle(0x1D); const VkBuffer source = FakeHandle(0x20D); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x70E); g_source.fill(0); g_staging.fill(0xAA); ResetSignalledValues(); VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x2D), true, HostVisibleMemory(), FakeGetDeviceProcAddr, true); RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer); const Etc2SubmitWait wait = emulation.PrepareSubmit(&command_buffer, 1, true); ASSERT_NE(wait.semaphore, VK_NULL_HANDLE); emulation.FreeMemory(device, source_memory, nullptr); const std::vector signalled = SignalledValues(); ASSERT_FALSE(signalled.empty()) << "free returned with a decode in flight"; EXPECT_GE(signalled.back(), wait.value); EXPECT_EQ(g_staging[0], 134); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, GuardsStagedCopiesWithAHostWriteBarrier) { const VkDevice device = FakeHandle(0x1B); const VkBuffer source = FakeHandle(0x20B); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer command_buffer = FakeHandle(0x70C); g_source.fill(0); g_staging.fill(0xAA); g_pending_barriers = 0; g_barriers_before_copy = 0; VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x2B), true, HostVisibleMemory(), FakeGetDeviceProcAddr, true); RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer); // An asynchronous decode writes the staging after the submit that reads it, // so the queue submission's host-to-device domain operation does not cover // those writes; the copy must be preceded by an explicit barrier. EXPECT_EQ(g_barriers_before_copy, 1U); EXPECT_EQ(g_barrier_src_stage, static_cast(VK_PIPELINE_STAGE_HOST_BIT)); EXPECT_NE(g_barrier_dst_stage & static_cast(VK_PIPELINE_STAGE_TRANSFER_BIT), 0U); EXPECT_EQ(g_barrier_src_access, static_cast(VK_ACCESS_HOST_WRITE_BIT)); EXPECT_NE(g_barrier_dst_access & static_cast(VK_ACCESS_TRANSFER_READ_BIT), 0U); emulation.PrepareSubmit(&command_buffer, 1, true); emulation.ReleaseCommandBuffer(command_buffer); } TEST(VulkanEtc2EmulationTest, SignalsDecodeValuesInHandOutOrder) { const VkDevice device = FakeHandle(0x1A); const VkBuffer source = FakeHandle(0x20A); const VkDeviceMemory source_memory = FakeHandle(kSourceMemory); const VkCommandBuffer first = FakeHandle(0x70A); const VkCommandBuffer second = FakeHandle(0x70B); g_source.fill(0); g_staging.fill(0xAA); ResetSignalledValues(); VulkanEtc2Emulation emulation; emulation.RegisterDevice(device, FakeHandle(0x2A), true, HostVisibleMemory(), FakeGetDeviceProcAddr, true); RecordDecodeOf134(&emulation, device, source, source_memory, first); RecordDecodeOf134(&emulation, device, source, source_memory, second); const Etc2SubmitWait one = emulation.PrepareSubmit(&first, 1, true); const Etc2SubmitWait two = emulation.PrepareSubmit(&second, 1, true); EXPECT_GT(two.value, one.value); emulation.ReleaseCommandBuffer(first); emulation.ReleaseCommandBuffer(second); const std::vector signalled = SignalledValues(); ASSERT_FALSE(signalled.empty()); EXPECT_EQ(signalled.back(), two.value); for (std::size_t index = 1; index < signalled.size(); ++index) { EXPECT_GT(signalled[index], signalled[index - 1]) << "at " << index; } } } // namespace } // namespace mocktail::graphics