From eae9fa703018959cfebe2ea99102560664fd3b2f Mon Sep 17 00:00:00 2001 From: vii Date: Sun, 13 Sep 2026 11:45:28 -0500 Subject: [PATCH] feat(graphics): emulate ETC2/EAC textures on Vulkan Hosts without native ETC2 now report ETC2/EAC support. Uploads into emulated images are decoded to RGBA8/R16 staging buffers at submit. MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns it off. --- include/mocktail/graphics/etc2_decoder.h | 39 + .../mocktail/graphics/vulkan_etc2_emulation.h | 107 +++ src/graphics/bionic_vulkan_loader_adapter.cc | 416 ++++++++- src/graphics/etc2_decoder.cc | 278 ++++++ src/graphics/vulkan_etc2_emulation.cc | 848 ++++++++++++++++++ stubs/CMakeLists.txt | 2 + tests/CMakeLists.txt | 27 +- tests/etc2_decoder_test.cc | 196 ++++ tests/vulkan_etc2_emulation_test.cc | 279 ++++++ 9 files changed, 2188 insertions(+), 4 deletions(-) create mode 100644 include/mocktail/graphics/etc2_decoder.h create mode 100644 include/mocktail/graphics/vulkan_etc2_emulation.h create mode 100644 src/graphics/etc2_decoder.cc create mode 100644 src/graphics/vulkan_etc2_emulation.cc create mode 100644 tests/etc2_decoder_test.cc create mode 100644 tests/vulkan_etc2_emulation_test.cc diff --git a/include/mocktail/graphics/etc2_decoder.h b/include/mocktail/graphics/etc2_decoder.h new file mode 100644 index 0000000..838b0ab --- /dev/null +++ b/include/mocktail/graphics/etc2_decoder.h @@ -0,0 +1,39 @@ +#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_ +#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_ + +#include +#include + +namespace mocktail::graphics { + +// ETC2 and EAC block encodings from the OpenGL ES 3.0 / Vulkan core formats. +// sRGB variants share the unorm encoding; the colour space belongs to the +// destination format. +enum class EtcFormat { + kEtc2Rgb8, + kEtc2Rgb8A1, + kEtc2Rgba8, + kEacR11, + kEacR11Signed, + kEacRg11, + kEacRg11Signed, +}; + +// Compressed bytes per 4x4 block. +std::size_t EtcBlockBytes(EtcFormat format); + +// Decoded bytes per texel: 4 (RGBA8) for ETC2 formats, 2 per channel +// (16-bit, little-endian) for EAC formats. +std::size_t EtcDecodedTexelBytes(EtcFormat format); + +// Decodes a width x height image stored as ceil(width/4) x ceil(height/4) +// blocks into row-major texels. Returns false, writing nothing, when a buffer +// is smaller than the image requires. +bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source, + std::size_t source_bytes, std::uint32_t width, + std::uint32_t height, std::uint8_t* destination, + std::size_t destination_bytes); + +} // namespace mocktail::graphics + +#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_ diff --git a/include/mocktail/graphics/vulkan_etc2_emulation.h b/include/mocktail/graphics/vulkan_etc2_emulation.h new file mode 100644 index 0000000..4bb72ce --- /dev/null +++ b/include/mocktail/graphics/vulkan_etc2_emulation.h @@ -0,0 +1,107 @@ +#ifndef MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_ +#define MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_ + +#include + +#include + +#include "mocktail/graphics/etc2_decoder.h" + +namespace mocktail::graphics { + +// Host format that stores a decoded ETC2/EAC image, with its block encoding. +struct Etc2EmulatedFormat { + VkFormat host_format = VK_FORMAT_UNDEFINED; + EtcFormat etc_format = EtcFormat::kEtc2Rgb8; +}; + +// Maps an ETC2/EAC Vulkan format to its decoded host format. Returns false for +// every other format. +bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out); + +// Format features reported for an emulated ETC2/EAC format: the host format's +// sampling and transfer features only, as for a native compressed format. +VkFormatFeatureFlags EmulatedEtc2FormatFeatures( + VkFormatFeatureFlags host_features); + +// Staging bytes consumed and decoded bytes produced by one 2D upload region. +// Returns false for an empty extent, a depth other than 1, or no layers. +bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent, + std::uint32_t layer_count, VkDeviceSize* compressed, + VkDeviceSize* decoded); + +// Reports ETC2/EAC support on hosts without native ETC2 (desktop GPUs) and +// stores those textures decoded. Images are created with the host format; +// each staged upload into one is redirected to a host-visible staging buffer +// that is filled by decoding the application's staging bytes when the +// command buffer is submitted. Every submit decodes again, as a native copy +// re-reads its source on each execution. +// Staging buffers live until their command buffer is begun, reset or freed. +// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off. +class VulkanEtc2Emulation final { + public: + VulkanEtc2Emulation(); + ~VulkanEtc2Emulation(); + VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete; + VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete; + + // True when this physical device lacks native ETC2 and emulation is on. + bool PhysicalDeviceNeedsEmulation(VkPhysicalDevice physical_device, + PFN_vkGetPhysicalDeviceFeatures host); + + void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device, + bool emulated, + const VkPhysicalDeviceMemoryProperties& memory, + PFN_vkGetDeviceProcAddr get_device_proc_addr); + void DestroyDevice(VkDevice device); + + VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info, + const VkAllocationCallbacks* allocator, VkImage* image); + void DestroyImage(VkDevice device, VkImage image, + const VkAllocationCallbacks* allocator); + VkResult CreateImageView(VkDevice device, + const VkImageViewCreateInfo* create_info, + const VkAllocationCallbacks* allocator, + VkImageView* view); + VkResult BindBufferMemory(VkDevice device, VkBuffer buffer, + VkDeviceMemory memory, VkDeviceSize offset); + VkResult BindBufferMemory2(VkDevice device, std::uint32_t count, + const VkBindBufferMemoryInfo* infos); + VkResult MapMemory(VkDevice device, VkDeviceMemory memory, + VkDeviceSize offset, VkDeviceSize size, + VkMemoryMapFlags flags, void** data); + VkResult MapMemory2(VkDevice device, const VkMemoryMapInfo* info, + void** data); + void UnmapMemory(VkDevice device, VkDeviceMemory memory); + VkResult UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo* info); + void FreeMemory(VkDevice device, VkDeviceMemory memory, + const VkAllocationCallbacks* allocator); + void DestroyBuffer(VkDevice device, VkBuffer buffer, + const VkAllocationCallbacks* allocator); + void CmdCopyBufferToImage(VkDevice device, VkCommandBuffer command_buffer, + VkBuffer source, VkImage destination, + VkImageLayout layout, std::uint32_t region_count, + const VkBufferImageCopy* regions); + void CmdCopyBufferToImage2(VkDevice device, VkCommandBuffer command_buffer, + const VkCopyBufferToImageInfo2* info); + void CmdExecuteCommands(VkDevice device, VkCommandBuffer command_buffer, + std::uint32_t count, + const VkCommandBuffer* secondaries); + + // Decodes every upload recorded into these command buffers and the + // secondary command buffers they execute. + void PrepareSubmit(const VkCommandBuffer* command_buffers, + std::uint32_t count); + // Destroys the staging buffers owned by a command buffer that is no longer + // pending. + void ReleaseCommandBuffer(VkCommandBuffer command_buffer); + + struct State; + + private: + State* state_; +}; + +} // namespace mocktail::graphics + +#endif // MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_ diff --git a/src/graphics/bionic_vulkan_loader_adapter.cc b/src/graphics/bionic_vulkan_loader_adapter.cc index 89ac60f..7747dd1 100644 --- a/src/graphics/bionic_vulkan_loader_adapter.cc +++ b/src/graphics/bionic_vulkan_loader_adapter.cc @@ -17,6 +17,7 @@ #include "mocktail/graphics/android_vulkan_wsi_adapter.h" #include "mocktail/graphics/present_mode_policy.h" +#include "mocktail/graphics/vulkan_etc2_emulation.h" #include "mocktail/graphics/vulkan_text_overlay_compositor.h" #include "mocktail/platform/platform_runtime.h" @@ -188,6 +189,7 @@ struct AdapterState { VkInstance latest_instance = VK_NULL_HANDLE; size_t active_instance_count = 0; mocktail::graphics::VulkanTextOverlayCompositor text_overlay; + mocktail::graphics::VulkanEtc2Emulation etc2; std::atomic note_present{nullptr}; std::atomic note_host_present_begin{nullptr}; std::atomic note_host_present_end{nullptr}; @@ -727,12 +729,87 @@ PFN_vkVoidFunction AdapterProc(const char* name) { MOCKTAIL_VK_PROC(vkQueueWaitIdle) MOCKTAIL_VK_PROC(vkDeviceWaitIdle) MOCKTAIL_VK_PROC(vkQueuePresentKHR) + MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures) + MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures2) + MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties) + MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties2) + MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties) + MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties2) + MOCKTAIL_VK_PROC(vkCreateImage) + MOCKTAIL_VK_PROC(vkDestroyImage) + MOCKTAIL_VK_PROC(vkCreateImageView) + MOCKTAIL_VK_PROC(vkBindBufferMemory) + MOCKTAIL_VK_PROC(vkBindBufferMemory2) + MOCKTAIL_VK_PROC(vkMapMemory) + MOCKTAIL_VK_PROC(vkMapMemory2) + MOCKTAIL_VK_PROC(vkUnmapMemory) + MOCKTAIL_VK_PROC(vkUnmapMemory2) + MOCKTAIL_VK_PROC(vkFreeMemory) + MOCKTAIL_VK_PROC(vkDestroyBuffer) + MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage) + MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage2) + MOCKTAIL_VK_PROC(vkCmdExecuteCommands) #undef MOCKTAIL_VK_PROC +#define MOCKTAIL_VK_ALIAS(alias, function) \ + if (std::strcmp(name, #alias) == 0) { \ + return reinterpret_cast(function); \ + } + MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFeatures2KHR, + vkGetPhysicalDeviceFeatures2) + MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFormatProperties2KHR, + vkGetPhysicalDeviceFormatProperties2) + MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceImageFormatProperties2KHR, + vkGetPhysicalDeviceImageFormatProperties2) + MOCKTAIL_VK_ALIAS(vkBindBufferMemory2KHR, vkBindBufferMemory2) + MOCKTAIL_VK_ALIAS(vkMapMemory2KHR, vkMapMemory2) + MOCKTAIL_VK_ALIAS(vkUnmapMemory2KHR, vkUnmapMemory2) + MOCKTAIL_VK_ALIAS(vkCmdCopyBufferToImage2KHR, vkCmdCopyBufferToImage2) +#undef MOCKTAIL_VK_ALIAS return nullptr; } +bool NameInList(const char* name, const char* const* names, + std::size_t count) { + return name != nullptr && + std::any_of(names, names + count, [name](const char* candidate) { + return std::strcmp(name, candidate) == 0; + }); +} + +// Device commands wrapped for ETC2/EAC emulation. +bool IsEtc2DeviceProc(const char* name) { + static constexpr const char* kNames[] = { + "vkCreateImage", "vkDestroyImage", + "vkCreateImageView", "vkBindBufferMemory", + "vkBindBufferMemory2", "vkBindBufferMemory2KHR", + "vkMapMemory", "vkMapMemory2", + "vkMapMemory2KHR", "vkUnmapMemory", + "vkUnmapMemory2", "vkUnmapMemory2KHR", + "vkFreeMemory", "vkDestroyBuffer", + "vkCmdCopyBufferToImage", "vkCmdCopyBufferToImage2", + "vkCmdCopyBufferToImage2KHR", "vkCmdExecuteCommands", + }; + return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0])); +} + +// Physical-device queries that report emulated ETC2/EAC support. +bool IsEtc2PhysicalDeviceProc(const char* name) { + static constexpr const char* kNames[] = { + "vkGetPhysicalDeviceFeatures", + "vkGetPhysicalDeviceFeatures2", + "vkGetPhysicalDeviceFeatures2KHR", + "vkGetPhysicalDeviceFormatProperties", + "vkGetPhysicalDeviceFormatProperties2", + "vkGetPhysicalDeviceFormatProperties2KHR", + "vkGetPhysicalDeviceImageFormatProperties", + "vkGetPhysicalDeviceImageFormatProperties2", + "vkGetPhysicalDeviceImageFormatProperties2KHR", + }; + return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0])); +} + bool IsDeviceAdapterProc(const char* name) { - return name != nullptr && (std::strcmp(name, "vkDestroyDevice") == 0 || + return name != nullptr && (IsEtc2DeviceProc(name) || std::strcmp(name, "vkDestroyDevice") == 0 || std::strcmp(name, "vkGetDeviceQueue") == 0 || std::strcmp(name, "vkGetDeviceQueue2") == 0 || std::strcmp(name, "vkCreateSwapchainKHR") == 0 || @@ -1042,6 +1119,58 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) { return result; } +template +Function HostPhysicalDeviceProc(const char* name, const char* alias) { + VkInstance instance = VK_NULL_HANDLE; + { + AdapterState& state = State(); + std::lock_guard lock(state.mutex); + instance = state.latest_instance; + } + PFN_vkVoidFunction proc = HostInstanceProc(instance, name); + if (proc == nullptr && alias != nullptr) { + proc = HostInstanceProc(instance, alias); + } + return reinterpret_cast(proc); +} + +bool Etc2EmulatedPhysicalDevice(VkPhysicalDevice physical_device) { + return State().etc2.PhysicalDeviceNeedsEmulation( + physical_device, HostPhysicalDeviceProc( + "vkGetPhysicalDeviceFeatures", nullptr)); +} + +void ReleaseEtc2PoolCommandBuffers(VkDevice device, + VkCommandPool command_pool) { + std::vector command_buffers; + { + AdapterState& state = State(); + std::lock_guard lock(state.mutex); + for (const auto& binding : state.command_buffer_bindings) { + if (binding.device == device && binding.command_pool == command_pool) { + command_buffers.push_back(binding.command_buffer); + } + } + } + for (VkCommandBuffer command_buffer : command_buffers) { + State().etc2.ReleaseCommandBuffer(command_buffer); + } +} + +void PrepareEtc2Submit2(std::uint32_t submit_count, + const VkSubmitInfo2* submits) { + if (submits == nullptr) { + return; + } + for (std::uint32_t index = 0; index < submit_count; ++index) { + for (std::uint32_t info = 0; info < submits[index].commandBufferInfoCount; + ++info) { + State().etc2.PrepareSubmit( + &submits[index].pCommandBufferInfos[info].commandBuffer, 1); + } + } +} + } // namespace extern "C" { @@ -1247,7 +1376,7 @@ vkGetInstanceProcAddr(VkInstance instance, const char* name) { } if (const PFN_vkVoidFunction adapter = AdapterProc(name); adapter != nullptr) { - if (IsDeviceAdapterProc(name) && + if ((IsDeviceAdapterProc(name) || IsEtc2PhysicalDeviceProc(name)) && HostInstanceProc(instance, name) == nullptr) { return nullptr; } @@ -1306,6 +1435,31 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice( VkDeviceCreateInfo host_info = *create_info; EnabledDeviceFeatures enabled = InspectEnabledFeatures(*create_info); + const bool etc2_emulated = State().etc2.PhysicalDeviceNeedsEmulation( + physical_device, reinterpret_cast( + HostInstanceProc(instance, + "vkGetPhysicalDeviceFeatures"))); + VkPhysicalDeviceFeatures host_enabled_features{}; + VkPhysicalDeviceFeatures2* requested_features2 = nullptr; + VkBool32 requested_features2_etc2 = VK_FALSE; + if (etc2_emulated) { + enabled.root.features.textureCompressionETC2 = VK_FALSE; + if (create_info->pEnabledFeatures != nullptr) { + host_enabled_features = *create_info->pEnabledFeatures; + host_enabled_features.textureCompressionETC2 = VK_FALSE; + host_info.pEnabledFeatures = &host_enabled_features; + } + // The feature chain is caller-owned; clear the emulated bit only for the + // host call and restore it afterwards. + requested_features2 = const_cast( + reinterpret_cast(FindFeature( + create_info->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2))); + if (requested_features2 != nullptr) { + requested_features2_etc2 = + requested_features2->features.textureCompressionETC2; + requested_features2->features.textureCompressionETC2 = VK_FALSE; + } + } enabled.root.pNext = &enabled.vulkan11; enabled.vulkan11.pNext = &enabled.vulkan12; enabled.vulkan12.pNext = nullptr; @@ -1341,11 +1495,25 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice( const VkResult result = host_create(physical_device, &host_info, allocator, device); + if (requested_features2 != nullptr) { + requested_features2->features.textureCompressionETC2 = + requested_features2_etc2; + } if (result != VK_SUCCESS) { return result; } RegisterHostDeviceDispatch(*device, physical_device, host_get_device_proc_addr); + VkPhysicalDeviceMemoryProperties memory_properties{}; + if (const auto host_memory_properties = + reinterpret_cast( + HostInstanceProc(instance, + "vkGetPhysicalDeviceMemoryProperties")); + host_memory_properties != nullptr) { + host_memory_properties(physical_device, &memory_properties); + } + State().etc2.RegisterDevice(*device, physical_device, etc2_emulated, + memory_properties, host_get_device_proc_addr); std::uint32_t queue_family_count = 0; const auto host_get_queue_families = @@ -1403,6 +1571,7 @@ vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* allocator) { const auto host_destroy = reinterpret_cast( HostDeviceProc(device, "vkDestroyDevice")); State().text_overlay.DestroyDevice(device); + State().etc2.DestroyDevice(device); if (host_destroy != nullptr) { host_destroy(device, allocator); } @@ -1650,6 +1819,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return VK_ERROR_INITIALIZATION_FAILED; } + ReleaseEtc2PoolCommandBuffers(device, command_pool); const VkResult result = host_reset(device, command_pool, flags); observation.SetResult(result); return result; @@ -1706,6 +1876,11 @@ VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return; } + if (command_buffers != nullptr) { + for (std::uint32_t index = 0; index < command_buffer_count; ++index) { + State().etc2.ReleaseCommandBuffer(command_buffers[index]); + } + } host_free(device, command_pool, command_buffer_count, command_buffers); RemoveHostCommandBuffers(device, command_pool, command_buffer_count, command_buffers); @@ -1722,6 +1897,7 @@ VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return; } + ReleaseEtc2PoolCommandBuffers(device, command_pool); host_destroy(device, command_pool, allocator); RemoveHostCommandPoolBindings(device, command_pool); observation.SetResult(VK_SUCCESS); @@ -1737,6 +1913,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return VK_ERROR_INITIALIZATION_FAILED; } + State().etc2.ReleaseCommandBuffer(command_buffer); const VkResult result = host_begin(command_buffer, begin_info); observation.SetResult(result); return result; @@ -1765,6 +1942,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return VK_ERROR_INITIALIZATION_FAILED; } + State().etc2.ReleaseCommandBuffer(command_buffer); const VkResult result = host_reset(command_buffer, flags); observation.SetResult(result); return result; @@ -1801,6 +1979,12 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return VK_ERROR_INITIALIZATION_FAILED; } + if (submits != nullptr) { + for (std::uint32_t index = 0; index < submit_count; ++index) { + State().etc2.PrepareSubmit(submits[index].pCommandBuffers, + submits[index].commandBufferCount); + } + } const VkResult result = State().text_overlay.QueueSubmit( queue, submit_count, submits, fence, host_submit); observation.SetResult(result); @@ -1817,6 +2001,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return VK_ERROR_INITIALIZATION_FAILED; } + PrepareEtc2Submit2(submit_count, submits); const VkResult result = State().text_overlay.QueueSubmit2( queue, submit_count, submits, fence, host_submit); observation.SetResult(result); @@ -1833,6 +2018,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR( observation.SetResult(VK_ERROR_INITIALIZATION_FAILED); return VK_ERROR_INITIALIZATION_FAILED; } + PrepareEtc2Submit2(submit_count, submits); const VkResult result = State().text_overlay.QueueSubmit2( queue, submit_count, submits, fence, reinterpret_cast(host_submit)); @@ -2025,4 +2211,230 @@ vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* present_info) { return normalized_result; } +VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures( + VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures* features) { + const auto host = HostPhysicalDeviceProc( + "vkGetPhysicalDeviceFeatures", nullptr); + if (host == nullptr || features == nullptr) { + return; + } + host(physical_device, features); + if (Etc2EmulatedPhysicalDevice(physical_device)) { + features->textureCompressionETC2 = VK_TRUE; + } +} + +VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures2( + VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures2* features) { + const auto host = HostPhysicalDeviceProc( + "vkGetPhysicalDeviceFeatures2", "vkGetPhysicalDeviceFeatures2KHR"); + if (host == nullptr || features == nullptr) { + return; + } + host(physical_device, features); + if (Etc2EmulatedPhysicalDevice(physical_device)) { + features->features.textureCompressionETC2 = VK_TRUE; + } +} + +VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties( + VkPhysicalDevice physical_device, VkFormat format, + VkFormatProperties* properties) { + const auto host = + HostPhysicalDeviceProc( + "vkGetPhysicalDeviceFormatProperties", nullptr); + if (host == nullptr || properties == nullptr) { + return; + } + mocktail::graphics::Etc2EmulatedFormat mapped; + if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) || + !Etc2EmulatedPhysicalDevice(physical_device)) { + host(physical_device, format, properties); + return; + } + host(physical_device, mapped.host_format, properties); + properties->linearTilingFeatures = 0; + properties->optimalTilingFeatures = + mocktail::graphics::EmulatedEtc2FormatFeatures( + properties->optimalTilingFeatures); + properties->bufferFeatures = 0; +} + +VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties2( + VkPhysicalDevice physical_device, VkFormat format, + VkFormatProperties2* properties) { + const auto host = + HostPhysicalDeviceProc( + "vkGetPhysicalDeviceFormatProperties2", + "vkGetPhysicalDeviceFormatProperties2KHR"); + if (host == nullptr || properties == nullptr) { + return; + } + mocktail::graphics::Etc2EmulatedFormat mapped; + if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) || + !Etc2EmulatedPhysicalDevice(physical_device)) { + host(physical_device, format, properties); + return; + } + host(physical_device, mapped.host_format, properties); + VkFormatProperties& base = properties->formatProperties; + base.linearTilingFeatures = 0; + base.optimalTilingFeatures = + mocktail::graphics::EmulatedEtc2FormatFeatures(base.optimalTilingFeatures); + base.bufferFeatures = 0; + if (auto* extended = const_cast( + reinterpret_cast(FindFeature( + properties->pNext, VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3))); + extended != nullptr) { + extended->linearTilingFeatures = 0; + extended->optimalTilingFeatures = + mocktail::graphics::EmulatedEtc2FormatFeatures( + static_cast( + extended->optimalTilingFeatures)); + extended->bufferFeatures = 0; + } +} + +VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties( + VkPhysicalDevice physical_device, VkFormat format, VkImageType type, + VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, + VkImageFormatProperties* properties) { + const auto host = + HostPhysicalDeviceProc( + "vkGetPhysicalDeviceImageFormatProperties", nullptr); + if (host == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + mocktail::graphics::Etc2EmulatedFormat mapped; + if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) || + !Etc2EmulatedPhysicalDevice(physical_device)) { + return host(physical_device, format, type, tiling, usage, flags, + properties); + } + if (tiling != VK_IMAGE_TILING_OPTIMAL || type != VK_IMAGE_TYPE_2D) { + return VK_ERROR_FORMAT_NOT_SUPPORTED; + } + return host(physical_device, mapped.host_format, type, tiling, usage, + flags & ~static_cast( + VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT), + properties); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2( + VkPhysicalDevice physical_device, + const VkPhysicalDeviceImageFormatInfo2* info, + VkImageFormatProperties2* properties) { + const auto host = + HostPhysicalDeviceProc( + "vkGetPhysicalDeviceImageFormatProperties2", + "vkGetPhysicalDeviceImageFormatProperties2KHR"); + if (host == nullptr || info == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + mocktail::graphics::Etc2EmulatedFormat mapped; + if (!mocktail::graphics::LookupEmulatedEtc2Format(info->format, &mapped) || + !Etc2EmulatedPhysicalDevice(physical_device)) { + return host(physical_device, info, properties); + } + if (info->tiling != VK_IMAGE_TILING_OPTIMAL || + info->type != VK_IMAGE_TYPE_2D) { + return VK_ERROR_FORMAT_NOT_SUPPORTED; + } + VkPhysicalDeviceImageFormatInfo2 host_info = *info; + host_info.format = mapped.host_format; + host_info.flags &= ~static_cast( + VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT); + return host(physical_device, &host_info, properties); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage( + VkDevice device, const VkImageCreateInfo* create_info, + const VkAllocationCallbacks* allocator, VkImage* image) { + return State().etc2.CreateImage(device, create_info, allocator, image); +} + +VKAPI_ATTR void VKAPI_CALL vkDestroyImage( + VkDevice device, VkImage image, const VkAllocationCallbacks* allocator) { + State().etc2.DestroyImage(device, image, allocator); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView( + VkDevice device, const VkImageViewCreateInfo* create_info, + const VkAllocationCallbacks* allocator, VkImageView* view) { + return State().etc2.CreateImageView(device, create_info, allocator, view); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice device, + VkBuffer buffer, + VkDeviceMemory memory, + VkDeviceSize offset) { + return State().etc2.BindBufferMemory(device, buffer, memory, offset); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2( + VkDevice device, std::uint32_t count, + const VkBindBufferMemoryInfo* infos) { + return State().etc2.BindBufferMemory2(device, count, infos); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice device, + VkDeviceMemory memory, + VkDeviceSize offset, + VkDeviceSize size, + VkMemoryMapFlags flags, + void** data) { + return State().etc2.MapMemory(device, memory, offset, size, flags, data); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory2(VkDevice device, + const VkMemoryMapInfo* info, + void** data) { + return State().etc2.MapMemory2(device, info, data); +} + +VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice device, + VkDeviceMemory memory) { + State().etc2.UnmapMemory(device, memory); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkUnmapMemory2(VkDevice device, + const VkMemoryUnmapInfo* info) { + return State().etc2.UnmapMemory2(device, info); +} + +VKAPI_ATTR void VKAPI_CALL vkFreeMemory( + VkDevice device, VkDeviceMemory memory, + const VkAllocationCallbacks* allocator) { + State().etc2.FreeMemory(device, memory, allocator); +} + +VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer( + VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + State().etc2.DestroyBuffer(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage( + VkCommandBuffer command_buffer, VkBuffer source, VkImage destination, + VkImageLayout layout, std::uint32_t region_count, + const VkBufferImageCopy* regions) { + State().etc2.CmdCopyBufferToImage( + HostDispatchForCommandBuffer(command_buffer).device, command_buffer, + source, destination, layout, region_count, regions); +} + +VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage2( + VkCommandBuffer command_buffer, const VkCopyBufferToImageInfo2* info) { + State().etc2.CmdCopyBufferToImage2( + HostDispatchForCommandBuffer(command_buffer).device, command_buffer, + info); +} + +VKAPI_ATTR void VKAPI_CALL vkCmdExecuteCommands( + VkCommandBuffer command_buffer, std::uint32_t count, + const VkCommandBuffer* secondaries) { + State().etc2.CmdExecuteCommands( + HostDispatchForCommandBuffer(command_buffer).device, command_buffer, + count, secondaries); +} + } // extern "C" diff --git a/src/graphics/etc2_decoder.cc b/src/graphics/etc2_decoder.cc new file mode 100644 index 0000000..a027d5e --- /dev/null +++ b/src/graphics/etc2_decoder.cc @@ -0,0 +1,278 @@ +#include "mocktail/graphics/etc2_decoder.h" + +#include +#include +#include + +namespace mocktail::graphics { +namespace { + +constexpr std::array kDistance = {3, 6, 11, 16, 23, 32, 41, 64}; + +// Indexed by (msb << 1) | lsb. +constexpr int kEtc1Modifiers[8][4] = { + {2, 8, -2, -8}, {5, 17, -5, -17}, {9, 29, -9, -29}, + {13, 42, -13, -42}, {18, 60, -18, -60}, {24, 80, -24, -80}, + {33, 106, -33, -106}, {47, 183, -47, -183}, +}; + +constexpr int kEacModifiers[16][8] = { + {-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12}, + {-2, -5, -8, -13, 1, 4, 7, 12}, {-2, -4, -6, -13, 1, 3, 5, 12}, + {-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10}, + {-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10}, + {-2, -6, -8, -10, 1, 5, 7, 9}, {-2, -5, -8, -10, 1, 4, 7, 9}, + {-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9}, + {-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9}, + {-4, -6, -8, -9, 3, 5, 7, 8}, {-3, -5, -7, -9, 2, 4, 6, 8}, +}; + +std::uint8_t Clamp255(int value) { + return static_cast(std::clamp(value, 0, 255)); +} + +int Expand4(int value) { return (value << 4) | value; } +int Expand5(int value) { return (value << 3) | (value >> 2); } +int Expand6(int value) { return (value << 2) | (value >> 4); } +int Expand7(int value) { return (value << 1) | (value >> 6); } + +// Decodes one ETC2 RGB block into 16 column-major RGBA texels (x * 4 + y). +void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough, + std::array* out) { + static constexpr int kDelta[8] = {0, 1, 2, 3, -4, -3, -2, -1}; + const bool diff_or_opaque = (b[3] & 2) != 0; + const bool opaque = !punchthrough || diff_or_opaque; + const int msb = (b[4] << 8) | b[5]; + const int lsb = (b[6] << 8) | b[7]; + auto index_of = [msb, lsb](int texel) { + return (((msb >> texel) & 1) << 1) | ((lsb >> texel) & 1); + }; + auto write = [out](int texel, int r, int g, int bl, int a) { + const std::size_t o = static_cast(texel) * 4; + (*out)[o] = Clamp255(r); + (*out)[o + 1] = Clamp255(g); + (*out)[o + 2] = Clamp255(bl); + (*out)[o + 3] = static_cast(a); + }; + + const int r_sum = (b[0] >> 3) + kDelta[b[0] & 7]; + const int g_sum = (b[1] >> 3) + kDelta[b[1] & 7]; + const int b_sum = (b[2] >> 3) + kDelta[b[2] & 7]; + const bool individual = !punchthrough && !diff_or_opaque; + + if (!individual && (r_sum < 0 || r_sum > 31 || g_sum < 0 || g_sum > 31)) { + std::array, 4> paint{}; + if (r_sum < 0 || r_sum > 31) { + const std::array c1 = { + Expand4((((b[0] >> 3) & 3) << 2) | (b[0] & 3)), + Expand4(b[1] >> 4), Expand4(b[1] & 15)}; + const std::array c2 = {Expand4(b[2] >> 4), Expand4(b[2] & 15), + Expand4(b[3] >> 4)}; + const int d = kDistance[(((b[3] >> 2) & 3) << 1) | (b[3] & 1)]; + for (int c = 0; c < 3; ++c) { + paint[0][c] = c1[c]; + paint[1][c] = c2[c] + d; + paint[2][c] = c2[c]; + paint[3][c] = c2[c] - d; + } + } else { + const std::array c1 = { + Expand4((b[0] >> 3) & 15), + Expand4(((b[0] & 7) << 1) | ((b[1] >> 4) & 1)), + Expand4((b[1] & 8) | ((b[1] & 3) << 1) | (b[2] >> 7))}; + const std::array c2 = { + Expand4((b[2] >> 3) & 15), Expand4(((b[2] & 7) << 1) | (b[3] >> 7)), + Expand4((b[3] >> 3) & 15)}; + const int v1 = (c1[0] << 16) | (c1[1] << 8) | c1[2]; + const int v2 = (c2[0] << 16) | (c2[1] << 8) | c2[2]; + const int d = + kDistance[(b[3] & 4) | ((b[3] & 1) << 1) | (v1 >= v2 ? 1 : 0)]; + for (int c = 0; c < 3; ++c) { + paint[0][c] = c1[c] + d; + paint[1][c] = c1[c] - d; + paint[2][c] = c2[c] + d; + paint[3][c] = c2[c] - d; + } + } + for (int texel = 0; texel < 16; ++texel) { + const int index = index_of(texel); + if (!opaque && index == 2) { + write(texel, 0, 0, 0, 0); + } else { + write(texel, paint[index][0], paint[index][1], paint[index][2], 255); + } + } + return; + } + + if (!individual && (b_sum < 0 || b_sum > 31)) { + const int ro = Expand6((b[0] >> 1) & 63); + const int go = Expand7(((b[0] & 1) << 6) | ((b[1] >> 1) & 63)); + const int bo = Expand6(((b[1] & 1) << 5) | (b[2] & 0x18) | + ((b[2] & 3) << 1) | (b[3] >> 7)); + const int rh = Expand6(((b[3] & 0x7c) >> 1) | (b[3] & 1)); + const int gh = Expand7(b[4] >> 1); + const int bh = Expand6(((b[4] & 1) << 5) | (b[5] >> 3)); + const int rv = Expand6(((b[5] & 7) << 3) | (b[6] >> 5)); + const int gv = Expand7(((b[6] & 31) << 2) | (b[7] >> 6)); + const int bv = Expand6(b[7] & 63); + for (int x = 0; x < 4; ++x) { + for (int y = 0; y < 4; ++y) { + write(x * 4 + y, (x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2, + (x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2, + (x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255); + } + } + return; + } + + std::array, 2> base{}; + for (int c = 0; c < 3; ++c) { + if (individual) { + base[0][c] = Expand4(b[c] >> 4); + base[1][c] = Expand4(b[c] & 15); + } else { + base[0][c] = Expand5(b[c] >> 3); + base[1][c] = Expand5((b[c] >> 3) + kDelta[b[c] & 7]); + } + } + const int tables[2] = {b[3] >> 5, (b[3] >> 2) & 7}; + const bool flipped = (b[3] & 1) != 0; + for (int x = 0; x < 4; ++x) { + for (int y = 0; y < 4; ++y) { + const int texel = x * 4 + y; + const int index = index_of(texel); + if (!opaque && index == 2) { + write(texel, 0, 0, 0, 0); + continue; + } + const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0); + const int modifier = + (!opaque && index == 0) ? 0 : kEtc1Modifiers[tables[sub]][index]; + write(texel, base[sub][0] + modifier, base[sub][1] + modifier, + base[sub][2] + modifier, 255); + } + } +} + +// EAC 3-bit index for column-major texel (x * 4 + y). +int EacIndex(const std::uint8_t* b, int texel) { + std::uint64_t bits = 0; + for (int i = 2; i < 8; ++i) { + bits = (bits << 8) | b[i]; + } + return static_cast((bits >> (45 - texel * 3)) & 7); +} + +int EacModifier(const std::uint8_t* b, int texel) { + return kEacModifiers[b[1] & 15][EacIndex(b, texel)]; +} + +std::uint16_t DecodeR11(const std::uint8_t* b, int texel, bool is_signed) { + const int multiplier = b[1] >> 4; + const int modifier = EacModifier(b, texel); + const int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier; + if (!is_signed) { + const int value = std::clamp(b[0] * 8 + 4 + scaled, 0, 2047); + return static_cast((value << 5) | (value >> 6)); + } + const int base = std::max(-127, static_cast(static_cast(b[0]))); + const int value = std::clamp(base * 8 + scaled, -1023, 1023); + const int magnitude = value < 0 ? -value : value; + const int expanded = (magnitude << 5) | (magnitude >> 5); + return static_cast( + static_cast(value < 0 ? -expanded : expanded)); +} + +} // namespace + +std::size_t EtcBlockBytes(EtcFormat format) { + switch (format) { + case EtcFormat::kEtc2Rgba8: + case EtcFormat::kEacRg11: + case EtcFormat::kEacRg11Signed: + return 16; + default: + return 8; + } +} + +std::size_t EtcDecodedTexelBytes(EtcFormat format) { + switch (format) { + case EtcFormat::kEacR11: + case EtcFormat::kEacR11Signed: + return 2; + default: + return 4; + } +} + +bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source, + std::size_t source_bytes, std::uint32_t width, + std::uint32_t height, std::uint8_t* destination, + std::size_t destination_bytes) { + if (source == nullptr || destination == nullptr || width == 0 || + height == 0) { + return false; + } + const std::uint64_t blocks_wide = (static_cast(width) + 3) / 4; + const std::uint64_t blocks_high = + (static_cast(height) + 3) / 4; + const std::size_t texel_bytes = EtcDecodedTexelBytes(format); + const std::size_t block_bytes = EtcBlockBytes(format); + if (blocks_wide * blocks_high * block_bytes > source_bytes || + static_cast(width) * height * texel_bytes > + destination_bytes) { + return false; + } + + std::array rgba{}; + for (std::uint64_t by = 0; by < blocks_high; ++by) { + for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) { + const std::uint8_t* block = + source + (by * blocks_wide + bx) * block_bytes; + if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) { + DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba); + } else if (format == EtcFormat::kEtc2Rgba8) { + DecodeRgbBlock(block + 8, false, &rgba); + } + for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) { + for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) { + const int texel = static_cast(x * 4 + y); + std::uint8_t* out = + destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes; + switch (format) { + case EtcFormat::kEtc2Rgb8: + case EtcFormat::kEtc2Rgb8A1: + case EtcFormat::kEtc2Rgba8: + std::copy_n(rgba.data() + texel * 4, 4, out); + if (format == EtcFormat::kEtc2Rgba8) { + const int alpha = + block[0] + EacModifier(block, texel) * (block[1] >> 4); + out[3] = Clamp255(alpha); + } + break; + case EtcFormat::kEacR11: + case EtcFormat::kEacR11Signed: + case EtcFormat::kEacRg11: + case EtcFormat::kEacRg11Signed: { + const bool is_signed = format == EtcFormat::kEacR11Signed || + format == EtcFormat::kEacRg11Signed; + const int channels = texel_bytes / 2; + for (int channel = 0; channel < channels; ++channel) { + const std::uint16_t value = + DecodeR11(block + channel * 8, texel, is_signed); + out[channel * 2] = static_cast(value); + out[channel * 2 + 1] = static_cast(value >> 8); + } + break; + } + } + } + } + } + } + return true; +} + +} // namespace mocktail::graphics diff --git a/src/graphics/vulkan_etc2_emulation.cc b/src/graphics/vulkan_etc2_emulation.cc new file mode 100644 index 0000000..487b224 --- /dev/null +++ b/src/graphics/vulkan_etc2_emulation.cc @@ -0,0 +1,848 @@ +#include "mocktail/graphics/vulkan_etc2_emulation.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mocktail::graphics { + +bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out) { + Etc2EmulatedFormat mapped; + switch (format) { + case VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK: + mapped = {VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgb8}; + break; + case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK: + mapped = {VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgb8}; + break; + case VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK: + mapped = {VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgb8A1}; + break; + case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK: + mapped = {VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgb8A1}; + break; + case VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK: + mapped = {VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgba8}; + break; + case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK: + mapped = {VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgba8}; + break; + case VK_FORMAT_EAC_R11_UNORM_BLOCK: + mapped = {VK_FORMAT_R16_UNORM, EtcFormat::kEacR11}; + break; + case VK_FORMAT_EAC_R11_SNORM_BLOCK: + mapped = {VK_FORMAT_R16_SNORM, EtcFormat::kEacR11Signed}; + break; + case VK_FORMAT_EAC_R11G11_UNORM_BLOCK: + mapped = {VK_FORMAT_R16G16_UNORM, EtcFormat::kEacRg11}; + break; + case VK_FORMAT_EAC_R11G11_SNORM_BLOCK: + mapped = {VK_FORMAT_R16G16_SNORM, EtcFormat::kEacRg11Signed}; + break; + default: + return false; + } + if (out != nullptr) { + *out = mapped; + } + return true; +} + +VkFormatFeatureFlags EmulatedEtc2FormatFeatures( + VkFormatFeatureFlags host_features) { + constexpr VkFormatFeatureFlags kCompressedFeatures = + 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; + return host_features & kCompressedFeatures; +} + +bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent, + std::uint32_t layer_count, VkDeviceSize* compressed, + VkDeviceSize* decoded) { + if (extent.width == 0 || extent.height == 0 || extent.depth != 1 || + layer_count == 0 || compressed == nullptr || decoded == nullptr) { + return false; + } + const VkDeviceSize blocks = + ((static_cast(extent.width) + 3) / 4) * + ((static_cast(extent.height) + 3) / 4); + *compressed = blocks * EtcBlockBytes(format) * layer_count; + *decoded = static_cast(extent.width) * extent.height * + EtcDecodedTexelBytes(format) * layer_count; + return true; +} + +namespace { + +bool EmulationDisabledByEnvironment() { + static const bool disabled = [] { + const char* value = std::getenv("MOCKTAIL_DISABLE_ETC2_EMULATION"); + return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0; + }(); + return disabled; +} + +bool ShouldLog(std::atomic* counter, unsigned limit) { + return counter->fetch_add(1, std::memory_order_relaxed) < limit; +} + +struct HostDevice { + VkDevice device = VK_NULL_HANDLE; + bool emulated = false; + VkPhysicalDeviceMemoryProperties memory{}; + PFN_vkCreateImage create_image = nullptr; + PFN_vkDestroyImage destroy_image = nullptr; + PFN_vkCreateImageView create_image_view = nullptr; + PFN_vkBindBufferMemory bind_buffer_memory = nullptr; + PFN_vkBindBufferMemory2 bind_buffer_memory2 = nullptr; + PFN_vkMapMemory map_memory = nullptr; + PFN_vkMapMemory2 map_memory2 = nullptr; + PFN_vkUnmapMemory unmap_memory = nullptr; + PFN_vkUnmapMemory2 unmap_memory2 = nullptr; + PFN_vkFreeMemory free_memory = nullptr; + PFN_vkDestroyBuffer destroy_buffer = nullptr; + PFN_vkCmdCopyBufferToImage copy_buffer_to_image = nullptr; + PFN_vkCmdCopyBufferToImage2 copy_buffer_to_image2 = nullptr; + PFN_vkCmdExecuteCommands execute_commands = nullptr; + PFN_vkCreateBuffer create_buffer = nullptr; + PFN_vkAllocateMemory allocate_memory = nullptr; + PFN_vkGetBufferMemoryRequirements get_buffer_memory_requirements = nullptr; +}; + +template +Function DeviceProc(PFN_vkGetDeviceProcAddr get, VkDevice device, + const char* name, const char* alias = nullptr) { + PFN_vkVoidFunction proc = get(device, name); + if (proc == nullptr && alias != nullptr) { + proc = get(device, alias); + } + return reinterpret_cast(proc); +} + +struct BufferBinding { + VkDeviceMemory memory = VK_NULL_HANDLE; + VkDeviceSize offset = 0; +}; + +struct Mapping { + void* data = nullptr; + VkDeviceSize offset = 0; + VkDeviceSize size = VK_WHOLE_SIZE; +}; + +struct ImageRecord { + VkDevice device = VK_NULL_HANDLE; + Etc2EmulatedFormat format; +}; + +struct Staging { + VkDevice device = VK_NULL_HANDLE; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +struct PendingUpload { + VkDevice device = VK_NULL_HANDLE; + VkBuffer source = VK_NULL_HANDLE; + VkDeviceSize source_offset = 0; + VkDeviceSize compressed = 0; + VkDeviceSize decoded = 0; + VkDeviceSize target_offset = 0; + EtcFormat format = EtcFormat::kEtc2Rgb8; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t layers = 0; + std::uint8_t* target = nullptr; +}; + +struct CommandRecord { + std::vector uploads; + std::vector staging; + std::vector secondaries; +}; + +std::atomic g_image_logs{0}; +std::atomic g_decode_logs{0}; +std::atomic g_failure_logs{0}; + +void LogFailure(const char* message) { + if (ShouldLog(&g_failure_logs, 8)) { + std::fprintf(stderr, " [vulkan] ETC2 emulation: %s\n", message); + } +} + +bool CreateStaging(const HostDevice& dev, VkDeviceSize size, Staging* staging, + std::uint8_t** mapped) { + if (dev.create_buffer == nullptr || dev.allocate_memory == nullptr || + dev.get_buffer_memory_requirements == nullptr || + dev.bind_buffer_memory == nullptr || dev.map_memory == nullptr || + dev.destroy_buffer == nullptr || dev.free_memory == nullptr || + size == 0) { + return false; + } + VkBufferCreateInfo buffer_info{}; + buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_info.size = size; + buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VkBuffer buffer = VK_NULL_HANDLE; + if (dev.create_buffer(dev.device, &buffer_info, nullptr, &buffer) != + VK_SUCCESS) { + return false; + } + VkMemoryRequirements requirements{}; + dev.get_buffer_memory_requirements(dev.device, buffer, &requirements); + constexpr VkMemoryPropertyFlags kRequired = + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + std::uint32_t type = UINT32_MAX; + for (std::uint32_t index = 0; index < dev.memory.memoryTypeCount; ++index) { + if ((requirements.memoryTypeBits & (1U << index)) != 0 && + (dev.memory.memoryTypes[index].propertyFlags & kRequired) == + kRequired) { + type = index; + break; + } + } + VkDeviceMemory memory = VK_NULL_HANDLE; + VkMemoryAllocateInfo allocate_info{}; + allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocate_info.allocationSize = requirements.size; + allocate_info.memoryTypeIndex = type; + void* data = nullptr; + if (type == UINT32_MAX || + dev.allocate_memory(dev.device, &allocate_info, nullptr, &memory) != + VK_SUCCESS) { + dev.destroy_buffer(dev.device, buffer, nullptr); + return false; + } + if (dev.bind_buffer_memory(dev.device, buffer, memory, 0) != VK_SUCCESS || + dev.map_memory(dev.device, memory, 0, VK_WHOLE_SIZE, 0, &data) != + VK_SUCCESS) { + dev.destroy_buffer(dev.device, buffer, nullptr); + dev.free_memory(dev.device, memory, nullptr); + return false; + } + *staging = {dev.device, buffer, memory}; + *mapped = static_cast(data); + return true; +} + +// Freeing mapped memory unmaps it implicitly. +void DestroyStaging(const HostDevice& dev, const Staging& staging) { + if (dev.destroy_buffer != nullptr) { + dev.destroy_buffer(dev.device, staging.buffer, nullptr); + } + if (dev.free_memory != nullptr) { + dev.free_memory(dev.device, staging.memory, nullptr); + } +} + +template +bool PlanRegions(const Etc2EmulatedFormat& format, VkDevice device, + VkBuffer source, std::uint32_t count, const Region* regions, + std::vector* rewritten, + std::vector* uploads, VkDeviceSize* total) { + *total = 0; + if (regions == nullptr || count == 0) { + return false; + } + for (std::uint32_t index = 0; index < count; ++index) { + const Region& region = regions[index]; + PendingUpload upload; + if (!Etc2UploadByteCounts(format.etc_format, region.imageExtent, + region.imageSubresource.layerCount, + &upload.compressed, &upload.decoded)) { + return false; + } + upload.device = device; + upload.source = source; + upload.source_offset = region.bufferOffset; + upload.target_offset = *total; + upload.format = format.etc_format; + upload.width = region.imageExtent.width; + upload.height = region.imageExtent.height; + upload.layers = region.imageSubresource.layerCount; + Region copy = region; + copy.bufferOffset = *total; + rewritten->push_back(copy); + uploads->push_back(upload); + *total += upload.decoded; + } + return true; +} + +} // namespace + +struct VulkanEtc2Emulation::State { + std::mutex mutex; + std::mutex devices_mutex; + std::unordered_map physical_devices; + std::vector> devices; + std::unordered_map images; + std::unordered_map buffers; + std::unordered_map mappings; + std::unordered_map commands; + std::atomic has_commands{false}; + + const HostDevice* Find(VkDevice device) { + std::lock_guard lock(devices_mutex); + for (const auto& candidate : devices) { + if (candidate->device == device) { + return candidate.get(); + } + } + return nullptr; + } + + bool LookupImage(const HostDevice& dev, VkImage image, + Etc2EmulatedFormat* format) { + if (!dev.emulated) { + return false; + } + std::lock_guard lock(mutex); + const auto found = images.find(image); + if (found == images.end()) { + return false; + } + *format = found->second.format; + return true; + } + + void Record(VkCommandBuffer command_buffer, + std::vector uploads, const Staging& staging) { + std::lock_guard lock(mutex); + CommandRecord& record = commands[command_buffer]; + record.uploads.insert(record.uploads.end(), uploads.begin(), + uploads.end()); + record.staging.push_back(staging); + has_commands.store(true, std::memory_order_release); + } +}; + +VulkanEtc2Emulation::VulkanEtc2Emulation() : state_(new State) {} + +VulkanEtc2Emulation::~VulkanEtc2Emulation() { delete state_; } + +bool VulkanEtc2Emulation::PhysicalDeviceNeedsEmulation( + VkPhysicalDevice physical_device, PFN_vkGetPhysicalDeviceFeatures host) { + if (EmulationDisabledByEnvironment() || host == nullptr || + physical_device == VK_NULL_HANDLE) { + return false; + } + { + std::lock_guard lock(state_->mutex); + const auto found = state_->physical_devices.find(physical_device); + if (found != state_->physical_devices.end()) { + return found->second; + } + } + VkPhysicalDeviceFeatures features{}; + host(physical_device, &features); + const bool needed = features.textureCompressionETC2 != VK_TRUE; + std::lock_guard lock(state_->mutex); + state_->physical_devices[physical_device] = needed; + return needed; +} + +void VulkanEtc2Emulation::RegisterDevice( + VkDevice device, VkPhysicalDevice physical_device, bool emulated, + const VkPhysicalDeviceMemoryProperties& memory, + PFN_vkGetDeviceProcAddr get_device_proc_addr) { + static_cast(physical_device); + if (device == VK_NULL_HANDLE || get_device_proc_addr == nullptr) { + return; + } + auto dev = std::make_unique(); + const auto get = get_device_proc_addr; + dev->device = device; + dev->emulated = emulated; + dev->memory = memory; + dev->create_image = DeviceProc(get, device, "vkCreateImage"); + dev->destroy_image = + DeviceProc(get, device, "vkDestroyImage"); + dev->create_image_view = + DeviceProc(get, device, "vkCreateImageView"); + dev->bind_buffer_memory = + DeviceProc(get, device, "vkBindBufferMemory"); + dev->bind_buffer_memory2 = DeviceProc( + get, device, "vkBindBufferMemory2", "vkBindBufferMemory2KHR"); + dev->map_memory = DeviceProc(get, device, "vkMapMemory"); + dev->map_memory2 = DeviceProc(get, device, "vkMapMemory2", + "vkMapMemory2KHR"); + dev->unmap_memory = + DeviceProc(get, device, "vkUnmapMemory"); + dev->unmap_memory2 = DeviceProc( + get, device, "vkUnmapMemory2", "vkUnmapMemory2KHR"); + dev->free_memory = DeviceProc(get, device, "vkFreeMemory"); + dev->destroy_buffer = + DeviceProc(get, device, "vkDestroyBuffer"); + dev->copy_buffer_to_image = DeviceProc( + get, device, "vkCmdCopyBufferToImage"); + dev->copy_buffer_to_image2 = DeviceProc( + get, device, "vkCmdCopyBufferToImage2", "vkCmdCopyBufferToImage2KHR"); + dev->execute_commands = + DeviceProc(get, device, "vkCmdExecuteCommands"); + dev->create_buffer = + DeviceProc(get, device, "vkCreateBuffer"); + dev->allocate_memory = + DeviceProc(get, device, "vkAllocateMemory"); + dev->get_buffer_memory_requirements = + DeviceProc( + get, device, "vkGetBufferMemoryRequirements"); + if (emulated) { + std::fprintf(stderr, + " [vulkan] ETC2/EAC emulation enabled: compressed uploads " + "decode to RGBA8/R16 host images\n"); + } + std::lock_guard lock(state_->devices_mutex); + state_->devices.erase( + std::remove_if(state_->devices.begin(), state_->devices.end(), + [device](const std::unique_ptr& candidate) { + return candidate->device == device; + }), + state_->devices.end()); + state_->devices.push_back(std::move(dev)); +} + +void VulkanEtc2Emulation::DestroyDevice(VkDevice device) { + std::vector doomed; + { + std::lock_guard lock(state_->mutex); + for (auto it = state_->commands.begin(); it != state_->commands.end();) { + const bool owned = std::any_of( + it->second.staging.begin(), it->second.staging.end(), + [device](const Staging& staging) { return staging.device == device; }); + if (owned) { + doomed.insert(doomed.end(), it->second.staging.begin(), + it->second.staging.end()); + it = state_->commands.erase(it); + } else { + ++it; + } + } + for (auto it = state_->images.begin(); it != state_->images.end();) { + it = it->second.device == device ? state_->images.erase(it) : std::next(it); + } + state_->has_commands.store(!state_->commands.empty(), + std::memory_order_release); + } + if (const HostDevice* dev = state_->Find(device); dev != nullptr) { + for (const Staging& staging : doomed) { + DestroyStaging(*dev, staging); + } + } + std::lock_guard lock(state_->devices_mutex); + state_->devices.erase( + std::remove_if(state_->devices.begin(), state_->devices.end(), + [device](const std::unique_ptr& candidate) { + return candidate->device == device; + }), + state_->devices.end()); +} + +VkResult VulkanEtc2Emulation::CreateImage(VkDevice device, + const VkImageCreateInfo* create_info, + const VkAllocationCallbacks* allocator, + VkImage* image) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->create_image == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + Etc2EmulatedFormat format; + if (!dev->emulated || create_info == nullptr || + !LookupEmulatedEtc2Format(create_info->format, &format)) { + return dev->create_image(device, create_info, allocator, image); + } + VkImageCreateInfo host_info = *create_info; + host_info.format = format.host_format; + host_info.flags &= ~static_cast( + VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT); + const VkResult result = dev->create_image(device, &host_info, allocator, image); + if (result == VK_SUCCESS && image != nullptr) { + { + std::lock_guard lock(state_->mutex); + state_->images[*image] = {device, format}; + } + if (ShouldLog(&g_image_logs, 4)) { + std::fprintf(stderr, + " [vulkan] ETC2 image %ux%u mips=%u format=%d -> host " + "format=%d\n", + create_info->extent.width, create_info->extent.height, + create_info->mipLevels, + static_cast(create_info->format), + static_cast(format.host_format)); + } + } + return result; +} + +void VulkanEtc2Emulation::DestroyImage(VkDevice device, VkImage image, + const VkAllocationCallbacks* allocator) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->destroy_image == nullptr) { + return; + } + if (dev->emulated) { + std::lock_guard lock(state_->mutex); + state_->images.erase(image); + } + dev->destroy_image(device, image, allocator); +} + +VkResult VulkanEtc2Emulation::CreateImageView( + VkDevice device, const VkImageViewCreateInfo* create_info, + const VkAllocationCallbacks* allocator, VkImageView* view) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->create_image_view == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + Etc2EmulatedFormat format; + if (!dev->emulated || create_info == nullptr || + !LookupEmulatedEtc2Format(create_info->format, &format)) { + return dev->create_image_view(device, create_info, allocator, view); + } + VkImageViewCreateInfo host_info = *create_info; + host_info.format = format.host_format; + return dev->create_image_view(device, &host_info, allocator, view); +} + +VkResult VulkanEtc2Emulation::BindBufferMemory(VkDevice device, VkBuffer buffer, + VkDeviceMemory memory, + VkDeviceSize offset) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->bind_buffer_memory == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + const VkResult result = + dev->bind_buffer_memory(device, buffer, memory, offset); + if (result == VK_SUCCESS && dev->emulated) { + std::lock_guard lock(state_->mutex); + state_->buffers[buffer] = {memory, offset}; + } + return result; +} + +VkResult VulkanEtc2Emulation::BindBufferMemory2( + VkDevice device, std::uint32_t count, const VkBindBufferMemoryInfo* infos) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->bind_buffer_memory2 == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + const VkResult result = dev->bind_buffer_memory2(device, count, infos); + if (result == VK_SUCCESS && dev->emulated && infos != nullptr) { + std::lock_guard lock(state_->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + state_->buffers[infos[index].buffer] = {infos[index].memory, + infos[index].memoryOffset}; + } + } + return result; +} + +VkResult VulkanEtc2Emulation::MapMemory(VkDevice device, VkDeviceMemory memory, + VkDeviceSize offset, VkDeviceSize size, + VkMemoryMapFlags flags, void** data) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->map_memory == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + const VkResult result = + dev->map_memory(device, memory, offset, size, flags, data); + if (result == VK_SUCCESS && dev->emulated && data != nullptr) { + std::lock_guard lock(state_->mutex); + state_->mappings[memory] = {*data, offset, size}; + } + return result; +} + +VkResult VulkanEtc2Emulation::MapMemory2(VkDevice device, + const VkMemoryMapInfo* info, + void** data) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->map_memory2 == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + const VkResult result = dev->map_memory2(device, info, data); + if (result == VK_SUCCESS && dev->emulated && info != nullptr && + data != nullptr) { + std::lock_guard lock(state_->mutex); + state_->mappings[info->memory] = {*data, info->offset, info->size}; + } + return result; +} + +void VulkanEtc2Emulation::UnmapMemory(VkDevice device, VkDeviceMemory memory) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->unmap_memory == nullptr) { + return; + } + if (dev->emulated) { + std::lock_guard lock(state_->mutex); + state_->mappings.erase(memory); + } + dev->unmap_memory(device, memory); +} + +VkResult VulkanEtc2Emulation::UnmapMemory2(VkDevice device, + const VkMemoryUnmapInfo* info) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->unmap_memory2 == nullptr) { + return VK_ERROR_INITIALIZATION_FAILED; + } + if (dev->emulated && info != nullptr) { + std::lock_guard lock(state_->mutex); + state_->mappings.erase(info->memory); + } + return dev->unmap_memory2(device, info); +} + +void VulkanEtc2Emulation::FreeMemory(VkDevice device, VkDeviceMemory memory, + const VkAllocationCallbacks* allocator) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->free_memory == nullptr) { + return; + } + if (dev->emulated) { + std::lock_guard lock(state_->mutex); + state_->mappings.erase(memory); + } + dev->free_memory(device, memory, allocator); +} + +void VulkanEtc2Emulation::DestroyBuffer(VkDevice device, VkBuffer buffer, + const VkAllocationCallbacks* allocator) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->destroy_buffer == nullptr) { + return; + } + if (dev->emulated) { + std::lock_guard lock(state_->mutex); + state_->buffers.erase(buffer); + } + dev->destroy_buffer(device, buffer, allocator); +} + +void VulkanEtc2Emulation::CmdCopyBufferToImage( + VkDevice device, VkCommandBuffer command_buffer, VkBuffer source, + VkImage destination, VkImageLayout layout, std::uint32_t region_count, + const VkBufferImageCopy* regions) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->copy_buffer_to_image == nullptr) { + return; + } + Etc2EmulatedFormat format; + if (!state_->LookupImage(*dev, destination, &format)) { + dev->copy_buffer_to_image(command_buffer, source, destination, layout, + region_count, regions); + return; + } + std::vector rewritten; + std::vector uploads; + VkDeviceSize total = 0; + Staging staging; + std::uint8_t* mapped = nullptr; + if (!PlanRegions(format, device, source, region_count, regions, &rewritten, + &uploads, &total)) { + LogFailure("skipped an upload with an unsupported region"); + return; + } + if (!CreateStaging(*dev, total, &staging, &mapped)) { + LogFailure("could not allocate a host-visible staging buffer"); + return; + } + for (PendingUpload& upload : uploads) { + upload.target = mapped + upload.target_offset; + } + state_->Record(command_buffer, std::move(uploads), staging); + dev->copy_buffer_to_image(command_buffer, staging.buffer, destination, + layout, region_count, rewritten.data()); +} + +void VulkanEtc2Emulation::CmdCopyBufferToImage2( + VkDevice device, VkCommandBuffer command_buffer, + const VkCopyBufferToImageInfo2* info) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->copy_buffer_to_image2 == nullptr || + info == nullptr) { + return; + } + Etc2EmulatedFormat format; + if (!state_->LookupImage(*dev, info->dstImage, &format)) { + dev->copy_buffer_to_image2(command_buffer, info); + return; + } + std::vector rewritten; + std::vector uploads; + VkDeviceSize total = 0; + Staging staging; + std::uint8_t* mapped = nullptr; + if (!PlanRegions(format, device, info->srcBuffer, info->regionCount, + info->pRegions, &rewritten, &uploads, &total)) { + LogFailure("skipped an upload with an unsupported region"); + return; + } + if (!CreateStaging(*dev, total, &staging, &mapped)) { + LogFailure("could not allocate a host-visible staging buffer"); + return; + } + for (PendingUpload& upload : uploads) { + upload.target = mapped + upload.target_offset; + } + state_->Record(command_buffer, std::move(uploads), staging); + VkCopyBufferToImageInfo2 host_info = *info; + host_info.srcBuffer = staging.buffer; + host_info.pRegions = rewritten.data(); + dev->copy_buffer_to_image2(command_buffer, &host_info); +} + +void VulkanEtc2Emulation::CmdExecuteCommands( + VkDevice device, VkCommandBuffer command_buffer, std::uint32_t count, + const VkCommandBuffer* secondaries) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->execute_commands == nullptr) { + return; + } + if (dev->emulated && secondaries != nullptr && + state_->has_commands.load(std::memory_order_acquire)) { + std::lock_guard lock(state_->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (state_->commands.count(secondaries[index]) != 0) { + state_->commands[command_buffer].secondaries.push_back( + secondaries[index]); + } + } + } + dev->execute_commands(command_buffer, count, secondaries); +} + +void VulkanEtc2Emulation::PrepareSubmit(const VkCommandBuffer* command_buffers, + std::uint32_t count) { + if (command_buffers == nullptr || count == 0 || + !state_->has_commands.load(std::memory_order_acquire)) { + return; + } + struct Work { + PendingUpload upload; + std::uint8_t* source = nullptr; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkDeviceSize memory_offset = 0; + }; + std::vector work; + { + std::lock_guard lock(state_->mutex); + auto collect = [this, &work](VkCommandBuffer command_buffer) { + const auto record = state_->commands.find(command_buffer); + if (record == state_->commands.end()) { + return; + } + for (const PendingUpload& upload : record->second.uploads) { + const auto binding = state_->buffers.find(upload.source); + if (binding == state_->buffers.end()) { + LogFailure("upload source buffer has no tracked memory binding"); + continue; + } + Work item; + item.upload = upload; + const VkDeviceSize absolute = + binding->second.offset + upload.source_offset; + const auto mapping = state_->mappings.find(binding->second.memory); + if (mapping == state_->mappings.end()) { + item.memory = binding->second.memory; + item.memory_offset = absolute; + } else if (absolute >= mapping->second.offset && + (mapping->second.size == VK_WHOLE_SIZE || + (absolute - mapping->second.offset <= + mapping->second.size && + upload.compressed <= mapping->second.size - + (absolute - + mapping->second.offset)))) { + item.source = static_cast(mapping->second.data) + + (absolute - mapping->second.offset); + } else { + LogFailure("upload source lies outside the mapped range"); + continue; + } + work.push_back(item); + } + }; + for (std::uint32_t index = 0; index < count; ++index) { + collect(command_buffers[index]); + const auto record = state_->commands.find(command_buffers[index]); + if (record != state_->commands.end()) { + const std::vector secondaries = + record->second.secondaries; + for (VkCommandBuffer secondary : secondaries) { + collect(secondary); + } + } + } + } + for (const Work& item : work) { + const PendingUpload& upload = item.upload; + const HostDevice* dev = state_->Find(upload.device); + std::uint8_t* source = item.source; + if (source == nullptr) { + void* data = nullptr; + if (dev == nullptr || dev->map_memory == nullptr || + dev->map_memory(dev->device, item.memory, 0, VK_WHOLE_SIZE, 0, + &data) != VK_SUCCESS) { + LogFailure("could not map an unmapped upload source"); + continue; + } + source = static_cast(data) + item.memory_offset; + } + const VkDeviceSize compressed_layer = upload.compressed / upload.layers; + const VkDeviceSize decoded_layer = upload.decoded / upload.layers; + for (std::uint32_t layer = 0; layer < upload.layers; ++layer) { + if (!DecodeEtcImage(upload.format, source + layer * compressed_layer, + compressed_layer, upload.width, upload.height, + upload.target + layer * decoded_layer, + decoded_layer)) { + LogFailure("could not decode an upload layer"); + } + } + if (item.source == nullptr && dev != nullptr && + dev->unmap_memory != nullptr) { + dev->unmap_memory(dev->device, item.memory); + } + if (ShouldLog(&g_decode_logs, 4)) { + std::fprintf(stderr, + " [vulkan] ETC2 upload decoded %ux%u layers=%u\n", + upload.width, upload.height, upload.layers); + } + } +} + +void VulkanEtc2Emulation::ReleaseCommandBuffer(VkCommandBuffer command_buffer) { + if (!state_->has_commands.load(std::memory_order_acquire)) { + return; + } + std::vector doomed; + { + std::lock_guard lock(state_->mutex); + const auto record = state_->commands.find(command_buffer); + if (record == state_->commands.end()) { + return; + } + doomed = std::move(record->second.staging); + state_->commands.erase(record); + state_->has_commands.store(!state_->commands.empty(), + std::memory_order_release); + } + for (const Staging& staging : doomed) { + if (const HostDevice* dev = state_->Find(staging.device); dev != nullptr) { + DestroyStaging(*dev, staging); + } + } +} + +} // namespace mocktail::graphics diff --git a/stubs/CMakeLists.txt b/stubs/CMakeLists.txt index 3ac9bca..16181fd 100644 --- a/stubs/CMakeLists.txt +++ b/stubs/CMakeLists.txt @@ -106,6 +106,8 @@ target_link_libraries(stub_glesv2 PRIVATE dl) add_library(stub_vulkan SHARED ${CMAKE_SOURCE_DIR}/src/graphics/bionic_vulkan_loader_adapter.cc ${CMAKE_SOURCE_DIR}/src/graphics/vulkan_text_overlay_compositor.cc + ${CMAKE_SOURCE_DIR}/src/graphics/vulkan_etc2_emulation.cc + ${CMAKE_SOURCE_DIR}/src/graphics/etc2_decoder.cc ) configure_stub(stub_vulkan "libvulkan.so") target_include_directories(stub_vulkan PRIVATE ${CMAKE_SOURCE_DIR}/include) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 48ad0ac..909be40 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -470,6 +470,27 @@ target_link_libraries(vulkan_text_overlay_compositor_test PRIVATE ) mocktail_apply_compile_options(vulkan_text_overlay_compositor_test) +add_executable(etc2_decoder_test + etc2_decoder_test.cc + ${PROJECT_SOURCE_DIR}/src/graphics/etc2_decoder.cc +) +target_include_directories(etc2_decoder_test PRIVATE + ${PROJECT_SOURCE_DIR}/include +) +target_link_libraries(etc2_decoder_test PRIVATE GTest::gtest_main) +mocktail_apply_compile_options(etc2_decoder_test) + +add_executable(vulkan_etc2_emulation_test + vulkan_etc2_emulation_test.cc + ${PROJECT_SOURCE_DIR}/src/graphics/vulkan_etc2_emulation.cc + ${PROJECT_SOURCE_DIR}/src/graphics/etc2_decoder.cc +) +target_include_directories(vulkan_etc2_emulation_test PRIVATE + ${PROJECT_SOURCE_DIR}/include +) +target_link_libraries(vulkan_etc2_emulation_test PRIVATE GTest::gtest_main) +mocktail_apply_compile_options(vulkan_etc2_emulation_test) + add_executable(video_driver_policy_test video_driver_policy_test.cc ) @@ -885,6 +906,8 @@ gtest_discover_tests(failure_dialog_test) gtest_discover_tests(frame_rate_policy_test) gtest_discover_tests(performance_policy_test) gtest_discover_tests(texture_memory_policy_test) +gtest_discover_tests(device_memory_profile_test) +gtest_discover_tests(display_size_test) gtest_discover_tests(http_client_policy_test) gtest_discover_tests(http_client_spin_guard_test) gtest_discover_tests(crash_report_policy_test) @@ -906,8 +929,6 @@ gtest_discover_tests(external_launch_broker_test) gtest_discover_tests(webview_helper_launcher_test) gtest_discover_tests(webview_helper_policy_test) gtest_discover_tests(webview_roblox_cookie_test) -gtest_discover_tests(device_memory_profile_test) -gtest_discover_tests(display_size_test) gtest_discover_tests(startup_state_machine_test) gtest_discover_tests(game_session_coordinator_test) gtest_discover_tests(owned_pthread_test) @@ -929,6 +950,8 @@ gtest_discover_tests(input_pump_pacer_test) gtest_discover_tests(vulkan_surface_recovery_gate_test) gtest_discover_tests(vulkan_present_progress_gate_test) gtest_discover_tests(vulkan_text_overlay_compositor_test) +gtest_discover_tests(etc2_decoder_test) +gtest_discover_tests(vulkan_etc2_emulation_test) gtest_discover_tests(video_driver_policy_test) gtest_discover_tests(window_surface_lifecycle_test) gtest_discover_tests(window_creation_policy_test) diff --git a/tests/etc2_decoder_test.cc b/tests/etc2_decoder_test.cc new file mode 100644 index 0000000..8ead981 --- /dev/null +++ b/tests/etc2_decoder_test.cc @@ -0,0 +1,196 @@ +#include "mocktail/graphics/etc2_decoder.h" + +#include + +#include +#include +#include + +namespace mocktail::graphics { +namespace { + +using Block8 = std::array; + +struct Rgba { + int r; + int g; + int b; + int a; +}; + +std::vector Decode(EtcFormat format, + const std::vector& source, + std::uint32_t width, std::uint32_t height) { + std::vector output( + static_cast(width) * height * EtcDecodedTexelBytes(format), + 0xAB); + EXPECT_TRUE(DecodeEtcImage(format, source.data(), source.size(), width, + height, output.data(), output.size())); + return output; +} + +std::vector Bytes(const Block8& block) { + return {block.begin(), block.end()}; +} + +void ExpectRgba(const std::vector& texels, std::uint32_t width, + std::uint32_t x, std::uint32_t y, Rgba expected) { + const std::size_t offset = (static_cast(y) * width + x) * 4; + ASSERT_LT(offset + 3, texels.size()); + EXPECT_EQ(texels[offset], expected.r) << "x=" << x << " y=" << y; + EXPECT_EQ(texels[offset + 1], expected.g) << "x=" << x << " y=" << y; + EXPECT_EQ(texels[offset + 2], expected.b) << "x=" << x << " y=" << y; + EXPECT_EQ(texels[offset + 3], expected.a) << "x=" << x << " y=" << y; +} + +std::uint16_t Unsigned16(const std::vector& texels, + std::size_t index) { + return static_cast(texels[index * 2] | + (texels[index * 2 + 1] << 8)); +} + +std::int16_t Signed16(const std::vector& texels, + std::size_t index) { + return static_cast(Unsigned16(texels, index)); +} + +TEST(Etc2DecoderTest, ReportsBlockAndTexelSizes) { + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgb8), 8U); + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgb8A1), 8U); + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgba8), 16U); + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacR11), 8U); + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacR11Signed), 8U); + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacRg11), 16U); + EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacRg11Signed), 16U); + EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgb8), 4U); + EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgb8A1), 4U); + EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgba8), 4U); + EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEacR11), 2U); + EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEacRg11Signed), 4U); +} + +TEST(Etc2DecoderTest, DecodesIndividualMode) { + // Base colours 8 (left sub-block) and 0 (right), codeword 0 (2/8). + // Pixel (0,0) index 1 (+8), pixels in column 1 index 2 (-2). + const Block8 block = {0x80, 0x80, 0x80, 0x00, 0x00, 0xF0, 0x00, 0x01}; + const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); + ExpectRgba(texels, 4, 0, 0, {144, 144, 144, 255}); + ExpectRgba(texels, 4, 0, 1, {138, 138, 138, 255}); + ExpectRgba(texels, 4, 1, 0, {134, 134, 134, 255}); + ExpectRgba(texels, 4, 2, 0, {2, 2, 2, 255}); +} + +TEST(Etc2DecoderTest, DecodesDifferentialMode) { + // R/G/B = 16 with delta +1: base colours 132 and 140, modifier +2. + const Block8 block = {0x81, 0x81, 0x81, 0x02, 0x00, 0x00, 0x00, 0x00}; + const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); + ExpectRgba(texels, 4, 0, 0, {134, 134, 134, 255}); + ExpectRgba(texels, 4, 2, 3, {142, 142, 142, 255}); +} + +TEST(Etc2DecoderTest, DecodesTMode) { + // Red overflows: colour 1 (0,255,0), colour 2 (0,255,136), distance 11. + const Block8 block = {0x04, 0xF0, 0x0F, 0x86, 0x00, 0x12, 0x00, 0x03}; + const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); + ExpectRgba(texels, 4, 0, 0, {11, 255, 147, 255}); + ExpectRgba(texels, 4, 0, 1, {0, 244, 125, 255}); + ExpectRgba(texels, 4, 1, 0, {0, 255, 136, 255}); + ExpectRgba(texels, 4, 1, 1, {0, 255, 0, 255}); +} + +TEST(Etc2DecoderTest, DecodesHMode) { + // Green overflows: colour 1 (0,0,0), colour 2 (255,17,255), distance 3. + const Block8 block = {0x00, 0x04, 0x78, 0xFA, 0x80, 0x01, 0x80, 0x00}; + const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); + ExpectRgba(texels, 4, 0, 0, {255, 20, 255, 255}); + ExpectRgba(texels, 4, 3, 3, {252, 14, 252, 255}); + ExpectRgba(texels, 4, 1, 0, {3, 3, 3, 255}); +} + +TEST(Etc2DecoderTest, DecodesPlanarMode) { + // Blue overflows: O=(130,64,0), H=(0,255,0), V=(255,0,255). + const Block8 block = {0x40, 0x40, 0x04, 0x02, 0xFE, 0x07, 0xE0, 0x3F}; + const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); + ExpectRgba(texels, 4, 0, 0, {130, 64, 0, 255}); + ExpectRgba(texels, 4, 3, 0, {33, 207, 0, 255}); + ExpectRgba(texels, 4, 0, 3, {224, 16, 191, 255}); + ExpectRgba(texels, 4, 3, 3, {126, 159, 191, 255}); +} + +TEST(Etc2DecoderTest, DecodesPunchthroughAlpha) { + // Opaque bit clear: index 2 is transparent black, index 0 has no modifier. + const Block8 block = {0x81, 0x81, 0x81, 0x00, 0x00, 0x01, 0x00, 0x02}; + const auto texels = Decode(EtcFormat::kEtc2Rgb8A1, Bytes(block), 4, 4); + ExpectRgba(texels, 4, 0, 0, {0, 0, 0, 0}); + ExpectRgba(texels, 4, 0, 1, {140, 140, 140, 255}); + ExpectRgba(texels, 4, 1, 0, {132, 132, 132, 255}); + ExpectRgba(texels, 4, 2, 0, {140, 140, 140, 255}); +} + +TEST(Etc2DecoderTest, DecodesRgba8WithAlphaBlockFirst) { + // Alpha: base 100, multiplier 2, table 0; pixel (0,0) index 7 (+14). + const std::vector block = { + 100, 0x20, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + const auto texels = Decode(EtcFormat::kEtc2Rgba8, block, 4, 4); + ExpectRgba(texels, 4, 0, 0, {138, 138, 138, 128}); + ExpectRgba(texels, 4, 1, 0, {138, 138, 138, 94}); +} + +TEST(Etc2DecoderTest, DecodesUnsignedR11) { + const Block8 block = {128, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}; + const auto texels = Decode(EtcFormat::kEacR11, Bytes(block), 4, 4); + EXPECT_EQ(Unsigned16(texels, 0), 36497); + EXPECT_EQ(Unsigned16(texels, 1), 32143); + + const Block8 zero_multiplier = {0, 0x00, 0, 0, 0, 0, 0, 0}; + const auto flat = Decode(EtcFormat::kEacR11, Bytes(zero_multiplier), 1, 1); + EXPECT_EQ(Unsigned16(flat, 0), 32); +} + +TEST(Etc2DecoderTest, DecodesSignedR11) { + const Block8 block = {0x80, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}; + const auto texels = Decode(EtcFormat::kEacR11Signed, Bytes(block), 4, 4); + EXPECT_EQ(Signed16(texels, 0), -28956); + EXPECT_EQ(Signed16(texels, 1), -32767); +} + +TEST(Etc2DecoderTest, DecodesRg11AsTwoChannels) { + const std::vector block = { + 128, 0x10, 0xE0, 0, 0, 0, 0, 0, + 0, 0x00, 0x00, 0, 0, 0, 0, 0, + }; + const auto texels = Decode(EtcFormat::kEacRg11, block, 1, 1); + EXPECT_EQ(Unsigned16(texels, 0), 36497); + EXPECT_EQ(Unsigned16(texels, 1), 32); +} + +TEST(Etc2DecoderTest, DecodesPartialAndMultipleBlocks) { + const Block8 t_mode = {0x04, 0xF0, 0x0F, 0x86, 0x00, 0x12, 0x00, 0x03}; + const Block8 differential = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; + std::vector source = Bytes(t_mode); + source.insert(source.end(), differential.begin(), differential.end()); + const auto texels = Decode(EtcFormat::kEtc2Rgb8, source, 5, 1); + ExpectRgba(texels, 5, 0, 0, {11, 255, 147, 255}); + ExpectRgba(texels, 5, 1, 0, {0, 255, 136, 255}); + ExpectRgba(texels, 5, 4, 0, {134, 134, 134, 255}); +} + +TEST(Etc2DecoderTest, RejectsShortBuffers) { + const std::vector source(8, 0); + std::vector output(4 * 4 * 4, 0); + EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), 7, 4, 4, + output.data(), output.size())); + EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), + source.size(), 4, 4, output.data(), + output.size() - 1)); + EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), + source.size(), 5, 4, output.data(), + output.size())); + EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, nullptr, 0, 0, 0, + output.data(), output.size())); +} + +} // namespace +} // namespace mocktail::graphics diff --git a/tests/vulkan_etc2_emulation_test.cc b/tests/vulkan_etc2_emulation_test.cc new file mode 100644 index 0000000..10525bd --- /dev/null +++ b/tests/vulkan_etc2_emulation_test.cc @@ -0,0 +1,279 @@ +#include "mocktail/graphics/vulkan_etc2_emulation.h" + +#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, 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{}; +std::array g_staging{}; + +VKAPI_ATTR VkResult VKAPI_CALL FakeCreateImage(VkDevice, + const VkImageCreateInfo*, + const VkAllocationCallbacks*, + VkImage* image) { + *image = FakeHandle(0x100); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL FakeCreateBuffer(VkDevice, + const VkBufferCreateInfo*, + const VkAllocationCallbacks*, + VkBuffer* buffer) { + *buffer = FakeHandle(kStagingBuffer); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL FakeBufferRequirements( + VkDevice, VkBuffer, VkMemoryRequirements* requirements) { + requirements->size = g_staging.size(); + requirements->memoryTypeBits = 1; +} + +VKAPI_ATTR VkResult VKAPI_CALL FakeAllocateMemory( + VkDevice, const VkMemoryAllocateInfo*, const VkAllocationCallbacks*, + VkDeviceMemory* memory) { + *memory = FakeHandle(kStagingMemory); + 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*) {} +VKAPI_ATTR void VKAPI_CALL FakeFreeMemory(VkDevice, VkDeviceMemory, + const VkAllocationCallbacks*) {} +VKAPI_ATTR void VKAPI_CALL FakeCopyBufferToImage(VkCommandBuffer, VkBuffer, + VkImage, VkImageLayout, + std::uint32_t, + const VkBufferImageCopy*) {} + +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("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); + 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); +} + +} // namespace +} // namespace mocktail::graphics