From 43e9df10af73c5c195ea27e6b95a295aa7d0cd64 Mon Sep 17 00:00:00 2001 From: Joshua Tam <297250+joshuatam@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:57:06 +0800 Subject: [PATCH 1/4] chore: update gitignore and android build script --- .gitignore | 6 ++++-- scripts/build/android.sh | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f058853a..4508ea4f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,7 @@ /.cache /.ccls -build-android/ -/build-android-* +build-android* + +# IntelliJ idea +.idea \ No newline at end of file diff --git a/scripts/build/android.sh b/scripts/build/android.sh index 0b536e2c..f97b9973 100755 --- a/scripts/build/android.sh +++ b/scripts/build/android.sh @@ -47,7 +47,8 @@ cmake -S "${REPO_ROOT}" -B "${BUILD_DIR}" \ -DLSFGVK_ANDROID_WINE=ON \ -DVOLK_STATIC_DEFINES=VK_USE_PLATFORM_ANDROID_KHR \ -DCMAKE_CXX_FLAGS="-DVK_USE_PLATFORM_ANDROID_KHR" \ - -DCMAKE_C_FLAGS="-DVK_USE_PLATFORM_ANDROID_KHR" + -DCMAKE_C_FLAGS="-DVK_USE_PLATFORM_ANDROID_KHR" \ + -DCMAKE_SHARED_LINKER_FLAGS="-Wl,-z,max-page-size=16384" cmake --build "${BUILD_DIR}" --parallel From b3ddbef8aa4065fd7d6f2276799c8d4b28f05260 Mon Sep 17 00:00:00 2001 From: Joshua Tam <297250+joshuatam@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:39:19 +0800 Subject: [PATCH 2/4] refactor(android): streamline external memory and improve extension handling Remove the `AHardwareBuffer` backed image creation path due to its limited compatibility and lack of robust support across various Android ICDs. The layer now exclusively uses `VK_KHR_external_memory_fd` for external memory sharing on Android. Implement filtering for requested device extensions (`VK_KHR_external_memory`, `VK_KHR_external_memory_fd`, `VK_KHR_external_semaphore`, `VK_KHR_external_semaphore_fd`). This prevents device creation failures by only enabling extensions actually supported by the physical device. Update the Android build script to use a consistent `liblsfg-vk-layer.so` filename and recommend installation to `jniLibs/arm64-v8a/` for better Android app integration. --- include/layer.hpp | 13 ++-- include/mini/image.hpp | 31 --------- scripts/build/android.sh | 8 +-- src/context.cpp | 1 - src/hooks.cpp | 38 +++++++--- src/layer.cpp | 40 +++++------ src/mini/image.cpp | 147 --------------------------------------- 7 files changed, 58 insertions(+), 220 deletions(-) diff --git a/include/layer.hpp b/include/layer.hpp index 9b6176b1..29901947 100644 --- a/include/layer.hpp +++ b/include/layer.hpp @@ -177,14 +177,6 @@ namespace Layer { const VkSemaphoreGetFdInfoKHR* pGetFdInfo, int* pFd); -#ifdef __ANDROID__ - /// Call to the original vkGetAndroidHardwareBufferPropertiesANDROID function. - VkResult ovkGetAndroidHardwareBufferPropertiesANDROID( - VkDevice device, - const AHardwareBuffer* hardwareBuffer, - VkAndroidHardwareBufferPropertiesANDROID* pProperties); -#endif - /// Call to the original vkGetDeviceQueue function. void ovkGetDeviceQueue( VkDevice device, @@ -199,6 +191,11 @@ namespace Layer { bool ovkGetPhysicalDeviceFeatures2( VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures2* pFeatures); + /// Enumerate device extensions (returns false when unavailable or failed). + bool ovkEnumerateDeviceExtensionProperties( + VkPhysicalDevice physicalDevice, + uint32_t* pPropertyCount, + VkExtensionProperties* pProperties); /// The instance the layer initialized on. VkInstance ovkInstance(); diff --git a/include/mini/image.hpp b/include/mini/image.hpp index d944d47e..5b77e3d3 100644 --- a/include/mini/image.hpp +++ b/include/mini/image.hpp @@ -2,10 +2,6 @@ #include -#ifdef __ANDROID__ -#include -#endif - #include namespace Mini { @@ -35,28 +31,6 @@ namespace Mini { Image(VkDevice device, VkPhysicalDevice physicalDevice, VkExtent2D extent, VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspectFlags, int* fd); -#ifdef __ANDROID__ - /// - /// Create the image backed by an AHardwareBuffer (Android path). - /// Allocates an AHB, wraps it in a VkImage, and exposes the AHB pointer - /// for sharing with framegen via createContextFromAHB. - /// - /// @param device Vulkan device - /// @param physicalDevice Vulkan physical device - /// @param extent Extent of the image in pixels. - /// @param format Vulkan format of the image - /// @param usage Usage flags for the image - /// @param aspectFlags Aspect flags for the image view - /// - /// @throws LSFG::vulkan_error if object creation fails. - /// - Image(VkDevice device, VkPhysicalDevice physicalDevice, VkExtent2D extent, VkFormat format, - VkImageUsageFlags usage, VkImageAspectFlags aspectFlags); - - /// Get the AHardwareBuffer handle (Android only). - [[nodiscard]] AHardwareBuffer* getAhb() const { return this->ahb; } -#endif - /// /// Create a plain device-local image with no external memory /// (single-device framegen path: the image is shared with the framegen @@ -89,11 +63,6 @@ namespace Mini { std::shared_ptr image; std::shared_ptr memory; -#ifdef __ANDROID__ - AHardwareBuffer* ahb{}; // owned, released via custom deleter - std::shared_ptr ahbRef; // shared ownership for copy/move -#endif - VkExtent2D extent{}; VkFormat format{}; VkImageAspectFlags aspectFlags{}; diff --git a/scripts/build/android.sh b/scripts/build/android.sh index f97b9973..5b24aed7 100755 --- a/scripts/build/android.sh +++ b/scripts/build/android.sh @@ -56,11 +56,11 @@ HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')" STRIP_BIN="${ANDROID_NDK}/toolchains/llvm/prebuilt/${HOST_TAG:-${HOST_OS}-x86_64}/bin/llvm-strip" if [[ -x "${STRIP_BIN}" ]]; then "${STRIP_BIN}" --strip-unneeded \ - -o "${DIST_DIR}/liblsfg-vk-${ABI}.so" \ + -o "${DIST_DIR}/liblsfg-vk-layer.so" \ "${BUILD_DIR}/liblsfg-vk.so" else echo "warning: llvm-strip not found at ${STRIP_BIN}; copying unstripped library" >&2 - cp "${BUILD_DIR}/liblsfg-vk.so" "${DIST_DIR}/liblsfg-vk-${ABI}.so" + cp "${BUILD_DIR}/liblsfg-vk.so" "${DIST_DIR}/liblsfg-vk-layer.so" fi cp "${REPO_ROOT}/VkLayer_LS_frame_generation.json" "${BUILD_DIR}/" @@ -68,8 +68,8 @@ cp "${REPO_ROOT}/VkLayer_LS_frame_generation.json" "${DIST_DIR}/" echo "" echo "Build complete. Artifacts:" -echo " ${DIST_DIR}/liblsfg-vk-${ABI}.so" +echo " ${DIST_DIR}/liblsfg-vk-layer.so" echo " ${DIST_DIR}/VkLayer_LS_frame_generation.json" echo "" echo "For GameNative Android app updates, copy the arm64-v8a shared library to:" -echo " app/src/main/assets/lsfg_vk/android_arm64_v8a/liblsfg-vk-layer.so" +echo " app/src/main/jniLibs/arm64-v8a/liblsfg-vk-layer.so" diff --git a/src/context.cpp b/src/context.cpp index 63613a43..7e0db300 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -9,7 +9,6 @@ #include "layer.hpp" #ifdef __ANDROID__ -#include #include #endif diff --git a/src/hooks.cpp b/src/hooks.cpp index 1d4910e8..8c7f5f35 100644 --- a/src/hooks.cpp +++ b/src/hooks.cpp @@ -61,17 +61,39 @@ namespace { const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice) { - // add extensions + // add extensions, skipping any the driver doesn't expose — appending + // an unsupported name would fail the game's entire device creation + std::vector wanted{ + "VK_KHR_external_memory", + "VK_KHR_external_memory_fd", + "VK_KHR_external_semaphore", + "VK_KHR_external_semaphore_fd" + }; + uint32_t supportedCount{}; + std::vector supportedExts; + if (Layer::ovkEnumerateDeviceExtensionProperties(physicalDevice, + &supportedCount, nullptr) && supportedCount > 0) { + supportedExts.resize(supportedCount); + if (!Layer::ovkEnumerateDeviceExtensionProperties(physicalDevice, + &supportedCount, supportedExts.data())) + supportedExts.clear(); + } + if (!supportedExts.empty()) { + std::erase_if(wanted, [&supportedExts](const char* name) { + const bool found = std::ranges::any_of(supportedExts, + [name](const VkExtensionProperties& p) { + return std::string(static_cast(p.extensionName)) == name; + }); + if (!found) + std::cerr << "lsfg-vk: skipping unsupported device extension " + << name << "\n"; + return !found; + }); + } auto extensions = Utils::addExtensions( pCreateInfo->ppEnabledExtensionNames, pCreateInfo->enabledExtensionCount, - { - "VK_KHR_external_memory", - "VK_KHR_external_memory_fd", - "VK_KHR_external_semaphore", - "VK_KHR_external_semaphore_fd" - } - ); + wanted); VkDeviceCreateInfo createInfo = *pCreateInfo; createInfo.enabledExtensionCount = static_cast(extensions.size()); createInfo.ppEnabledExtensionNames = extensions.data(); diff --git a/src/layer.cpp b/src/layer.cpp index 709feb6c..fa9c173c 100644 --- a/src/layer.cpp +++ b/src/layer.cpp @@ -4,7 +4,6 @@ #include "hooks.hpp" #ifdef __ANDROID__ -#include #include #endif @@ -34,6 +33,7 @@ namespace { PFN_vkGetPhysicalDeviceProperties next_vkGetPhysicalDeviceProperties{}; PFN_vkGetPhysicalDeviceFeatures next_vkGetPhysicalDeviceFeatures{}; PFN_vkGetPhysicalDeviceFeatures2 next_vkGetPhysicalDeviceFeatures2{}; + PFN_vkEnumerateDeviceExtensionProperties next_vkEnumerateDeviceExtensionProperties{}; PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR next_vkGetPhysicalDeviceSurfaceCapabilitiesKHR{}; VkInstance g_instanceHandle{}; @@ -58,9 +58,6 @@ namespace { PFN_vkDestroySemaphore next_vkDestroySemaphore{}; PFN_vkGetMemoryFdKHR next_vkGetMemoryFdKHR{}; PFN_vkGetSemaphoreFdKHR next_vkGetSemaphoreFdKHR{}; -#ifdef __ANDROID__ - PFN_vkGetAndroidHardwareBufferPropertiesANDROID next_vkGetAndroidHardwareBufferPropertiesANDROID{}; -#endif PFN_vkGetDeviceQueue next_vkGetDeviceQueue{}; PFN_vkQueueSubmit next_vkQueueSubmit{}; PFN_vkCreateFence next_vkCreateFence{}; @@ -167,6 +164,10 @@ namespace { "vkGetPhysicalDeviceFeatures", &next_vkGetPhysicalDeviceFeatures); initInstanceFunc(*pInstance, "vkGetPhysicalDeviceFeatures2", &next_vkGetPhysicalDeviceFeatures2); + // optional: used to skip unsupported device extensions instead of + // failing the game's device creation + initInstanceFunc(*pInstance, + "vkEnumerateDeviceExtensionProperties", &next_vkEnumerateDeviceExtensionProperties); g_instanceHandle = *pInstance; std::cerr << "lsfg-vk: Vulkan instance layer initialized successfully.\n"; @@ -244,18 +245,14 @@ namespace { success &= initDeviceFunc(*pDevice, "vkDestroyImage", &next_vkDestroyImage); success &= initDeviceFunc(*pDevice, "vkGetImageMemoryRequirements", &next_vkGetImageMemoryRequirements); success &= initDeviceFunc(*pDevice, "vkBindImageMemory", &next_vkBindImageMemory); - success &= initDeviceFunc(*pDevice, "vkGetMemoryFdKHR", &next_vkGetMemoryFdKHR); + // optional: only used by the desktop OPAQUE_FD sharing path + initDeviceFunc(*pDevice, "vkGetMemoryFdKHR", &next_vkGetMemoryFdKHR); success &= initDeviceFunc(*pDevice, "vkAllocateMemory", &next_vkAllocateMemory); success &= initDeviceFunc(*pDevice, "vkFreeMemory", &next_vkFreeMemory); success &= initDeviceFunc(*pDevice, "vkCreateSemaphore", &next_vkCreateSemaphore); success &= initDeviceFunc(*pDevice, "vkDestroySemaphore", &next_vkDestroySemaphore); - success &= initDeviceFunc(*pDevice, "vkGetSemaphoreFdKHR", &next_vkGetSemaphoreFdKHR); -#ifdef __ANDROID__ - // AHB function is optional — not all ICDs (e.g. Vortek wrapper) support it. - // If unavailable, the AHB image path will fail at point-of-use, but - // the layer still initializes so it can fall back gracefully. - initDeviceFunc(*pDevice, "vkGetAndroidHardwareBufferPropertiesANDROID", &next_vkGetAndroidHardwareBufferPropertiesANDROID); -#endif + // optional: only used by the desktop OPAQUE_FD sharing path + initDeviceFunc(*pDevice, "vkGetSemaphoreFdKHR", &next_vkGetSemaphoreFdKHR); success &= initDeviceFunc(*pDevice, "vkGetDeviceQueue", &next_vkGetDeviceQueue); success &= initDeviceFunc(*pDevice, "vkQueueSubmit", &next_vkQueueSubmit); success &= initDeviceFunc(*pDevice, "vkCreateFence", &next_vkCreateFence); @@ -402,6 +399,14 @@ namespace Layer { next_vkGetPhysicalDeviceFeatures2(physicalDevice, pFeatures); return true; } + bool ovkEnumerateDeviceExtensionProperties( + VkPhysicalDevice physicalDevice, + uint32_t* pPropertyCount, + VkExtensionProperties* pProperties) { + if (!next_vkEnumerateDeviceExtensionProperties) return false; + return next_vkEnumerateDeviceExtensionProperties( + physicalDevice, nullptr, pPropertyCount, pProperties) == VK_SUCCESS; + } VkInstance ovkInstance() { return g_instanceHandle; } @@ -537,24 +542,17 @@ namespace Layer { VkDevice device, const VkMemoryGetFdInfoKHR* pGetFdInfo, int* pFd) { + if (!next_vkGetMemoryFdKHR) return VK_ERROR_EXTENSION_NOT_PRESENT; return next_vkGetMemoryFdKHR(device, pGetFdInfo, pFd); } VkResult ovkGetSemaphoreFdKHR( VkDevice device, const VkSemaphoreGetFdInfoKHR* pGetFdInfo, int* pFd) { + if (!next_vkGetSemaphoreFdKHR) return VK_ERROR_EXTENSION_NOT_PRESENT; return next_vkGetSemaphoreFdKHR(device, pGetFdInfo, pFd); } -#ifdef __ANDROID__ - VkResult ovkGetAndroidHardwareBufferPropertiesANDROID( - VkDevice device, - const AHardwareBuffer* hardwareBuffer, - VkAndroidHardwareBufferPropertiesANDROID* pProperties) { - return next_vkGetAndroidHardwareBufferPropertiesANDROID(device, hardwareBuffer, pProperties); - } -#endif - void ovkGetDeviceQueue( VkDevice device, uint32_t queueFamilyIndex, diff --git a/src/mini/image.cpp b/src/mini/image.cpp index 9e4aa406..a1e119cc 100644 --- a/src/mini/image.cpp +++ b/src/mini/image.cpp @@ -4,11 +4,6 @@ #include -#ifdef __ANDROID__ -#include -#include -#endif - #include #include #include @@ -191,145 +186,3 @@ Image Image::createDeviceLocal(VkDevice device, VkPhysicalDevice physicalDevice, ); return img; } - -#ifdef __ANDROID__ -Image::Image(VkDevice device, VkPhysicalDevice physicalDevice, - VkExtent2D extent, VkFormat format, - VkImageUsageFlags usage, VkImageAspectFlags aspectFlags) - : extent(extent), format(format), aspectFlags(aspectFlags) { - // Convert VkFormat to AHardwareBuffer format - uint32_t ahbFormat = 0; - switch (format) { - case VK_FORMAT_R8G8B8A8_UNORM: ahbFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; break; - case VK_FORMAT_R16G16B16A16_SFLOAT: ahbFormat = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT; break; - default: - throw LSFG::vulkan_error(VK_ERROR_FORMAT_NOT_SUPPORTED, - "Unsupported VkFormat for AHB allocation"); - } - - // Allocate AHardwareBuffer - AHardwareBuffer_Desc ahbDesc{ - .width = extent.width, - .height = extent.height, - .layers = 1, - .format = ahbFormat, - .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE - | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT, - .stride = 0, - .rfu0 = 0, - .rfu1 = 0, - }; - AHardwareBuffer* ahbHandle{}; - if (AHardwareBuffer_allocate(&ahbDesc, &ahbHandle) != 0 || ahbHandle == nullptr) - throw LSFG::vulkan_error(VK_ERROR_OUT_OF_DEVICE_MEMORY, - "Failed to allocate AHardwareBuffer for image"); - this->ahb = ahbHandle; - - // Create VkImage wrapping the AHB external memory. - // NOTE: We skip vkGetAndroidHardwareBufferPropertiesANDROID because - // the Vortek ICD wrapper doesn't pass it through. Instead we use - // vkGetImageMemoryRequirements after image creation to get the - // allocation size and memory type bits. - VkExternalMemoryImageCreateInfo extImageInfo{ - .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, - .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID, - }; - VkImageCreateInfo desc{ - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .pNext = &extImageInfo, - .imageType = VK_IMAGE_TYPE_2D, - .format = format, - .extent = { extent.width, extent.height, 1 }, - .mipLevels = 1, - .arrayLayers = 1, - .samples = VK_SAMPLE_COUNT_1_BIT, - .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_STORAGE_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT - | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, - }; - VkImage imageHandle{}; - auto res = Layer::ovkCreateImage(device, &desc, nullptr, &imageHandle); - if (res != VK_SUCCESS || imageHandle == VK_NULL_HANDLE) - throw LSFG::vulkan_error(res, "Failed to create Vulkan image from AHB"); - - // Get memory requirements from the image — this gives us allocationSize - // and memoryTypeBits without needing vkGetAndroidHardwareBufferPropertiesANDROID. - VkMemoryRequirements memReqs; - Layer::ovkGetImageMemoryRequirements(device, imageHandle, &memReqs); - - // Find a compatible device-local memory type from the requirements - VkPhysicalDeviceMemoryProperties memProps; - Layer::ovkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProps); - - uint32_t typeIndex = UINT32_MAX; - for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) { - if ((memReqs.memoryTypeBits & (1u << i)) && - (memProps.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { - typeIndex = i; - break; - } - } - if (typeIndex == UINT32_MAX) { - // Fallback: pick first compatible type (may not be device-local) - for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) { - if (memReqs.memoryTypeBits & (1u << i)) { - typeIndex = i; - break; - } - } - } - if (typeIndex == UINT32_MAX) - throw LSFG::vulkan_error(VK_ERROR_UNKNOWN, "No memory type matches AHB image requirements"); - - // Import AHB into Vulkan memory with dedicated allocation for the image - VkMemoryDedicatedAllocateInfo dedicatedInfo{ - .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, - .image = imageHandle, - }; - VkImportAndroidHardwareBufferInfoANDROID importInfo{ - .sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID, - .pNext = &dedicatedInfo, - .buffer = ahbHandle, - }; - VkMemoryAllocateInfo allocInfo{ - .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - .pNext = &importInfo, - .allocationSize = memReqs.size, - .memoryTypeIndex = typeIndex, - }; - VkDeviceMemory memoryHandle{}; - res = Layer::ovkAllocateMemory(device, &allocInfo, nullptr, &memoryHandle); - if (res != VK_SUCCESS || memoryHandle == VK_NULL_HANDLE) - throw LSFG::vulkan_error(res, "Failed to import AHB into Vulkan memory"); - - res = Layer::ovkBindImageMemory(device, imageHandle, memoryHandle, 0); - if (res != VK_SUCCESS) - throw LSFG::vulkan_error(res, "Failed to bind AHB memory to Vulkan image"); - - // Store objects with proper cleanup - this->image = std::shared_ptr( - new VkImage(imageHandle), - [dev = device](VkImage* img) { - Layer::ovkDestroyImage(dev, *img, nullptr); - } - ); - this->memory = std::shared_ptr( - new VkDeviceMemory(memoryHandle), - [dev = device](VkDeviceMemory* mem) { - Layer::ovkFreeMemory(dev, *mem, nullptr); - } - ); - // Shared ownership of the AHB — released when last reference dies - this->ahbRef = std::shared_ptr( - ahbHandle, - [](AHardwareBuffer* b) { - if (b) AHardwareBuffer_release(b); - } - ); -} -#endif - From b2e989cbf951d8932f452b10c79bc7a38ef4fb87 Mon Sep 17 00:00:00 2001 From: Joshua Tam <297250+joshuatam@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:21:33 +0800 Subject: [PATCH 3/4] fix: Improve layer stability and prevent freezes in external mode Enhances robustness by: - Replacing unbounded `vkDeviceWaitIdle` with bounded queue drains in external mode to prevent driver deadlocks. - Introducing a present thread watchdog to detect and report stalls in blocking Vulkan operations. - Adding an epoch system to invalidate framegen contexts after config reloads, forcing swapchain recreation. - Serializing access to shared layer state with a mutex to avoid race conditions during concurrent operations. - Ensuring `VkPresentIdKHR` is only passed with real frame presents to prevent game freezes on skipped generated frames. - Parking timed-out acquire semaphores to handle late signals from wrapper ICDs. --- framegen/v3.1_src/lsfg.cpp | 31 +++++++++- framegen/v3.1p_src/lsfg.cpp | 31 +++++++++- include/context.hpp | 10 ++++ src/context.cpp | 116 ++++++++++++++++++++++++++++++++++-- src/hooks.cpp | 41 +++++++++++++ 5 files changed, 219 insertions(+), 10 deletions(-) diff --git a/framegen/v3.1_src/lsfg.cpp b/framegen/v3.1_src/lsfg.cpp index afad4550..fb4efe5a 100644 --- a/framegen/v3.1_src/lsfg.cpp +++ b/framegen/v3.1_src/lsfg.cpp @@ -27,6 +27,21 @@ namespace { std::optional device; std::unordered_map contexts; bool externalMode = false; + + // Bounded drain of the framegen queue: a fence-only submit completes when + // all previously submitted work does. Used instead of vkQueueWaitIdle / + // vkDeviceWaitIdle in external mode, where an unbounded wait on the game's + // queue can freeze the present thread forever if the queue is wedged. + void drainQueueBounded(VkDevice dev, VkQueue queue) { + const VkFenceCreateInfo fenceInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; + VkFence fence{}; + if (vkCreateFence(dev, &fenceInfo, nullptr, &fence) != VK_SUCCESS) + return; + if (vkQueueSubmit(queue, 0, nullptr, fence) == VK_SUCCESS) + vkWaitForFences(dev, 1, &fence, VK_TRUE, 2'000'000'000ULL); + vkDestroyFence(dev, fence, nullptr); + } } void LSFG_3_1::initialize(uint64_t deviceUUID, @@ -110,7 +125,14 @@ void LSFG_3_1::deleteContext(int32_t id) { if (it == contexts.end()) throw LSFG::vulkan_error(VK_ERROR_DEVICE_LOST, "No such context"); - vkDeviceWaitIdle(device->device.handle()); + // external mode: the device belongs to the game — idling it from a layer + // hook races the game's own queue access and can deadlock the driver. + // All framegen work is submitted on this one queue, so drain just it, + // with a bounded wait so a wedged queue can't freeze the present thread. + if (externalMode) + drainQueueBounded(device->device.handle(), device->device.getComputeQueue()); + else + vkDeviceWaitIdle(device->device.handle()); contexts.erase(it); } @@ -118,10 +140,15 @@ void LSFG_3_1::finalize() { if (!instance.has_value() || !device.has_value()) return; - vkDeviceWaitIdle(device->device.handle()); + // see deleteContext: never idle the game's whole device in external mode + if (externalMode) + drainQueueBounded(device->device.handle(), device->device.getComputeQueue()); + else + vkDeviceWaitIdle(device->device.handle()); contexts.clear(); device.reset(); instance.reset(); + externalMode = false; } #ifdef __ANDROID__ diff --git a/framegen/v3.1p_src/lsfg.cpp b/framegen/v3.1p_src/lsfg.cpp index 88ef043b..b32d5d2d 100644 --- a/framegen/v3.1p_src/lsfg.cpp +++ b/framegen/v3.1p_src/lsfg.cpp @@ -27,6 +27,21 @@ namespace { std::optional device; std::unordered_map contexts; bool externalMode = false; + + // Bounded drain of the framegen queue: a fence-only submit completes when + // all previously submitted work does. Used instead of vkQueueWaitIdle / + // vkDeviceWaitIdle in external mode, where an unbounded wait on the game's + // queue can freeze the present thread forever if the queue is wedged. + void drainQueueBounded(VkDevice dev, VkQueue queue) { + const VkFenceCreateInfo fenceInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; + VkFence fence{}; + if (vkCreateFence(dev, &fenceInfo, nullptr, &fence) != VK_SUCCESS) + return; + if (vkQueueSubmit(queue, 0, nullptr, fence) == VK_SUCCESS) + vkWaitForFences(dev, 1, &fence, VK_TRUE, 2'000'000'000ULL); + vkDestroyFence(dev, fence, nullptr); + } } void LSFG_3_1P::initialize(uint64_t deviceUUID, @@ -110,7 +125,14 @@ void LSFG_3_1P::deleteContext(int32_t id) { if (it == contexts.end()) throw LSFG::vulkan_error(VK_ERROR_DEVICE_LOST, "No such context"); - vkDeviceWaitIdle(device->device.handle()); + // external mode: the device belongs to the game — idling it from a layer + // hook races the game's own queue access and can deadlock the driver. + // All framegen work is submitted on this one queue, so drain just it, + // with a bounded wait so a wedged queue can't freeze the present thread. + if (externalMode) + drainQueueBounded(device->device.handle(), device->device.getComputeQueue()); + else + vkDeviceWaitIdle(device->device.handle()); contexts.erase(it); } @@ -118,10 +140,15 @@ void LSFG_3_1P::finalize() { if (!instance.has_value() || !device.has_value()) return; - vkDeviceWaitIdle(device->device.handle()); + // see deleteContext: never idle the game's whole device in external mode + if (externalMode) + drainQueueBounded(device->device.handle(), device->device.getComputeQueue()); + else + vkDeviceWaitIdle(device->device.handle()); contexts.clear(); device.reset(); instance.reset(); + externalMode = false; } #ifdef __ANDROID__ diff --git a/include/context.hpp b/include/context.hpp index 4cedc19d..ae382de9 100644 --- a/include/context.hpp +++ b/include/context.hpp @@ -57,6 +57,11 @@ class LsContext { /// failures. The present hook passes frames through untouched when set. [[nodiscard]] bool isDisabled() const { return this->forceDisabled; } + /// Whether a config reload finalized the framegen module after this + /// context was created. Its framegen context id is dead; the present hook + /// must force the game to recreate the swapchain instead of presenting. + [[nodiscard]] bool isStale() const; + /// /// Pace and forward a present without frame generation (multiplier <= 1). /// Applies the same vsync-locked fps limiter as the framegen path so the @@ -76,6 +81,7 @@ class LsContext { VkExtent2D extent; std::shared_ptr lsfgCtxId; // lsfg context id + uint64_t lsfgEpoch{0}; // framegen module epoch this context was built against Mini::Image frame_0, frame_1; // frames shared with lsfg. write to frame_0 when fc % 2 == 0 std::vector out_n; // output images shared with lsfg, indexed by framegen id @@ -89,6 +95,10 @@ class LsContext { uint32_t copyFenceTimeouts{0}; bool forceDisabled{false}; + // acquire semaphores handed to a timed-out vkAcquireNextImageKHR are + // parked here instead of destroyed (wrapper ICDs may signal them late) + std::vector retiredSemaphores; + int64_t pacerAnchorNs{0}; // schedule anchor of the current real frame int64_t pacerNextDueNs{0}; // next vsync-grid slot for the real frame int64_t lastRealPresentNs{0}; // previous real-frame entry, for the EWMA diff --git a/src/context.cpp b/src/context.cpp index 7e0db300..23854544 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,18 @@ #include #include +namespace { + // Bumped whenever a config reload finalizes the shared framegen module. + // LsContexts created before the bump hold dead framegen context ids; the + // present hook forces the game to recreate their swapchains instead of + // presenting through them. + std::atomic g_lsfgModuleEpoch{1}; +} + +bool LsContext::isStale() const { + return this->lsfgEpoch != g_lsfgModuleEpoch.load(std::memory_order_relaxed); +} + #ifdef __ANDROID__ #include #include @@ -47,6 +60,44 @@ namespace { return static_cast(ts.tv_sec) * 1'000'000'000LL + ts.tv_nsec; } + // Present-thread watchdog: the present hook publishes the stage it is + // about to block in; a background thread reports any stage held for more + // than 2 seconds. "idle" means the layer returned to the game — a freeze + // while idle is a stall on the game/DXVK side, not in the layer. + std::atomic g_presentStage{"idle"}; + std::atomic g_presentStageSetNs{0}; + std::atomic g_watchdogStarted{false}; + + void setStage(const char* stage) { + g_presentStage.store(stage, std::memory_order_relaxed); + g_presentStageSetNs.store(nowNs(), std::memory_order_relaxed); + } + + void startWatchdog() { + bool expected = false; + if (!g_watchdogStarted.compare_exchange_strong(expected, true)) + return; + std::thread([] { + const char* lastStage = nullptr; + int64_t lastSetNs = 0; + while (true) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + const char* stage = g_presentStage.load(std::memory_order_relaxed); + const int64_t setNs = g_presentStageSetNs.load(std::memory_order_relaxed); + if (setNs == 0 || nowNs() - setNs < 2'000'000'000LL) + continue; + // report once per distinct stall, then every pass while stuck + if (stage == lastStage && setNs == lastSetNs) + continue; + lastStage = stage; + lastSetNs = setNs; + std::cerr << "lsfg-vk: WATCHDOG: present thread stuck in '" + << stage << "' for " << (nowNs() - setNs) / 1'000'000 + << " ms\n"; + } + }).detach(); + } + void sleepUntilNs(int64_t targetNs) { const int64_t delta = targetNs - nowNs(); if (delta <= 0) return; @@ -240,6 +291,7 @@ LsContext::LsContext(const Hooks::DeviceInfo& info, VkSwapchainKHR swapchain, LSFG_3_1P::finalize(); LSFG_3_1::finalize(); + g_lsfgModuleEpoch.fetch_add(1, std::memory_order_relaxed); // print config std::cerr << "lsfg-vk: Reloaded configuration for " << name.second << ":\n"; @@ -250,8 +302,12 @@ LsContext::LsContext(const Hooks::DeviceInfo& info, VkSwapchainKHR swapchain, std::cerr << " HDR Mode: " << (conf.hdr ? "Enabled" : "Disabled") << '\n'; if (conf.e_present != 2) std::cerr << " ! Present Mode: " << conf.e_present << '\n'; - if (conf.multiplier <= 1) return; + if (conf.multiplier <= 1) { + this->lsfgEpoch = g_lsfgModuleEpoch.load(std::memory_order_relaxed); + return; + } } + this->lsfgEpoch = g_lsfgModuleEpoch.load(std::memory_order_relaxed); // we could take the format from the swapchain, // but honestly this is safer. const VkFormat format = conf.hdr @@ -338,7 +394,15 @@ LsContext::LsContext(const Hooks::DeviceInfo& info, VkSwapchainKHR swapchain, this->lsfgCtxId = std::shared_ptr( new int32_t(ctxId), [lsfgDeleteContext = lsfgDeleteContext](const int32_t* id) { - lsfgDeleteContext(*id); + // never throw out of a destructor/noexcept hook: a config reload + // may have finalized the module, orphaning this id + try { + lsfgDeleteContext(*id); + } catch (const std::exception& e) { + std::cerr << "lsfg-vk: deleteContext(" << *id << ") failed: " + << e.what() << '\n'; + } + delete id; // NOLINT(cppcoreguidelines-owning-memory) } ); @@ -412,7 +476,15 @@ LsContext::LsContext(const Hooks::DeviceInfo& info, VkSwapchainKHR swapchain, this->lsfgCtxId = std::shared_ptr( new int32_t(lsfgCreateContext(fds.at(0), fds.at(1), outFds, extent, format)), [lsfgDeleteContext = lsfgDeleteContext](const int32_t* id) { - lsfgDeleteContext(*id); + // never throw out of a destructor/noexcept hook: a config reload + // may have finalized the module, orphaning this id + try { + lsfgDeleteContext(*id); + } catch (const std::exception& e) { + std::cerr << "lsfg-vk: deleteContext(" << *id << ") failed: " + << e.what() << '\n'; + } + delete id; // NOLINT(cppcoreguidelines-owning-memory) } ); @@ -648,9 +720,15 @@ void LsContext::dumpFrameImages(const Hooks::DeviceInfo& info, uint32_t presentI VkResult LsContext::presentPassthrough(VkQueue queue, const VkPresentInfoKHR* pPresentInfo) { #ifdef __ANDROID__ + startWatchdog(); this->paceBaseFrame(Config::activeConf.fpsLimit); -#endif + setStage("passthrough present"); + const auto res = Layer::ovkQueuePresentKHR(queue, pPresentInfo); + setStage("idle"); + return res; +#else return Layer::ovkQueuePresentKHR(queue, pPresentInfo); +#endif } VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, VkQueue queue, @@ -664,6 +742,7 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk // Base frame-rate limiter (see paceBaseFrame): the on-screen rate becomes // fps_limit * multiplier. + startWatchdog(); this->paceBaseFrame(conf.fpsLimit); // Track the delivered real-frame interval as an EWMA. A cap is a ceiling, @@ -684,7 +763,9 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk // If the previous frame's copy still hasn't signaled, the sync path is // stuck; pass the frame through untouched. Enough consecutive timeouts // disable framegen for this swapchain entirely. + setStage("copy fence wait"); const auto fenceRes = this->preCopyFence.wait(kSyncFenceTimeoutNs); + setStage("idle"); if (fenceRes == VK_SUCCESS) this->copyFenceTimeouts = 0; if (fenceRes != VK_SUCCESS) { @@ -702,7 +783,10 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk .pSwapchains = &this->swapchain, .pImageIndices = &presentIdx, }; - return Layer::ovkQueuePresentKHR(queue, &passInfo); + setStage("fence-timeout passthrough present"); + const auto passRes = Layer::ovkQueuePresentKHR(queue, &passInfo); + setStage("idle"); + return passRes; } this->preCopyFence.reset(); @@ -760,12 +844,15 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk // 2. Tell framegen to generate intermediary frames. Graph-side fence // timeouts feed the same watchdog as the copy fence. std::vector noOutSems; // empty + setStage("framegen submit"); try { if (conf.performance) LSFG_3_1P::presentContext(*this->lsfgCtxId, -1, noOutSems); else LSFG_3_1::presentContext(*this->lsfgCtxId, -1, noOutSems); + setStage("idle"); } catch (const LSFG::vulkan_error& e) { + setStage("idle"); if (e.error() == VK_TIMEOUT && ++this->copyFenceTimeouts >= kMaxFenceTimeouts && !this->forceDisabled) { @@ -827,9 +914,16 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk // saturated GPU; a timed-out generated frame is dropped pass.acquireSemaphores.at(i) = Mini::Semaphore(info.device); uint32_t imageIdx{}; + setStage("gen acquire"); auto res = Layer::ovkAcquireNextImageKHR(info.device, this->swapchain, 200'000'000ull, pass.acquireSemaphores.at(i).handle(), VK_NULL_HANDLE, &imageIdx); + setStage("idle"); if (res == VK_TIMEOUT || res == VK_NOT_READY) { + // park the semaphore instead of destroying it: some wrapper ICDs + // complete a timed-out acquire later and would signal a destroyed + // semaphore. Freed with the context. + this->retiredSemaphores.emplace_back( + std::move(pass.acquireSemaphores.at(i))); this->statsGenSkips++; continue; } @@ -884,10 +978,15 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk pass.prevPostCopySemaphores.at(i).handle() }); // present swapchain image + // Never attach the game's pNext chain here: it can carry a + // VkPresentIdKHR, and a generated frame may be SKIPPED. A present id + // that is never queued wedges the game's vkWaitForPresentKHR forever + // (DXVK throttles with present_wait — the whole game freezes). The id + // rides on the real present below, which always happens. VkSemaphore postCopySem = pass.postCopySemaphores.at(i).handle(); const VkPresentInfoKHR presentInfo{ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, - .pNext = i == 0 ? pNext : nullptr, + .pNext = nullptr, .waitSemaphoreCount = 1, .pWaitSemaphores = &postCopySem, .swapchainCount = 1, @@ -895,7 +994,9 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk .pImageIndices = &imageIdx, }; const int64_t genPresentStartNs = nowNs(); + setStage("gen present"); res = Layer::ovkQueuePresentKHR(queue, &presentInfo); + setStage("idle"); this->statsGenPresentNs += static_cast(nowNs() - genPresentStartNs); if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) throw LSFG::vulkan_error(res, "Failed to present swapchain image"); @@ -921,6 +1022,7 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk .at(static_cast(lastPresentedGen)).handle()); const VkPresentInfoKHR finalPresentInfo{ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .pNext = pNext, // present id and friends belong to the real frame .waitSemaphoreCount = static_cast(finalWaits.size()), .pWaitSemaphores = finalWaits.data(), .swapchainCount = 1, @@ -928,7 +1030,9 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk .pImageIndices = &presentIdx, }; const int64_t realPresentStartNs = nowNs(); + setStage("real present"); auto res = Layer::ovkQueuePresentKHR(queue, &finalPresentInfo); + setStage("idle"); this->statsRealPresentNs += static_cast(nowNs() - realPresentStartNs); if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) throw LSFG::vulkan_error(res, "Failed to present swapchain image"); diff --git a/src/hooks.cpp b/src/hooks.cpp index 8c7f5f35..681f8e48 100644 --- a/src/hooks.cpp +++ b/src/hooks.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,13 @@ namespace { /// Map of devices to related information. std::unordered_map deviceToInfo; + // Serializes every hook that touches the shared maps, the framegen module + // state or the framegen queue. Two windows presenting from two threads + // would otherwise race the singleton (and vkQueue* calls made by the + // layer), which can hard-lock the driver — most visibly when a config + // reload finalizes the framegen module mid-present. + std::mutex hookMutex; + /// /// Add extensions to the device create info. /// (function pointers are not initialized yet) @@ -177,6 +185,8 @@ namespace { VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks*, VkDevice* pDevice) { + const std::lock_guard lock(hookMutex); + deviceToInfo.emplace(*pDevice, DeviceInfo { .device = *pDevice, .physicalDevice = physicalDevice, @@ -187,6 +197,8 @@ namespace { /// Erase the device information when the device is destroyed. void myvkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) noexcept { + const std::lock_guard lock(hookMutex); + deviceToInfo.erase(device); Layer::ovkDestroyDevice(device, pAllocator); } @@ -203,6 +215,8 @@ namespace { const VkSwapchainCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSwapchainKHR* pSwapchain) noexcept { + const std::lock_guard lock(hookMutex); + // find device auto it = deviceToInfo.find(device); if (it == deviceToInfo.end()) { @@ -240,8 +254,11 @@ namespace { // retire potential old swapchain if (pCreateInfo->oldSwapchain) { + std::cerr << "lsfg-vk: retiring old swapchain context\n"; swapchains.erase(pCreateInfo->oldSwapchain); swapchainToDeviceTable.erase(pCreateInfo->oldSwapchain); + swapchainToPresent.erase(pCreateInfo->oldSwapchain); + std::cerr << "lsfg-vk: old swapchain context retired\n"; } // create swapchain @@ -291,6 +308,8 @@ namespace { VkResult myvkQueuePresentKHR( VkQueue queue, const VkPresentInfoKHR* pPresentInfo) noexcept { + const std::lock_guard lock(hookMutex); + // find swapchain device auto it = swapchainToDeviceTable.find(*pPresentInfo->pSwapchains); if (it == swapchainToDeviceTable.end()) { @@ -379,6 +398,8 @@ namespace { } } if (!pacingOnly) { + Utils::logLimitN("swapReconf", 3, + "config changed, forcing swapchain recreation"); Layer::ovkQueuePresentKHR(queue, pPresentInfo); return VK_ERROR_OUT_OF_DATE_KHR; } @@ -390,6 +411,17 @@ namespace { return VK_ERROR_OUT_OF_DATE_KHR; } + // a config reload finalized the framegen module after this context + // was created — its framegen context id is dead. Present the frame + // untouched and force the game to recreate the swapchain. + if (swapchain.isStale()) { + Utils::logLimitN("swapStale", 3, + "stale framegen context, forcing swapchain recreation"); + Layer::ovkQueuePresentKHR(queue, pPresentInfo); + return VK_ERROR_OUT_OF_DATE_KHR; + } + Utils::resetLimitN("swapStale"); + // no framegen at multiplier <= 1, but the fps cap still applies if (conf.multiplier <= 1) return swapchain.presentPassthrough(queue, pPresentInfo); @@ -406,6 +438,8 @@ namespace { queue, semaphores, *pPresentInfo->pImageIndices); Utils::resetLimitN("swapPresent"); + Utils::resetLimitN("swapReconf"); + Utils::resetLimitN("swapStale"); } catch (const std::exception& e) { Utils::logLimitN("swapPresent", 5, "An error occurred while presenting the swapchain:\n" @@ -420,10 +454,17 @@ namespace { VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks* pAllocator) noexcept { + const std::lock_guard lock(hookMutex); + + const bool hadContext = swapchains.find(swapchain) != swapchains.end(); + if (hadContext) + std::cerr << "lsfg-vk: destroying swapchain context\n"; swapchains.erase(swapchain); swapchainToDeviceTable.erase(swapchain); swapchainToPresent.erase(swapchain); Layer::ovkDestroySwapchainKHR(device, swapchain, pAllocator); + if (hadContext) + std::cerr << "lsfg-vk: swapchain context destroyed\n"; } } From c6f3bc8423aa45109b6950127b788c1b8789d44b Mon Sep 17 00:00:00 2001 From: Joshua Tam <297250+joshuatam@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:29:29 +0800 Subject: [PATCH 4/4] revert android.sh for github action --- scripts/build/android.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build/android.sh b/scripts/build/android.sh index 5b24aed7..7cd1e397 100755 --- a/scripts/build/android.sh +++ b/scripts/build/android.sh @@ -56,11 +56,11 @@ HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')" STRIP_BIN="${ANDROID_NDK}/toolchains/llvm/prebuilt/${HOST_TAG:-${HOST_OS}-x86_64}/bin/llvm-strip" if [[ -x "${STRIP_BIN}" ]]; then "${STRIP_BIN}" --strip-unneeded \ - -o "${DIST_DIR}/liblsfg-vk-layer.so" \ + -o "${DIST_DIR}/liblsfg-vk-${ABI}.so" \ "${BUILD_DIR}/liblsfg-vk.so" else echo "warning: llvm-strip not found at ${STRIP_BIN}; copying unstripped library" >&2 - cp "${BUILD_DIR}/liblsfg-vk.so" "${DIST_DIR}/liblsfg-vk-layer.so" + cp "${BUILD_DIR}/liblsfg-vk.so" "${DIST_DIR}/liblsfg-vk-${ABI}.so" fi cp "${REPO_ROOT}/VkLayer_LS_frame_generation.json" "${BUILD_DIR}/" @@ -68,7 +68,7 @@ cp "${REPO_ROOT}/VkLayer_LS_frame_generation.json" "${DIST_DIR}/" echo "" echo "Build complete. Artifacts:" -echo " ${DIST_DIR}/liblsfg-vk-layer.so" +echo " ${DIST_DIR}/liblsfg-vk-${ABI}.so" echo " ${DIST_DIR}/VkLayer_LS_frame_generation.json" echo "" echo "For GameNative Android app updates, copy the arm64-v8a shared library to:"