diff --git a/Runtime/DisplayXRWindowSpaceUI.cs b/Runtime/DisplayXRWindowSpaceUI.cs
index 828fb02..4df30ef 100644
--- a/Runtime/DisplayXRWindowSpaceUI.cs
+++ b/Runtime/DisplayXRWindowSpaceUI.cs
@@ -112,8 +112,9 @@ 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
+ 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
@@ -656,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)
{
- float cw = 0f, ch = 0f;
+ return GetWindowPixelSize(out width, out height) != kPanelSizeNone;
+ }
+
+ private static int GetWindowPixelSize(out float cw, out float ch)
+ {
+ cw = 0f; ch = 0f;
int source = kPanelSizeNone;
// Workspace tile (#323): under the shell Unity's window is launched
@@ -675,14 +690,33 @@ 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;
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;
@@ -695,7 +729,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,10 +738,33 @@ 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 static 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).
- private bool TryGetTileCanvasSize(out float w, out float h)
+ private static bool TryGetTileCanvasSize(out float w, out float h)
{
w = h = 0f;
try
@@ -725,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;
}
}
diff --git a/Runtime/Provider/DisplayXRProviderNative.cs b/Runtime/Provider/DisplayXRProviderNative.cs
index 83551da..44b15cc 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 dcfe636..1e1e4f3 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 666b353..67022e0 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