From d88341bf9400bb740a2cc2de787646fa5e66e44e Mon Sep 17 00:00:00 2001 From: byungjul Date: Fri, 2 Oct 2026 15:08:32 +0900 Subject: [PATCH] fix(vulkan): destroy the enable2 device only after xrDestroySession (DisplayXR/displayxr-runtime#1779) dxr_prov_session_stop called dxr_pvk_destroy() before xrEndSession, and that destroyed the VkDevice + VkInstance we created through XR_KHR_vulkan_enable2. The session still runs on them: the runtime's repaint thread keeps presenting the last frame until xrEndSession. Every session stop and every quit crashed in vkQueuePresentKHR on that thread (SIGSEGV). Split the teardown: - dxr_pvk_destroy(): bridges, overlays, fence, command pool. Still before xrEndSession. - dxr_pvk_destroy_device(): device + instance, after xrDestroySession and xrDestroyInstance. Co-Authored-By: Claude Opus 5.5 --- .../displayxr_provider_gfx_vulkan.cpp | 15 +++++++++++++++ .../displayxr_provider_gfx_vulkan.h | 13 ++++++++++--- .../displayxr_provider_session.cpp | 12 +++++++++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp index b7143b5a..5eb14f2b 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp @@ -1507,6 +1507,21 @@ dxr_pvk_destroy(void) s_pvk.api.vkDestroyFence(s_pvk.device, s_pvk.copy_fence, NULL); if (s_pvk.cmd_pool && s_pvk.api.vkDestroyCommandPool) s_pvk.api.vkDestroyCommandPool(s_pvk.device, s_pvk.cmd_pool, NULL); + s_pvk.copy_fence = VK_NULL_HANDLE; + s_pvk.cmd_pool = VK_NULL_HANDLE; + + // NOT the device or the instance (DisplayXR/displayxr-runtime#1779): we made + // them through XR_KHR_vulkan_enable2 and the session still runs on them. The + // runtime's repaint thread keeps presenting the last frame on that device + // until xrEndSession; destroying it here crashed every session stop / quit + // in vkQueuePresentKHR. dxr_pvk_destroy_device() runs after xrDestroySession. + pvk_log("[DisplayXR-PROV-VK] session objects destroyed (device kept until xrDestroySession)\n"); +} + +void +dxr_pvk_destroy_device(void) +{ + if (s_pvk.device && s_pvk.api.vkDeviceWaitIdle) s_pvk.api.vkDeviceWaitIdle(s_pvk.device); if (s_pvk.device && s_pvk.api.vkDestroyDevice) s_pvk.api.vkDestroyDevice(s_pvk.device, NULL); if (s_pvk.instance && s_pvk.api.vkDestroyInstance) diff --git a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h index bfa0c0b1..25217747 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h +++ b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h @@ -80,7 +80,7 @@ int dxr_pvk_create_device(XrInstance instance, XrSystemId system_id, /// Fill an XrGraphicsBindingVulkan2KHR for xrCreateSession, chaining `next` /// (the win32 window binding). Returns a pointer to storage owned by this TU, -/// valid until dxr_pvk_destroy(). NULL if the device was never created. +/// valid until dxr_pvk_destroy_device(). NULL if the device was never created. const void *dxr_pvk_session_binding(const void *next); /// Adopt Unity's Vulkan objects, captured from IUnityGraphicsVulkan by @@ -206,10 +206,17 @@ int dxr_pvk_overlay_copy_to_swapchain_image(int kind, uint32_t image_index); /// Drop the overlay's bridge and swapchain images (layer resize / teardown). void dxr_pvk_overlay_destroy(int kind); -/// Tear down every Vulkan object this TU owns (bridges, semaphores, command -/// pool, device, instance). Safe to call when nothing was created. +/// Tear down the per-session objects (bridges, overlays, fence, command pool). +/// Call BEFORE xrEndSession. Leaves the device and instance alive: the session +/// still runs on them (DisplayXR/displayxr-runtime#1779). Safe to call when +/// nothing was created. void dxr_pvk_destroy(void); +/// Destroy the VkDevice + VkInstance made through XR_KHR_vulkan_enable2. Call +/// only AFTER xrDestroySession: the runtime's compositor (its repaint thread +/// included) uses them until then. Safe to call when nothing was created. +void dxr_pvk_destroy_device(void); + /// 1 once dxr_pvk_create_device() has succeeded. int dxr_pvk_device_ready(void); diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.cpp b/native~/displayxr_xrprovider/displayxr_provider_session.cpp index 50dc610e..dcfe6367 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_session.cpp @@ -182,6 +182,7 @@ extern "C" void *dxr_pvk_unity_image_ptr(int eye); extern "C" int dxr_pvk_copy_to_swapchain_image(int eye, uint32_t image_index); extern "C" void dxr_pvk_signal_unity_done(void); extern "C" void dxr_pvk_destroy(void); +extern "C" void dxr_pvk_destroy_device(void); extern "C" int dxr_pvk_device_ready(void); // 2D overlay layers (#336). Kinds match DXR_PVK_OVERLAY_* in the VK header. #define PS_PVK_OVERLAY_LOCAL2D 0 @@ -5060,14 +5061,19 @@ void dxr_prov_session_stop(void) ps_probe_cleanup(); // weave-to-texture PROBE shared texture + handle (experiment) if (s_ps.swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(s_ps.swapchain); #if defined(ENABLE_VULKAN) - // Vulkan (#247/#249): drop the bridges BEFORE xrDestroySession — the bridge images live - // on the session device, which the runtime tears down with the session. Unity's own - // objects are only dereferenced, never destroyed. No-op on the D3D paths. + // Vulkan (#247/#249): drop the bridges BEFORE xrDestroySession, while their device is + // still current for the session. Unity's own objects are only dereferenced, never + // destroyed. No-op on the D3D paths. if (s_ps.graphics_api == DXR_GFX_VULKAN) dxr_pvk_destroy(); #endif if (s_ps.session && s_ps.session_ready && s_ps.pfn_end_session) s_ps.pfn_end_session(s_ps.session); if (s_ps.session && s_ps.pfn_destroy_session) s_ps.pfn_destroy_session(s_ps.session); if (s_ps.instance && s_ps.pfn_destroy_instance) s_ps.pfn_destroy_instance(s_ps.instance); +#if defined(ENABLE_VULKAN) + // ...and the enable2 device + instance only now: the runtime's compositor, repaint + // thread included, uses them until the session is gone (DisplayXR/displayxr-runtime#1779). + if (s_ps.graphics_api == DXR_GFX_VULKAN) dxr_pvk_destroy_device(); +#endif #ifdef _WIN32 // Bridge + cross-device fence + own-device resources.