From ad6597eee11a5324950fd2364d0f6952fb9731df Mon Sep 17 00:00:00 2001 From: byungjul Date: Tue, 6 Oct 2026 13:45:56 +0900 Subject: [PATCH] feat(#266): move the window onto the 3D panel on Linux The runtime already reports the panel's desktop rect on Linux (XR_DXR_display_info v18: X root coordinates, e.g. HDMI-1 (3840,0 3840x2160) confirmed=1), but dxr_prov_move_window_to_display returned 0 off Windows, so DisplayXRTargetDisplay / DisplayXRProvider.MoveWindowToDisplay did nothing and the app stayed on the primary display (and in 2D, with tracking paused) until dragged across. - The provider keeps the panel extent as well as its origin (v18). - Linux arm of dxr_prov_move_window_to_display: same gating as Windows (needs an origin; skips an unconfirmed fallback rect), then displayxr_linux_move_app_window_to_rect centres Unity's window on the panel with _NET_MOVERESIZE_WINDOW, or leaves it if its centre is already there. Main-thread X connection under its mutex, never s_dpy. The transparent overlay follows Unity's window as usual. - Docs: TargetDisplay / MoveWindowToDisplay now say Windows and Linux. Co-Authored-By: Claude Opus 5.5 --- Runtime/DisplayXRTargetDisplay.cs | 7 ++- Runtime/Provider/DisplayXRProvider.cs | 5 +- native~/displayxr_exports.h | 5 ++ native~/displayxr_linux.c | 55 +++++++++++++++++++ .../displayxr_provider_session.cpp | 29 ++++++++++ 5 files changed, 96 insertions(+), 5 deletions(-) diff --git a/Runtime/DisplayXRTargetDisplay.cs b/Runtime/DisplayXRTargetDisplay.cs index f480882..6bbd1ef 100644 --- a/Runtime/DisplayXRTargetDisplay.cs +++ b/Runtime/DisplayXRTargetDisplay.cs @@ -27,7 +27,8 @@ namespace DisplayXR /// /// Requires a runtime advertising XR_DXR_display_info v16+. Against an /// older one the panel's desktop position is simply not reported; the component logs - /// once and does nothing. It is also Windows-only today. + /// once and does nothing. It works on Windows and Linux (X11/XWayland); macOS is not + /// implemented yet. /// /// /// @@ -66,7 +67,7 @@ void OnEnable() /// /// Move now, if the session is running. Returns false when it is not, when the - /// runtime does not report the panel position, or off Windows. + /// runtime does not report the panel position, or on macOS. /// public bool MoveNow() { @@ -87,7 +88,7 @@ public bool MoveNow() Debug.LogWarning( "[DisplayXR] TargetDisplay: the runtime did not report the panel's " + "desktop position (needs XR_DXR_display_info v16+), so the window was " + - "left where Windows opened it. Making the panel the Windows main " + + "left where the OS opened it. Making the panel the main " + "display remains the workaround."); } } diff --git a/Runtime/Provider/DisplayXRProvider.cs b/Runtime/Provider/DisplayXRProvider.cs index e8857ef..d941109 100644 --- a/Runtime/Provider/DisplayXRProvider.cs +++ b/Runtime/Provider/DisplayXRProvider.cs @@ -142,8 +142,9 @@ public static bool RequestRenderingMode(uint modeIndex) => /// including the no-op case where it is already there. /// /// Needs a runtime advertising XR_DXR_display_info v16+; returns false - /// against anything older, and false off Windows. Call it once the session is - /// running — the panel's desktop position is read at session start. + /// against anything older, and false on macOS (Windows and Linux are supported). + /// Call it once the session is running — the panel's desktop position is read at + /// session start. /// /// /// The move is performed in native inside a per-monitor-DPI-aware context on diff --git a/native~/displayxr_exports.h b/native~/displayxr_exports.h index cb3dc0d..950eeb2 100644 --- a/native~/displayxr_exports.h +++ b/native~/displayxr_exports.h @@ -408,6 +408,11 @@ DISPLAYXR_EXPORT int displayxr_linux_drag_window(int right_pressed); /// (#332) 1 while the transparent overlay is up and the click-through exports act; 0 for /// the opaque child or without X. Cached state only — cheap to poll every frame. DISPLAYXR_EXPORT int displayxr_linux_click_through_active(void); + +/// (#266) Centre Unity's window on the given rect (X root coordinates), unless it is +/// already there. Called by dxr_prov_move_window_to_display with the panel rect the +/// runtime reported. Returns 1 when the window is on (or was moved to) the rect. +DISPLAYXR_EXPORT int displayxr_linux_move_app_window_to_rect(int x, int y, int w, int h); #endif #ifdef __cplusplus diff --git a/native~/displayxr_linux.c b/native~/displayxr_linux.c index ccba15c..50cb16c 100644 --- a/native~/displayxr_linux.c +++ b/native~/displayxr_linux.c @@ -1295,6 +1295,61 @@ displayxr_linux_drag_window(int right_pressed) return active; } +// --------------------------------------------------------------------------- +// Move the app window onto the 3D panel (#266) +// --------------------------------------------------------------------------- +// +// dxr_prov_move_window_to_display (DisplayXRProvider.MoveWindowToDisplay / +// DisplayXRTargetDisplay) hands us the panel rect the runtime reported, in X root +// coordinates. Centre Unity's window on it (its client area; the WM keeps any frame), +// clamped so a window larger than the panel keeps its top-left on the panel. Already +// on the panel (its centre inside the rect): leave it, as Windows does — a pointless +// move is a window jump the user did not have before. +// +// C# main thread: the main-thread connection and its mutex, never s_dpy (THREADS, +// above). Works for the opaque child path too; in transparent mode the overlay +// follows Unity's window in displayxr_linux_track_window. +DISPLAYXR_EXPORT int +displayxr_linux_move_app_window_to_rect(int px, int py, int pw, int ph) +{ + if (!lin_load_xlib() || !s_win || pw <= 0 || ph <= 0) { + lin_log("[DisplayXR-LNX] move_to_display: no app window\n"); + return 0; + } + int rc = 0; + pthread_mutex_lock(&s_hit_mutex); + if (!s_hit_dpy) s_hit_dpy = s_x.XOpenDisplay(NULL); + if (s_hit_dpy) { + XWin child = 0, gr = 0; + int cx = 0, cy = 0, gx = 0, gy = 0; + unsigned int cw = 0, ch = 0, gb = 0, gd = 0; + if (s_x.XTranslateCoordinates(s_hit_dpy, s_win, s_x.XDefaultRootWindow(s_hit_dpy), 0, 0, + &cx, &cy, &child) && + s_x.XGetGeometry(s_hit_dpy, s_win, &gr, &gx, &gy, &cw, &ch, &gb, &gd)) { + char m[224]; + int mx = cx + (int)cw / 2, my = cy + (int)ch / 2; + if (mx >= px && mx < px + pw && my >= py && my < py + ph) { + lin_log("[DisplayXR-LNX] move_to_display: already on the 3D display\n"); + rc = 1; + } else { + int x = px + (pw - (int)cw) / 2; + int y = py + (ph - (int)ch) / 2; + if (x < px) x = px; + if (y < py) y = py; + lin_move_client(s_hit_dpy, x, y); + snprintf(m, sizeof(m), + "[DisplayXR-LNX] move_to_display: (%d,%d) %ux%u -> (%d,%d) on the panel " + "(%d,%d %dx%d)\n", + cx, cy, cw, ch, x, y, px, py, pw, ph); + lin_log(m); + rc = 1; + } + } + } + pthread_mutex_unlock(&s_hit_mutex); + return rc; +} + // --------------------------------------------------------------------------- // Foreground query (#332) // --------------------------------------------------------------------------- diff --git a/native~/displayxr_xrprovider/displayxr_provider_session.cpp b/native~/displayxr_xrprovider/displayxr_provider_session.cpp index 86a3f73..26b6785 100644 --- a/native~/displayxr_xrprovider/displayxr_provider_session.cpp +++ b/native~/displayxr_xrprovider/displayxr_provider_session.cpp @@ -552,6 +552,7 @@ typedef struct ProviderSession { // per-monitor-aware process sees them. Valid only against a runtime advertising // XR_DXR_display_info v16+. int32_t desktop_origin_x, desktop_origin_y; + uint32_t desktop_w, desktop_h; // panel extent (v18 only; 0 = unknown) int desktop_origin_valid; // (#266 / v18) desktop_origin_valid says the runtime gave us AN origin; this says // it CONFIRMED a 3D panel is there (XrDisplayDesktopInfoDXR::isPanelConfirmed). v16 @@ -4866,6 +4867,8 @@ int dxr_prov_session_start(const char *runtime_json_path, if (v18) { s_ps.desktop_origin_x = ddi.desktopRect.offset.x; s_ps.desktop_origin_y = ddi.desktopRect.offset.y; + s_ps.desktop_w = (uint32_t)ddi.desktopRect.extent.width; + s_ps.desktop_h = (uint32_t)ddi.desktopRect.extent.height; s_ps.desktop_origin_valid = 1; s_ps.panel_confirmed = (ddi.isPanelConfirmed == XR_TRUE) ? 1 : 0; ps_log("[DisplayXR-PROV] Display desktop rect: (%d,%d %dx%d) confirmed=%d " @@ -4877,6 +4880,7 @@ int dxr_prov_session_start(const char *runtime_json_path, } else if (v16) { s_ps.desktop_origin_x = ddp.left; s_ps.desktop_origin_y = ddp.top; + s_ps.desktop_w = s_ps.desktop_h = 0; s_ps.desktop_origin_valid = 1; s_ps.panel_confirmed = -1; // v16 runtime: confirmation not available ps_log("[DisplayXR-PROV] Display desktop origin: (%d,%d) [v16; no v18 " @@ -7640,6 +7644,9 @@ uint32_t dxr_prov_get_active_mode_index(void) { return s_ps.active_mode_index; } // inside a per-monitor-aware context removes that failure mode by construction rather // than by remembering to convert at each call site. extern "C" void *displayxr_find_unity_hwnd(void); +#if defined(__linux__) && !defined(__ANDROID__) +extern "C" int displayxr_linux_move_app_window_to_rect(int x, int y, int w, int h); +#endif int dxr_prov_get_display_desktop_origin(int *out_x, int *out_y) { @@ -7721,6 +7728,28 @@ int dxr_prov_move_window_to_display(void) if (prev) SetThreadDpiAwarenessContext(prev); return rc; +#elif defined(__linux__) && !defined(__ANDROID__) + // (#266, Linux) The runtime reports the panel rect in X root coordinates (its desktop + // resolver matches the panel to an XRandR output), which is the space Unity's X11 + // window lives in under X11 and XWayland alike — no DPI virtualisation to undo here. + // Same gating as Windows; the move itself is in displayxr_linux.c, on the main-thread + // X connection. + if (!s_ps.desktop_origin_valid) { + ps_log("[DisplayXR-PROV] move_window_to_display: no desktop origin (runtime < v16)\n"); + return 0; + } + if (s_ps.panel_confirmed == 0) { + ps_log("[DisplayXR-PROV] move_window_to_display: runtime did not confirm a 3D panel " + "(fallback-to-primary rect) - leaving the window where it is (#266 v18)\n"); + return 0; + } + uint32_t pw = s_ps.desktop_w, ph = s_ps.desktop_h; + if (pw == 0 || ph == 0) { // v16: no extent reported; the panel's native size is the best guess + pw = s_ps.display_info.pixel_width; + ph = s_ps.display_info.pixel_height; + } + return displayxr_linux_move_app_window_to_rect((int)s_ps.desktop_origin_x, + (int)s_ps.desktop_origin_y, (int)pw, (int)ph); #else return 0; #endif