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