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..cffa0978 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, edit + // mode, or not acquired yet). + private int m_Slot = -1; + 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. @@ -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,8 +292,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) - DisplayXRProviderNative.dxr_prov_get_wsui_bridge( - (uint)m_RtSize.x, (uint)m_RtSize.y, + if (m_Slot < 0) return; // no slot: this layer isn't shown + 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) @@ -298,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. @@ -320,10 +328,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 +352,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) 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) { @@ -357,11 +367,79 @@ 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. 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 true; + if (!Application.isPlaying || m_SlotApiMissing) return false; + try + { + m_Slot = DisplayXRNative.displayxr_window_space_ui_acquire_slot(); + } + catch (System.EntryPointNotFoundException) + { + // 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; + 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; + 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; + DisplayXRNative.displayxr_window_space_ui_set_layer_slot(m_Slot, x, y, w, h, disparity); + } + private void ReleaseBridgeTex() { if (m_BridgeTex != null) @@ -374,7 +452,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) @@ -410,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 @@ -516,7 +595,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 +766,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..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,15 +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 -#define DXR_EVENT_VK_OVERLAY_COPY_WSUI 0x44585202 +// wsui slot N is DXR_EVENT_VK_OVERLAY_COPY_WSUI0 + N (N < DXR_WSUI_MAX_SLOTS). +#define DXR_EVENT_VK_OVERLAY_COPY_WSUI0 0x44585210 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 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) { @@ -79,7 +79,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_MAX_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) @@ -106,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 @@ -117,11 +118,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_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; - 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..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,7 +1276,13 @@ pvk_overlay(int kind) static const char * pvk_overlay_label(int kind) { - return kind == DXR_PVK_OVERLAY_LOCAL2D ? "local2d" : "wsui"; + 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 @@ -1371,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; @@ -1456,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 bc97442b..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 @@ -155,8 +156,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 + 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 6ce1b775..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 @@ -184,7 +185,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); @@ -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 @@ -220,6 +222,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 DXR_WSUI_MAX_SLOTS +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 +603,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 +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(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 +2421,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 +2435,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 +2468,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 +2482,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 +2507,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 +2534,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 +2559,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 +2592,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 +2640,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 +2668,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 +2686,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 +2735,29 @@ 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(slot, (uint32_t)tw, (uint32_t)th)) 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; - if (!ps_create_wsui((uint32_t)tw, (uint32_t)th)) return 0; - if (!dxr_pvk_overlay_has_content(PS_PVK_OVERLAY_WSUI)) 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 +2767,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 +2851,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 +3139,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) @@ -3187,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}; @@ -4995,14 +5022,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 +7137,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 — @@ -7189,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 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);