From 97d592ce7646bebae1bac9466e73b07f22226cdd Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 2 Oct 2026 00:15:16 -0700 Subject: [PATCH 1/2] fix(#291): window-space UI takes its panel aspect from the composited window, not Screen.* In a transparent-overlay app Unity's own window is cloaked and parked, so Screen.* reports whatever size Unity persisted (observed 512x728 and 3872x2248 next to a 3840x2160 overlay; it grows by the window frame on every run). DisplayXRWindowSpaceUI derived its RT aspect from it, so the runtime's stretch into the panel rect was not the identity and the HUD came out squeezed (lenovo-avatar's radio/tuning HUDs). The provider now publishes the live size of the window (or workspace tile) the runtime composites into, the number the eye swapchain is already sized from, as a cached value (dxr_prov_get_composited_size). On Linux the measurement uses the X connection the runtime borrows, so it must not be queried from Unity's main thread. The window-space UI prefers it over Screen.*, after the workspace-tile canvas (#323). It falls back to Screen.* with no provider session or against an older native plugin. Only the aspect is used, so the native-DPI pixels need no conversion. Co-Authored-By: Claude Opus 5.5 --- Runtime/DisplayXRWindowSpaceUI.cs | 42 +++++++++++++++++-- Runtime/Provider/DisplayXRProviderNative.cs | 9 ++++ .../displayxr_provider_session.cpp | 22 ++++++++++ .../displayxr_provider_session.h | 8 ++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/Runtime/DisplayXRWindowSpaceUI.cs b/Runtime/DisplayXRWindowSpaceUI.cs index 828fb02e..6956b10a 100644 --- a/Runtime/DisplayXRWindowSpaceUI.cs +++ b/Runtime/DisplayXRWindowSpaceUI.cs @@ -112,6 +112,7 @@ public class DisplayXRWindowSpaceUI : MonoBehaviour private const int kPanelSizeNone = 0; private const int kPanelSizeTile = 1; private const int kPanelSizeScreen = 2; + private const int kPanelSizeComposited = 3; private int m_PanelSizeSource = kPanelSizeNone; private int m_ShellMode = -1; // -1 = not yet queried @@ -675,8 +676,20 @@ private bool TryGetPanelPixelSize(out float pw, out float ph) cw = tw; ch = th; source = kPanelSizeTile; } - // Built-app / Play Mode: the runtime composites into Unity's main - // window, so Screen.* is meaningful. + // The window the runtime actually composites into, as the provider last + // measured it (the eye swapchain is sized from the same number). In a + // transparent-overlay app that is the visible overlay window, while Unity's own + // window is cloaked/parked at whatever size Unity persisted (observed 512x728 and + // 3872x2248 next to a 3840x2160 overlay; it grows by the frame size each run) — + // so Screen.* gave a wrong aspect and squeezed the HUD. Only the ASPECT is used + // (see ComputeRtSize), so the native-DPI pixels need no conversion. + else if (TryGetCompositedSize(out float qw, out float qh)) + { + cw = qw; ch = qh; + source = kPanelSizeComposited; + } + // No provider session (or an older native plugin): Screen.*, which is right + // whenever the runtime composites into Unity's main window. else if (Screen.width > 0 && Screen.height > 0) { cw = Screen.width; ch = Screen.height; @@ -695,7 +708,7 @@ private bool TryGetPanelPixelSize(out float pw, out float ph) { m_PanelSizeSource = source; Debug.Log($"[DisplayXR] wsui: panel size source = " + - $"{(source == kPanelSizeTile ? "workspace tile" : "Screen.*")} " + + $"{(source == kPanelSizeTile ? "workspace tile" : source == kPanelSizeComposited ? "composited window" : "Screen.*")} " + $"({cw}x{ch})"); } @@ -704,6 +717,29 @@ private bool TryGetPanelPixelSize(out float pw, out float ph) return true; } + // The provider's last measured composited window size; false with no provider + // session, before the first measurement, or against a native plugin without the + // export (then never asked again). + private static bool s_CompositedSizeMissing; + private bool TryGetCompositedSize(out float w, out float h) + { + w = h = 0f; + if (s_CompositedSizeMissing || !DisplayXRProviderDriver.IsActive) return false; + try + { + if (DisplayXRProviderNative.dxr_prov_get_composited_size(out uint cw, out uint ch) == 0 || + cw == 0 || ch == 0) + return false; + w = cw; h = ch; + return true; + } + catch (System.EntryPointNotFoundException) + { + s_CompositedSizeMissing = true; + return false; + } + } + // The live workspace-tile canvas in px, or false when there is no tile size // yet (slot not bound, older runtime, or no native binary at all in the // editor before a Play session). diff --git a/Runtime/Provider/DisplayXRProviderNative.cs b/Runtime/Provider/DisplayXRProviderNative.cs index 83551da9..44b15ccc 100644 --- a/Runtime/Provider/DisplayXRProviderNative.cs +++ b/Runtime/Provider/DisplayXRProviderNative.cs @@ -313,6 +313,15 @@ public static extern void dxr_prov_get_local2d_bridge( uint width, uint height, out System.IntPtr nativePtr, out uint outWidth, out uint outHeight); + /// + /// Live size in px of the window (or workspace tile) the runtime composites this app + /// into, as last measured by the provider; 0 when no session runs or no size is known. + /// In a transparent-overlay app Unity's own window is cloaked, so Screen.* does + /// not describe it. Use the aspect; the absolute values are native-DPI pixels. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern int dxr_prov_get_composited_size(out uint width, out uint height); + /// Set the Local2D dest rect in client-window pixels. w<=0||h<=0 clears. [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] public static extern void dxr_prov_set_local2d_rect(int x, int y, int w, int h); diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.cpp b/native~/displayxr_xrprovider/displayxr_provider_session.cpp index dcfe6367..1e1e4f30 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_session.cpp @@ -1094,6 +1094,25 @@ extern "C" int dxr_prov_workspace_tile_size(uint32_t *w, uint32_t *h) return 0; } +// The last LIVE size ps_window_size measured (tile canvas or the bound window's client), +// packed w<<32|h; 0 = none yet / only the display-dims fallback. Published for C# +// (dxr_prov_get_composited_size): DisplayXRWindowSpaceUI derives its panel aspect from it, +// because in a transparent-overlay app Screen.* is the size of Unity's own CLOAKED window, +// which has nothing to do with the window the runtime composites into. A cached value, not a +// live query, on purpose: on Linux the measurement goes through the X connection the runtime +// borrows (see displayxr_linux.c), which must not be driven from Unity's main thread. +static std::atomic s_live_window_px{0}; + +int dxr_prov_get_composited_size(uint32_t *w, uint32_t *h) +{ + uint64_t v = s_live_window_px.load(); + uint32_t ww = (uint32_t)(v >> 32), hh = (uint32_t)(v & 0xffffffffu); + if (!s_ps.running || ww == 0 || hh == 0) return 0; + if (w) *w = ww; + if (h) *h = hh; + return 1; +} + // The size the app + provider render/author for. In workspace-tile mode (#225) // this is the shell-driven tile canvas (so the render follows 3D-window resize); // otherwise the bound overlay's live client size (= Unity's window client area). @@ -1108,6 +1127,7 @@ static void ps_window_size(uint32_t *w, uint32_t *h) ps_query_tile_size(); // lazy first fetch until the slot binds if (s_ps.tile_px_w > 0 && s_ps.tile_px_h > 0) { *w = s_ps.tile_px_w; *h = s_ps.tile_px_h; + s_live_window_px.store(((uint64_t)s_ps.tile_px_w << 32) | s_ps.tile_px_h); return; } } @@ -1130,6 +1150,7 @@ static void ps_window_size(uint32_t *w, uint32_t *h) // self-hosting), which falls through to the display-info default below. displayxr_linux_window_size(&ww, &hh); #endif + if (ww > 0 && hh > 0) s_live_window_px.store(((uint64_t)ww << 32) | hh); // a real measurement if (ww == 0 || hh == 0) { ww = s_ps.display_info.is_valid ? s_ps.display_info.pixel_width : 1920; hh = s_ps.display_info.is_valid ? s_ps.display_info.pixel_height : 1080; @@ -5023,6 +5044,7 @@ int dxr_prov_session_start(const char *runtime_json_path, void dxr_prov_session_stop(void) { + s_live_window_px.store(0); // the next session measures its own window 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); diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.h b/native~/displayxr_xrprovider/displayxr_provider_session.h index 666b3535..67022e09 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.h +++ b/native~/displayxr_xrprovider/displayxr_provider_session.h @@ -308,6 +308,14 @@ DISPLAYXR_EXPORT int dxr_prov_get_zone_stereo_matrices(uint32_t zone, float *lv, /// Fill `zone`'s window-client pixel rect (top-left origin). Returns 1 on success. DISPLAYXR_EXPORT int dxr_prov_get_zone_rect_px(uint32_t zone, int *x, int *y, int *w, int *h); +/// The live size in pixels of the window (or workspace tile) the runtime composites this +/// app into — the size the eye swapchain is derived from — as last measured by the provider. +/// Returns 1 and fills *w/*h while a session runs and a real size is known; 0 otherwise. +/// In a transparent-overlay app Unity's own window is cloaked and Screen.* does not describe +/// it, so a window-space UI derives its panel aspect from this instead. Pixels of one window, +/// so the ASPECT is DPI-independent; don't mix the absolute values with managed geometry. +DISPLAYXR_EXPORT int dxr_prov_get_composited_size(uint32_t *w, uint32_t *h); + /// Local2D layer (#166 Phase B): lazily create the provider's Local2D overlay /// swapchain + cross-device bridge sized to w×h and return the Unity-device handle /// of the bridge. C# (DisplayXRLocal2D) Graphics.CopyTexture's its canvas RT into it From 1c9b0d3b7c81d7083970fb268176935f06627f80 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 2 Oct 2026 01:20:31 -0700 Subject: [PATCH 2/2] =?UTF-8?q?feat(#291):=20DisplayXRWindowSpaceUI.TryGet?= =?UTF-8?q?WindowPixelSize=20=E2=80=94=20the=20frame=20HUD=20rects=20are?= =?UTF-8?q?=20fractions=20of?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps that lay out HUD rects (e.g. lenovo-avatar's HudPanelLayout) used Screen.* so their math matched the RT aspect the component derived from it. With the composited-window source that is no longer the same number in a transparent-overlay app on Windows, so expose the component's own source chain (workspace tile, then composited window, then Screen.*) as a public static. The shell-mode and size helpers become static to share it. Co-Authored-By: Claude Opus 5.5 --- Runtime/DisplayXRWindowSpaceUI.cs | 43 +++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/Runtime/DisplayXRWindowSpaceUI.cs b/Runtime/DisplayXRWindowSpaceUI.cs index 6956b10a..4df30ef3 100644 --- a/Runtime/DisplayXRWindowSpaceUI.cs +++ b/Runtime/DisplayXRWindowSpaceUI.cs @@ -114,7 +114,7 @@ public class DisplayXRWindowSpaceUI : MonoBehaviour private const int kPanelSizeScreen = 2; private const int kPanelSizeComposited = 3; private int m_PanelSizeSource = kPanelSizeNone; - private int m_ShellMode = -1; // -1 = not yet queried + private static int s_ShellMode = -1; // -1 = not yet queried (fixed for the process) // This component's private stage (see DisplayXROverlayStage): its canvas and // camera sit there, far from any scene content and from every other overlay's @@ -657,9 +657,23 @@ void LateUpdate() } } - private bool TryGetPanelPixelSize(out float pw, out float ph) + /// + /// The size in pixels of the window this app's window-space UI is composited into — + /// the frame / etc. are fractions of, and the + /// size the HUD RT aspect is derived from. Lay out HUD rects against THIS, not + /// Screen.*: in a transparent-overlay app on Windows Screen.* is Unity's + /// own cloaked window, which can have any size (#291). Source order: workspace tile + /// (shell) → the provider's composited window → Screen.*. Only the aspect is + /// meaningful across sources (native-DPI pixels). Returns false when nothing is known. + /// + public static bool TryGetWindowPixelSize(out float width, out float height) + { + return GetWindowPixelSize(out width, out height) != kPanelSizeNone; + } + + private static int GetWindowPixelSize(out float cw, out float ch) { - float cw = 0f, ch = 0f; + cw = 0f; ch = 0f; int source = kPanelSizeNone; // Workspace tile (#323): under the shell Unity's window is launched @@ -696,6 +710,13 @@ private bool TryGetPanelPixelSize(out float pw, out float ph) source = kPanelSizeScreen; } + return source; + } + + private bool TryGetPanelPixelSize(out float pw, out float ph) + { + int source = GetWindowPixelSize(out float cw, out float ch); + if (source == kPanelSizeNone) { pw = ph = 0f; @@ -721,7 +742,7 @@ private bool TryGetPanelPixelSize(out float pw, out float ph) // session, before the first measurement, or against a native plugin without the // export (then never asked again). private static bool s_CompositedSizeMissing; - private bool TryGetCompositedSize(out float w, out float h) + private static bool TryGetCompositedSize(out float w, out float h) { w = h = 0f; if (s_CompositedSizeMissing || !DisplayXRProviderDriver.IsActive) return false; @@ -743,7 +764,7 @@ private bool TryGetCompositedSize(out float w, out float h) // The live workspace-tile canvas in px, or false when there is no tile size // yet (slot not bound, older runtime, or no native binary at all in the // editor before a Play session). - private bool TryGetTileCanvasSize(out float w, out float h) + private static bool TryGetTileCanvasSize(out float w, out float h) { w = h = 0f; try @@ -761,20 +782,20 @@ private bool TryGetTileCanvasSize(out float w, out float h) // Shell mode is fixed for the process (an env var read at load), so cache it // rather than P/Invoking twice a frame. An older native binary without the // export would throw per call, so a missing entry point also latches here. - private bool IsShellMode + private static bool IsShellMode { get { - if (m_ShellMode < 0) + if (s_ShellMode < 0) { try { - m_ShellMode = DisplayXRNative.displayxr_is_shell_mode() != 0 ? 1 : 0; + s_ShellMode = DisplayXRNative.displayxr_is_shell_mode() != 0 ? 1 : 0; } - catch (System.EntryPointNotFoundException) { m_ShellMode = 0; } - catch (System.DllNotFoundException) { m_ShellMode = 0; } + catch (System.EntryPointNotFoundException) { s_ShellMode = 0; } + catch (System.DllNotFoundException) { s_ShellMode = 0; } } - return m_ShellMode > 0; + return s_ShellMode > 0; } }