From 37a0ee3f6c27336e843600253a4ff2aca9280289 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 1 Oct 2026 01:29:04 +0200 Subject: [PATCH 1/6] vulkan: import host memory as buffers with resource_flags::shared_host Enables VK_EXT_external_memory_host when available so add-ons can share frames with another process through mapped memory, the only transport a Wine/Proton process can use with a native one. Co-Authored-By: Claude Opus 5.5 --- deps/CMakeLists.txt | 1 + include/reshade_api_resource.hpp | 6 +++ source/vulkan/vulkan_hooks_device.cpp | 7 +++ source/vulkan/vulkan_impl_device.cpp | 63 +++++++++++++++++++++++++++ source/vulkan/vulkan_impl_device.hpp | 3 ++ 5 files changed, 80 insertions(+) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 45f5ad3ed..2f656b83e 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -81,6 +81,7 @@ set(GLAD_EXTENSIONS ) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND GLAD_EXTENSIONS + VK_EXT_external_memory_host VK_KHR_wayland_surface VK_KHR_xcb_surface VK_KHR_xlib_surface diff --git a/include/reshade_api_resource.hpp b/include/reshade_api_resource.hpp index 9e2b37e1f..094d09329 100644 --- a/include/reshade_api_resource.hpp +++ b/include/reshade_api_resource.hpp @@ -244,6 +244,12 @@ namespace reshade::api /// Resource is backed by sparse memory. /// sparse_binding = 0x40000, + /// + /// Used together with on a buffer to import existing host memory instead of allocating: the "shared_handle" parameter of points to a variable that holds the host pointer. + /// The pointer and the buffer size must satisfy the device's minimum imported host pointer alignment. The memory must stay valid until the resource is destroyed. + /// This flag is only supported in Vulkan, with VK_EXT_external_memory_host. + /// + shared_host = 0x100000, }; RESHADE_DEFINE_ENUM_FLAG_OPERATORS(resource_flags); diff --git a/source/vulkan/vulkan_hooks_device.cpp b/source/vulkan/vulkan_hooks_device.cpp index 0b22e0ee6..528d46a89 100644 --- a/source/vulkan/vulkan_hooks_device.cpp +++ b/source/vulkan/vulkan_hooks_device.cpp @@ -282,6 +282,13 @@ VkResult VKAPI_CALL vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDevi add_extension(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, false); #endif +#if VK_EXT_external_memory_host + // Lets add-ons import host memory shared with another process as a buffer (resource_flags::shared_host) + // The extension depends on external memory, which is core in Vulkan 1.1 + if (instance.api_version >= VK_API_VERSION_1_1) + add_extension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME, false); +#endif + #if VK_KHR_external_memory_win32 add_extension(VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME, false); #endif diff --git a/source/vulkan/vulkan_impl_device.cpp b/source/vulkan/vulkan_impl_device.cpp index 2995061fc..0945c30b9 100644 --- a/source/vulkan/vulkan_impl_device.cpp +++ b/source/vulkan/vulkan_impl_device.cpp @@ -399,6 +399,11 @@ bool reshade::vulkan::device_impl::create_resource(const api::resource_desc &des { *out_resource = { 0 }; +#if VK_EXT_external_memory_host + if ((desc.flags & api::resource_flags::shared_host) != 0) + return shared_handle != nullptr && import_host_buffer(desc, *shared_handle, out_resource); +#endif + assert((desc.usage & initial_state) == initial_state || initial_state == api::resource_usage::general || initial_state == api::resource_usage::cpu_access); VmaAllocation allocation = VMA_NULL; @@ -731,6 +736,64 @@ bool reshade::vulkan::device_impl::create_resource(const api::resource_desc &des return false; } +#if VK_EXT_external_memory_host +bool reshade::vulkan::device_impl::import_host_buffer(const api::resource_desc &desc, void *host_pointer, api::resource *out_resource) +{ + if (!vk.EXT_external_memory_host || desc.type != api::resource_type::buffer || host_pointer == nullptr) + return false; + + VkMemoryHostPointerPropertiesEXT host_properties { VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT }; + if (vk.GetMemoryHostPointerPropertiesEXT(_orig, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host_pointer, &host_properties) != VK_SUCCESS || host_properties.memoryTypeBits == 0) + { + reshade::log::message(reshade::log::level::error, "Host pointer %p cannot be imported (check its alignment).", host_pointer); + return false; + } + + VkBufferCreateInfo create_info { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + convert_resource_desc(desc, create_info); + VkExternalMemoryBufferCreateInfo external_memory_info { VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO }; + external_memory_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; + create_info.pNext = &external_memory_info; + + VkImportMemoryHostPointerInfoEXT import_info { VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT }; + import_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; + import_info.pHostPointer = host_pointer; + + // A dedicated pool carries the import structure into the allocation, like the Win32 import path in create_resource, so that destroy_resource frees it the usual way + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; + alloc_info.memoryTypeBits = host_properties.memoryTypeBits; + VmaPoolCreateInfo pool_info = {}; + pool_info.pMemoryAllocateNext = &import_info; + if (vmaFindMemoryTypeIndexForBufferInfo(_alloc, &create_info, &alloc_info, &pool_info.memoryTypeIndex) != VK_SUCCESS || + vmaCreatePool(_alloc, &pool_info, &alloc_info.pool) != VK_SUCCESS) + return false; + + VkBuffer object = VK_NULL_HANDLE; + VmaAllocation allocation = VMA_NULL; + VmaAllocationInfo allocation_info = {}; + if (vmaCreateBuffer(_alloc, &create_info, &alloc_info, &object, &allocation, &allocation_info) != VK_SUCCESS) + { + reshade::log::message(reshade::log::level::error, "Failed to import host memory at %p as a %llu byte buffer.", host_pointer, static_cast(desc.buffer.size)); + vmaDestroyPool(_alloc, alloc_info.pool); + return false; + } + + object_data data; + data.allocation = allocation; + data.pool = alloc_info.pool; + data.memory = allocation_info.deviceMemory; + data.memory_offset = allocation_info.offset; + data.create_info = create_info; + data.create_info.pNext = nullptr; // Clear out structure chain pointer, since it becomes invalid once leaving the current scope + + register_object(object, std::move(data)); + + *out_resource = { (uint64_t)object }; + return true; +} +#endif + void reshade::vulkan::device_impl::destroy_resource(api::resource resource) { if (resource == 0) diff --git a/source/vulkan/vulkan_impl_device.hpp b/source/vulkan/vulkan_impl_device.hpp index 809f2691a..e005150b0 100644 --- a/source/vulkan/vulkan_impl_device.hpp +++ b/source/vulkan/vulkan_impl_device.hpp @@ -161,6 +161,9 @@ namespace reshade::vulkan const VkPhysicalDeviceFeatures _enabled_features; private: +#if VK_EXT_external_memory_host + bool import_host_buffer(const api::resource_desc &desc, void *host_pointer, api::resource *out_resource); +#endif bool create_descriptor_set_layout(const api::pipeline_layout_param ¶m, VkDescriptorSetLayout *out_set_layout, std::vector &embedded_samplers); VmaAllocator _alloc = nullptr; From f4fc5332a1d2d68e3a132c84b3e30149cf1e373a Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 1 Oct 2026 01:36:42 +0200 Subject: [PATCH 2/6] vulkan: allow copies into swap chain images Adds VK_IMAGE_USAGE_TRANSFER_DST_BIT alongside the usage ReShade already requests, so add-ons can copy results back into the back buffer. Co-Authored-By: Claude Opus 5.5 --- source/vulkan/vulkan_hooks_swapchain.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/vulkan/vulkan_hooks_swapchain.cpp b/source/vulkan/vulkan_hooks_swapchain.cpp index e6843d44e..a71f0290f 100644 --- a/source/vulkan/vulkan_hooks_swapchain.cpp +++ b/source/vulkan/vulkan_hooks_swapchain.cpp @@ -76,7 +76,8 @@ VkResult VKAPI_CALL vkCreateSwapchainKHR(VkDevice device, const VkSwapchainCreat if (device_impl->_primary_graphics_queue != nullptr) { // Add required usage flags to create info - create_info.imageUsage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + // Transfer destination lets add-ons write results back into the back buffer with a copy (e.g. frames processed out of process) + create_info.imageUsage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; #if VK_KHR_swapchain_mutable_format // Add required format variants, so e.g. both linear and sRGB views can be created for the swap chain images From 1a5cf40644000a3f26c702ccf2c501c1dd1f5722 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 1 Oct 2026 01:42:54 +0200 Subject: [PATCH 3/6] vulkan: load vkGetMemoryHostPointerPropertiesEXT through the device The glad loader sends names containing "Properties" to vkGetInstanceProcAddr, which returned a function that crashed vkCreateDevice for this device-level entry point. Exclude it like the "Handle" functions. Co-Authored-By: Claude Opus 5.5 --- source/vulkan/vulkan_hooks_device.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/vulkan/vulkan_hooks_device.cpp b/source/vulkan/vulkan_hooks_device.cpp index 528d46a89..01bfe2111 100644 --- a/source/vulkan/vulkan_hooks_device.cpp +++ b/source/vulkan/vulkan_hooks_device.cpp @@ -542,7 +542,7 @@ VkResult VKAPI_CALL vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDevi 0 == std::strcmp(name_without_prefix, "SubmitDebugUtilsMessageEXT") || 0 == std::strcmp(name_without_prefix, "CreateDebugUtilsMessengerEXT") || 0 == std::strcmp(name_without_prefix, "DestroyDebugUtilsMessengerEXT") || - (std::strstr(name_without_prefix, "Properties") != nullptr && std::strstr(name_without_prefix, "AccelerationStructures") == nullptr && std::strstr(name_without_prefix, "Handle") == nullptr) || + (std::strstr(name_without_prefix, "Properties") != nullptr && std::strstr(name_without_prefix, "AccelerationStructures") == nullptr && std::strstr(name_without_prefix, "Handle") == nullptr && std::strstr(name_without_prefix, "HostPointer") == nullptr) || (std::strstr(name_without_prefix, "Surface") != nullptr && std::strstr(name_without_prefix, "DeviceGroupSurface") == nullptr) || (std::strstr(name_without_prefix, "PhysicalDevice") != nullptr)) return reinterpret_cast(device.dispatch_table.GetInstanceProcAddr(device.instance_handle, name)); From f385f98f9b067d25402797aa1ef678d538ef1c8f Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 1 Oct 2026 14:03:32 +0200 Subject: [PATCH 4/6] linux: look up add-on modules with _dl_find_object find_addon runs for every add-on callback of every event, and dladdr1 takes the loader lock each time. _dl_find_object is lock-free. Needs glibc 2.35. --- source/addon_manager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/addon_manager.cpp b/source/addon_manager.cpp index 2edaadc0e..3dafcdab1 100644 --- a/source/addon_manager.cpp +++ b/source/addon_manager.cpp @@ -47,9 +47,9 @@ namespace { void *get_module_handle(const void *address) { - Dl_info info = {}; - struct link_map *map = nullptr; - return address != nullptr && dladdr1(address, &info, reinterpret_cast(&map), RTLD_DL_LINKMAP) != 0 ? map : nullptr; + // Called for every add-on callback of every event, so this must not take the loader lock like dladdr1 does + dl_find_object result; + return address != nullptr && _dl_find_object(const_cast(address), &result) == 0 ? result.dlfo_link_map : nullptr; } void *const g_module_handle = get_module_handle(reinterpret_cast(&get_module_handle)); From 483810bfcd3be4a7f5f76c81b2cec36eefa68f73 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 1 Oct 2026 19:06:16 +0200 Subject: [PATCH 5/6] depth: let unknown compare ops abstain in reversed depth detection Draws that test with always/equal or have unresolved dynamic depth state set the unknown bit, and the exact match then discarded every vote. UI and sky draws made detection fail in most games. Unknown evidence now abstains; contradicting evidence still blocks the vote. --- examples/09-depth/generic_depth_detection.hpp | 8 +++++--- tests/linux_portability.cpp | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/09-depth/generic_depth_detection.hpp b/examples/09-depth/generic_depth_detection.hpp index e0425008e..b0fd4f388 100644 --- a/examples/09-depth/generic_depth_detection.hpp +++ b/examples/09-depth/generic_depth_detection.hpp @@ -5,7 +5,7 @@ namespace depth_detection { // Bit masks permit lossless merging across command lists. Unknown is an - // explicit bit, so unsupported state cannot accidentally vote for normal Z. + // explicit bit, so unsupported state is never mistaken for normal Z. enum evidence : uint8_t { none = 0, normal = 1, reversed = 2, unknown = 4 }; constexpr uint8_t clear_evidence(float depth) { @@ -37,8 +37,10 @@ namespace depth_detection candidate = none; consistent_frames = 0; } - const uint8_t vote = (clears == normal && comparisons == normal) ? normal : - (clears == reversed && comparisons == reversed) ? reversed : none; + // Unknown evidence (always/equal tests, unresolved dynamic state) says nothing about the + // convention, so it only abstains. Contradicting evidence still blocks the vote. + const uint8_t clear_direction = clears & (normal | reversed); + const uint8_t vote = (clear_direction == normal || clear_direction == reversed) && clear_direction == (comparisons & (normal | reversed)) ? clear_direction : none; if (vote == none || vote != candidate) { candidate = vote; diff --git a/tests/linux_portability.cpp b/tests/linux_portability.cpp index bad0c5b5e..88fecfb3b 100644 --- a/tests/linux_portability.cpp +++ b/tests/linux_portability.cpp @@ -741,11 +741,23 @@ static void test_depth_detection() assert(state.finished); assert(state.observe(1, 120, convention, convention) == none); } - for (uint8_t clears : { uint8_t(none), uint8_t(normal), uint8_t(reversed), uint8_t(normal | reversed), uint8_t(unknown) }) - for (uint8_t comparisons : { uint8_t(none), uint8_t(normal), uint8_t(reversed), uint8_t(normal | reversed), uint8_t(unknown) }) + // Draws with state that gives no ordering evidence (always/equal tests, unresolved dynamic state) + // are common next to the real geometry and must not veto it, which is what Suzerain's UI and sky do + for (uint8_t convention : { uint8_t(normal), uint8_t(reversed) }) + for (uint8_t noise : { uint8_t(none), uint8_t(unknown) }) + { + observation state; + for (uint64_t frame = 0; frame < 119; ++frame) + assert(state.observe(1, frame, convention | noise, convention | noise) == none); + assert(state.observe(1, 119, convention | noise, convention | noise) == convention); + } + for (uint8_t clears : { uint8_t(none), uint8_t(normal), uint8_t(reversed), uint8_t(normal | reversed), uint8_t(unknown), uint8_t(normal | unknown), uint8_t(reversed | unknown), uint8_t(normal | reversed | unknown) }) + for (uint8_t comparisons : { uint8_t(none), uint8_t(normal), uint8_t(reversed), uint8_t(normal | reversed), uint8_t(unknown), uint8_t(normal | unknown), uint8_t(reversed | unknown), uint8_t(normal | reversed | unknown) }) { - if ((clears == normal || clears == reversed) && clears == comparisons) + const uint8_t clear_direction = clears & (normal | reversed), compare_direction = comparisons & (normal | reversed); + if ((clear_direction == normal || clear_direction == reversed) && clear_direction == compare_direction) continue; + // Contradictory or missing evidence never votes, however long it is observed observation state; for (uint64_t frame = 0; frame < 600; ++frame) assert(state.observe(1, frame, clears, comparisons) == none); From a2f3b356d3cd96324234300e0cc46d89531981dc Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 1 Oct 2026 19:06:16 +0200 Subject: [PATCH 6/6] depth: keep the shader resource view when the backup texture is reused fc8ce90c clears the selected view when the previous depth-stencil is untracked, but the following check skips creating a view if the revived backup texture is unchanged. A game that alternates between two depth buffers (RPCS3) ended every second frame without a view and rebound the effects on every frame. Carry the previous view over in that case. --- examples/09-depth/generic_depth_addon.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/09-depth/generic_depth_addon.cpp b/examples/09-depth/generic_depth_addon.cpp index 89be4475e..9dddb9c78 100644 --- a/examples/09-depth/generic_depth_addon.cpp +++ b/examples/09-depth/generic_depth_addon.cpp @@ -1475,6 +1475,11 @@ static void on_begin_render_effects(effect_runtime *runtime, command_list *cmd_l if (!device->create_resource_view(depth_stencil_backup->backup_texture, resource_usage::shader_resource, srv_desc, &data.selected_shader_resource)) break; } + else + { + // Untracking the previous depth-stencil above cleared the view, but it still points to the revived backup texture + data.selected_shader_resource = prev_shader_resource; + } data.using_backup_texture = true; }