From 854969457fc22ed01dfe7a0ca3bb4adf331241cf Mon Sep 17 00:00:00 2001 From: komaruworld Date: Fri, 18 Sep 2026 16:55:29 +0300 Subject: [PATCH] Fix textures and device info from Nightcap Ported from CoderDayton/nightcap --- .github/workflows/packages.yml | 8 +- CMakeLists.txt | 1 + include/mocktail/graphics/etc2_decoder.h | 53 + include/mocktail/graphics/texture_override.h | 55 + .../mocktail/graphics/vulkan_etc2_emulation.h | 102 ++ include/runtime/device_memory_profile.h | 23 + include/runtime/display_size.h | 66 + packaging/arch/PKGBUILD | 1 + packaging/aur/mocktail-bin/.SRCINFO | 1 + packaging/aur/mocktail-bin/PKGBUILD | 1 + packaging/aur/mocktail-git/.SRCINFO | 1 + packaging/aur/mocktail-git/PKGBUILD | 1 + packaging/aur/mocktail/.SRCINFO | 1 + packaging/aur/mocktail/PKGBUILD | 1 + packaging/nix/package.nix | 2 + scripts/install_thin_dependencies.sh | 3 + src/graphics/bionic_vulkan_loader_adapter.cc | 461 +++++- src/graphics/etc2_decoder.cc | 398 +++++ src/graphics/texture_override.cc | 182 +++ src/graphics/vulkan_etc2_emulation.cc | 1308 +++++++++++++++++ src/jnivm/jnivm.cc | 72 +- src/legacy/legacy_runtime.cc | 131 +- src/runtime/device_memory_profile.cc | 35 + src/runtime/graphics_launch_policy.cc | 5 +- src/runtime/runtime_paths.cc | 5 +- stubs/CMakeLists.txt | 7 + tests/supported_launch_policy_test.cc | 2 +- 27 files changed, 2862 insertions(+), 64 deletions(-) create mode 100644 include/mocktail/graphics/etc2_decoder.h create mode 100644 include/mocktail/graphics/texture_override.h create mode 100644 include/mocktail/graphics/vulkan_etc2_emulation.h create mode 100644 include/runtime/device_memory_profile.h create mode 100644 include/runtime/display_size.h create mode 100644 src/graphics/etc2_decoder.cc create mode 100644 src/graphics/texture_override.cc create mode 100644 src/graphics/vulkan_etc2_emulation.cc create mode 100644 src/runtime/device_memory_profile.cc diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index b10c25d..bfe81c6 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -57,7 +57,7 @@ jobs: cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \ nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \ libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \ - vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \ + vulkan-headers zlib libplacebo libpng bubblewrap xdg-dbus-proxy \ patchelf jq unzip file ca-certificates dbus zsync # Reduced ABI-compatible packages belong only in this builder. @@ -172,7 +172,7 @@ jobs: libyaml-dev libelf-dev libminizip-dev libcapstone-dev \ libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \ libutf8proc-dev libfontconfig1-dev libegl-dev \ - libvulkan-dev libplacebo-dev zlib1g-dev + libvulkan-dev libplacebo-dev libpng-dev zlib1g-dev cmake -S . -B build-deb -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -220,7 +220,7 @@ jobs: minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \ webkitgtk6.0-devel utf8proc-devel fontconfig-devel \ libglvnd-devel vulkan-headers vulkan-loader-devel \ - libplacebo-devel zlib-ng-compat-devel + libplacebo-devel libpng-devel zlib-ng-compat-devel cmake -S . -B build-rpm -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -264,7 +264,7 @@ jobs: cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \ nlohmann-json libyaml libelf minizip capstone gtk4 \ libadwaita webkitgtk-6.0 libutf8proc fontconfig \ - libplacebo vulkan-headers vulkan-icd-loader libglvnd zlib + libplacebo libpng vulkan-headers vulkan-icd-loader libglvnd zlib useradd --create-home builder chown -R builder:builder /workspace runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dacda6..28d2dd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -264,6 +264,7 @@ add_library(mocktail_runtime STATIC src/runtime/command_line.cc src/runtime/crash_report_policy.cc src/runtime/discord_rpc.cc + src/runtime/device_memory_profile.cc src/runtime/device_profile.cc src/runtime/environment.cc src/runtime/external_launch_broker.cc diff --git a/include/mocktail/graphics/etc2_decoder.h b/include/mocktail/graphics/etc2_decoder.h new file mode 100644 index 0000000..6a4fccf --- /dev/null +++ b/include/mocktail/graphics/etc2_decoder.h @@ -0,0 +1,53 @@ +#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_ +#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_ + +#include +#include + +namespace mocktail::graphics { + +enum class EtcFormat { + kEtc2Rgb8, + kEtc2Rgb8A1, + kEtc2Rgba8, + kEacR11, + kEacR11Signed, + kEacRg11, + kEacRg11Signed, +}; + +std::size_t EtcBlockBytes(EtcFormat format); + +std::size_t EtcDecodedTexelBytes(EtcFormat format); + +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); + +bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source, + std::size_t source_bytes, std::uint32_t width, + std::uint32_t height, + std::uint32_t first_block_row, + std::uint32_t block_row_count, + std::uint8_t* destination, + std::size_t destination_bytes); + +struct EtcDecodeJob { + EtcFormat format = EtcFormat::kEtc2Rgb8; + const std::uint8_t* source = nullptr; + std::size_t source_bytes = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint8_t* destination = nullptr; + std::size_t destination_bytes = 0; + bool ok = false; +}; + +// Synchronous; job destination buffers must not overlap. +void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count, + unsigned worker_count); + +} // namespace mocktail::graphics + +#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_ diff --git a/include/mocktail/graphics/texture_override.h b/include/mocktail/graphics/texture_override.h new file mode 100644 index 0000000..3633ac9 --- /dev/null +++ b/include/mocktail/graphics/texture_override.h @@ -0,0 +1,55 @@ +#ifndef MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_ +#define MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mocktail::graphics { + +struct RgbaImage { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::vector pixels; +}; + +std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size); +std::string HashName(std::uint64_t hash); + +bool ReadPngRgba(const std::string& path, RgbaImage* image); +bool WritePngRgba(const std::string& path, const RgbaImage& image); + +void ResampleRgba(const RgbaImage& source, std::uint32_t width, + std::uint32_t height, std::uint8_t* destination); +void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width, + std::uint32_t source_height, std::uint32_t width, + std::uint32_t height, std::uint8_t* destination); + +class TextureOverrides { + public: + TextureOverrides(std::string override_dir, std::string dump_dir); + static TextureOverrides FromEnvironment(); + + bool enabled() const { return !override_dir_.empty() || !dump_dir_.empty(); } + bool dumping() const { return !dump_dir_.empty(); } + + std::shared_ptr Lookup(std::uint64_t hash); + void Dump(std::uint64_t hash, std::uint32_t width, std::uint32_t height, + const std::uint8_t* rgba); + + private: + std::string override_dir_; + std::string dump_dir_; + std::mutex mutex_; + std::unordered_map> cache_; + std::unordered_set dumped_; +}; + +} // namespace mocktail::graphics + +#endif // MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_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..2ead982 --- /dev/null +++ b/include/mocktail/graphics/vulkan_etc2_emulation.h @@ -0,0 +1,102 @@ +#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 { + +struct Etc2EmulatedFormat { + VkFormat host_format = VK_FORMAT_UNDEFINED; + EtcFormat etc_format = EtcFormat::kEtc2Rgb8; +}; + +bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out); + +VkFormatFeatureFlags EmulatedEtc2FormatFeatures( + VkFormatFeatureFlags host_features); + +bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent, + std::uint32_t layer_count, VkDeviceSize* compressed, + VkDeviceSize* decoded); + +bool Etc2SupportAdvertised(const char* value); +bool Etc2SupportAdvertised(); + +inline constexpr std::uint32_t kSmallTextureMaxExtent = 64; +inline constexpr std::uint32_t kDefaultSmallTextureUpscale = 4; +std::uint32_t SmallTextureUpscale(const char* value); + +class VulkanEtc2Emulation final { + public: + VulkanEtc2Emulation(); + ~VulkanEtc2Emulation(); + VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete; + VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete; + + 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 CmdCopyImage(VkDevice device, VkCommandBuffer command_buffer, + VkImage source, VkImageLayout source_layout, + VkImage destination, VkImageLayout destination_layout, + std::uint32_t region_count, const VkImageCopy* regions); + void CmdCopyImage2(VkDevice device, VkCommandBuffer command_buffer, + const VkCopyImageInfo2* info); + void CmdExecuteCommands(VkDevice device, VkCommandBuffer command_buffer, + std::uint32_t count, + const VkCommandBuffer* secondaries); + + // Resubmission must reread the source bytes. + void PrepareSubmit(const VkCommandBuffer* command_buffers, + std::uint32_t count); + // Call only after the command buffer 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/include/runtime/device_memory_profile.h b/include/runtime/device_memory_profile.h new file mode 100644 index 0000000..3107942 --- /dev/null +++ b/include/runtime/device_memory_profile.h @@ -0,0 +1,23 @@ +#ifndef MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_ +#define MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_ + +#include + +namespace mocktail { +namespace runtime { + +struct DeviceMemoryProfile { + std::int32_t total_memory_mb = 0; + std::int32_t memory_class_mb = 0; + std::int32_t large_memory_class_mb = 0; + std::int64_t low_memory_killer_background_threshold = 0; + std::int64_t low_memory_killer_foreground_threshold = 0; + bool low_ram_device = false; +}; + +DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes); + +} // namespace runtime +} // namespace mocktail + +#endif // MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_ diff --git a/include/runtime/display_size.h b/include/runtime/display_size.h new file mode 100644 index 0000000..b2fd917 --- /dev/null +++ b/include/runtime/display_size.h @@ -0,0 +1,66 @@ +#ifndef MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_ +#define MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_ + +#include +#include + +namespace mocktail { +namespace runtime { + +struct DisplaySize { + std::int32_t width = 1920; + std::int32_t height = 1080; +}; + +// Physical pixels; window size is captured at startup. +inline constexpr const char* kDisplaySizeEnvironment = + "MOCKTAIL_DISPLAY_SIZE_INTERNAL"; +inline constexpr const char* kWindowSizeEnvironment = + "MOCKTAIL_WINDOW_SIZE_INTERNAL"; + +inline DisplaySize ParseDisplaySize(const char* value) { + const DisplaySize fallback; + if (value == nullptr) { + return fallback; + } + std::int64_t parts[2] = {0, 0}; + const char* cursor = value; + for (int index = 0; index < 2; ++index) { + if (*cursor < '0' || *cursor > '9') { + return fallback; + } + std::int64_t parsed = 0; + while (*cursor >= '0' && *cursor <= '9') { + parsed = parsed * 10 + (*cursor - '0'); + if (parsed > std::numeric_limits::max()) { + return fallback; + } + ++cursor; + } + if (parsed == 0) { + return fallback; + } + parts[index] = parsed; + if (index == 0) { + if (*cursor != 'x') { + return fallback; + } + ++cursor; + } + } + if (*cursor != '\0') { + return fallback; + } + return {static_cast(parts[0]), + static_cast(parts[1])}; +} + +inline std::int32_t PixelsToMillimetersAt160Dpi(std::int32_t pixels) { + return static_cast(static_cast(pixels) * 254 / + 1600); +} + +} // namespace runtime +} // namespace mocktail + +#endif // MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_ diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index b2b5455..57b0bbb 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -16,6 +16,7 @@ depends=( 'libelf' 'libglvnd' 'libplacebo' + 'libpng' 'libutf8proc' 'libyaml' 'minizip' diff --git a/packaging/aur/mocktail-bin/.SRCINFO b/packaging/aur/mocktail-bin/.SRCINFO index f5d6545..5bcf3a1 100644 --- a/packaging/aur/mocktail-bin/.SRCINFO +++ b/packaging/aur/mocktail-bin/.SRCINFO @@ -17,6 +17,7 @@ pkgbase = mocktail-bin depends = libelf depends = libglvnd depends = libplacebo + depends = libpng depends = libsoup3 depends = libutf8proc depends = libyaml diff --git a/packaging/aur/mocktail-bin/PKGBUILD b/packaging/aur/mocktail-bin/PKGBUILD index 0a47638..b6752d5 100644 --- a/packaging/aur/mocktail-bin/PKGBUILD +++ b/packaging/aur/mocktail-bin/PKGBUILD @@ -20,6 +20,7 @@ depends=( 'libelf' 'libglvnd' 'libplacebo' + 'libpng' 'libsoup3' 'libutf8proc' 'libyaml' diff --git a/packaging/aur/mocktail-git/.SRCINFO b/packaging/aur/mocktail-git/.SRCINFO index effcd15..76e6507 100644 --- a/packaging/aur/mocktail-git/.SRCINFO +++ b/packaging/aur/mocktail-git/.SRCINFO @@ -24,6 +24,7 @@ pkgbase = mocktail-git depends = libelf depends = libglvnd depends = libplacebo + depends = libpng depends = libsoup3 depends = libutf8proc depends = libyaml diff --git a/packaging/aur/mocktail-git/PKGBUILD b/packaging/aur/mocktail-git/PKGBUILD index 99a101b..5856f82 100644 --- a/packaging/aur/mocktail-git/PKGBUILD +++ b/packaging/aur/mocktail-git/PKGBUILD @@ -20,6 +20,7 @@ depends=( 'libelf' 'libglvnd' 'libplacebo' + 'libpng' 'libsoup3' 'libutf8proc' 'libyaml' diff --git a/packaging/aur/mocktail/.SRCINFO b/packaging/aur/mocktail/.SRCINFO index 5514bdc..7088c55 100644 --- a/packaging/aur/mocktail/.SRCINFO +++ b/packaging/aur/mocktail/.SRCINFO @@ -24,6 +24,7 @@ pkgbase = mocktail depends = libelf depends = libglvnd depends = libplacebo + depends = libpng depends = libsoup3 depends = libutf8proc depends = libyaml diff --git a/packaging/aur/mocktail/PKGBUILD b/packaging/aur/mocktail/PKGBUILD index a0544f8..2a985bb 100644 --- a/packaging/aur/mocktail/PKGBUILD +++ b/packaging/aur/mocktail/PKGBUILD @@ -20,6 +20,7 @@ depends=( 'libelf' 'libglvnd' 'libplacebo' + 'libpng' 'libsoup3' 'libutf8proc' 'libyaml' diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix index 3ad2043..abe2a4b 100644 --- a/packaging/nix/package.nix +++ b/packaging/nix/package.nix @@ -23,6 +23,7 @@ elfutils, libglvnd, libplacebo, + libpng, libsoup_3, libutf8proc, libyaml, @@ -92,6 +93,7 @@ stdenv.mkDerivation (finalAttrs: { elfutils libglvnd libplacebo + libpng libsoup_3 libutf8proc libyaml diff --git a/scripts/install_thin_dependencies.sh b/scripts/install_thin_dependencies.sh index 5228fec..5431a9b 100755 --- a/scripts/install_thin_dependencies.sh +++ b/scripts/install_thin_dependencies.sh @@ -119,6 +119,7 @@ libyaml fontconfig minizip libplacebo +libpng util-linux' INSTALL_ARGS='-S --needed --noconfirm' ;; @@ -144,6 +145,7 @@ libyaml fontconfig minizip libplacebo +libpng util-linux' if HostUsesMusl; then PACKAGES="$PACKAGES gcompat" @@ -172,6 +174,7 @@ libyaml fontconfig minizip libplacebo +libpng coreutils findutils util-linux' diff --git a/src/graphics/bionic_vulkan_loader_adapter.cc b/src/graphics/bionic_vulkan_loader_adapter.cc index 89ac60f..58d1bce 100644 --- a/src/graphics/bionic_vulkan_loader_adapter.cc +++ b/src/graphics/bionic_vulkan_loader_adapter.cc @@ -1,3 +1,4 @@ +// Includes changes by vii from CoderDayton/nightcap. // Android Vulkan loader ABI -> host Vulkan loader + SDL3 WSI. #include @@ -17,6 +18,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 +190,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 +730,90 @@ 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(vkCmdCopyImage) + MOCKTAIL_VK_PROC(vkCmdCopyImage2) + 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(vkCmdCopyImage2KHR, vkCmdCopyImage2) + 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; + }); +} + +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", + "vkCmdCopyImage", "vkCmdCopyImage2", + "vkCmdCopyImage2KHR", + "vkCmdCopyBufferToImage2KHR", "vkCmdExecuteCommands", + }; + return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0])); +} + +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 || @@ -959,11 +1040,23 @@ struct FpsWaitTrace { std::atomic total_ns{0}; std::atomic max_ns{0}; std::atomic window_start_ns{0}; + std::atomic max_gap_ns{0}; + std::atomic last_start_ns{0}; void Record(std::uint64_t start_ns) { if (name == nullptr || start_ns == 0) { return; } + const std::uint64_t previous_start = + last_start_ns.exchange(start_ns, std::memory_order_relaxed); + if (previous_start != 0 && start_ns > previous_start) { + const std::uint64_t gap_ns = start_ns - previous_start; + std::uint64_t max_gap = max_gap_ns.load(std::memory_order_relaxed); + while (gap_ns > max_gap && + !max_gap_ns.compare_exchange_weak(max_gap, gap_ns, + std::memory_order_relaxed)) { + } + } const std::uint64_t wait_ns = MonotonicNanos() - start_ns; total_ns.fetch_add(wait_ns, std::memory_order_relaxed); std::uint64_t max_wait = max_ns.load(std::memory_order_relaxed); @@ -984,14 +1077,18 @@ struct FpsWaitTrace { const std::uint64_t total = total_ns.exchange(0, std::memory_order_relaxed); const std::uint64_t peak = max_ns.exchange(0, std::memory_order_relaxed); const std::uint64_t count = samples.exchange(0, std::memory_order_relaxed); + const std::uint64_t gap_peak = + max_gap_ns.exchange(0, std::memory_order_relaxed); window_start_ns.store(start_ns, std::memory_order_relaxed); if (count == 0) { return; } - std::fprintf(stderr, " [fps] %s n=%llu avg=%llu us max=%llu us\n", name, - static_cast(count), + std::fprintf(stderr, + " [fps] %s n=%llu avg=%llu us max=%llu us gap_max=%llu us\n", + name, static_cast(count), static_cast(total / count / 1000ULL), - static_cast(peak / 1000ULL)); + static_cast(peak / 1000ULL), + static_cast(gap_peak / 1000ULL)); } }; @@ -1042,6 +1139,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 +1396,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 +1455,30 @@ 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; + } + // Restore the caller-owned feature bit after the host call. + 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 +1514,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 +1590,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 +1838,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 +1895,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 +1916,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 +1932,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 +1961,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 +1998,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 +2020,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 +2037,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 +2230,250 @@ 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) && + mocktail::graphics::Etc2SupportAdvertised()) { + 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) && + mocktail::graphics::Etc2SupportAdvertised()) { + 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 vkCmdCopyImage( + VkCommandBuffer command_buffer, VkImage source, + VkImageLayout source_layout, VkImage destination, + VkImageLayout destination_layout, std::uint32_t region_count, + const VkImageCopy* regions) { + State().etc2.CmdCopyImage( + HostDispatchForCommandBuffer(command_buffer).device, command_buffer, + source, source_layout, destination, destination_layout, region_count, + regions); +} + +VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage2(VkCommandBuffer command_buffer, + const VkCopyImageInfo2* info) { + State().etc2.CmdCopyImage2( + 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..8720456 --- /dev/null +++ b/src/graphics/etc2_decoder.cc @@ -0,0 +1,398 @@ +#include "mocktail/graphics/etc2_decoder.h" + +#include +#include +#include + +#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); } + +// Texels use column-major indexing: 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); + } + } +} + +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; + } +} + +namespace { + +constexpr std::uint64_t kBlocksPerBand = 16384; +constexpr std::uint64_t kParallelMinimumBlocks = 65536; + +bool ImageFits(EtcFormat format, const std::uint8_t* source, + std::size_t source_bytes, std::uint32_t width, + std::uint32_t height, const 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; + return blocks_wide * blocks_high * EtcBlockBytes(format) <= source_bytes && + static_cast(width) * height * + EtcDecodedTexelBytes(format) <= + destination_bytes; +} + +} // namespace + +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) { + return DecodeEtcImageBlockRows(format, source, source_bytes, width, height, 0, + (height + 3) / 4, destination, + destination_bytes); +} + +namespace { + +struct DecodeBand { + EtcDecodeJob* job; + std::uint32_t first_block_row; + std::uint32_t block_row_count; +}; + +struct DecodeBandQueue { + std::vector bands; + std::atomic next{0}; + + void Run() { + for (;;) { + const std::size_t index = next.fetch_add(1, std::memory_order_relaxed); + if (index >= bands.size()) { + return; + } + const DecodeBand& band = bands[index]; + const EtcDecodeJob& job = *band.job; + DecodeEtcImageBlockRows(job.format, job.source, job.source_bytes, + job.width, job.height, band.first_block_row, + band.block_row_count, job.destination, + job.destination_bytes); + } + } +}; + +void* RunDecodeBandQueue(void* queue) { + static_cast(queue)->Run(); + return nullptr; +} + +} // namespace + +void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count, + unsigned worker_count) { + if (jobs == nullptr) { + return; + } + DecodeBandQueue queue; + std::vector& bands = queue.bands; + std::uint64_t total_blocks = 0; + for (std::size_t index = 0; index < count; ++index) { + EtcDecodeJob& job = jobs[index]; + job.ok = ImageFits(job.format, job.source, job.source_bytes, job.width, + job.height, job.destination, job.destination_bytes); + if (!job.ok) { + continue; + } + const std::uint32_t blocks_wide = (job.width + 3) / 4; + const std::uint32_t blocks_high = (job.height + 3) / 4; + const std::uint32_t rows_per_band = static_cast( + std::max(1, kBlocksPerBand / blocks_wide)); + for (std::uint32_t row = 0; row < blocks_high; row += rows_per_band) { + bands.push_back( + {&job, row, std::min(rows_per_band, blocks_high - row)}); + } + total_blocks += static_cast(blocks_wide) * blocks_high; + } + + const std::size_t threads = + total_blocks < kParallelMinimumBlocks + ? 1 + : std::min(std::max(worker_count, 1U), bands.size()); + std::vector workers; + workers.reserve(threads > 0 ? threads - 1 : 0); + for (std::size_t index = 1; index < threads; ++index) { + pthread_t worker; + if (pthread_create(&worker, nullptr, RunDecodeBandQueue, &queue) != 0) { + break; + } + workers.push_back(worker); + } + queue.Run(); + for (pthread_t worker : workers) { + pthread_join(worker, nullptr); + } +} + +bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source, + std::size_t source_bytes, std::uint32_t width, + std::uint32_t height, + std::uint32_t first_block_row, + std::uint32_t block_row_count, + std::uint8_t* destination, + std::size_t destination_bytes) { + if (!ImageFits(format, source, source_bytes, width, height, destination, + destination_bytes)) { + return false; + } + const std::uint64_t blocks_wide = (static_cast(width) + 3) / 4; + const std::uint64_t blocks_high = + (static_cast(height) + 3) / 4; + if (static_cast(first_block_row) + block_row_count > + blocks_high) { + return false; + } + const std::size_t texel_bytes = EtcDecodedTexelBytes(format); + const std::size_t block_bytes = EtcBlockBytes(format); + + std::array rgba{}; + const std::uint64_t end_block_row = + static_cast(first_block_row) + block_row_count; + for (std::uint64_t by = first_block_row; by < end_block_row; ++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/texture_override.cc b/src/graphics/texture_override.cc new file mode 100644 index 0000000..6a6ec71 --- /dev/null +++ b/src/graphics/texture_override.cc @@ -0,0 +1,182 @@ +#include "mocktail/graphics/texture_override.h" + +#include + +#include +#include +#include +#include + +namespace mocktail::graphics { + +std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size) { + std::uint64_t hash = 0xcbf29ce484222325ULL; + for (std::size_t index = 0; index < size; ++index) { + hash ^= data[index]; + hash *= 0x100000001b3ULL; + } + return hash; +} + +std::string HashName(std::uint64_t hash) { + char text[17]; + std::snprintf(text, sizeof(text), "%016llx", + static_cast(hash)); + return text; +} + +bool ReadPngRgba(const std::string& path, RgbaImage* image) { + if (image == nullptr) { + return false; + } + png_image png{}; + png.version = PNG_IMAGE_VERSION; + if (png_image_begin_read_from_file(&png, path.c_str()) == 0) { + return false; + } + png.format = PNG_FORMAT_RGBA; + RgbaImage result; + result.width = png.width; + result.height = png.height; + result.pixels.resize(PNG_IMAGE_SIZE(png)); + if (png_image_finish_read(&png, nullptr, result.pixels.data(), 0, + nullptr) == 0) { + png_image_free(&png); + return false; + } + *image = std::move(result); + return true; +} + +bool WritePngRgba(const std::string& path, const RgbaImage& image) { + if (image.width == 0 || image.height == 0 || + image.pixels.size() != + static_cast(image.width) * image.height * 4) { + return false; + } + png_image png{}; + png.version = PNG_IMAGE_VERSION; + png.width = image.width; + png.height = image.height; + png.format = PNG_FORMAT_RGBA; + const int written = png_image_write_to_file(&png, path.c_str(), 0, + image.pixels.data(), 0, nullptr); + png_image_free(&png); + return written != 0; +} + +void ResampleRgba(const RgbaImage& source, std::uint32_t width, + std::uint32_t height, std::uint8_t* destination) { + if (source.pixels.size() < + static_cast(source.width) * source.height * 4) { + return; + } + ResampleRgba(source.pixels.data(), source.width, source.height, width, + height, destination); +} + +void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width, + std::uint32_t source_height, std::uint32_t width, + std::uint32_t height, std::uint8_t* destination) { + if (source == nullptr || destination == nullptr || width == 0 || + height == 0 || source_width == 0 || source_height == 0) { + return; + } + for (std::uint32_t y = 0; y < height; ++y) { + const std::uint64_t y0 = static_cast(y) * source_height / height; + std::uint64_t y1 = + (static_cast(y) + 1) * source_height / height; + if (y1 <= y0) { + y1 = y0 + 1; + } + for (std::uint32_t x = 0; x < width; ++x) { + const std::uint64_t x0 = static_cast(x) * source_width / width; + std::uint64_t x1 = + (static_cast(x) + 1) * source_width / width; + if (x1 <= x0) { + x1 = x0 + 1; + } + std::uint64_t sum[4] = {0, 0, 0, 0}; + for (std::uint64_t sy = y0; sy < y1; ++sy) { + for (std::uint64_t sx = x0; sx < x1; ++sx) { + const std::uint8_t* pixel = source + (sy * source_width + sx) * 4; + for (int channel = 0; channel < 4; ++channel) { + sum[channel] += pixel[channel]; + } + } + } + const std::uint64_t count = (y1 - y0) * (x1 - x0); + std::uint8_t* out = destination + (static_cast(y) * width + x) * 4; + for (int channel = 0; channel < 4; ++channel) { + out[channel] = + static_cast((sum[channel] + count / 2) / count); + } + } + } +} + +namespace { + +std::string EnvironmentString(const char* name) { + const char* value = std::getenv(name); + return value != nullptr ? value : ""; +} + +} // namespace + +TextureOverrides::TextureOverrides(std::string override_dir, + std::string dump_dir) + : override_dir_(std::move(override_dir)), dump_dir_(std::move(dump_dir)) {} + +TextureOverrides TextureOverrides::FromEnvironment() { + return TextureOverrides(EnvironmentString("MOCKTAIL_TEXTURE_OVERRIDE_DIR"), + EnvironmentString("MOCKTAIL_TEXTURE_DUMP_DIR")); +} + +std::shared_ptr TextureOverrides::Lookup(std::uint64_t hash) { + if (override_dir_.empty()) { + return nullptr; + } + std::lock_guard lock(mutex_); + const auto cached = cache_.find(hash); + if (cached != cache_.end()) { + return cached->second; + } + std::shared_ptr loaded; + RgbaImage image; + const std::string path = override_dir_ + "/" + HashName(hash) + ".png"; + if (ReadPngRgba(path, &image) && image.width > 0 && image.height > 0) { + loaded = std::make_shared(std::move(image)); + std::fprintf(stderr, " [vulkan] texture override loaded: %s (%ux%u)\n", + path.c_str(), loaded->width, loaded->height); + } + cache_[hash] = loaded; + return loaded; +} + +void TextureOverrides::Dump(std::uint64_t hash, std::uint32_t width, + std::uint32_t height, const std::uint8_t* rgba) { + if (dump_dir_.empty() || rgba == nullptr || width == 0 || height == 0) { + return; + } + { + std::lock_guard lock(mutex_); + if (!dumped_.insert(hash).second) { + return; + } + } + std::error_code error; + std::filesystem::create_directories(dump_dir_, error); + RgbaImage image; + image.width = width; + image.height = height; + image.pixels.assign(rgba, rgba + static_cast(width) * height * 4); + const std::string path = dump_dir_ + "/" + HashName(hash) + "_" + + std::to_string(width) + "x" + + std::to_string(height) + ".png"; + if (!WritePngRgba(path, image)) { + std::fprintf(stderr, " [vulkan] texture dump failed: %s\n", path.c_str()); + } +} + +} // 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..25d4def --- /dev/null +++ b/src/graphics/vulkan_etc2_emulation.cc @@ -0,0 +1,1308 @@ +#include "mocktail/graphics/vulkan_etc2_emulation.h" + +#include "mocktail/graphics/texture_override.h" + +#include +#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; +} + +} // namespace + +bool Etc2SupportAdvertised(const char* value) { + return value == nullptr || std::strcmp(value, "0") != 0; +} + +bool Etc2SupportAdvertised() { + static const bool advertised = + Etc2SupportAdvertised(std::getenv("MOCKTAIL_ADVERTISE_ETC2")); + return advertised; +} + +std::uint32_t SmallTextureUpscale(const char* value) { + if (value == nullptr || value[0] < '0' || value[0] > '9') { + return kDefaultSmallTextureUpscale; + } + const long parsed = std::strtol(value, nullptr, 10); + if (parsed < 1) { + return 1; + } + return parsed > 8 ? 8 : static_cast(parsed); +} + +namespace { + +bool ShouldLog(std::atomic* counter, unsigned limit) { + return counter->fetch_add(1, std::memory_order_relaxed) < limit; +} + +unsigned DecodeWorkerCount() { + static const unsigned count = + std::min(8U, std::max(1U, std::thread::hardware_concurrency() / 2)); + return count; +} + +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_vkCmdCopyImage copy_image = nullptr; + PFN_vkCmdCopyImage2 copy_image2 = nullptr; + PFN_vkCmdBlitImage blit_image = 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; + std::uint32_t scale = 1; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::shared_ptr override; + std::shared_ptr base; +}; + +bool IsColorFormat(EtcFormat format) { + return format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1 || + format == EtcFormat::kEtc2Rgba8; +} + +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 source_row_stride = 0; + VkDeviceSize source_layer_stride = 0; + VkDeviceSize source_span = 0; + VkDeviceSize compressed = 0; + VkDeviceSize decoded = 0; + VkDeviceSize target_offset = 0; + EtcFormat format = EtcFormat::kEtc2Rgb8; + VkImage image = VK_NULL_HANDLE; + std::uint32_t mip_level = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t layers = 0; + std::uint32_t scale = 1; + std::uint32_t source_width = 0; + std::uint32_t source_height = 0; + std::uint32_t target_width = 0; + std::uint32_t target_height = 0; + bool full_mip = false; + VkDeviceSize target_bytes = 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; +} + +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); + } +} + +// Clip block padding before scaling to the host mip. +bool ScaledAxis(std::uint32_t base, std::uint32_t scale, std::uint32_t mip, + std::int32_t offset, std::uint32_t extent, + std::uint32_t* texels, std::uint32_t* host_start, + std::uint32_t* host_end) { + const std::uint32_t app_mip = std::max(1, base >> mip); + const std::uint32_t host_mip = + std::max(1, (base * scale) >> mip); + if (offset < 0 || static_cast(offset) >= app_mip) { + return false; + } + const std::uint32_t app_end = + std::min(app_mip, static_cast(offset) + extent); + const std::uint32_t start = static_cast(offset) * scale; + const std::uint32_t end = + app_end == app_mip ? host_mip : std::min(host_mip, app_end * scale); + if (end <= start) { + return false; + } + *texels = app_end - static_cast(offset); + *host_start = start; + *host_end = end; + return true; +} + +bool HostRegion(const ImageRecord* record, std::uint32_t mip, + VkOffset3D offset, VkExtent3D extent, VkOffset3D* start, + VkOffset3D* end) { + if (record == nullptr || record->scale <= 1) { + *start = offset; + *end = {offset.x + static_cast(extent.width), + offset.y + static_cast(extent.height), + offset.z + static_cast(extent.depth)}; + return true; + } + std::uint32_t texels = 0; + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + if (!ScaledAxis(record->width, record->scale, mip, offset.x, extent.width, + &texels, &x0, &x1) || + !ScaledAxis(record->height, record->scale, mip, offset.y, + extent.height, &texels, &y0, &y1)) { + return false; + } + *start = {static_cast(x0), static_cast(y0), 0}; + *end = {static_cast(x1), static_cast(y1), 1}; + return true; +} + +template +bool PlanRegions(const ImageRecord& record, VkDevice device, VkBuffer source, + VkImage destination, std::uint32_t count, + const Region* regions, std::vector* rewritten, + std::vector* uploads, VkDeviceSize* total) { + *total = 0; + if (regions == nullptr || count == 0) { + return false; + } + const std::uint32_t scale = record.scale; + for (std::uint32_t index = 0; index < count; ++index) { + const Region& region = regions[index]; + PendingUpload upload; + if (!Etc2UploadByteCounts(record.format.etc_format, region.imageExtent, + region.imageSubresource.layerCount, + &upload.compressed, &upload.decoded)) { + return false; + } + if (scale > 1 && region.imageSubresource.layerCount != 1) { + return false; + } + upload.device = device; + upload.source = source; + upload.source_offset = region.bufferOffset; + upload.target_offset = *total; + upload.format = record.format.etc_format; + upload.image = destination; + upload.mip_level = region.imageSubresource.mipLevel; + upload.width = region.imageExtent.width; + upload.height = region.imageExtent.height; + upload.layers = region.imageSubresource.layerCount; + const VkDeviceSize row_texels = + region.bufferRowLength != 0 ? region.bufferRowLength : upload.width; + const VkDeviceSize layer_rows = + region.bufferImageHeight != 0 ? region.bufferImageHeight : upload.height; + if (row_texels < upload.width || layer_rows < upload.height) { + return false; + } + const VkDeviceSize block_bytes = EtcBlockBytes(upload.format); + const VkDeviceSize block_rows = + (static_cast(upload.height) + 3) / 4; + const VkDeviceSize packed_row = + ((static_cast(upload.width) + 3) / 4) * block_bytes; + upload.source_row_stride = ((row_texels + 3) / 4) * block_bytes; + upload.source_layer_stride = + upload.source_row_stride * ((layer_rows + 3) / 4); + upload.source_span = (upload.layers - 1) * upload.source_layer_stride + + (block_rows - 1) * upload.source_row_stride + packed_row; + upload.scale = scale; + upload.source_width = upload.width; + upload.source_height = upload.height; + upload.target_width = upload.width; + upload.target_height = upload.height; + upload.target_bytes = upload.decoded; + Region copy = region; + copy.bufferOffset = *total; + copy.bufferRowLength = 0; + copy.bufferImageHeight = 0; + if (scale > 1) { + const std::uint32_t mip = region.imageSubresource.mipLevel; + const auto axis = [&](std::uint32_t base, std::int32_t offset, + std::uint32_t extent, std::uint32_t* source, + std::uint32_t* target, std::int32_t* host_offset, + std::uint32_t* host_extent) { + std::uint32_t start = 0; + std::uint32_t end = 0; + if (!ScaledAxis(base, scale, mip, offset, extent, source, &start, + &end)) { + return false; + } + *target = end - start; + *host_offset = static_cast(start); + *host_extent = end - start; + return true; + }; + if (!axis(record.width, region.imageOffset.x, region.imageExtent.width, + &upload.source_width, &upload.target_width, + ©.imageOffset.x, ©.imageExtent.width) || + !axis(record.height, region.imageOffset.y, + region.imageExtent.height, &upload.source_height, + &upload.target_height, ©.imageOffset.y, + ©.imageExtent.height)) { + return false; + } + upload.target_bytes = static_cast(upload.target_width) * + upload.target_height * + EtcDecodedTexelBytes(record.format.etc_format); + upload.full_mip = + region.imageOffset.x == 0 && region.imageOffset.y == 0 && + upload.source_width == std::max(1, record.width >> mip) && + upload.source_height == std::max(1, record.height >> mip); + } + rewritten->push_back(copy); + uploads->push_back(upload); + *total += upload.target_bytes; + } + 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}; + std::mutex scratch_mutex; + std::vector scratch_source; + std::vector scratch_decoded; + TextureOverrides overrides = TextureOverrides::FromEnvironment(); + const std::uint32_t upscale = + SmallTextureUpscale(std::getenv("MOCKTAIL_SMALL_TEXTURE_UPSCALE")); + + std::shared_ptr FindOverride(const PendingUpload& upload, + const std::uint8_t* compressed, + const std::uint8_t* decoded) { + if (!overrides.enabled() || upload.layers != 1 || + !IsColorFormat(upload.format)) { + return nullptr; + } + std::shared_ptr replacement; + if (upload.mip_level == 0) { + const std::uint64_t hash = HashBytes(compressed, upload.compressed); + overrides.Dump(hash, upload.width, upload.height, decoded); + replacement = overrides.Lookup(hash); + std::lock_guard lock(mutex); + const auto record = images.find(upload.image); + if (record != images.end()) { + record->second.override = replacement; + } + } else { + std::lock_guard lock(mutex); + const auto record = images.find(upload.image); + if (record != images.end()) { + replacement = record->second.override; + } + } + return replacement; + } + + void EmitUpload(const PendingUpload& upload, const std::uint8_t* compressed, + const std::uint8_t* decoded) { + const std::shared_ptr replacement = + FindOverride(upload, compressed, decoded); + if (replacement != nullptr) { + ResampleRgba(*replacement, upload.target_width, upload.target_height, + upload.target); + } else if (upload.scale > 1) { + std::shared_ptr base; + if (upload.full_mip) { + std::lock_guard lock(mutex); + const auto record = images.find(upload.image); + if (record != images.end()) { + if (upload.mip_level == 0) { + auto level0 = std::make_shared(); + level0->width = upload.source_width; + level0->height = upload.source_height; + level0->pixels.assign(decoded, decoded + upload.decoded); + if (upload.source_width != upload.width || + upload.source_height != upload.height) { + level0->pixels.clear(); + for (std::uint32_t y = 0; y < upload.source_height; ++y) { + const std::uint8_t* row = + decoded + static_cast(y) * upload.width * 4; + level0->pixels.insert(level0->pixels.end(), row, + row + upload.source_width * 4); + } + } + record->second.base = level0; + } + base = record->second.base; + } + } + if (base != nullptr) { + ResampleRgba(*base, upload.target_width, upload.target_height, + upload.target); + return; + } + const std::uint8_t* source = decoded; + std::vector cropped; + if (upload.source_width != upload.width || + upload.source_height != upload.height) { + const std::size_t row = static_cast(upload.source_width) * 4; + cropped.resize(row * upload.source_height); + for (std::uint32_t y = 0; y < upload.source_height; ++y) { + std::memcpy(cropped.data() + y * row, + decoded + static_cast(y) * upload.width * 4, + row); + } + source = cropped.data(); + } + ResampleRgba(source, upload.source_width, upload.source_height, + upload.target_width, upload.target_height, upload.target); + } else { + std::memcpy(upload.target, decoded, upload.decoded); + } + } + + 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 BlitScaledCopy(const HostDevice& dev, VkCommandBuffer command_buffer, + VkImage source, VkImageLayout source_layout, + VkImage destination, VkImageLayout destination_layout, + std::uint32_t count, const VkImageCopy* regions); + + bool LookupImage(const HostDevice& dev, VkImage image, + ImageRecord* record) { + if (!dev.emulated) { + return false; + } + std::lock_guard lock(mutex); + const auto found = images.find(image); + if (found == images.end()) { + return false; + } + *record = found->second; + 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->copy_image = DeviceProc(get, device, "vkCmdCopyImage"); + dev->copy_image2 = DeviceProc( + get, device, "vkCmdCopyImage2", "vkCmdCopyImage2KHR"); + dev->blit_image = DeviceProc(get, device, "vkCmdBlitImage"); + 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); + std::uint32_t scale = 1; + if (state_->upscale > 1 && IsColorFormat(format.etc_format) && + create_info->imageType == VK_IMAGE_TYPE_2D && + create_info->arrayLayers == 1 && create_info->extent.depth == 1 && + create_info->extent.width <= kSmallTextureMaxExtent && + create_info->extent.height <= kSmallTextureMaxExtent) { + scale = state_->upscale; + host_info.extent.width *= scale; + host_info.extent.height *= scale; + } + 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, + scale, + create_info->extent.width, + create_info->extent.height, + nullptr, + nullptr}; + } + 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; + } + ImageRecord record; + if (!state_->LookupImage(*dev, destination, &record)) { + 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(record, device, source, destination, 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; + } + ImageRecord record; + if (!state_->LookupImage(*dev, info->dstImage, &record)) { + 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(record, device, info->srcBuffer, info->dstImage, + 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); +} + +bool VulkanEtc2Emulation::State::BlitScaledCopy( + const HostDevice& dev, VkCommandBuffer command_buffer, VkImage source, + VkImageLayout source_layout, VkImage destination, + VkImageLayout destination_layout, std::uint32_t count, + const VkImageCopy* regions) { + ImageRecord source_record; + ImageRecord destination_record; + const bool source_scaled = + LookupImage(dev, source, &source_record) && source_record.scale > 1; + const bool destination_scaled = + LookupImage(dev, destination, &destination_record) && + destination_record.scale > 1; + if ((!source_scaled && !destination_scaled) || dev.blit_image == nullptr || + regions == nullptr) { + return false; + } + std::vector blits; + blits.reserve(count); + for (std::uint32_t index = 0; index < count; ++index) { + const VkImageCopy& region = regions[index]; + VkImageBlit blit{}; + blit.srcSubresource = region.srcSubresource; + blit.dstSubresource = region.dstSubresource; + if (!HostRegion(source_scaled ? &source_record : nullptr, + region.srcSubresource.mipLevel, region.srcOffset, + region.extent, &blit.srcOffsets[0], &blit.srcOffsets[1]) || + !HostRegion(destination_scaled ? &destination_record : nullptr, + region.dstSubresource.mipLevel, region.dstOffset, + region.extent, &blit.dstOffsets[0], &blit.dstOffsets[1])) { + LogFailure("skipped an image copy with an unsupported region"); + return true; + } + blits.push_back(blit); + } + const bool same_scale = source_scaled && destination_scaled && + source_record.scale == destination_record.scale; + dev.blit_image(command_buffer, source, source_layout, destination, + destination_layout, static_cast(blits.size()), + blits.data(), same_scale ? VK_FILTER_NEAREST : VK_FILTER_LINEAR); + return true; +} + +void VulkanEtc2Emulation::CmdCopyImage( + VkDevice device, VkCommandBuffer command_buffer, VkImage source, + VkImageLayout source_layout, VkImage destination, + VkImageLayout destination_layout, std::uint32_t region_count, + const VkImageCopy* regions) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->copy_image == nullptr) { + return; + } + if (dev->emulated && state_->upscale > 1 && + state_->BlitScaledCopy(*dev, command_buffer, source, source_layout, + destination, destination_layout, region_count, + regions)) { + return; + } + dev->copy_image(command_buffer, source, source_layout, destination, + destination_layout, region_count, regions); +} + +void VulkanEtc2Emulation::CmdCopyImage2(VkDevice device, + VkCommandBuffer command_buffer, + const VkCopyImageInfo2* info) { + const HostDevice* dev = state_->Find(device); + if (dev == nullptr || dev->copy_image2 == nullptr || info == nullptr) { + return; + } + if (dev->emulated && state_->upscale > 1 && info->pRegions != nullptr) { + std::vector regions; + regions.reserve(info->regionCount); + for (std::uint32_t index = 0; index < info->regionCount; ++index) { + const VkImageCopy2& region = info->pRegions[index]; + regions.push_back({region.srcSubresource, region.srcOffset, + region.dstSubresource, region.dstOffset, + region.extent}); + } + if (state_->BlitScaledCopy(*dev, command_buffer, info->srcImage, + info->srcImageLayout, info->dstImage, + info->dstImageLayout, info->regionCount, + regions.data())) { + return; + } + } + dev->copy_image2(command_buffer, 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.source_span <= 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); + } + } + } + } + // Decode in cached RAM; mapped Vulkan memory makes scattered reads slow. + // Map each source allocation once: Vulkan forbids concurrent mappings. + struct TemporaryMapping { + const HostDevice* dev = nullptr; + std::uint8_t* data = nullptr; + }; + std::unordered_map temporary; + struct Copy { + const std::uint8_t* source = nullptr; + VkDeviceSize compressed = 0; + VkDeviceSize decoded = 0; + }; + std::vector copies; + std::vector copied_uploads; + VkDeviceSize compressed_total = 0; + VkDeviceSize decoded_total = 0; + for (const Work& item : work) { + const PendingUpload& upload = item.upload; + const std::uint8_t* source = item.source; + if (source == nullptr) { + auto found = temporary.find(item.memory); + if (found == temporary.end()) { + const HostDevice* dev = state_->Find(upload.device); + 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; + } + found = temporary + .emplace(item.memory, + TemporaryMapping{ + dev, static_cast(data)}) + .first; + } + source = found->second.data + item.memory_offset; + } + copies.push_back({source, upload.compressed, upload.decoded}); + copied_uploads.push_back(&upload); + compressed_total += upload.compressed; + decoded_total += upload.decoded; + } + + std::lock_guard scratch_lock(state_->scratch_mutex); + std::vector& scratch_source = state_->scratch_source; + std::vector& scratch_decoded = state_->scratch_decoded; + if (scratch_source.size() < compressed_total) { + scratch_source.resize(compressed_total); + } + if (scratch_decoded.size() < decoded_total) { + scratch_decoded.resize(decoded_total); + } + std::vector jobs; + VkDeviceSize compressed_offset = 0; + VkDeviceSize decoded_offset = 0; + for (std::size_t index = 0; index < copies.size(); ++index) { + const Copy& copy = copies[index]; + const PendingUpload& upload = *copied_uploads[index]; + std::uint8_t* source = scratch_source.data() + compressed_offset; + std::uint8_t* decoded = scratch_decoded.data() + decoded_offset; + const VkDeviceSize compressed_layer = upload.compressed / upload.layers; + const VkDeviceSize decoded_layer = upload.decoded / upload.layers; + if (upload.source_span == copy.compressed) { + std::memcpy(source, copy.source, copy.compressed); + } else { + const VkDeviceSize block_rows = + (static_cast(upload.height) + 3) / 4; + const VkDeviceSize packed_row = compressed_layer / block_rows; + for (std::uint32_t layer = 0; layer < upload.layers; ++layer) { + for (VkDeviceSize row = 0; row < block_rows; ++row) { + std::memcpy(source + layer * compressed_layer + row * packed_row, + copy.source + layer * upload.source_layer_stride + + row * upload.source_row_stride, + packed_row); + } + } + } + for (std::uint32_t layer = 0; layer < upload.layers; ++layer) { + EtcDecodeJob job; + job.format = upload.format; + job.source = source + layer * compressed_layer; + job.source_bytes = compressed_layer; + job.width = upload.width; + job.height = upload.height; + job.destination = decoded + layer * decoded_layer; + job.destination_bytes = decoded_layer; + jobs.push_back(job); + } + compressed_offset += copy.compressed; + decoded_offset += copy.decoded; + if (ShouldLog(&g_decode_logs, 4)) { + std::fprintf(stderr, + " [vulkan] ETC2 upload decoded %ux%u layers=%u\n", + upload.width, upload.height, upload.layers); + } + } + for (const auto& [memory, mapping] : temporary) { + if (mapping.dev->unmap_memory != nullptr) { + mapping.dev->unmap_memory(mapping.dev->device, memory); + } + } + DecodeEtcJobs(jobs.data(), jobs.size(), DecodeWorkerCount()); + for (const EtcDecodeJob& job : jobs) { + if (!job.ok) { + LogFailure("could not decode an upload layer"); + } + } + compressed_offset = 0; + decoded_offset = 0; + for (const PendingUpload* upload : copied_uploads) { + state_->EmitUpload(*upload, scratch_source.data() + compressed_offset, + scratch_decoded.data() + decoded_offset); + compressed_offset += upload->compressed; + decoded_offset += upload->decoded; + } +} + +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/src/jnivm/jnivm.cc b/src/jnivm/jnivm.cc index 6f9df95..d9fd7e2 100644 --- a/src/jnivm/jnivm.cc +++ b/src/jnivm/jnivm.cc @@ -23,6 +23,8 @@ #include #include +#include "runtime/display_size.h" + namespace jnivm { void Class::RegisterMethod(const std::string& method_name, @@ -562,13 +564,25 @@ bool ResolvedDarkTheme() { } jint IntResultForName(const char* name) { - if (std::strcmp(name, "getScreenWidth") == 0 || - std::strcmp(name, "getWidth") == 0) { - return 1280; + if (std::strcmp(name, "getScreenWidth") == 0) { + return mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kDisplaySizeEnvironment)) + .width; } - if (std::strcmp(name, "getScreenHeight") == 0 || - std::strcmp(name, "getHeight") == 0) { - return 720; + if (std::strcmp(name, "getScreenHeight") == 0) { + return mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kDisplaySizeEnvironment)) + .height; + } + if (std::strcmp(name, "getWidth") == 0) { + return mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kWindowSizeEnvironment)) + .width; + } + if (std::strcmp(name, "getHeight") == 0) { + return mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kWindowSizeEnvironment)) + .height; } if (std::strcmp(name, "getDensityDpi") == 0) { return 160; @@ -2275,8 +2289,11 @@ jobject MakeDeviceStaticParamsObject() { pseudo_object->object_fields["soc_model"] = MakeString(identity.soc_model.c_str()); pseudo_object->boolean_fields["cpu64Bit"] = JNI_TRUE; - pseudo_object->int_fields["screenWidth"] = 1280; - pseudo_object->int_fields["screenHeight"] = 720; + const mocktail::runtime::DisplaySize host_display = + mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kDisplaySizeEnvironment)); + pseudo_object->int_fields["screenWidth"] = host_display.width; + pseudo_object->int_fields["screenHeight"] = host_display.height; pseudo_object->int_fields["screenDensityDpi"] = 160; pseudo_object->int_fields["apiVersion"] = 33; pseudo_object->int_fields["sdkVersion"] = 33; @@ -2446,14 +2463,17 @@ void EnsureAndroidObjectGraph() { SetObjectFieldRaw(decor_view, "holder", surface_holder); SetObjectFieldRaw(decor_view, "surface", surface); SetObjectFieldRaw(decor_view, "display", display); - SetIntFieldRaw(decor_view, "width", 1280); - SetIntFieldRaw(decor_view, "height", 720); + const mocktail::runtime::DisplaySize host_window = + mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kWindowSizeEnvironment)); + SetIntFieldRaw(decor_view, "width", host_window.width); + SetIntFieldRaw(decor_view, "height", host_window.height); SetObjectFieldRaw(surface_view, "holder", surface_holder); SetObjectFieldRaw(surface_view, "surface", surface); SetObjectFieldRaw(surface_view, "rootView", root_view); - SetIntFieldRaw(surface_view, "width", 1280); - SetIntFieldRaw(surface_view, "height", 720); + SetIntFieldRaw(surface_view, "width", host_window.width); + SetIntFieldRaw(surface_view, "height", host_window.height); SetObjectFieldRaw(surface_holder, "surface", surface); SetObjectFieldRaw(surface_holder, "surfaceFrame", @@ -2461,11 +2481,14 @@ void EnsureAndroidObjectGraph() { SetBooleanFieldRaw(surface, "valid", JNI_TRUE); SetBooleanFieldRaw(surface, "isValid", JNI_TRUE); - SetIntFieldRaw(display, "width", 1280); - SetIntFieldRaw(display, "height", 720); + const mocktail::runtime::DisplaySize host_display = + mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kDisplaySizeEnvironment)); + SetIntFieldRaw(display, "width", host_display.width); + SetIntFieldRaw(display, "height", host_display.height); SetIntFieldRaw(display, "rotation", 0); - SetIntFieldRaw(display_metrics, "widthPixels", 1280); - SetIntFieldRaw(display_metrics, "heightPixels", 720); + SetIntFieldRaw(display_metrics, "widthPixels", host_display.width); + SetIntFieldRaw(display_metrics, "heightPixels", host_display.height); SetIntFieldRaw(display_metrics, "densityDpi", 160); SetFloatFieldRaw(display_metrics, "density", 1.0f); SetFloatFieldRaw(display_metrics, "scaledDensity", 1.0f); @@ -2476,9 +2499,10 @@ void EnsureAndroidObjectGraph() { : PlatformIdentity{}; SetIntFieldRaw(configuration, "orientation", 2); SetIntFieldRaw(configuration, "densityDpi", 160); - SetIntFieldRaw(configuration, "screenWidthDp", 1280); - SetIntFieldRaw(configuration, "screenHeightDp", 720); - SetIntFieldRaw(configuration, "smallestScreenWidthDp", 720); + SetIntFieldRaw(configuration, "screenWidthDp", host_window.width); + SetIntFieldRaw(configuration, "screenHeightDp", host_window.height); + SetIntFieldRaw(configuration, "smallestScreenWidthDp", + std::min(host_window.width, host_window.height)); SetIntFieldRaw(configuration, "touchscreen", identity.touch_enabled ? 3 : 1); SetIntFieldRaw(configuration, "keyboard", identity.keyboard_enabled ? 2 : 1); SetIntFieldRaw(configuration, "keyboardHidden", @@ -4952,10 +4976,14 @@ jobject CreateAndroidConfiguration(JNIEnv* env) { SetIntFieldRaw(configuration, "navigation", 1); SetIntFieldRaw(configuration, "navigationHidden", 1); SetIntFieldRaw(configuration, "orientation", 2); - SetIntFieldRaw(configuration, "screenHeightDp", 720); + const mocktail::runtime::DisplaySize config_window = + mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kWindowSizeEnvironment)); + SetIntFieldRaw(configuration, "screenHeightDp", config_window.height); SetIntFieldRaw(configuration, "screenLayout", 0); - SetIntFieldRaw(configuration, "screenWidthDp", 1280); - SetIntFieldRaw(configuration, "smallestScreenWidthDp", 720); + SetIntFieldRaw(configuration, "screenWidthDp", config_window.width); + SetIntFieldRaw(configuration, "smallestScreenWidthDp", + std::min(config_window.width, config_window.height)); SetIntFieldRaw(configuration, "touchscreen", identity.touch_enabled ? 3 : 1); SetIntFieldRaw(configuration, "uiMode", 0); return configuration; diff --git a/src/legacy/legacy_runtime.cc b/src/legacy/legacy_runtime.cc index 90d50b1..7002d02 100644 --- a/src/legacy/legacy_runtime.cc +++ b/src/legacy/legacy_runtime.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,8 @@ #include "libc_shim/libc_shim.h" #include "linker/linker.h" #include "mocktail/graphics/bionic_egl_bridge.h" +#include "runtime/device_memory_profile.h" +#include "runtime/display_size.h" #include "runtime/environment.h" #include "runtime/discord_rpc.h" #include "runtime/jnivm_platform_web_callbacks.h" @@ -70,6 +73,7 @@ #include "runtime/roblox_text_input_jni_bridge.h" #include "runtime/runtime_config.h" #include "runtime/runtime_paths.h" +#include "runtime/texture_memory_policy.h" #include "services/client_settings_service.h" #include "services/http_client.h" #include "window/window.h" @@ -2393,6 +2397,21 @@ void InstallNativeGlJavaImplementation(JNIEnv* env, jclass native_gl_java_class) jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless); +mocktail::runtime::DisplaySize HostDisplaySize() { + return mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kDisplaySizeEnvironment)); +} + +mocktail::runtime::DisplaySize HostWindowPixelSize() { + const mocktail::window::WindowViewportSnapshot viewport = + mocktail::window::GetWindowViewportSnapshot(); + if (viewport.valid()) { + return {viewport.pixel_width, viewport.pixel_height}; + } + return mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kWindowSizeEnvironment)); +} + jobject BuildDeviceParams(JNIEnv* env) { jobject params = NewObject(env, "com/roblox/engine/jni/model/DeviceParams"); if (!params) { @@ -2414,27 +2433,40 @@ jobject BuildDeviceParams(JNIEnv* env) { SetStringField(env, params, "appVersion", app_version.c_str()); SetStringField(env, params, "country", "US"); SetStringField(env, params, "manufacturer", manufacturer.c_str()); - SetStringField(env, params, "displayResolution", "1280x720"); + const mocktail::runtime::DisplaySize display_size = + mocktail::runtime::ParseDisplaySize( + std::getenv(mocktail::runtime::kDisplaySizeEnvironment)); + const std::string display_resolution = std::to_string(display_size.width) + + "x" + + std::to_string(display_size.height); + SetStringField(env, params, "displayResolution", display_resolution.c_str()); SetStringField(env, params, "networkType", "wifi"); SetStringField(env, params, "deviceSku", device_sku.c_str()); SetStringField(env, params, "socModel", soc_model.c_str()); SetStringField(env, params, "appBuildVariant", "googleProdRelease"); SetStringField(env, params, "testDeviceName", device_name.c_str()); - const bool is_low_ram = !IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE"); - SetIntField(env, params, "deviceTotalMemoryMB", is_low_ram ? 2048 : 4096); - SetIntField(env, params, "displayPhysicalWidthPixels", 1280); - SetIntField(env, params, "displayPhysicalHeightPixels", 720); - SetIntField(env, params, "memoryClass", is_low_ram ? 256 : 512); - SetIntField(env, params, "largeMemoryClass", is_low_ram ? 512 : 1024); + std::uint64_t host_memory_bytes = mocktail::runtime::DetectHostMemoryBytes(); + if (IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE")) { + host_memory_bytes = + std::max(host_memory_bytes, 4096ULL * 1024ULL * 1024ULL); + } + const mocktail::runtime::DeviceMemoryProfile memory = + mocktail::runtime::BuildDeviceMemoryProfile(host_memory_bytes); + SetIntField(env, params, "deviceTotalMemoryMB", memory.total_memory_mb); + SetIntField(env, params, "displayPhysicalWidthPixels", display_size.width); + SetIntField(env, params, "displayPhysicalHeightPixels", + display_size.height); + SetIntField(env, params, "memoryClass", memory.memory_class_mb); + SetIntField(env, params, "largeMemoryClass", memory.large_memory_class_mb); SetLongField(env, params, "lowMemoryKillerBackgroundAppThreshold", - is_low_ram ? 256 : 0); + memory.low_memory_killer_background_threshold); SetLongField(env, params, "lowMemoryKillerForegroundAppThreshold", - is_low_ram ? 512 : 0); + memory.low_memory_killer_foreground_threshold); SetBooleanField(env, params, "cpu64Bit", JNI_TRUE); SetBooleanField(env, params, "isChrome", JNI_FALSE); SetBooleanField(env, params, "isLowRamDevice", - is_low_ram ? JNI_TRUE : JNI_FALSE); + memory.low_ram_device ? JNI_TRUE : JNI_FALSE); return params; } @@ -2534,8 +2566,9 @@ jobject BuildAppBridgeInitParams(JNIEnv* env, jstring client_settings, SetStringField(env, params, "locale", "en_us"); SetRobloxServiceUrlFields(env, params); - SetIntField(env, params, "screenWidth", 1280); - SetIntField(env, params, "screenHeight", 720); + const mocktail::runtime::DisplaySize init_display = HostDisplaySize(); + SetIntField(env, params, "screenWidth", init_display.width); + SetIntField(env, params, "screenHeight", init_display.height); SetIntField(env, params, "densityDpi", 160); SetIntField(env, params, "sdkVersion", 33); jboolean headless = is_headless ? JNI_TRUE : JNI_FALSE; @@ -2569,13 +2602,19 @@ jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless) { SetStringField(env, params, "deviceName", platform_device_name.c_str()); SetStringField(env, params, "locale", "en_us"); SetStringField(env, params, "assetFolderPath", DefaultAssetPath().c_str()); - SetIntField(env, params, "width", 1280); - SetIntField(env, params, "height", 720); - SetIntField(env, params, "screenWidth", 1280); - SetIntField(env, params, "screenHeight", 720); + const mocktail::runtime::DisplaySize platform_window = HostWindowPixelSize(); + const mocktail::runtime::DisplaySize platform_display = HostDisplaySize(); + SetIntField(env, params, "width", platform_window.width); + SetIntField(env, params, "height", platform_window.height); + SetIntField(env, params, "screenWidth", platform_display.width); + SetIntField(env, params, "screenHeight", platform_display.height); SetIntField(env, params, "densityDpi", 160); - SetIntField(env, params, "viewportWidthMm", 203); - SetIntField(env, params, "viewportHeightMm", 114); + SetIntField(env, params, "viewportWidthMm", + mocktail::runtime::PixelsToMillimetersAt160Dpi( + platform_window.width)); + SetIntField(env, params, "viewportHeightMm", + mocktail::runtime::PixelsToMillimetersAt160Dpi( + platform_window.height)); const mocktail::window::WindowViewportSnapshot viewport = mocktail::window::GetWindowViewportSnapshot(); SetFloatField(env, params, "dpiScale", viewport.dpi_scale); @@ -2684,10 +2723,12 @@ jobject BuildStartAppParams(JNIEnv* env, jstring app_params, SetStringField(env, params, "deviceName", start_device_name.c_str()); SetStringField(env, params, "locale", "en_us"); SetRobloxServiceUrlFields(env, params); - SetIntField(env, params, "width", 1280); - SetIntField(env, params, "height", 720); - SetIntField(env, params, "screenWidth", 1280); - SetIntField(env, params, "screenHeight", 720); + const mocktail::runtime::DisplaySize start_window = HostWindowPixelSize(); + const mocktail::runtime::DisplaySize start_display = HostDisplaySize(); + SetIntField(env, params, "width", start_window.width); + SetIntField(env, params, "height", start_window.height); + SetIntField(env, params, "screenWidth", start_display.width); + SetIntField(env, params, "screenHeight", start_display.height); SetIntField(env, params, "densityDpi", 160); SetIntField(env, params, "sdkVersion", 33); SetIntField(env, params, "placeId", 0); @@ -2815,8 +2856,9 @@ jobject BuildMockSurface(JNIEnv* env) { if (!surface) { return nullptr; } - SetIntField(env, surface, "width", 1280); - SetIntField(env, surface, "height", 720); + const mocktail::runtime::DisplaySize surface_size = HostWindowPixelSize(); + SetIntField(env, surface, "width", surface_size.width); + SetIntField(env, surface, "height", surface_size.height); SetBooleanField(env, surface, "valid", JNI_TRUE); SetBooleanField(env, surface, "isValid", JNI_TRUE); return surface; @@ -3913,7 +3955,10 @@ std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n" on_surface_created(env, game_activity, handle, surface); } if (run_lifecycle_callbacks && on_surface_changed) { - on_surface_changed(env, game_activity, handle, surface, 4, 1280, 720); + const mocktail::runtime::DisplaySize changed_size = + HostWindowPixelSize(); + on_surface_changed(env, game_activity, handle, surface, 4, + changed_size.width, changed_size.height); } if (run_lifecycle_callbacks && on_surface_redraw_needed) { on_surface_redraw_needed(env, game_activity, handle, surface); @@ -4084,8 +4129,9 @@ std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n" ShouldRunStartupStep("MOCKTAIL_UPDATE_APP_UI_SIZES", false)) { env = ensure_env(); std::cout << " [engine] updateAppUISizes\n" << std::flush; - context->native_update_app_ui_sizes(env, native_gl_class, 1280, 720, 0, 0, - 0); + const mocktail::runtime::DisplaySize ui_size = HostWindowPixelSize(); + context->native_update_app_ui_sizes(env, native_gl_class, ui_size.width, + ui_size.height, 0, 0, 0); std::cout << " [engine] updateAppUISizes returned\n" << std::flush; } @@ -4726,6 +4772,37 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, << std::flush; } } + if (window_initialised) { + auto* sdl_window = + static_cast(mocktail::window::GetBackendWindow()); + const SDL_DisplayID display = + sdl_window != nullptr ? SDL_GetDisplayForWindow(sdl_window) : 0; + const SDL_DisplayMode* mode = + display != 0 ? SDL_GetCurrentDisplayMode(display) : nullptr; + if (mode != nullptr && mode->w > 0 && mode->h > 0) { + // SDL mode dimensions need pixel_density to become physical pixels. + const float density = mode->pixel_density > 0.0f ? mode->pixel_density + : 1.0f; + const std::string display_size = + std::to_string(std::lround(mode->w * density)) + "x" + + std::to_string(std::lround(mode->h * density)); + setenv(mocktail::runtime::kDisplaySizeEnvironment, display_size.c_str(), + 1); + std::cout << " [window] host display " << display_size << " pixels\n" + << std::flush; + } + } + if (window_initialised) { + const mocktail::window::WindowViewportSnapshot viewport = + mocktail::window::GetWindowViewportSnapshot(); + if (viewport.valid()) { + const std::string window_size = std::to_string(viewport.pixel_width) + + "x" + + std::to_string(viewport.pixel_height); + setenv(mocktail::runtime::kWindowSizeEnvironment, window_size.c_str(), + 1); + } + } const SDL_SystemTheme system_theme = window_initialised ? SDL_GetSystemTheme() : SDL_SYSTEM_THEME_UNKNOWN; const bool system_dark_theme = system_theme == SDL_SYSTEM_THEME_DARK; diff --git a/src/runtime/device_memory_profile.cc b/src/runtime/device_memory_profile.cc new file mode 100644 index 0000000..d1e06cb --- /dev/null +++ b/src/runtime/device_memory_profile.cc @@ -0,0 +1,35 @@ +#include "runtime/device_memory_profile.h" + +#include +#include + +namespace mocktail { +namespace runtime { +namespace { + +constexpr std::uint64_t kMebibyte = 1024U * 1024U; +constexpr std::uint64_t kMinimumNormalRamBytes = 4096U * kMebibyte; + +} // namespace + +DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes) { + DeviceMemoryProfile profile; + if (host_memory_bytes < kMinimumNormalRamBytes) { + profile.total_memory_mb = 2048; + profile.memory_class_mb = 256; + profile.large_memory_class_mb = 512; + profile.low_memory_killer_background_threshold = 256; + profile.low_memory_killer_foreground_threshold = 512; + profile.low_ram_device = true; + return profile; + } + profile.total_memory_mb = static_cast(std::min( + host_memory_bytes / kMebibyte, + static_cast(std::numeric_limits::max()))); + profile.memory_class_mb = 512; + profile.large_memory_class_mb = 1024; + return profile; +} + +} // namespace runtime +} // namespace mocktail diff --git a/src/runtime/graphics_launch_policy.cc b/src/runtime/graphics_launch_policy.cc index df1ae0a..14dd8b7 100644 --- a/src/runtime/graphics_launch_policy.cc +++ b/src/runtime/graphics_launch_policy.cc @@ -18,11 +18,8 @@ namespace mocktail { namespace runtime { namespace { -// TextureManager2 selects its mobile MipPackStream path on Android Vulkan and -// can keep world textures at low-resolution mips. TM1 preserves full texture -// fidelity until that path is compatible with the host Vulkan adapter. constexpr char kVulkanClientSettingsOverrides[] = - R"({"FStringGraphicsTextureManager2DenyPattern2":".*","FStringGraphicsVulkanShaderMTDenyPattern":"4318:.*"})"; + R"({"FStringGraphicsVulkanShaderMTDenyPattern":"4318:.*"})"; constexpr const char* kIcdDirectories[] = { "/usr/share/vulkan/icd.d", diff --git a/src/runtime/runtime_paths.cc b/src/runtime/runtime_paths.cc index 3ad83fd..8a8ffe8 100644 --- a/src/runtime/runtime_paths.cc +++ b/src/runtime/runtime_paths.cc @@ -1,3 +1,4 @@ +// Includes changes by vii from CoderDayton/nightcap. #include "runtime/runtime_paths.h" #define JSON_NOEXCEPTION 1 @@ -603,7 +604,9 @@ bool ExportRuntimePathEnvironment(const RuntimePaths& paths, SetEnvironmentDefault("MOCKTAIL_ANDROID_CACHE_HOST_ROOT", paths.android_cache_root(), error) && SetEnvironmentDefault("MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH", - paths.vulkan_shader_cache_file(), error); + paths.vulkan_shader_cache_file(), error) && + SetEnvironmentDefault("MOCKTAIL_TEXTURE_OVERRIDE_DIR", + paths.config_root() / "textures", error); } bool PrepareManagedPayloadWorkingDirectory(const RuntimePaths& paths, diff --git a/stubs/CMakeLists.txt b/stubs/CMakeLists.txt index 3ac9bca..84c19be 100644 --- a/stubs/CMakeLists.txt +++ b/stubs/CMakeLists.txt @@ -1,3 +1,4 @@ +# Includes changes by vii from CoderDayton/nightcap. # Copyright 2026 Mocktail Project Authors # Licensed under the Apache License, Version 2.0. @@ -106,13 +107,19 @@ 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 + ${CMAKE_SOURCE_DIR}/src/graphics/texture_override.cc ) configure_stub(stub_vulkan "libvulkan.so") target_include_directories(stub_vulkan PRIVATE ${CMAKE_SOURCE_DIR}/include) pkg_check_modules(LIBPLACEBO REQUIRED IMPORTED_TARGET libplacebo) +pkg_check_modules(LIBPNG REQUIRED IMPORTED_TARGET libpng) target_link_libraries(stub_vulkan PRIVATE Mocktail::SdlVulkanWsi PkgConfig::LIBPLACEBO + PkgConfig::LIBPNG + Threads::Threads dl ) diff --git a/tests/supported_launch_policy_test.cc b/tests/supported_launch_policy_test.cc index 68665e8..78986e9 100644 --- a/tests/supported_launch_policy_test.cc +++ b/tests/supported_launch_policy_test.cc @@ -182,7 +182,7 @@ int RunGraphicsPolicyProbe(const char* backend) { std::string(anv_memory_limit) == expected_anv_limit && std::string(submit_thread) == "1" && std::string(overrides).find( - "\"FStringGraphicsTextureManager2DenyPattern2\":\".*\"") != + "FStringGraphicsTextureManager2DenyPattern2") == std::string::npos && std::string(overrides).find( "FStringGraphicsVulkanShaderMTDenyPattern") !=