From 60d2c69e75e6a1fce0f9a9817a81caa7208e2149 Mon Sep 17 00:00:00 2001 From: byungjul Date: Thu, 1 Oct 2026 17:00:23 +0900 Subject: [PATCH 1/2] feat(#336): several window-space UI layers at once (wsui slots) The native wsui registry had a single slot, so when two DisplayXRWindowSpaceUI components were enabled together (lenovo-avatar opens its radio and tuning HUDs on the same key) the last one to register won and the other never showed. Same on every backend. - wsui module: DXR_WSUI_MAX_SLOTS (4) entries; acquire/release_slot plus _slot variants of set_texture / set_layer / clear / get_pending. The slot-less API is kept and addresses slot 0. - session: per-slot state (ProviderWsui wsui[PS_MAX_WSUI]); create, getter (dxr_prov_get_wsui_bridge_slot), submit and teardown take a slot, and one window-space layer is submitted per registered slot. Layer array sized for zones + Local2D + all wsui slots. - Vulkan: one overlay kind per slot (DXR_PVK_OVERLAY_WSUI0 + N) and one copy render event per slot. - C#: each DisplayXRWindowSpaceUI acquires its own slot in OnEnable and releases it in OnDisable (which used to clear the one shared layer even while another HUD was still up). Falls back to the slot-less API on a native plugin without slots. Behaviour change on all backends: two or more enabled DisplayXRWindowSpaceUI components are now all composited (previously only the last registered one), and disabling one no longer clears the others. Co-Authored-By: Claude Opus 5.5 --- Runtime/DisplayXRNative.cs | 17 + Runtime/DisplayXRWindowSpaceUI.cs | 81 +++- Runtime/Provider/DisplayXRProviderNative.cs | 20 +- native~/displayxr_unity_plugin.cpp | 21 +- native~/displayxr_window_space_ui.cpp | 124 ++++-- native~/displayxr_window_space_ui.h | 19 + .../displayxr_provider_gfx_vulkan.cpp | 4 +- .../displayxr_provider_gfx_vulkan.h | 4 +- .../displayxr_provider_session.cpp | 376 ++++++++++-------- .../displayxr_provider_session.h | 11 +- 10 files changed, 446 insertions(+), 231 deletions(-) diff --git a/Runtime/DisplayXRNative.cs b/Runtime/DisplayXRNative.cs index 0622b3de..43689d6b 100644 --- a/Runtime/DisplayXRNative.cs +++ b/Runtime/DisplayXRNative.cs @@ -247,6 +247,23 @@ public static extern void displayxr_window_space_ui_set_layer( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] public static extern void displayxr_window_space_ui_clear(); + // Per-slot window-space UI (#336 follow-up). Several DisplayXRWindowSpaceUI + // components can be live at once; each takes a slot so they no longer overwrite + // each other. The slot-less calls above address slot 0. + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern int displayxr_window_space_ui_acquire_slot(); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern void displayxr_window_space_ui_release_slot(int slot); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern void displayxr_window_space_ui_set_texture_slot( + int slot, IntPtr nativeTex, int width, int height); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern void displayxr_window_space_ui_set_layer_slot( + int slot, float x, float y, float width, float height, float disparity); + // ==================================================================== // Local2D overlay (#439/#491) — modern mask-based 2D-over-3D layer // (XrCompositionLayerLocal2DDXR, "glass over 3D"). Replaces the legacy diff --git a/Runtime/DisplayXRWindowSpaceUI.cs b/Runtime/DisplayXRWindowSpaceUI.cs index 37705685..d3ba64e4 100644 --- a/Runtime/DisplayXRWindowSpaceUI.cs +++ b/Runtime/DisplayXRWindowSpaceUI.cs @@ -133,6 +133,11 @@ public class DisplayXRWindowSpaceUI : MonoBehaviour // (#336) Vulkan has no wrappable bridge: the getter returns a session token in // place of m_BridgePtr, and the copy is a plugin event on the render thread. private bool m_VulkanBridge; + // This component's wsui slot (#336 follow-up): -1 = none (every slot taken, or + // not acquired yet). m_LegacySlot = the native plugin predates slots, so the + // slot-less API (one shared layer) is used. + private int m_Slot = -1; + private bool m_LegacySlot; private System.IntPtr m_RenderEventFunc = System.IntPtr.Zero; // Saved state, restored in OnDisable. @@ -245,9 +250,10 @@ void OnEnable() m_Canvas.worldCamera = m_OverlayCamera; // ---- Tell native about our texture + initial layer descriptor ---- - DisplayXRNative.displayxr_window_space_ui_set_layer( + AcquireSlot(); + NativeSetLayer( positionX, positionY, width, height, disparity); - DisplayXRNative.displayxr_window_space_ui_set_texture( + NativeSetTexture( OverlayTexture.GetNativeTexturePtr(), m_RtSize.x, m_RtSize.y); // ---- Windows-only: query the cross-device bridge ---- @@ -286,9 +292,15 @@ private void TryAcquireBridge() { // Custom display-provider mode: the provider owns a SEPARATE // D3D12 device, so it exposes its own cross-device wsui bridge. (#166) - DisplayXRProviderNative.dxr_prov_get_wsui_bridge( - (uint)m_RtSize.x, (uint)m_RtSize.y, - out bridgePtr, out bw, out bh); + if (m_Slot < 0) return; // no slot: this layer isn't shown + if (m_LegacySlot) + DisplayXRProviderNative.dxr_prov_get_wsui_bridge( + (uint)m_RtSize.x, (uint)m_RtSize.y, + out bridgePtr, out bw, out bh); + else + DisplayXRProviderNative.dxr_prov_get_wsui_bridge_slot( + m_Slot, (uint)m_RtSize.x, (uint)m_RtSize.y, + out bridgePtr, out bw, out bh); } if (bridgePtr == System.IntPtr.Zero || bw == 0 || bh == 0) { @@ -320,10 +332,10 @@ private void TryAcquireBridge() // file static and survives, but re-push it anyway so a restart can never // leave the runtime with a stale/never-seen layer, and invalidate the // change-detection cache below so LateUpdate re-pushes too. - DisplayXRNative.displayxr_window_space_ui_set_texture( + NativeSetTexture( OverlayTexture != null ? OverlayTexture.GetNativeTexturePtr() : System.IntPtr.Zero, m_RtSize.x, m_RtSize.y); - DisplayXRNative.displayxr_window_space_ui_set_layer( + NativeSetLayer( positionX, positionY, width, height, disparity); m_LastX = positionX; m_LastY = positionY; m_LastW = width; m_LastH = height; @@ -344,12 +356,14 @@ private void IssueVulkanCopy() if (m_BridgePtr == System.IntPtr.Zero) return; try { - DisplayXRProviderNative.dxr_prov_wsui_request_copy(); - if (DisplayXRProviderNative.dxr_prov_wsui_needs_copy() == 0) return; + if (m_Slot < 0 || m_LegacySlot) return; + DisplayXRProviderNative.dxr_prov_wsui_request_copy_slot(m_Slot); + if (DisplayXRProviderNative.dxr_prov_wsui_needs_copy_slot(m_Slot) == 0) return; if (m_RenderEventFunc == System.IntPtr.Zero) m_RenderEventFunc = DisplayXRProviderNative.dxr_prov_get_render_event_func(); if (m_RenderEventFunc != System.IntPtr.Zero) - GL.IssuePluginEvent(m_RenderEventFunc, DisplayXRProviderNative.kVkOverlayCopyWsuiEvent); + GL.IssuePluginEvent(m_RenderEventFunc, + DisplayXRProviderNative.kVkOverlayCopyWsuiEvent0 + m_Slot); } catch (System.EntryPointNotFoundException) { @@ -362,6 +376,47 @@ private void IssueVulkanCopy() } } + // Take a wsui slot for this component. Idempotent. + private void AcquireSlot() + { + if (m_Slot >= 0) return; + try + { + m_Slot = DisplayXRNative.displayxr_window_space_ui_acquire_slot(); + m_LegacySlot = false; + if (m_Slot < 0) + Debug.LogWarning($"[DisplayXR] wsui: '{name}' has no free window-space slot — " + + "too many DisplayXRWindowSpaceUI components are enabled at once."); + } + catch (System.EntryPointNotFoundException) + { + m_Slot = 0; // older native plugin: the single shared layer + m_LegacySlot = true; + } + } + + private void ReleaseSlot() + { + if (m_Slot < 0) return; + if (m_LegacySlot) DisplayXRNative.displayxr_window_space_ui_clear(); + else DisplayXRNative.displayxr_window_space_ui_release_slot(m_Slot); + m_Slot = -1; + } + + private void NativeSetTexture(System.IntPtr tex, int w, int h) + { + if (m_Slot < 0) return; + if (m_LegacySlot) DisplayXRNative.displayxr_window_space_ui_set_texture(tex, w, h); + else DisplayXRNative.displayxr_window_space_ui_set_texture_slot(m_Slot, tex, w, h); + } + + private void NativeSetLayer(float x, float y, float w, float h, float disparity) + { + if (m_Slot < 0) return; + if (m_LegacySlot) DisplayXRNative.displayxr_window_space_ui_set_layer(x, y, w, h, disparity); + else DisplayXRNative.displayxr_window_space_ui_set_layer_slot(m_Slot, x, y, w, h, disparity); + } + private void ReleaseBridgeTex() { if (m_BridgeTex != null) @@ -374,7 +429,7 @@ private void ReleaseBridgeTex() void OnDisable() { - DisplayXRNative.displayxr_window_space_ui_clear(); + ReleaseSlot(); // Restore the canvas's original mode + transform + layer. if (m_StateSaved && m_Canvas != null) @@ -516,7 +571,7 @@ void LateUpdate() width != m_LastW || height != m_LastH || disparity != m_LastDisparity) { - DisplayXRNative.displayxr_window_space_ui_set_layer( + NativeSetLayer( positionX, positionY, width, height, disparity); m_LastX = positionX; m_LastY = positionY; m_LastW = width; m_LastH = height; @@ -687,7 +742,7 @@ private void ResizeOverlayTexture(Vector2Int size) m_OverlayCamera.targetTexture = OverlayTexture; if (m_CanvasRect != null) m_CanvasRect.sizeDelta = size; - DisplayXRNative.displayxr_window_space_ui_set_texture( + NativeSetTexture( OverlayTexture.GetNativeTexturePtr(), size.x, size.y); ReleaseBridgeTex(); TryAcquireBridge(); diff --git a/Runtime/Provider/DisplayXRProviderNative.cs b/Runtime/Provider/DisplayXRProviderNative.cs index f621cd8a..83551da9 100644 --- a/Runtime/Provider/DisplayXRProviderNative.cs +++ b/Runtime/Provider/DisplayXRProviderNative.cs @@ -341,17 +341,23 @@ public static extern void dxr_prov_get_local2d_bridge( /// Keep in sync with DXR_EVENT_VK_OVERLAY_COPY_LOCAL2D (displayxr_unity_plugin.cpp). public const int kVkOverlayCopyLocal2DEvent = 0x44585201; - /// (#336) wsui twin of . + /// (#336) wsui twin of , per slot. [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - public static extern int dxr_prov_wsui_needs_copy(); + public static extern int dxr_prov_wsui_needs_copy_slot(int slot); - /// (#336) wsui twin of . + /// (#336) wsui twin of , per slot. [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - public static extern void dxr_prov_wsui_request_copy(); + public static extern void dxr_prov_wsui_request_copy_slot(int slot); - /// Event id: copy the wsui canvas RT into its Vulkan overlay bridge. - /// Keep in sync with DXR_EVENT_VK_OVERLAY_COPY_WSUI (displayxr_unity_plugin.cpp). - public const int kVkOverlayCopyWsuiEvent = 0x44585202; + /// Event id for wsui slot 0; slot N is this + N. Keep in sync with + /// DXR_EVENT_VK_OVERLAY_COPY_WSUI0 (displayxr_unity_plugin.cpp). + public const int kVkOverlayCopyWsuiEvent0 = 0x44585210; + + /// Per-slot form of . + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern void dxr_prov_get_wsui_bridge_slot( + int slot, uint width, uint height, + out System.IntPtr nativePtr, out uint outWidth, out uint outHeight); /// /// Per-eye foreground-clip data (#166 Phase B): the eye's foreground far diff --git a/native~/displayxr_unity_plugin.cpp b/native~/displayxr_unity_plugin.cpp index 5f584631..f20a57ab 100644 --- a/native~/displayxr_unity_plugin.cpp +++ b/native~/displayxr_unity_plugin.cpp @@ -59,12 +59,14 @@ static bool s_vk_captured = false; // // Keep the ids in sync with DisplayXRProviderNative.kVkOverlayCopy*Event. #define DXR_EVENT_VK_OVERLAY_COPY_LOCAL2D 0x44585201 -#define DXR_EVENT_VK_OVERLAY_COPY_WSUI 0x44585202 +// wsui slot N is DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + N (N < DXR_WSUI_SLOTS). +#define DXR_EVENT_VK_OVERLAY_COPY_WSUI0 0x44585210 +#define DXR_WSUI_SLOTS 4 extern "C" int displayxr_local2d_get_pending(void **out_tex, int *out_w, int *out_h); -extern "C" int displayxr_window_space_ui_get_pending(void **out_tex, int *out_tex_w, int *out_tex_h, - float *out_x, float *out_y, - float *out_lw, float *out_lh, float *out_disp); +extern "C" int displayxr_window_space_ui_get_pending_slot(int slot, void **out_tex, int *out_tex_w, + int *out_tex_h, float *out_x, float *out_y, + float *out_lw, float *out_lh, float *out_disp); extern "C" int dxr_pvk_overlay_ready(int kind); extern "C" int dxr_pvk_overlay_record_unity_copy(int kind, void *cmd_buf, void *src_image, int64_t src_format, uint32_t src_w, uint32_t src_h); @@ -79,7 +81,8 @@ static void configure_vulkan_events(void) cfg.graphicsQueueAccess = kUnityVulkanGraphicsQueueAccess_DontCare; cfg.flags = kUnityVulkanEventConfigFlag_EnsurePreviousFrameSubmission; s_unity_vk->ConfigureEvent(DXR_EVENT_VK_OVERLAY_COPY_LOCAL2D, &cfg); - s_unity_vk->ConfigureEvent(DXR_EVENT_VK_OVERLAY_COPY_WSUI, &cfg); + for (int i = 0; i < DXR_WSUI_SLOTS; i++) + s_unity_vk->ConfigureEvent(DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + i, &cfg); } static void vk_overlay_copy(int kind, void *tex, int w, int h) @@ -117,11 +120,13 @@ static void UNITY_INTERFACE_API on_render_event(int event_id) if (event_id == DXR_EVENT_VK_OVERLAY_COPY_LOCAL2D) { void *tex = NULL; int w = 0, h = 0; if (displayxr_local2d_get_pending(&tex, &w, &h)) vk_overlay_copy(0 /* LOCAL2D */, tex, w, h); - } else if (event_id == DXR_EVENT_VK_OVERLAY_COPY_WSUI) { + } else if (event_id >= DXR_EVENT_VK_OVERLAY_COPY_WSUI0 && + event_id < DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + DXR_WSUI_SLOTS) { + int slot = event_id - DXR_EVENT_VK_OVERLAY_COPY_WSUI0; void *tex = NULL; int w = 0, h = 0; float x, y, lw, lh, disp; - if (displayxr_window_space_ui_get_pending(&tex, &w, &h, &x, &y, &lw, &lh, &disp)) - vk_overlay_copy(1 /* WSUI */, tex, w, h); + if (displayxr_window_space_ui_get_pending_slot(slot, &tex, &w, &h, &x, &y, &lw, &lh, &disp)) + vk_overlay_copy(1 + slot /* DXR_PVK_OVERLAY_WSUI0 + slot */, tex, w, h); } #else (void)event_id; diff --git a/native~/displayxr_window_space_ui.cpp b/native~/displayxr_window_space_ui.cpp index 33a2b58d..9f449783 100644 --- a/native~/displayxr_window_space_ui.cpp +++ b/native~/displayxr_window_space_ui.cpp @@ -4,7 +4,8 @@ // Window-space UI overlay (issue #67) — implementation. // // See displayxr_window_space_ui.h for the architecture overview. This file now -// owns only the single pending Unity texture + layer descriptor (set from C#). +// owns only the pending Unity texture + layer descriptor of each wsui slot (set +// from C#). // The custom IUnityXRDisplay provider reads that pending state via // displayxr_window_space_ui_get_pending and drives its OWN window-space // composition layer from its own session/device (ps_submit_wsui in @@ -27,6 +28,11 @@ namespace { // --- Pending state (set from Unity C# game thread) --------------------------- +// +// One entry per window-space layer. Several DisplayXRWindowSpaceUI components can be +// live at once (lenovo-avatar opens two HUD panels together); a single shared entry +// made the last one to register win and the others vanish. Each component acquires +// its own slot; the provider submits one layer per registered slot. struct PendingState { void * volatile native_tex; volatile int width; @@ -36,9 +42,16 @@ struct PendingState { volatile float w; volatile float h; volatile float disparity; + volatile int in_use; // handed out by acquire_slot }; -PendingState s_pending = {}; +PendingState s_pending[DXR_WSUI_MAX_SLOTS] = {}; + +inline PendingState * +slot_ptr(int slot) +{ + return (slot >= 0 && slot < DXR_WSUI_MAX_SLOTS) ? &s_pending[slot] : nullptr; +} } // anonymous namespace @@ -46,14 +59,90 @@ PendingState s_pending = {}; // C ABI exports — called from Unity C# via P/Invoke // ============================================================================= +extern "C" int +displayxr_window_space_ui_acquire_slot(void) +{ + // Main thread only (component OnEnable), so no atomics needed for the hand-out. + for (int i = 0; i < DXR_WSUI_MAX_SLOTS; i++) { + if (!s_pending[i].in_use) { + s_pending[i] = PendingState{}; + s_pending[i].in_use = 1; + displayxr_log("[DisplayXR] wsui: slot %d acquired\n", i); + return i; + } + } + displayxr_log("[DisplayXR] wsui: all %d slots in use — layer not shown\n", DXR_WSUI_MAX_SLOTS); + return -1; +} + +extern "C" void +displayxr_window_space_ui_release_slot(int slot) +{ + PendingState *p = slot_ptr(slot); + if (!p) return; + p->native_tex = nullptr; + p->in_use = 0; + displayxr_log("[DisplayXR] wsui: slot %d released\n", slot); +} + +extern "C" void +displayxr_window_space_ui_set_texture_slot(int slot, void *nativeTex, int width, int height) +{ + PendingState *p = slot_ptr(slot); + if (!p) return; + p->native_tex = nativeTex; + p->width = width; + p->height = height; + displayxr_log("[DisplayXR] wsui_set_texture[%d]: tex=%p %dx%d\n", slot, nativeTex, width, height); +} + +extern "C" void +displayxr_window_space_ui_set_layer_slot(int slot, float x, float y, float width, float height, + float disparity) +{ + PendingState *p = slot_ptr(slot); + if (!p) return; + p->x = x; + p->y = y; + p->w = width; + p->h = height; + p->disparity = disparity; +} + +extern "C" void +displayxr_window_space_ui_clear_slot(int slot) +{ + PendingState *p = slot_ptr(slot); + if (!p) return; + p->native_tex = nullptr; + displayxr_log("[DisplayXR] wsui_clear[%d]\n", slot); +} + +extern "C" int +displayxr_window_space_ui_get_pending_slot(int slot, void **out_tex, int *out_tex_w, int *out_tex_h, + float *out_x, float *out_y, + float *out_lw, float *out_lh, float *out_disp) +{ + PendingState *p = slot_ptr(slot); + void *tex = p ? p->native_tex : nullptr; + if (out_tex) *out_tex = tex; + if (!p) return 0; + if (out_tex_w) *out_tex_w = p->width; + if (out_tex_h) *out_tex_h = p->height; + if (out_x) *out_x = p->x; + if (out_y) *out_y = p->y; + if (out_lw) *out_lw = p->w; + if (out_lh) *out_lh = p->h; + if (out_disp) *out_disp = p->disparity; + return (tex != nullptr && p->width > 0 && p->height > 0) ? 1 : 0; +} + +// --- Single-layer API (pre-slot callers) — slot 0 ----------------------------- + extern "C" void displayxr_window_space_ui_set_texture(void *nativeTex, int width, int height) { - s_pending.native_tex = nativeTex; - s_pending.width = width; - s_pending.height = height; - displayxr_log("[DisplayXR] wsui_set_texture: tex=%p %dx%d\n", - nativeTex, width, height); + displayxr_window_space_ui_set_texture_slot(0, nativeTex, width, height); } extern "C" void @@ -61,18 +150,13 @@ displayxr_window_space_ui_set_layer(float x, float y, float width, float height, float disparity) { - s_pending.x = x; - s_pending.y = y; - s_pending.w = width; - s_pending.h = height; - s_pending.disparity = disparity; + displayxr_window_space_ui_set_layer_slot(0, x, y, width, height, disparity); } extern "C" void displayxr_window_space_ui_clear(void) { - s_pending.native_tex = nullptr; - displayxr_log("[DisplayXR] wsui_clear\n"); + displayxr_window_space_ui_clear_slot(0); } extern "C" int @@ -80,14 +164,6 @@ displayxr_window_space_ui_get_pending(void **out_tex, int *out_tex_w, int *out_t float *out_x, float *out_y, float *out_lw, float *out_lh, float *out_disp) { - void *tex = s_pending.native_tex; - if (out_tex) *out_tex = tex; - if (out_tex_w) *out_tex_w = s_pending.width; - if (out_tex_h) *out_tex_h = s_pending.height; - if (out_x) *out_x = s_pending.x; - if (out_y) *out_y = s_pending.y; - if (out_lw) *out_lw = s_pending.w; - if (out_lh) *out_lh = s_pending.h; - if (out_disp) *out_disp = s_pending.disparity; - return (tex != nullptr && s_pending.width > 0 && s_pending.height > 0) ? 1 : 0; + return displayxr_window_space_ui_get_pending_slot(0, out_tex, out_tex_w, out_tex_h, + out_x, out_y, out_lw, out_lh, out_disp); } diff --git a/native~/displayxr_window_space_ui.h b/native~/displayxr_window_space_ui.h index 3b48d64d..53e0121b 100644 --- a/native~/displayxr_window_space_ui.h +++ b/native~/displayxr_window_space_ui.h @@ -44,6 +44,25 @@ extern "C" { // - Windows D3D11: ID3D11Texture2D* // - Windows D3D12: ID3D12Resource* // Pass NULL to deregister. +// Window-space layers that can be live at once. Each DisplayXRWindowSpaceUI takes a +// slot of its own (acquire_slot) so two HUDs no longer overwrite each other's +// registration; the provider submits one layer per registered slot. +#define DXR_WSUI_MAX_SLOTS 4 + +// Slot API. acquire returns -1 when every slot is taken. Main thread. +DISPLAYXR_EXPORT int displayxr_window_space_ui_acquire_slot(void); +DISPLAYXR_EXPORT void displayxr_window_space_ui_release_slot(int slot); +DISPLAYXR_EXPORT void displayxr_window_space_ui_set_texture_slot(int slot, void *nativeTex, + int width, int height); +DISPLAYXR_EXPORT void displayxr_window_space_ui_set_layer_slot(int slot, float x, float y, + float width, float height, + float disparity); +DISPLAYXR_EXPORT void displayxr_window_space_ui_clear_slot(int slot); +int displayxr_window_space_ui_get_pending_slot(int slot, void **out_tex, int *out_tex_w, + int *out_tex_h, float *out_x, float *out_y, + float *out_lw, float *out_lh, float *out_disp); + +// Single-layer API, kept for existing callers: these address slot 0. DISPLAYXR_EXPORT void displayxr_window_space_ui_set_texture(void *nativeTex, int width, int height); // Update the layer descriptor (fractional window coords + per-eye disparity). diff --git a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp index 27db73b9..f1c07d06 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp @@ -1275,7 +1275,9 @@ pvk_overlay(int kind) static const char * pvk_overlay_label(int kind) { - return kind == DXR_PVK_OVERLAY_LOCAL2D ? "local2d" : "wsui"; + static const char *const names[DXR_PVK_OVERLAY_COUNT] = {"local2d", "wsui0", "wsui1", + "wsui2", "wsui3"}; + return (kind >= 0 && kind < DXR_PVK_OVERLAY_COUNT) ? names[kind] : "?"; } static void diff --git a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h index bc97442b..69eb8a52 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h +++ b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h @@ -155,8 +155,8 @@ void dxr_pvk_signal_unity_done(void); enum { DXR_PVK_OVERLAY_LOCAL2D = 0, - DXR_PVK_OVERLAY_WSUI = 1, - DXR_PVK_OVERLAY_COUNT = 2, + DXR_PVK_OVERLAY_WSUI0 = 1, // wsui slot N is DXR_PVK_OVERLAY_WSUI0 + N + DXR_PVK_OVERLAY_COUNT = 1 + 4, // Local2D + DXR_WSUI_MAX_SLOTS }; /// Record the overlay layer's swapchain VkImages (session device) and format. diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.cpp b/native~/displayxr_xrprovider/displayxr_provider_session.cpp index 6ce1b775..b9f3d333 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_session.cpp @@ -184,7 +184,7 @@ extern "C" void dxr_pvk_destroy(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 -#define PS_PVK_OVERLAY_WSUI 1 +#define PS_PVK_OVERLAY_WSUI0 1 // wsui slot N is kind PS_PVK_OVERLAY_WSUI0 + N extern "C" void dxr_pvk_overlay_set_swapchain_images(int kind, const void *, uint32_t, int64_t); extern "C" int dxr_pvk_overlay_create_bridge(int kind, uint32_t, uint32_t, int64_t); extern "C" int dxr_pvk_overlay_has_content(int kind); @@ -220,6 +220,45 @@ static int s_wsui_vk_disabled = 0; // which stay the validated Phase-B path. Each extra zone carries its OWN zone-sized // swapchain + cross-device bridge(s) + located views + Unity render pass, so N 3D // zones weave into N window-pixel rects (mirrors the runtime cube_zones handle test). +// Window-space UI (HUD) overlay layer (#67/#166): own overlay swapchain + +// cross-device bridge. C# (DisplayXRWindowSpaceUI) Graphics.CopyTexture's the +// canvas RT into bridge_unity each frame; submit copies bridge_own -> the acquired +// swapchain image (own device) and submits one window-space composition layer. +// One per wsui slot (#336 follow-up): several HUD components can be live at once. +#define PS_MAX_WSUI 4 // == DXR_WSUI_MAX_SLOTS (displayxr_window_space_ui.h) +typedef struct ProviderWsui { + XrSwapchain swapchain; + uint32_t w, h, image_count; + int64_t format; + int swapchain_created; + int image_acquired; + uint32_t acquired_index; + uint32_t registered_w, registered_h; // bridge+swapchain sized for this + uint32_t vk_failed_w, vk_failed_h; // Vulkan create failed at this size (#336) +#ifdef _WIN32 + XrSwapchainImageD3D12KHR images[PS_MAX_SWAPCHAIN_IMAGES]; + ID3D12Resource *bridge_own; // own_device side (copy source) + ID3D12Resource *bridge_unity; // Unity-device side (C# CopyTexture target) + HANDLE bridge_handle; + // D3D11 (#195): images on the runtime's device. `unity_tex` is the Unity-side + // texture C# CopyTexture targets. PLAYER zero-copy: a plain Unity-device texture, + // submit same-device-copies it into the acquired image. EDITOR bridge: the + // Unity-opened side of a shared texture whose own-device side is `unity_tex_own`; + // submit copies own->image on the own context. + XrSwapchainImageD3D11KHR images_d3d11[PS_MAX_SWAPCHAIN_IMAGES]; + ID3D11Texture2D *unity_tex; + ID3D11Texture2D *unity_tex_own; // own-device side (editor bridge) + HANDLE unity_tex_handle; +#else + // Metal (#206): arraySize=1 overlay swapchain images (id in .texture). + // No cross-device bridge — the runtime compositor is on Unity's OWN MTLDevice, so + // submit blits the C#-registered Unity id straight into the acquired + // image (same device, session queue). Cousin of the projection sc_images_metal. + XrSwapchainImageMetalKHR images_metal[PS_MAX_SWAPCHAIN_IMAGES]; +#endif // _WIN32 + +} ProviderWsui; + typedef struct ProviderExtraZone { int valid; uint32_t zone_id; @@ -562,39 +601,8 @@ typedef struct ProviderSession { uint32_t realloc_pending_w, realloc_pending_h; // last-seen target awaiting stability int realloc_stable; // consecutive frames the target held - // Window-space UI (HUD) overlay layer (#67/#166): own overlay swapchain + - // cross-device bridge. C# (DisplayXRWindowSpaceUI) Graphics.CopyTexture's the - // canvas RT into wsui_bridge_unity each frame; submit copies wsui_bridge_own -> - // the acquired swapchain image (own device) and submits a 2nd composition layer. - XrSwapchain wsui_swapchain; - uint32_t wsui_w, wsui_h, wsui_image_count; - int64_t wsui_format; - int wsui_swapchain_created; - int wsui_image_acquired; - uint32_t wsui_acquired_index; - uint32_t wsui_registered_w, wsui_registered_h; // bridge+swapchain sized for this - uint32_t wsui_vk_failed_w, wsui_vk_failed_h; // Vulkan create failed at this size (#336) -#ifdef _WIN32 - XrSwapchainImageD3D12KHR wsui_images[PS_MAX_SWAPCHAIN_IMAGES]; - ID3D12Resource *wsui_bridge_own; // own_device side (copy source) - ID3D12Resource *wsui_bridge_unity; // Unity-device side (C# CopyTexture target) - HANDLE wsui_bridge_handle; - // D3D11 (#195): images on the runtime's device. `wsui_unity_tex` is the Unity-side - // texture C# CopyTexture targets. PLAYER zero-copy: a plain Unity-device texture, - // submit same-device-copies it into the acquired image. EDITOR bridge: the - // Unity-opened side of a shared texture whose own-device side is `wsui_unity_tex_own`; - // submit copies own->image on the own context. - XrSwapchainImageD3D11KHR wsui_images_d3d11[PS_MAX_SWAPCHAIN_IMAGES]; - ID3D11Texture2D *wsui_unity_tex; - ID3D11Texture2D *wsui_unity_tex_own; // own-device side (editor bridge) - HANDLE wsui_unity_tex_handle; -#else - // Metal (#206): arraySize=1 overlay swapchain images (id in .texture). - // No cross-device bridge — the runtime compositor is on Unity's OWN MTLDevice, so - // submit blits the C#-registered Unity id straight into the acquired - // image (same device, session queue). Cousin of the projection sc_images_metal. - XrSwapchainImageMetalKHR wsui_images_metal[PS_MAX_SWAPCHAIN_IMAGES]; -#endif // _WIN32 + // Window-space UI (HUD) layers, one per wsui slot — see ProviderWsui. + ProviderWsui wsui[PS_MAX_WSUI]; // Local2D layer (#166 Phase B, XR_DXR_local_3d_zone) — post-weave 2D content at a // client-window PIXEL rect (the 2D band). Same cross-device-bridge shape as wsui, @@ -2395,9 +2403,9 @@ int dxr_prov_reconcile_size(void) // s_pending getter exported by the wsui module (displayxr_window_space_ui.cpp). // C# DisplayXRWindowSpaceUI sets it via set_texture/set_layer regardless of which // session is active, so the provider can drive its own window-space layer. -extern "C" int displayxr_window_space_ui_get_pending(void **out_tex, int *out_tex_w, int *out_tex_h, - float *out_x, float *out_y, - float *out_lw, float *out_lh, float *out_disp); +extern "C" int displayxr_window_space_ui_get_pending_slot(int slot, void **out_tex, int *out_tex_w, + int *out_tex_h, float *out_x, float *out_y, + float *out_lw, float *out_lh, float *out_disp); // s_pending getter exported by the Local2D module (displayxr_local2d.cpp). The Metal // Local2D arm blits this Unity id straight into its overlay swapchain image @@ -2414,8 +2422,10 @@ extern "C" int displayxr_dedicated_parent_client_origin(int *ox, int *oy); // w×h. Format = B8G8R8A8_UNORM (87) to match Unity's URP wsui RT — CopyTextureRegion // is invalid across formats and the runtime's DComp path turns a mismatch into a // device-removal (#82 / runtime#216). Mirrors ps_create_bridge but arraySize=1. -static int ps_create_wsui(uint32_t w, uint32_t h) +static int ps_create_wsui(int slot, uint32_t w, uint32_t h) { + if (slot < 0 || slot >= PS_MAX_WSUI) return 0; + ProviderWsui *ws = &s_ps.wsui[slot]; // Vulkan (#247) Phase 1 covers the PRIMARY stereo path only. The secondary // composition layers (wsui / Local2D / extra 3D zones) each need their own // external-memory bridge and are not wired yet — bail cleanly rather than fall @@ -2426,16 +2436,16 @@ static int ps_create_wsui(uint32_t w, uint32_t h) // the render thread only, filled by the plugin-event copy. if (s_ps.graphics_api == DXR_GFX_VULKAN) { if (s_wsui_vk_disabled || w == 0 || h == 0 || !dxr_pvk_device_ready()) return 0; - if (s_ps.wsui_swapchain_created && s_ps.wsui_registered_w == w && s_ps.wsui_registered_h == h) + if (ws->swapchain_created && ws->registered_w == w && ws->registered_h == h) return 1; - if (s_ps.wsui_vk_failed_w == w && s_ps.wsui_vk_failed_h == h) return 0; - s_ps.wsui_vk_failed_w = w; s_ps.wsui_vk_failed_h = h; // cleared on success below - if (s_ps.wsui_swapchain && s_ps.pfn_destroy_swapchain) { - dxr_pvk_overlay_destroy(PS_PVK_OVERLAY_WSUI); - s_ps.pfn_destroy_swapchain(s_ps.wsui_swapchain); + if (ws->vk_failed_w == w && ws->vk_failed_h == h) return 0; + ws->vk_failed_w = w; ws->vk_failed_h = h; // cleared on success below + if (ws->swapchain && s_ps.pfn_destroy_swapchain) { + dxr_pvk_overlay_destroy((PS_PVK_OVERLAY_WSUI0 + slot)); + s_ps.pfn_destroy_swapchain(ws->swapchain); } - s_ps.wsui_swapchain = XR_NULL_HANDLE; - s_ps.wsui_swapchain_created = 0; + ws->swapchain = XR_NULL_HANDLE; + ws->swapchain_created = 0; uint32_t fmt_count = 0; s_ps.pfn_enumerate_swapchain_formats(s_ps.session, 0, &fmt_count, NULL); @@ -2459,13 +2469,13 @@ static int ps_create_wsui(uint32_t w, uint32_t h) ci.format = format; ci.sampleCount = 1; ci.width = w; ci.height = h; ci.faceCount = 1; ci.arraySize = 1; ci.mipCount = 1; - if (XR_FAILED(s_ps.pfn_create_swapchain(s_ps.session, &ci, &s_ps.wsui_swapchain))) { + if (XR_FAILED(s_ps.pfn_create_swapchain(s_ps.session, &ci, &ws->swapchain))) { ps_log("[DisplayXR-PROV] wsui: xrCreateSwapchain failed\n"); - s_ps.wsui_swapchain = XR_NULL_HANDLE; + ws->swapchain = XR_NULL_HANDLE; return 0; } uint32_t count = 0; - s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, 0, &count, NULL); + s_ps.pfn_enumerate_swapchain_images(ws->swapchain, 0, &count, NULL); if (count > PS_MAX_SWAPCHAIN_IMAGES) count = PS_MAX_SWAPCHAIN_IMAGES; XrSwapchainImageVulkanKHR_PS vk_imgs[PS_MAX_SWAPCHAIN_IMAGES] = {}; for (uint32_t i = 0; i < count; i++) { @@ -2473,21 +2483,21 @@ static int ps_create_wsui(uint32_t w, uint32_t h) vk_imgs[i].next = NULL; vk_imgs[i].image = NULL; } - if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, count, &count, + if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(ws->swapchain, count, &count, (XrSwapchainImageBaseHeader *)vk_imgs))) { ps_log("[DisplayXR-PROV] wsui: enumerate images (Vulkan) failed\n"); return 0; } - dxr_pvk_overlay_set_swapchain_images(PS_PVK_OVERLAY_WSUI, vk_imgs, count, format); - if (!dxr_pvk_overlay_create_bridge(PS_PVK_OVERLAY_WSUI, w, h, format)) { + dxr_pvk_overlay_set_swapchain_images((PS_PVK_OVERLAY_WSUI0 + slot), vk_imgs, count, format); + if (!dxr_pvk_overlay_create_bridge((PS_PVK_OVERLAY_WSUI0 + slot), w, h, format)) { ps_log("[DisplayXR-PROV] wsui: Vulkan overlay bridge create failed\n"); return 0; } - s_ps.wsui_w = w; s_ps.wsui_h = h; s_ps.wsui_format = format; - s_ps.wsui_image_count = count; - s_ps.wsui_registered_w = w; s_ps.wsui_registered_h = h; - s_ps.wsui_swapchain_created = 1; - s_ps.wsui_vk_failed_w = s_ps.wsui_vk_failed_h = 0; + ws->w = w; ws->h = h; ws->format = format; + ws->image_count = count; + ws->registered_w = w; ws->registered_h = h; + ws->swapchain_created = 1; + ws->vk_failed_w = ws->vk_failed_h = 0; ps_log("[DisplayXR-PROV] wsui: Vulkan swapchain %ux%u (%u imgs, fmt=%lld) + overlay bridge\n", w, h, count, (long long)format); return 1; @@ -2498,13 +2508,13 @@ static int ps_create_wsui(uint32_t w, uint32_t h) // submit blits the C#-registered Unity id straight in (same device). if (w == 0 || h == 0) return 0; if (s_ps.graphics_api != DXR_GFX_METAL || !s_ps.metal_queue) return 0; - if (s_ps.wsui_swapchain_created && s_ps.wsui_registered_w == w && s_ps.wsui_registered_h == h) + if (ws->swapchain_created && ws->registered_w == w && ws->registered_h == h) return 1; // already sized for this RT - if (s_ps.wsui_swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(s_ps.wsui_swapchain); - s_ps.wsui_swapchain = XR_NULL_HANDLE; - s_ps.wsui_swapchain_created = 0; - s_ps.wsui_image_acquired = 0; + if (ws->swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(ws->swapchain); + ws->swapchain = XR_NULL_HANDLE; + ws->swapchain_created = 0; + ws->image_acquired = 0; // Prefer BGRA8Unorm (80) to match Unity's B8G8R8A8_UNorm OverlayTexture so the // submit blit is same-format; fall back to the first enumerated format. @@ -2525,24 +2535,24 @@ static int ps_create_wsui(uint32_t w, uint32_t h) ci.sampleCount = 1; ci.width = w; ci.height = h; ci.faceCount = 1; ci.arraySize = 1; ci.mipCount = 1; - if (XR_FAILED(s_ps.pfn_create_swapchain(s_ps.session, &ci, &s_ps.wsui_swapchain))) { - ps_log("[DisplayXR-PROV] wsui: xrCreateSwapchain failed\n"); s_ps.wsui_swapchain = XR_NULL_HANDLE; return 0; + if (XR_FAILED(s_ps.pfn_create_swapchain(s_ps.session, &ci, &ws->swapchain))) { + ps_log("[DisplayXR-PROV] wsui: xrCreateSwapchain failed\n"); ws->swapchain = XR_NULL_HANDLE; return 0; } uint32_t count = 0; - s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, 0, &count, NULL); + s_ps.pfn_enumerate_swapchain_images(ws->swapchain, 0, &count, NULL); if (count > PS_MAX_SWAPCHAIN_IMAGES) count = PS_MAX_SWAPCHAIN_IMAGES; for (uint32_t i = 0; i < count; i++) { - s_ps.wsui_images_metal[i].type = XR_TYPE_SWAPCHAIN_IMAGE_METAL_KHR; - s_ps.wsui_images_metal[i].next = NULL; s_ps.wsui_images_metal[i].texture = NULL; + ws->images_metal[i].type = XR_TYPE_SWAPCHAIN_IMAGE_METAL_KHR; + ws->images_metal[i].next = NULL; ws->images_metal[i].texture = NULL; } - if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, count, &count, - (XrSwapchainImageBaseHeader *)s_ps.wsui_images_metal))) { + if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(ws->swapchain, count, &count, + (XrSwapchainImageBaseHeader *)ws->images_metal))) { ps_log("[DisplayXR-PROV] wsui: enumerate images (Metal) failed\n"); return 0; } - s_ps.wsui_w = w; s_ps.wsui_h = h; s_ps.wsui_format = format; - s_ps.wsui_image_count = count; - s_ps.wsui_registered_w = w; s_ps.wsui_registered_h = h; - s_ps.wsui_swapchain_created = 1; + ws->w = w; ws->h = h; ws->format = format; + ws->image_count = count; + ws->registered_w = w; ws->registered_h = h; + ws->swapchain_created = 1; ps_log("[DisplayXR-PROV] wsui: Metal swapchain %ux%u (%u imgs, fmt=%lld)\n", w, h, count, (long long)format); return 1; @@ -2550,20 +2560,20 @@ static int ps_create_wsui(uint32_t w, uint32_t h) int is_d3d11 = (s_ps.graphics_api == DXR_GFX_D3D11); if (w == 0 || h == 0) return 0; if (is_d3d11 ? !s_ps.unity_d3d11_device : (!s_ps.own_device || !s_ps.unity_device)) return 0; - if (s_ps.wsui_swapchain_created && s_ps.wsui_registered_w == w && s_ps.wsui_registered_h == h) + if (ws->swapchain_created && ws->registered_w == w && ws->registered_h == h) return 1; // already sized for this RT // Tear down any previous (RT resized). - if (s_ps.wsui_swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(s_ps.wsui_swapchain); - if (s_ps.wsui_bridge_unity) { s_ps.wsui_bridge_unity->Release(); s_ps.wsui_bridge_unity = NULL; } - if (s_ps.wsui_bridge_own) { s_ps.wsui_bridge_own->Release(); s_ps.wsui_bridge_own = NULL; } - if (s_ps.wsui_bridge_handle) { CloseHandle(s_ps.wsui_bridge_handle); s_ps.wsui_bridge_handle = NULL; } - if (s_ps.wsui_unity_tex) { s_ps.wsui_unity_tex->Release(); s_ps.wsui_unity_tex = NULL; } - if (s_ps.wsui_unity_tex_own){ s_ps.wsui_unity_tex_own->Release(); s_ps.wsui_unity_tex_own = NULL; } - s_ps.wsui_unity_tex_handle = NULL; // NT handle already closed in ps_alloc_shared_tex_d3d11 - s_ps.wsui_swapchain = XR_NULL_HANDLE; - s_ps.wsui_swapchain_created = 0; - s_ps.wsui_image_acquired = 0; + if (ws->swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(ws->swapchain); + if (ws->bridge_unity) { ws->bridge_unity->Release(); ws->bridge_unity = NULL; } + if (ws->bridge_own) { ws->bridge_own->Release(); ws->bridge_own = NULL; } + if (ws->bridge_handle) { CloseHandle(ws->bridge_handle); ws->bridge_handle = NULL; } + if (ws->unity_tex) { ws->unity_tex->Release(); ws->unity_tex = NULL; } + if (ws->unity_tex_own){ ws->unity_tex_own->Release(); ws->unity_tex_own = NULL; } + ws->unity_tex_handle = NULL; // NT handle already closed in ps_alloc_shared_tex_d3d11 + ws->swapchain = XR_NULL_HANDLE; + ws->swapchain_created = 0; + ws->image_acquired = 0; // --- Overlay swapchain (arraySize=1, format 87 to match Unity URP wsui RT) --- uint32_t fmt_count = 0; @@ -2583,43 +2593,43 @@ static int ps_create_wsui(uint32_t w, uint32_t h) ci.sampleCount = 1; ci.width = w; ci.height = h; ci.faceCount = 1; ci.arraySize = 1; ci.mipCount = 1; - if (XR_FAILED(s_ps.pfn_create_swapchain(s_ps.session, &ci, &s_ps.wsui_swapchain))) { - ps_log("[DisplayXR-PROV] wsui: xrCreateSwapchain failed\n"); s_ps.wsui_swapchain = XR_NULL_HANDLE; return 0; + if (XR_FAILED(s_ps.pfn_create_swapchain(s_ps.session, &ci, &ws->swapchain))) { + ps_log("[DisplayXR-PROV] wsui: xrCreateSwapchain failed\n"); ws->swapchain = XR_NULL_HANDLE; return 0; } uint32_t count = 0; - s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, 0, &count, NULL); + s_ps.pfn_enumerate_swapchain_images(ws->swapchain, 0, &count, NULL); if (count > PS_MAX_SWAPCHAIN_IMAGES) count = PS_MAX_SWAPCHAIN_IMAGES; if (is_d3d11) { for (uint32_t i = 0; i < count; i++) { - s_ps.wsui_images_d3d11[i].type = XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR; - s_ps.wsui_images_d3d11[i].next = NULL; s_ps.wsui_images_d3d11[i].texture = NULL; + ws->images_d3d11[i].type = XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR; + ws->images_d3d11[i].next = NULL; ws->images_d3d11[i].texture = NULL; } - if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, count, &count, - (XrSwapchainImageBaseHeader *)s_ps.wsui_images_d3d11))) { + if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(ws->swapchain, count, &count, + (XrSwapchainImageBaseHeader *)ws->images_d3d11))) { ps_log("[DisplayXR-PROV] wsui: enumerate images (D3D11) failed\n"); return 0; } } else { for (uint32_t i = 0; i < count; i++) { - s_ps.wsui_images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR; - s_ps.wsui_images[i].next = NULL; s_ps.wsui_images[i].texture = NULL; + ws->images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR; + ws->images[i].next = NULL; ws->images[i].texture = NULL; } - if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(s_ps.wsui_swapchain, count, &count, - (XrSwapchainImageBaseHeader *)s_ps.wsui_images))) { + if (XR_FAILED(s_ps.pfn_enumerate_swapchain_images(ws->swapchain, count, &count, + (XrSwapchainImageBaseHeader *)ws->images))) { ps_log("[DisplayXR-PROV] wsui: enumerate images failed\n"); return 0; } } - s_ps.wsui_image_count = count; + ws->image_count = count; if (is_d3d11 && s_ps.d3d11_bridge) { // D3D11 editor bridge: an own-device shared BGRA8 texture opened on Unity's device // (the C# CopyTexture target); submit copies own->image on the own context. - if (!ps_alloc_shared_tex_d3d11(w, h, 1, &s_ps.wsui_unity_tex_own, &s_ps.wsui_unity_tex, - &s_ps.wsui_unity_tex_handle, 87, "wsui bridge (D3D11)")) return 0; + if (!ps_alloc_shared_tex_d3d11(w, h, 1, &ws->unity_tex_own, &ws->unity_tex, + &ws->unity_tex_handle, 87, "wsui bridge (D3D11)")) return 0; } else if (is_d3d11) { // D3D11 zero-copy: a plain Unity-device BGRA8 texture as the C# CopyTexture target; // submit same-device-copies it into the acquired image (no bridge). - s_ps.wsui_unity_tex = ps_alloc_unity_tex(w, h, 1, 87); - if (!s_ps.wsui_unity_tex) { ps_log("[DisplayXR-PROV] wsui: Unity-device target alloc failed\n"); return 0; } + ws->unity_tex = ps_alloc_unity_tex(w, h, 1, 87); + if (!ws->unity_tex) { ps_log("[DisplayXR-PROV] wsui: Unity-device target alloc failed\n"); return 0; } } else { // --- Cross-device bridge (own_device shared BGRA8, opened on Unity's device) --- D3D12_HEAP_PROPERTIES heap = {}; heap.Type = D3D12_HEAP_TYPE_DEFAULT; @@ -2631,21 +2641,21 @@ static int ps_create_wsui(uint32_t w, uint32_t h) bd.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; bd.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; HRESULT hr = s_ps.own_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_SHARED, &bd, - D3D12_RESOURCE_STATE_COMMON, NULL, __uuidof(ID3D12Resource), (void **)&s_ps.wsui_bridge_own); + D3D12_RESOURCE_STATE_COMMON, NULL, __uuidof(ID3D12Resource), (void **)&ws->bridge_own); if (FAILED(hr)) { ps_log("[DisplayXR-PROV] wsui bridge create failed: 0x%08lx\n", hr); return 0; } - hr = s_ps.own_device->CreateSharedHandle(s_ps.wsui_bridge_own, NULL, GENERIC_ALL, NULL, &s_ps.wsui_bridge_handle); - if (FAILED(hr) || !s_ps.wsui_bridge_handle) { ps_log("[DisplayXR-PROV] wsui bridge share failed\n"); return 0; } - hr = s_ps.unity_device->OpenSharedHandle(s_ps.wsui_bridge_handle, __uuidof(ID3D12Resource), - (void **)&s_ps.wsui_bridge_unity); - if (FAILED(hr) || !s_ps.wsui_bridge_unity) { ps_log("[DisplayXR-PROV] wsui bridge open(Unity) failed\n"); return 0; } + hr = s_ps.own_device->CreateSharedHandle(ws->bridge_own, NULL, GENERIC_ALL, NULL, &ws->bridge_handle); + if (FAILED(hr) || !ws->bridge_handle) { ps_log("[DisplayXR-PROV] wsui bridge share failed\n"); return 0; } + hr = s_ps.unity_device->OpenSharedHandle(ws->bridge_handle, __uuidof(ID3D12Resource), + (void **)&ws->bridge_unity); + if (FAILED(hr) || !ws->bridge_unity) { ps_log("[DisplayXR-PROV] wsui bridge open(Unity) failed\n"); return 0; } } - s_ps.wsui_w = w; s_ps.wsui_h = h; s_ps.wsui_format = format; - s_ps.wsui_registered_w = w; s_ps.wsui_registered_h = h; - s_ps.wsui_swapchain_created = 1; + ws->w = w; ws->h = h; ws->format = format; + ws->registered_w = w; ws->registered_h = h; + ws->swapchain_created = 1; ps_log("[DisplayXR-PROV] wsui: swapchain %ux%u (%u imgs, fmt=%lld) target=%p (%s)\n", w, h, count, (long long)format, - is_d3d11 ? (void *)s_ps.wsui_unity_tex : (void *)s_ps.wsui_bridge_unity, + is_d3d11 ? (void *)ws->unity_tex : (void *)ws->bridge_unity, is_d3d11 ? "D3D11 zero-copy" : "D3D12 bridge"); return 1; #else @@ -2659,8 +2669,8 @@ static int ps_create_wsui(uint32_t w, uint32_t h) // bridge, then Graphics.CopyTexture's its canvas RT into it each frame. Lazily // creates the swapchain+bridge sized to w×h. Returns the Unity-side ID3D12Resource* // (or NULL). Declared DISPLAYXR_EXPORT in the header so it's exported for P/Invoke. -void dxr_prov_get_wsui_bridge(uint32_t w, uint32_t h, - void **out_ptr, uint32_t *out_w, uint32_t *out_h) +void dxr_prov_get_wsui_bridge_slot(int slot, uint32_t w, uint32_t h, + void **out_ptr, uint32_t *out_w, uint32_t *out_h) { if (out_ptr) *out_ptr = NULL; if (out_w) *out_w = 0; @@ -2677,39 +2687,48 @@ void dxr_prov_get_wsui_bridge(uint32_t w, uint32_t h, return; } #endif - if (!ps_create_wsui(w, h)) return; + if (slot < 0 || slot >= PS_MAX_WSUI) return; + ProviderWsui *ws = &s_ps.wsui[slot]; + if (!ps_create_wsui(slot, w, h)) return; #ifdef _WIN32 if (out_ptr) *out_ptr = (s_ps.graphics_api == DXR_GFX_D3D11) - ? (void *)s_ps.wsui_unity_tex : (void *)s_ps.wsui_bridge_unity; - if (out_w) *out_w = s_ps.wsui_w; - if (out_h) *out_h = s_ps.wsui_h; + ? (void *)ws->unity_tex : (void *)ws->bridge_unity; + if (out_w) *out_w = ws->w; + if (out_h) *out_h = ws->h; #endif } +void dxr_prov_get_wsui_bridge(uint32_t w, uint32_t h, + void **out_ptr, uint32_t *out_w, uint32_t *out_h) +{ + dxr_prov_get_wsui_bridge_slot(0, w, h, out_ptr, out_w, out_h); +} + // Per-frame: if a wsui texture is registered and the bridge is ready, copy // wsui_bridge_own -> the acquired overlay swapchain image (own device) and fill // out_layer. Returns 1 if the layer should be submitted, else 0. Called from // dxr_prov_submit_frame AFTER the projection bridge copy (own_cmd_list is free and // the shared-fence wait already ordered the own queue after Unity's writes). // The XrCompositionLayerWindowSpaceDXR fields every backend fills the same way. -static void ps_fill_wsui_layer(XrCompositionLayerWindowSpaceDXR *out_layer, +static void ps_fill_wsui_layer(const ProviderWsui *ws, XrCompositionLayerWindowSpaceDXR *out_layer, float lx, float ly, float lw, float lh, float ldisp) { out_layer->type = XR_TYPE_COMPOSITION_LAYER_WINDOW_SPACE_DXR; out_layer->next = NULL; out_layer->layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; - out_layer->subImage.swapchain = s_ps.wsui_swapchain; + out_layer->subImage.swapchain = ws->swapchain; out_layer->subImage.imageRect.offset = {0, 0}; - out_layer->subImage.imageRect.extent = {(int32_t)s_ps.wsui_w, (int32_t)s_ps.wsui_h}; + out_layer->subImage.imageRect.extent = {(int32_t)ws->w, (int32_t)ws->h}; out_layer->subImage.imageArrayIndex = 0; out_layer->x = lx; out_layer->y = ly; out_layer->width = lw; out_layer->height = lh; out_layer->disparity = ldisp; } -static int ps_submit_wsui(XrCompositionLayerWindowSpaceDXR *out_layer) +static int ps_submit_wsui(int slot, XrCompositionLayerWindowSpaceDXR *out_layer) { - if (!out_layer) return 0; + if (!out_layer || slot < 0 || slot >= PS_MAX_WSUI) return 0; + ProviderWsui *ws = &s_ps.wsui[slot]; memset(out_layer, 0, sizeof(*out_layer)); #if defined(ENABLE_VULKAN) // Vulkan (#336): twin of the Local2D arm in ps_submit_local2d. @@ -2717,25 +2736,25 @@ static int ps_submit_wsui(XrCompositionLayerWindowSpaceDXR *out_layer) if (s_wsui_vk_disabled) return 0; void *tex = NULL; int tw = 0, th = 0; float lx = 0, ly = 0, lw = 0, lh = 0, ldisp = 0; - if (!displayxr_window_space_ui_get_pending(&tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) + if (!displayxr_window_space_ui_get_pending_slot(slot, &tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) return 0; - if (!ps_create_wsui((uint32_t)tw, (uint32_t)th)) return 0; - if (!dxr_pvk_overlay_has_content(PS_PVK_OVERLAY_WSUI)) return 0; + if (!ps_create_wsui(slot, (uint32_t)tw, (uint32_t)th)) return 0; + if (!dxr_pvk_overlay_has_content((PS_PVK_OVERLAY_WSUI0 + slot))) return 0; uint32_t idx = 0; XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; - if (XR_FAILED(s_ps.pfn_acquire_swapchain_image(s_ps.wsui_swapchain, &ai, &idx)) || - idx >= s_ps.wsui_image_count) + if (XR_FAILED(s_ps.pfn_acquire_swapchain_image(ws->swapchain, &ai, &idx)) || + idx >= ws->image_count) return 0; XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = 1000000000; - s_ps.pfn_wait_swapchain_image(s_ps.wsui_swapchain, &wi); - int copied = dxr_pvk_overlay_copy_to_swapchain_image(PS_PVK_OVERLAY_WSUI, idx); + s_ps.pfn_wait_swapchain_image(ws->swapchain, &wi); + int copied = dxr_pvk_overlay_copy_to_swapchain_image((PS_PVK_OVERLAY_WSUI0 + slot), idx); XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - s_ps.pfn_release_swapchain_image(s_ps.wsui_swapchain, &ri); + s_ps.pfn_release_swapchain_image(ws->swapchain, &ri); if (!copied) return 0; - ps_fill_wsui_layer(out_layer, lx, ly, lw, lh, ldisp); + ps_fill_wsui_layer(ws, out_layer, lx, ly, lw, lh, ldisp); return 1; } #endif @@ -2745,77 +2764,77 @@ static int ps_submit_wsui(XrCompositionLayerWindowSpaceDXR *out_layer) if (s_ps.graphics_api != DXR_GFX_METAL) return 0; void *tex = NULL; int tw = 0, th = 0; float lx = 0, ly = 0, lw = 0, lh = 0, ldisp = 0; - if (!displayxr_window_space_ui_get_pending(&tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) + if (!displayxr_window_space_ui_get_pending_slot(slot, &tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) return 0; // no UI texture registered this frame - if (!ps_create_wsui((uint32_t)tw, (uint32_t)th)) return 0; - if (!s_ps.wsui_swapchain_created || s_ps.wsui_image_count == 0) return 0; + if (!ps_create_wsui(slot, (uint32_t)tw, (uint32_t)th)) return 0; + if (!ws->swapchain_created || ws->image_count == 0) return 0; uint32_t idx = 0; XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; - if (XR_FAILED(s_ps.pfn_acquire_swapchain_image(s_ps.wsui_swapchain, &ai, &idx)) || - idx >= s_ps.wsui_image_count) + if (XR_FAILED(s_ps.pfn_acquire_swapchain_image(ws->swapchain, &ai, &idx)) || + idx >= ws->image_count) return 0; XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = 1000000000; - s_ps.pfn_wait_swapchain_image(s_ps.wsui_swapchain, &wi); + s_ps.pfn_wait_swapchain_image(ws->swapchain, &wi); // Same-device blit Unity UI texture -> acquired image on the session queue. // Ordered after Unity's OverlayCamera render by the frame's order_weave wait // CB (session-queue FIFO — order_weave is committed earlier in submit); the // compositor's weave at xrEndFrame is FIFO after this blit on the same queue. - if (s_ps.wsui_images_metal[idx].texture) - displayxr_metal_blit_textures(s_ps.metal_queue, tex, s_ps.wsui_images_metal[idx].texture); + if (ws->images_metal[idx].texture) + displayxr_metal_blit_textures(s_ps.metal_queue, tex, ws->images_metal[idx].texture); XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - s_ps.pfn_release_swapchain_image(s_ps.wsui_swapchain, &ri); + s_ps.pfn_release_swapchain_image(ws->swapchain, &ri); - ps_fill_wsui_layer(out_layer, lx, ly, lw, lh, ldisp); + ps_fill_wsui_layer(ws, out_layer, lx, ly, lw, lh, ldisp); return 1; #elif defined(_WIN32) void *tex = NULL; int tw = 0, th = 0; float lx = 0, ly = 0, lw = 0, lh = 0, ldisp = 0; - if (!displayxr_window_space_ui_get_pending(&tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) + if (!displayxr_window_space_ui_get_pending_slot(slot, &tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) return 0; int is_d3d11 = (s_ps.graphics_api == DXR_GFX_D3D11); - if (!s_ps.wsui_swapchain_created || s_ps.wsui_image_count == 0 || - (is_d3d11 ? !s_ps.wsui_unity_tex : !s_ps.wsui_bridge_own)) + if (!ws->swapchain_created || ws->image_count == 0 || + (is_d3d11 ? !ws->unity_tex : !ws->bridge_own)) return 0; // C# hasn't requested the target yet (no get_wsui_bridge call) // Acquire + wait an overlay swapchain image. uint32_t idx = 0; XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; - if (XR_FAILED(s_ps.pfn_acquire_swapchain_image(s_ps.wsui_swapchain, &ai, &idx)) || - idx >= s_ps.wsui_image_count) + if (XR_FAILED(s_ps.pfn_acquire_swapchain_image(ws->swapchain, &ai, &idx)) || + idx >= ws->image_count) return 0; XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = 1000000000; - s_ps.pfn_wait_swapchain_image(s_ps.wsui_swapchain, &wi); + s_ps.pfn_wait_swapchain_image(ws->swapchain, &wi); if (is_d3d11 && s_ps.d3d11_bridge) { // D3D11 editor bridge: own-context copy own-side shared tex -> acquired image // (the frame's single fence Wait already ordered the own context after Unity). - if (s_ps.wsui_images_d3d11[idx].texture && s_ps.wsui_unity_tex_own && s_ps.own_d3d11_context) - s_ps.own_d3d11_context->CopyResource(s_ps.wsui_images_d3d11[idx].texture, s_ps.wsui_unity_tex_own); + if (ws->images_d3d11[idx].texture && ws->unity_tex_own && s_ps.own_d3d11_context) + s_ps.own_d3d11_context->CopyResource(ws->images_d3d11[idx].texture, ws->unity_tex_own); } else if (is_d3d11) { // D3D11 zero-copy: same-device copy Unity target -> acquired image (no bridge/fence). - if (s_ps.wsui_images_d3d11[idx].texture && s_ps.wsui_unity_tex && s_ps.unity_d3d11_context) - s_ps.unity_d3d11_context->CopyResource(s_ps.wsui_images_d3d11[idx].texture, s_ps.wsui_unity_tex); + if (ws->images_d3d11[idx].texture && ws->unity_tex && s_ps.unity_d3d11_context) + s_ps.unity_d3d11_context->CopyResource(ws->images_d3d11[idx].texture, ws->unity_tex); } else // own_device copy bridge -> swapchain image (single subresource). - if (s_ps.wsui_images[idx].texture && s_ps.own_cmd_list) { + if (ws->images[idx].texture && s_ps.own_cmd_list) { s_ps.own_cmd_alloc->Reset(); s_ps.own_cmd_list->Reset(s_ps.own_cmd_alloc, NULL); - ps_sc_image_barrier(s_ps.own_cmd_list, s_ps.wsui_images[idx].texture, + ps_sc_image_barrier(s_ps.own_cmd_list, ws->images[idx].texture, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_COPY_DEST); D3D12_TEXTURE_COPY_LOCATION dl = {}; - dl.pResource = s_ps.wsui_images[idx].texture; + dl.pResource = ws->images[idx].texture; dl.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; dl.SubresourceIndex = 0; D3D12_TEXTURE_COPY_LOCATION sl = {}; - sl.pResource = s_ps.wsui_bridge_own; + sl.pResource = ws->bridge_own; sl.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; sl.SubresourceIndex = 0; s_ps.own_cmd_list->CopyTextureRegion(&dl, 0, 0, 0, &sl, NULL); - ps_sc_image_barrier(s_ps.own_cmd_list, s_ps.wsui_images[idx].texture, + ps_sc_image_barrier(s_ps.own_cmd_list, ws->images[idx].texture, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_RENDER_TARGET); s_ps.own_cmd_list->Close(); ID3D12CommandList *lists[] = { s_ps.own_cmd_list }; @@ -2829,9 +2848,9 @@ static int ps_submit_wsui(XrCompositionLayerWindowSpaceDXR *out_layer) } XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - s_ps.pfn_release_swapchain_image(s_ps.wsui_swapchain, &ri); + s_ps.pfn_release_swapchain_image(ws->swapchain, &ri); - ps_fill_wsui_layer(out_layer, lx, ly, lw, lh, ldisp); + ps_fill_wsui_layer(ws, out_layer, lx, ly, lw, lh, ldisp); return 1; #else // Linux without ENABLE_VULKAN: no backend can submit wsui. @@ -3117,21 +3136,23 @@ int dxr_prov_local2d_needs_copy(void) return 0; } -int dxr_prov_wsui_needs_copy(void) +int dxr_prov_wsui_needs_copy_slot(int slot) { #if defined(ENABLE_VULKAN) - if (s_ps.graphics_api == DXR_GFX_VULKAN) - return dxr_pvk_overlay_needs_copy(PS_PVK_OVERLAY_WSUI); + if (s_ps.graphics_api == DXR_GFX_VULKAN && slot >= 0 && slot < PS_MAX_WSUI) + return dxr_pvk_overlay_needs_copy(PS_PVK_OVERLAY_WSUI0 + slot); #endif + (void)slot; return 0; } -void dxr_prov_wsui_request_copy(void) +void dxr_prov_wsui_request_copy_slot(int slot) { #if defined(ENABLE_VULKAN) - if (s_ps.graphics_api == DXR_GFX_VULKAN) - dxr_pvk_overlay_request_copy(PS_PVK_OVERLAY_WSUI); + if (s_ps.graphics_api == DXR_GFX_VULKAN && slot >= 0 && slot < PS_MAX_WSUI) + dxr_pvk_overlay_request_copy(PS_PVK_OVERLAY_WSUI0 + slot); #endif + (void)slot; } void dxr_prov_local2d_request_copy(void) @@ -4995,14 +5016,17 @@ int dxr_prov_session_start(const char *runtime_json_path, void dxr_prov_session_stop(void) { - if (s_ps.wsui_swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(s_ps.wsui_swapchain); + for (int i = 0; i < PS_MAX_WSUI; i++) { + ProviderWsui *ws = &s_ps.wsui[i]; + if (ws->swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(ws->swapchain); #ifdef _WIN32 - if (s_ps.wsui_bridge_unity) s_ps.wsui_bridge_unity->Release(); - if (s_ps.wsui_bridge_handle) CloseHandle(s_ps.wsui_bridge_handle); - if (s_ps.wsui_bridge_own) s_ps.wsui_bridge_own->Release(); - if (s_ps.wsui_unity_tex) s_ps.wsui_unity_tex->Release(); - if (s_ps.wsui_unity_tex_own) s_ps.wsui_unity_tex_own->Release(); // D3D11 editor bridge (NT handle closed at alloc) + if (ws->bridge_unity) ws->bridge_unity->Release(); + if (ws->bridge_handle) CloseHandle(ws->bridge_handle); + if (ws->bridge_own) ws->bridge_own->Release(); + if (ws->unity_tex) ws->unity_tex->Release(); + if (ws->unity_tex_own) ws->unity_tex_own->Release(); // D3D11 editor bridge (NT handle closed at alloc) #endif + } if (s_ps.l2d_swapchain && s_ps.pfn_destroy_swapchain) s_ps.pfn_destroy_swapchain(s_ps.l2d_swapchain); #ifdef _WIN32 if (s_ps.l2d_bridge_unity) s_ps.l2d_bridge_unity->Release(); @@ -7107,19 +7131,25 @@ int dxr_prov_submit_frame(uint32_t image_index) int has_l2d = ps_submit_local2d(&l2d_layer); // Window-space UI (HUD). Composites over everything (fractional coords + disparity). - XrCompositionLayerWindowSpaceDXR wsui_layer = {}; - int has_wsui = ps_submit_wsui(&wsui_layer); + XrCompositionLayerWindowSpaceDXR wsui_layer[PS_MAX_WSUI] = {}; + int wsui_has[PS_MAX_WSUI] = {}; + int has_wsui = 0; + for (int i = 0; i < PS_MAX_WSUI; i++) { + wsui_has[i] = ps_submit_wsui(i, &wsui_layer[i]); + has_wsui |= wsui_has[i]; + } // Order: 3D projections (primary + extra zones) under, Local2D bands over the 3D, // HUD on top (canonical zones rule — 1 zone projection per 3D zone + 1 Local2D per // 2D band, all ALPHA_BLEND). - const XrCompositionLayerBaseHeader *layers[PS_MAX_ZONES + 2]; + const XrCompositionLayerBaseHeader *layers[PS_MAX_ZONES + 1 + PS_MAX_WSUI]; // zones + Local2D + wsui slots uint32_t lc = 0; layers[lc++] = (const XrCompositionLayerBaseHeader *)&layer; for (uint32_t i = 0; i < s_ps.extra_zone_count; i++) if (extra_has[i]) layers[lc++] = (const XrCompositionLayerBaseHeader *)&extra_layer[i]; if (has_l2d) layers[lc++] = (const XrCompositionLayerBaseHeader *)&l2d_layer; - if (has_wsui) layers[lc++] = (const XrCompositionLayerBaseHeader *)&wsui_layer; + for (int i = 0; i < PS_MAX_WSUI; i++) + if (wsui_has[i]) layers[lc++] = (const XrCompositionLayerBaseHeader *)&wsui_layer[i]; // D3D11 (#195): make sure the runtime's weave in xrEndFrame reads FINISHED content. // EDITOR bridge: the primary + secondary copies all ran on the OWN immediate context — diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.h b/native~/displayxr_xrprovider/displayxr_provider_session.h index 265d00d2..666b3535 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.h +++ b/native~/displayxr_xrprovider/displayxr_provider_session.h @@ -326,9 +326,10 @@ DISPLAYXR_EXPORT int dxr_prov_local2d_needs_copy(void); /// No-op on the other backends. DISPLAYXR_EXPORT void dxr_prov_local2d_request_copy(void); -/// (#336) wsui twins of the two Local2D calls above (Vulkan only, inert elsewhere). -DISPLAYXR_EXPORT int dxr_prov_wsui_needs_copy(void); -DISPLAYXR_EXPORT void dxr_prov_wsui_request_copy(void); +/// (#336) wsui twins of the two Local2D calls above, per wsui slot (Vulkan only, +/// inert elsewhere). +DISPLAYXR_EXPORT int dxr_prov_wsui_needs_copy_slot(int slot); +DISPLAYXR_EXPORT void dxr_prov_wsui_request_copy_slot(int slot); /// Set the Local2D dest rect in client-window PIXELS (post-DPI). w<=0||h<=0 clears /// (layer inactive). Cheap; safe to call every frame. @@ -567,6 +568,10 @@ DISPLAYXR_EXPORT void dxr_prov_get_render_rect(uint32_t *out_w, uint32_t *out_h) /// handle of the bridge. C# (DisplayXRWindowSpaceUI) Graphics.CopyTexture's its /// canvas RT into it each frame; the provider submits the wsui composition layer. /// Returns *out_ptr = NULL when no session is running. +/// Per-slot form (one window-space layer per DisplayXRWindowSpaceUI). The slot-less +/// call above/below addresses slot 0. +DISPLAYXR_EXPORT void dxr_prov_get_wsui_bridge_slot(int slot, uint32_t w, uint32_t h, + void **out_ptr, uint32_t *out_w, uint32_t *out_h); DISPLAYXR_EXPORT void dxr_prov_get_wsui_bridge(uint32_t w, uint32_t h, void **out_ptr, uint32_t *out_w, uint32_t *out_h); From be788da4f1b522a0c9bdecdeeef10a7cc5484703 Mon Sep 17 00:00:00 2001 From: byungjul Date: Thu, 1 Oct 2026 17:21:21 +0900 Subject: [PATCH 2/2] fix(#336): review fixes for the wsui slots - A slot taken over by another component never shows the previous owner's last image: the Vulkan overlay remembers which registered texture its content was copied from (dxr_pvk_overlay_content_source) and submit only composites content from the texture registered NOW. Same check for Local2D. - One slot count: DXR_WSUI_MAX_SLOTS (displayxr_window_space_ui.h) now drives PS_MAX_WSUI, DXR_PVK_OVERLAY_COUNT, the per-slot render events and the overlay labels; the duplicate get_pending_slot externs are gone. - C#: a component that found every slot taken retries about once a second and appears when another HUD frees one; slots are only taken in Play mode (an [ExecuteAlways] edit-mode or prefab-stage instance would starve the play-mode ones); the legacy slot-less fallback is replaced by going inert (with a warning) on a native plugin without the slot API; the Vulkan-copy EntryPointNotFound disable now latches instead of re-arming every frame. Co-Authored-By: Claude Opus 5.5 --- Runtime/DisplayXRWindowSpaceUI.cs | 84 ++++++++++++------- native~/displayxr_unity_plugin.cpp | 16 ++-- .../displayxr_provider_gfx_vulkan.cpp | 26 ++++-- .../displayxr_provider_gfx_vulkan.h | 15 +++- .../displayxr_provider_session.cpp | 24 ++++-- 5 files changed, 108 insertions(+), 57 deletions(-) diff --git a/Runtime/DisplayXRWindowSpaceUI.cs b/Runtime/DisplayXRWindowSpaceUI.cs index d3ba64e4..cffa0978 100644 --- a/Runtime/DisplayXRWindowSpaceUI.cs +++ b/Runtime/DisplayXRWindowSpaceUI.cs @@ -133,11 +133,11 @@ public class DisplayXRWindowSpaceUI : MonoBehaviour // (#336) Vulkan has no wrappable bridge: the getter returns a session token in // place of m_BridgePtr, and the copy is a plugin event on the render thread. private bool m_VulkanBridge; - // This component's wsui slot (#336 follow-up): -1 = none (every slot taken, or - // not acquired yet). m_LegacySlot = the native plugin predates slots, so the - // slot-less API (one shared layer) is used. + // This component's wsui slot (#336 follow-up): -1 = none (every slot taken, edit + // mode, or not acquired yet). private int m_Slot = -1; - private bool m_LegacySlot; + private bool m_SlotApiMissing, m_WarnedNoSlot, m_VulkanCopyBroken; + private float m_NextSlotRetry; private System.IntPtr m_RenderEventFunc = System.IntPtr.Zero; // Saved state, restored in OnDisable. @@ -293,14 +293,9 @@ private void TryAcquireBridge() // Custom display-provider mode: the provider owns a SEPARATE // D3D12 device, so it exposes its own cross-device wsui bridge. (#166) if (m_Slot < 0) return; // no slot: this layer isn't shown - if (m_LegacySlot) - DisplayXRProviderNative.dxr_prov_get_wsui_bridge( - (uint)m_RtSize.x, (uint)m_RtSize.y, - out bridgePtr, out bw, out bh); - else - DisplayXRProviderNative.dxr_prov_get_wsui_bridge_slot( - m_Slot, (uint)m_RtSize.x, (uint)m_RtSize.y, - out bridgePtr, out bw, out bh); + DisplayXRProviderNative.dxr_prov_get_wsui_bridge_slot( + m_Slot, (uint)m_RtSize.x, (uint)m_RtSize.y, + out bridgePtr, out bw, out bh); } if (bridgePtr == System.IntPtr.Zero || bw == 0 || bh == 0) { @@ -310,7 +305,8 @@ private void TryAcquireBridge() m_BridgePtr = System.IntPtr.Zero; return; } - m_VulkanBridge = SystemInfo.graphicsDeviceType == GraphicsDeviceType.Vulkan; + m_VulkanBridge = !m_VulkanCopyBroken && + SystemInfo.graphicsDeviceType == GraphicsDeviceType.Vulkan; if (m_VulkanBridge) { // Vulkan (#336): bridgePtr is a session token, never a texture. @@ -356,7 +352,7 @@ private void IssueVulkanCopy() if (m_BridgePtr == System.IntPtr.Zero) return; try { - if (m_Slot < 0 || m_LegacySlot) return; + if (m_Slot < 0) return; DisplayXRProviderNative.dxr_prov_wsui_request_copy_slot(m_Slot); if (DisplayXRProviderNative.dxr_prov_wsui_needs_copy_slot(m_Slot) == 0) return; if (m_RenderEventFunc == System.IntPtr.Zero) @@ -371,50 +367,77 @@ private void IssueVulkanCopy() // throwing every frame. Debug.LogWarning("[DisplayXR] wsui: native plugin lacks the Vulkan overlay " + "exports (#336) — window-space UI disabled on Vulkan."); + m_VulkanCopyBroken = true; // latch: TryAcquireBridge must not re-arm it m_VulkanBridge = false; - m_BridgePtr = System.IntPtr.Zero; } } - // Take a wsui slot for this component. Idempotent. - private void AcquireSlot() + // Take a wsui slot for this component. Idempotent. Play mode only: there is no + // session in edit mode (Play Mode is the preview), and an [ExecuteAlways] edit-mode + // or prefab-stage instance holding a slot would starve the play-mode ones. + private bool AcquireSlot() { - if (m_Slot >= 0) return; + if (m_Slot >= 0) return true; + if (!Application.isPlaying || m_SlotApiMissing) return false; try { m_Slot = DisplayXRNative.displayxr_window_space_ui_acquire_slot(); - m_LegacySlot = false; - if (m_Slot < 0) - Debug.LogWarning($"[DisplayXR] wsui: '{name}' has no free window-space slot — " + - "too many DisplayXRWindowSpaceUI components are enabled at once."); } catch (System.EntryPointNotFoundException) { - m_Slot = 0; // older native plugin: the single shared layer - m_LegacySlot = true; + // Native plugin older than the managed side (they ship together, so this is + // a stale local build): stay inert rather than half-work. + m_SlotApiMissing = true; + Debug.LogWarning("[DisplayXR] wsui: native plugin lacks the slot API — " + + "window-space UI disabled. Rebuild/refresh the native plugin."); + return false; } + if (m_Slot < 0 && !m_WarnedNoSlot) + { + m_WarnedNoSlot = true; + Debug.LogWarning($"[DisplayXR] wsui: '{name}' has no free window-space slot — " + + "too many DisplayXRWindowSpaceUI components are enabled at once. " + + "It will appear when another one is disabled."); + } + return m_Slot >= 0; + } + + // A component that found every slot taken retries (about once a second), so it + // appears as soon as another HUD frees one instead of staying hidden until it is + // re-enabled. + private void RetryAcquireSlot() + { + if (m_Slot >= 0 || m_SlotApiMissing || !Application.isPlaying) return; + if (Time.unscaledTime < m_NextSlotRetry) return; + m_NextSlotRetry = Time.unscaledTime + 1f; + if (!AcquireSlot()) return; + m_WarnedNoSlot = false; + NativeSetLayer(positionX, positionY, width, height, disparity); + if (OverlayTexture != null) + NativeSetTexture(OverlayTexture.GetNativeTexturePtr(), m_RtSize.x, m_RtSize.y); + m_LastX = positionX; m_LastY = positionY; + m_LastW = width; m_LastH = height; + m_LastDisparity = disparity; + Debug.Log($"[DisplayXR] wsui: '{name}' got slot {m_Slot}"); } private void ReleaseSlot() { if (m_Slot < 0) return; - if (m_LegacySlot) DisplayXRNative.displayxr_window_space_ui_clear(); - else DisplayXRNative.displayxr_window_space_ui_release_slot(m_Slot); + DisplayXRNative.displayxr_window_space_ui_release_slot(m_Slot); m_Slot = -1; } private void NativeSetTexture(System.IntPtr tex, int w, int h) { if (m_Slot < 0) return; - if (m_LegacySlot) DisplayXRNative.displayxr_window_space_ui_set_texture(tex, w, h); - else DisplayXRNative.displayxr_window_space_ui_set_texture_slot(m_Slot, tex, w, h); + DisplayXRNative.displayxr_window_space_ui_set_texture_slot(m_Slot, tex, w, h); } private void NativeSetLayer(float x, float y, float w, float h, float disparity) { if (m_Slot < 0) return; - if (m_LegacySlot) DisplayXRNative.displayxr_window_space_ui_set_layer(x, y, w, h, disparity); - else DisplayXRNative.displayxr_window_space_ui_set_layer_slot(m_Slot, x, y, w, h, disparity); + DisplayXRNative.displayxr_window_space_ui_set_layer_slot(m_Slot, x, y, w, h, disparity); } private void ReleaseBridgeTex() @@ -465,6 +488,7 @@ void OnDisable() void LateUpdate() { + RetryAcquireSlot(); // Keep EVERYTHING under the canvas on the private layer, every frame (#289). // OnEnable parks the hierarchy on kPrivateLayer once, but the overlay // camera culls to that layer alone — so anything Instantiated under the diff --git a/native~/displayxr_unity_plugin.cpp b/native~/displayxr_unity_plugin.cpp index f20a57ab..52d51af9 100644 --- a/native~/displayxr_unity_plugin.cpp +++ b/native~/displayxr_unity_plugin.cpp @@ -10,6 +10,7 @@ // Unity's own device. #include "displayxr_unity_plugin.h" +#include "displayxr_window_space_ui.h" // DXR_WSUI_MAX_SLOTS, get_pending_slot #include @@ -59,17 +60,14 @@ static bool s_vk_captured = false; // // Keep the ids in sync with DisplayXRProviderNative.kVkOverlayCopy*Event. #define DXR_EVENT_VK_OVERLAY_COPY_LOCAL2D 0x44585201 -// wsui slot N is DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + N (N < DXR_WSUI_SLOTS). +// wsui slot N is DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + N (N < DXR_WSUI_MAX_SLOTS). #define DXR_EVENT_VK_OVERLAY_COPY_WSUI0 0x44585210 -#define DXR_WSUI_SLOTS 4 extern "C" int displayxr_local2d_get_pending(void **out_tex, int *out_w, int *out_h); -extern "C" int displayxr_window_space_ui_get_pending_slot(int slot, void **out_tex, int *out_tex_w, - int *out_tex_h, float *out_x, float *out_y, - float *out_lw, float *out_lh, float *out_disp); extern "C" int dxr_pvk_overlay_ready(int kind); extern "C" int dxr_pvk_overlay_record_unity_copy(int kind, void *cmd_buf, void *src_image, - int64_t src_format, uint32_t src_w, uint32_t src_h); + int64_t src_format, uint32_t src_w, uint32_t src_h, + void *src_id); static void configure_vulkan_events(void) { @@ -81,7 +79,7 @@ static void configure_vulkan_events(void) cfg.graphicsQueueAccess = kUnityVulkanGraphicsQueueAccess_DontCare; cfg.flags = kUnityVulkanEventConfigFlag_EnsurePreviousFrameSubmission; s_unity_vk->ConfigureEvent(DXR_EVENT_VK_OVERLAY_COPY_LOCAL2D, &cfg); - for (int i = 0; i < DXR_WSUI_SLOTS; i++) + for (int i = 0; i < DXR_WSUI_MAX_SLOTS; i++) s_unity_vk->ConfigureEvent(DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + i, &cfg); } @@ -109,7 +107,7 @@ static void vk_overlay_copy(int kind, void *tex, int w, int h) uint32_t sw = img.extent.width ? img.extent.width : (uint32_t)w; uint32_t sh = img.extent.height ? img.extent.height : (uint32_t)h; dxr_pvk_overlay_record_unity_copy(kind, (void *)st.commandBuffer, (void *)img.image, - (int64_t)img.format, sw, sh); + (int64_t)img.format, sw, sh, tex); } #endif @@ -121,7 +119,7 @@ static void UNITY_INTERFACE_API on_render_event(int event_id) void *tex = NULL; int w = 0, h = 0; if (displayxr_local2d_get_pending(&tex, &w, &h)) vk_overlay_copy(0 /* LOCAL2D */, tex, w, h); } else if (event_id >= DXR_EVENT_VK_OVERLAY_COPY_WSUI0 && - event_id < DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + DXR_WSUI_SLOTS) { + event_id < DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + DXR_WSUI_MAX_SLOTS) { int slot = event_id - DXR_EVENT_VK_OVERLAY_COPY_WSUI0; void *tex = NULL; int w = 0, h = 0; float x, y, lw, lh, disp; diff --git a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp index f1c07d06..b7143b5a 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.cpp @@ -263,6 +263,7 @@ struct PvkState { uint32_t sc_image_count = 0; VkFormat sc_format = VK_FORMAT_UNDEFINED; bool has_content = false; // Unity has copied into the bridge at least once + void *content_src = nullptr; // registered texture that copy came from } overlay[DXR_PVK_OVERLAY_COUNT]; // Session binding storage, handed to xrCreateSession. @@ -1031,8 +1032,8 @@ dxr_pvk_signal_unity_done(void) s_pvk.unity_api.vkDeviceWaitIdle(s_pvk.unity_device); // Age the overlay graveyard here: this runs exactly once per frame. Ticking per - // overlay copy made the delay shrink with every extra layer (Local2D + a HUD = two - // "frames" per frame). + // overlay copy (as the first version did) made the delay shrink with every extra + // layer — with Local2D + 3 HUDs, 4 "frames" passed in one. pvk_grave_tick(); } @@ -1275,9 +1276,13 @@ pvk_overlay(int kind) static const char * pvk_overlay_label(int kind) { - static const char *const names[DXR_PVK_OVERLAY_COUNT] = {"local2d", "wsui0", "wsui1", - "wsui2", "wsui3"}; - return (kind >= 0 && kind < DXR_PVK_OVERLAY_COUNT) ? names[kind] : "?"; + static char names[DXR_PVK_OVERLAY_COUNT][16]; + if (kind < 0 || kind >= DXR_PVK_OVERLAY_COUNT) return "?"; + if (!names[kind][0]) { + if (kind == DXR_PVK_OVERLAY_LOCAL2D) snprintf(names[kind], sizeof(names[kind]), "local2d"); + else snprintf(names[kind], sizeof(names[kind]), "wsui%d", kind - DXR_PVK_OVERLAY_WSUI0); + } + return names[kind]; } static void @@ -1373,7 +1378,8 @@ pvk_is_srgb8(VkFormat f) int dxr_pvk_overlay_record_unity_copy(int kind, void *cmd_buf, void *src_image, - int64_t src_format, uint32_t src_w, uint32_t src_h) + int64_t src_format, uint32_t src_w, uint32_t src_h, + void *src_id) { PvkState::Overlay *o = pvk_overlay(kind); if (!o || !o->bridge.valid || !cmd_buf || !src_image) return 0; @@ -1458,10 +1464,18 @@ dxr_pvk_overlay_record_unity_copy(int kind, void *cmd_buf, void *src_image, "bridge fmt=%d, %s)\n", pvk_overlay_label(kind), w, h, (int)sf, (int)b->format, same_order ? "copy" : "blit"); o->has_content = true; + o->content_src = src_id; s_ov_want_copy[kind] = 0; return 1; } +void * +dxr_pvk_overlay_content_source(int kind) +{ + PvkState::Overlay *o = pvk_overlay(kind); + return o ? o->content_src : NULL; +} + int dxr_pvk_overlay_copy_to_swapchain_image(int kind, uint32_t image_index) { diff --git a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h index 69eb8a52..bfa0c0b1 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h +++ b/native~/displayxr_xrprovider/displayxr_provider_gfx_vulkan.h @@ -44,6 +44,7 @@ #if defined(ENABLE_VULKAN) #include "../displayxr_vk_loader.h" +#include "../displayxr_window_space_ui.h" // DXR_WSUI_MAX_SLOTS #define XR_USE_GRAPHICS_API_VULKAN 0 // types are inlined in the .cpp, like D3D11/D3D12 #include @@ -156,7 +157,7 @@ void dxr_pvk_signal_unity_done(void); enum { DXR_PVK_OVERLAY_LOCAL2D = 0, DXR_PVK_OVERLAY_WSUI0 = 1, // wsui slot N is DXR_PVK_OVERLAY_WSUI0 + N - DXR_PVK_OVERLAY_COUNT = 1 + 4, // Local2D + DXR_WSUI_MAX_SLOTS + DXR_PVK_OVERLAY_COUNT = 1 + DXR_WSUI_MAX_SLOTS, // Local2D + one per wsui slot }; /// Record the overlay layer's swapchain VkImages (session device) and format. @@ -187,9 +188,17 @@ void dxr_pvk_overlay_request_copy(int kind); /// Unity RenderTexture already transitioned to TRANSFER_SRC_OPTIMAL by /// AccessTexture) into the overlay bridge's Unity-side alias, on Unity's /// `cmd_buf`. `src_format` is the source VkFormat; an RGBA source is blitted so -/// the channels land right in a BGRA bridge. Returns 1 if a copy was recorded. +/// the channels land right in a BGRA bridge. `src_id` is the texture's registered +/// native pointer, remembered as the bridge content's source (see +/// dxr_pvk_overlay_content_source). Returns 1 if a copy was recorded. int dxr_pvk_overlay_record_unity_copy(int kind, void *cmd_buf, void *src_image, - int64_t src_format, uint32_t src_w, uint32_t src_h); + int64_t src_format, uint32_t src_w, uint32_t src_h, + void *src_id); + +/// The registered texture the bridge's current content was copied from. Submit +/// compares it with the texture registered NOW, so a slot taken over by another +/// component never shows the previous owner's last image. Render thread only. +void *dxr_pvk_overlay_content_source(int kind); /// Per-frame session side: copy the overlay bridge into swapchain image `image_index`. int dxr_pvk_overlay_copy_to_swapchain_image(int kind, uint32_t image_index); diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.cpp b/native~/displayxr_xrprovider/displayxr_provider_session.cpp index b9f3d333..50dc610e 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_session.cpp @@ -13,6 +13,7 @@ #include #include "../displayxr_extensions.h" #include "../displayxr_shared_state.h" // displayxr_state_set_stereo_matrices (overlay hit-test) +#include "../displayxr_window_space_ui.h" // DXR_WSUI_MAX_SLOTS, get_pending_slot #include // s_session_generation (#336) #ifdef _WIN32 @@ -192,6 +193,7 @@ extern "C" int dxr_pvk_overlay_needs_copy(int kind); extern "C" void dxr_pvk_overlay_request_copy(int kind); extern "C" int dxr_pvk_overlay_copy_to_swapchain_image(int kind, uint32_t image_index); extern "C" void dxr_pvk_overlay_destroy(int kind); +extern "C" void *dxr_pvk_overlay_content_source(int kind); #endif // Bumped each time a session reaches READY. File-scope on purpose: s_ps is @@ -225,7 +227,7 @@ static int s_wsui_vk_disabled = 0; // canvas RT into bridge_unity each frame; submit copies bridge_own -> the acquired // swapchain image (own device) and submits one window-space composition layer. // One per wsui slot (#336 follow-up): several HUD components can be live at once. -#define PS_MAX_WSUI 4 // == DXR_WSUI_MAX_SLOTS (displayxr_window_space_ui.h) +#define PS_MAX_WSUI DXR_WSUI_MAX_SLOTS typedef struct ProviderWsui { XrSwapchain swapchain; uint32_t w, h, image_count; @@ -2403,9 +2405,6 @@ int dxr_prov_reconcile_size(void) // s_pending getter exported by the wsui module (displayxr_window_space_ui.cpp). // C# DisplayXRWindowSpaceUI sets it via set_texture/set_layer regardless of which // session is active, so the provider can drive its own window-space layer. -extern "C" int displayxr_window_space_ui_get_pending_slot(int slot, void **out_tex, int *out_tex_w, - int *out_tex_h, float *out_x, float *out_y, - float *out_lw, float *out_lh, float *out_disp); // s_pending getter exported by the Local2D module (displayxr_local2d.cpp). The Metal // Local2D arm blits this Unity id straight into its overlay swapchain image @@ -2739,7 +2738,11 @@ static int ps_submit_wsui(int slot, XrCompositionLayerWindowSpaceDXR *out_layer) if (!displayxr_window_space_ui_get_pending_slot(slot, &tex, &tw, &th, &lx, &ly, &lw, &lh, &ldisp)) return 0; if (!ps_create_wsui(slot, (uint32_t)tw, (uint32_t)th)) return 0; - if (!dxr_pvk_overlay_has_content((PS_PVK_OVERLAY_WSUI0 + slot))) return 0; + // Only content copied from the texture registered in this slot NOW: a slot taken + // over by another component must not show the previous owner's last image. + if (!dxr_pvk_overlay_has_content(PS_PVK_OVERLAY_WSUI0 + slot) || + dxr_pvk_overlay_content_source(PS_PVK_OVERLAY_WSUI0 + slot) != tex) + return 0; uint32_t idx = 0; XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; @@ -3208,8 +3211,11 @@ static int ps_submit_local2d(XrCompositionLayerLocal2DDXR *out_layer) void *tex = NULL; int tw = 0, th = 0; if (!displayxr_local2d_get_pending(&tex, &tw, &th)) return 0; if (!ps_create_local2d((uint32_t)tw, (uint32_t)th)) return 0; - // A fresh bridge holds uninitialised memory until Unity's first copy lands. - if (!dxr_pvk_overlay_has_content(PS_PVK_OVERLAY_LOCAL2D)) return 0; + // A fresh bridge holds uninitialised memory until Unity's first copy lands, and + // content copied from a texture no longer registered is someone else's. + if (!dxr_pvk_overlay_has_content(PS_PVK_OVERLAY_LOCAL2D) || + dxr_pvk_overlay_content_source(PS_PVK_OVERLAY_LOCAL2D) != tex) + return 0; uint32_t idx = 0; XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; @@ -7219,8 +7225,8 @@ int dxr_prov_submit_frame(uint32_t image_index) if (has_wsui && s_ps.graphics_api == DXR_GFX_VULKAN && r == XR_ERROR_LAYER_INVALID && !s_wsui_vk_disabled) { s_wsui_vk_disabled = 1; - ps_log("[DisplayXR-PROV] wsui: runtime rejected a frame carrying the window-space " - "layer on Vulkan (XR_ERROR_LAYER_INVALID) — wsui disabled until the next " + ps_log("[DisplayXR-PROV] wsui: runtime rejected a frame carrying window-space " + "layers on Vulkan (XR_ERROR_LAYER_INVALID) — wsui disabled until the next " "session\n"); } #endif