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);