From d33261fbdf2d15f9c735594e6ea98e869a43c27f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:56:23 -0700 Subject: [PATCH 01/21] feat(display_info): XrViewDisplayBindingsDXR + segment metric types (multi-screen M3) XR_DXR_display_info v22 (append-only, same version): XrViewDisplayBindingsDXR (1004999217, the reserved value) chained on XrViewState at xrLocateViews -- which views belong to which display, with the segment rect in window px. xrt_display_metrics.h gains XRT_MAX_SEGMENTS (2), xrt_segment_metrics (the compositor's segment table for the view math) and xrt_segment_view_routing (which views of a frame go to which segment). Co-Authored-By: Claude Fable 5.1 --- docs/specs/extensions/XR_DXR_display_info.md | 88 ++++++++++++++++-- src/external/openxr_includes/openxr/README.md | 2 +- .../openxr/XR_DXR_display_info.h | 72 +++++++++++++- src/xrt/include/xrt/xrt_display_metrics.h | 93 +++++++++++++++++++ 4 files changed, 245 insertions(+), 10 deletions(-) diff --git a/docs/specs/extensions/XR_DXR_display_info.md b/docs/specs/extensions/XR_DXR_display_info.md index 34cfdeebf..49f37e90c 100644 --- a/docs/specs/extensions/XR_DXR_display_info.md +++ b/docs/specs/extensions/XR_DXR_display_info.md @@ -1151,7 +1151,7 @@ append-only: no existing struct, value or behaviour changes. #define XR_TYPE_DISPLAY_DXR ((XrStructureType)1004999214) #define XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR ((XrStructureType)1004999215) #define XR_TYPE_SESSION_DISPLAY_BINDING_DXR ((XrStructureType)1004999216) -// 1004999217 is RESERVED for XrViewDisplayBindingsDXR (per-view display binding, M3). +#define XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR ((XrStructureType)1004999217) // M3, below typedef XrFlags64 XrDisplayFlagsDXR; XR_DISPLAY_PRIMARY_BIT_DXR = 0x1 // the OS desktop's primary monitor @@ -1235,6 +1235,81 @@ display processor that weaves the session is still the system default's — hono the binding in the compositor (segments, per-view display bindings) is M2/M3. `XrSystemProperties` is not affected by a session's binding. +#### Per-segment views and view display bindings (v22, multi-screen M3) + +A window that spans two displays is woven per **segment** — the part of its canvas on +each display, each by that display's own display processor (see +`docs/architecture/comp-segments.md`). From M3 a session that began +`XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` also gets **one view set per segment**, +each rendered from that display's own frustum. Decision D3 of ADR-047: no new view +configuration type — per-display views ride the existing multiview surface. + +```c +#define XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR ((XrStructureType)1004999217) + +typedef struct XrViewDisplayBindingDXR { + uint64_t displayId; // an id from xrEnumerateDisplaysDXR + XrRect2Di segmentRect; // the segment, WINDOW px (client area, top-left origin) + uint32_t firstView; // first view of the range + uint32_t viewCount; // the active rendering mode's view count +} XrViewDisplayBindingDXR; + +typedef struct XrViewDisplayBindingsDXR { // chained on XrViewState at xrLocateViews + XrStructureType type; // XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR + void* XR_MAY_ALIAS next; + uint32_t bindingCapacityInput; + uint32_t bindingCountOutput; + XrViewDisplayBindingDXR* bindings; +} XrViewDisplayBindingsDXR; +``` + +**The view count.** `PRIMARY_MULTIVIEW_DXR` now reports `device max views × +XRT_MAX_SEGMENTS` (capped at 8; `XRT_MAX_SEGMENTS` is 2 today — the plan's eventual cap is +4): 4 on a 2-view Leia system, 8 on the 4-view sim_display. The count is fixed for the +session (ADR-041). A window on one display locates exactly what it did before — the +active mode's views first, the larger tail aliased onto view 0 — so the only visible +change for such an app is the bigger count. Per-view recommended image sizes stay the +worst case (full canvas × scale). `PRIMARY_STEREO` stays exactly 2. + +**Per-segment views.** When the window is woven per segment, `xrLocateViews` locates, +for each segment in left-to-right order, the active mode's view count from: + +- **that display's eyes** — its display processor's predicted eyes when it is tracking a + viewer, else that display's nominal viewer; +- **Kooima with the segment as the canvas** — the segment relative to its own display, in + that display's metres. + +The view sets are **contiguous**; `XrViewActivityStateDXR::activeViewCount` is the +**sum**, and the inactive tail aliases view 0 as always. Every view pose is expressed in +ONE frame — the display space of the display holding most of the window. The other +display's segment is placed beside it the way the window's pixels continue across the +seam (each panel's own pixel pitch on its side of the seam), so a scene that crosses the +seam stays continuous: a display rig (`XrDisplayRigDXR`) still centres the whole window +on its pose. A window entirely on a non-default display is the one-segment case: its +views come from that display. + +`XrViewDisplayBindingsDXR` says which views are which: one binding per segment +(`displayId`, `segmentRect`, `firstView`, `viewCount`). Standard two-call idiom on the +binding array; it is written only on the call that returns views. `bindingCountOutput` +is 0 whenever the views are one set for the whole window. Render local view `j` of a +binding into a tile of `segmentRect.extent × the mode's view scale`, at the segment's +position inside view tile `j` — the runtime composes exactly that mosaic, so an app that +ignores the bindings and renders each active view at full-window size is still correct, +just at a higher cost. + +**When the views stay one set** (and the compositor crops it per segment, as in M2): +a `PRIMARY_STEREO` or `PRIMARY_MONO` session — whose two views are framed from the +display holding most of the window (its eyes, the window relative to it), the other +display showing its own crop of them or a flat 2D copy; a camera-centric locate +(`XrCameraRigDXR`, or the qwerty camera rig of a runtime-window session), which frames a +fixed-FOV camera on the whole window; a zone-scoped locate (`XrDisplayZoneDXR`); a window +covering more displays than `XRT_MAX_SEGMENTS`; a display of unknown physical size. + +**Scope (M3).** In-process Vulkan sessions on desktop Linux (X11/XWayland), the only +compositor that segments today. IPC/service sessions never segment: their tables are +always one set, `bindingCountOutput` 0. The rendering mode is session-wide, so every +segment carries the same view count. + ### Example Code: Querying Display Mode Support and Requesting 2D > ⚠️ **These inline examples predate the multiview model and the v13 header — read with care.** @@ -2008,10 +2083,11 @@ plug-in id, PRIMARY / TRACKED / SYSTEM_DEFAULT flags), per-display DISPLAY space (`xrCreateDisplaySpaceDXR`) and a session display binding (`XrSessionDisplayBindingDXR`). See *Per-display enumeration, DISPLAY spaces and session display binding (v22)*. -Still open, and planned in `docs/roadmap/multi-screen.md`: a window that spans two -displays woven per segment (M2), per-view display bindings on `XrViewState` and -per-display DISPLAY poses — i.e. meaningful relationships between two displays' spaces -(M3, `1004999217` reserved), and a second tracked panel with its own tracker (M5). +Since M3 the same version also carries per-segment views and `XrViewDisplayBindingsDXR` +(`1004999217`) — see *Per-segment views and view display bindings*. Still open, and +planned in `docs/roadmap/multi-screen.md`: per-display DISPLAY poses — i.e. meaningful +relationships between two displays' spaces — and a second tracked panel with its own +tracker (M5). There is deliberately no shared room frame between displays. --- @@ -2484,7 +2560,7 @@ the property) silently ignore the call — graceful degradation. | 19 | 2026-09-17 | David Fattal | **N-view view configuration** (#1486 option B / #80): added `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (`1004999212`) — the extension now extends the core `XrViewConfigurationType` enum. `xrEnumerateViewConfigurations` returns a list: a conformant 2-view `PRIMARY_STEREO` plus, when this extension is enabled, the DXR type reporting the device's max view count across modes. `xrEndFrame` rejects `viewCount > 2` under `PRIMARY_STEREO` and names the DXR type as the opt-in. Additive: no struct, wire or ABI change. Also carries the earlier RAW-mode wording clarification (#1370): eye positions are **relative to the display plane**, not "regardless of the reference space" — `XrViewDisplayRawDXR::displayPlanePose` reports that plane in the locate space, and render-ready views (legacy and rig-chained) honour `XrViewLocateInfo::space` on both legs. | | 20 | 2026-09-18 | David Fattal | **The mode floor** (#1499, follow-up to #1486): added `XR_DISPLAY_MODE_DENIAL_REASON_VIEW_CONFIG_CANNOT_FILL_DXR` (`6`) to `XrDisplayModeDenialReasonDXR`. A session never runs in, or requests, a rendering mode whose `viewCount` exceeds what its primary view configuration reports: `xrBeginSession` moves the display to a fillable mode before the first frame (announced with `XrEventDataRenderingModeChangedDXR`) and `xrRequestDisplayRenderingModeDXR` denies such a request with the new reason, locally, before it reaches the panel-lease holder. A `PRIMARY_MULTIVIEW_DXR` session is never floored or denied; a device that pins its mode and service mode both outrank the floor, and the runtime logs rather than silently clamps. **Enumerant only** — no struct, wire or ABI change. | | 21 | 2026-09-18 | David Fattal | **Fixed view count, per-frame activity** (ADR-041 Model E, #1528 / #1486): added `XrViewActivityStateDXR` (`1004999213`, chained on `XrViewState` at `xrLocateViews`). The located view count is fixed for the session (2 under `PRIMARY_STEREO`, the device maximum under `PRIMARY_MULTIVIEW_DXR`); `activeViewCount` says how many carry the active mode's views, the inactive tail is located at view 0's pose and ignored. `xrEndFrame`'s `viewCount` must equal the located count — an app rendering only the active views aliases the tail onto view 0's subimage. The pre-v21 1-view 2D submission stays accepted (deprecated, `DXR_UNDER_SUBMIT`). Additive: new chained struct only. | -| 22 | 2026-10-07 | David Fattal | **Per-display enumeration** (multi-screen M1, ADR-047): added `XrDisplayDXR` (`1004999214`) + `xrEnumerateDisplaysDXR`, `XrDisplaySpaceCreateInfoDXR` (`1004999215`) + `xrCreateDisplaySpaceDXR`, `XrSessionDisplayBindingDXR` (`1004999216`, chained on `XrSessionCreateInfo`), `XrDisplayFlagsDXR`; `1004999217` reserved for `XrViewDisplayBindingsDXR` (M3). Append-only: `XrSystemProperties` keeps describing the system-default display. v22 limits: every DISPLAY space is the session's single display plane; a binding to a non-default display changes the session's display-scoped Kooima inputs only, not which DP weaves. **Current header version (`XR_DXR_display_info_SPEC_VERSION == 22`).** | +| 22 | 2026-10-07 | David Fattal | **Per-display enumeration** (multi-screen M1, ADR-047): added `XrDisplayDXR` (`1004999214`) + `xrEnumerateDisplaysDXR`, `XrDisplaySpaceCreateInfoDXR` (`1004999215`) + `xrCreateDisplaySpaceDXR`, `XrSessionDisplayBindingDXR` (`1004999216`, chained on `XrSessionCreateInfo`), `XrDisplayFlagsDXR`; multi-screen M3 (same version, append-only) adds `XrViewDisplayBindingsDXR` (`1004999217`, chained on `XrViewState`), per-segment views for `PRIMARY_MULTIVIEW_DXR` sessions and a reported multiview count of device max × the system's view-set capacity (1, or 2 on desktop Linux with two DP-backed screens). Append-only: `XrSystemProperties` keeps describing the system-default display. v22 limits: every DISPLAY space is the session's single display plane; a binding to a non-default display changes the session's display-scoped Kooima inputs only, not which DP weaves. **Current header version (`XR_DXR_display_info_SPEC_VERSION == 22`).** | > The `XR_DXR_display_info_SPEC_VERSION` define in the header is the authoritative current > revision. Earlier revision numbers in this table reflect the proposal's editing history and do diff --git a/src/external/openxr_includes/openxr/README.md b/src/external/openxr_includes/openxr/README.md index fef24dd59..d2decc59e 100644 --- a/src/external/openxr_includes/openxr/README.md +++ b/src/external/openxr_includes/openxr/README.md @@ -40,7 +40,7 @@ Rules: | 1004999180 | `XR_DXR_macos_gl_binding` | relocated from 1004999010 (collided with display_info) — spec v2 | | 1004999190–199 | `XR_DXR_weave` | 190 = `XR_TYPE_WEAVE_SUBMIT_INFO_DXR`, 191 = `XR_TYPE_WEAVE_OUTPUT_DXR` (#625), 192 = `XR_TYPE_WEAVE_SUBMIT_RECTS_DXR` (batch, spec v3), 193 = `XR_TYPE_WEAVE_SUBMIT_OVERLAYS_DXR` (2D overlay atlas, spec v4, browser#18), 194 = `XR_TYPE_WEAVE_SUBMIT_LAYOUT_DXR` (N-view worst-case atlas, spec v6, #774), 195 = `XR_TYPE_WEAVE_BIND_WINDOW_INFO_DXR`, 196 = `XR_TYPE_WEAVE_WINDOW_GEOMETRY_DXR`, 197 = `XR_TYPE_WEAVE_SUBMIT_HANDLES_DXR` (Android / platform-neutral handles + geometry, spec v7, #1036), 198 = `XR_TYPE_WEAVE_SUBMIT_FLAT_REGIONS_DXR` (per-region hardware wish, spec v8, browser#88). **199 is RESERVED** for a spec v9 per-submit wish MASK (R8 texture instead of a rect list, for non-rectangular flat geometry) — do not assign it elsewhere | | 1004999200–209 | `XR_DXR_xlib_window_binding` | 200 = `XR_TYPE_XLIB_WINDOW_BINDING_CREATE_INFO_DXR` (#660 Phase 3) | -| 1004999210–219 | `XR_DXR_display_info` (v16+ additions) | 210 = `XR_TYPE_DISPLAY_DESKTOP_POSITION_DXR` (#715); fresh decade rather than reusing the 005/009 gaps in the original block. 211 = `XR_TYPE_DISPLAY_DESKTOP_INFO_DXR` (spec v18, #1301) — full panel desktop rect + GDI device name, superseding 210. 212 = `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (spec v19, #1486) — an `XrViewConfigurationType`, not an `XrStructureType`: the block is shared across every extended core enum. 213 = `XR_TYPE_VIEW_ACTIVITY_STATE_DXR` (spec v21, ADR-041) — per-frame `activeViewCount` chained on `XrViewState`, the other half of the fixed-view-count submission contract. 214 = `XR_TYPE_DISPLAY_DXR`, 215 = `XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR`, 216 = `XR_TYPE_SESSION_DISPLAY_BINDING_DXR` (spec v22, multi-screen M1 — per-display enumeration, DISPLAY spaces, session display binding). **217 is RESERVED** for `XrViewDisplayBindingsDXR` (per-view display binding on `XrViewState`, multi-screen M3); 218–219 free | +| 1004999210–219 | `XR_DXR_display_info` (v16+ additions) | 210 = `XR_TYPE_DISPLAY_DESKTOP_POSITION_DXR` (#715); fresh decade rather than reusing the 005/009 gaps in the original block. 211 = `XR_TYPE_DISPLAY_DESKTOP_INFO_DXR` (spec v18, #1301) — full panel desktop rect + GDI device name, superseding 210. 212 = `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (spec v19, #1486) — an `XrViewConfigurationType`, not an `XrStructureType`: the block is shared across every extended core enum. 213 = `XR_TYPE_VIEW_ACTIVITY_STATE_DXR` (spec v21, ADR-041) — per-frame `activeViewCount` chained on `XrViewState`, the other half of the fixed-view-count submission contract. 214 = `XR_TYPE_DISPLAY_DXR`, 215 = `XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR`, 216 = `XR_TYPE_SESSION_DISPLAY_BINDING_DXR` (spec v22, multi-screen M1 — per-display enumeration, DISPLAY spaces, session display binding). 217 = `XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR` (spec v22, multi-screen M3 — per-view display bindings chained on `XrViewState`); 218–219 free | | 1004999220–229 | `XR_DXR_android_surface_binding` | 220 = `XR_TYPE_ANDROID_WINDOW_GEOMETRY_DXR` (#1037), 221 = `XR_TYPE_EVENT_DATA_ANDROID_WINDOW_LAYOUT_HINT_DXR` (spec v2, #1396). The extension's `XR_TYPE_ANDROID_SURFACE_BINDING_CREATE_INFO_DXR` keeps **1004999005** — one of the two unused gaps in the `XR_DXR_display_info` decade — because that value was already published in the (then unimplemented) sketch in `docs/specs/extensions/XR_DXR_display_info.md` §4 | | 1004999230–239 | `XR_DXR_result_codes` | Not an extension — runtime-wide `XrResult` values returned from core entry points, where no extension can have been enabled. 230 = `XR_ERROR_RUNTIME_VERSION_SKEW_DXR` (browser#103), an **error**-class result, so it appears in the header negated as `-1004999230` | | 1004999240–249 | `XR_DXR_weave` (v9+ additions) | 240 = `XR_TYPE_WEAVE_IPC_CONNECTION_DXR` (`xrWeaveExportIpcConnectionDXR`, spec v9, browser#103); 241 = `XR_TYPE_WEAVE_DMABUF_DESC_DXR`, 242 = `XR_TYPE_WEAVE_OVERLAY_DMABUF_DESC_DXR`, 243 = `XR_TYPE_WEAVE_OUTPUT_DMABUF_DXR`, 244 = `XR_TYPE_WEAVE_SUBMIT_SYNC_DXR`, 245 = `XR_TYPE_WEAVE_OUTPUT_SYNC_DXR` (desktop-Linux dma-buf transport + sync_file fences, spec v10, #1699); 246 = `XR_TYPE_WEAVE_SNAP_GRID_INFO_DXR` (`xrWeaveSnapWindowGridDXR`, bulk grid snap, spec v11, #1723); 247 = `XR_TYPE_WEAVE_OUTPUT_ORIGIN_DXR`, 248 = `XR_TYPE_WEAVE_WINDOW_LOGICAL_ORIGIN_DXR` (woven origin per output for present-owner move sync, spec v12, browser-pvt#180); 249 = `XR_TYPE_WEAVE_SUBMIT_MONO_IN_2D_DXR` (one flat view instead of the weave while the panel is in hardware 2D, spec v13). The decade is now full. A fresh decade because the original 190–199 block is exhausted (199 is reserved for the wish mask) | diff --git a/src/external/openxr_includes/openxr/XR_DXR_display_info.h b/src/external/openxr_includes/openxr/XR_DXR_display_info.h index 931702200..85f1e691e 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_display_info.h +++ b/src/external/openxr_includes/openxr/XR_DXR_display_info.h @@ -664,12 +664,14 @@ typedef struct XrViewActivityStateDXR { // ---- v22: Per-display enumeration, display spaces, session display binding (multi-screen M1) ---- -// Type values 1004999214-1004999216 (display_info's 210-219 decade, see -// README.md). 1004999217 is RESERVED for XrViewDisplayBindingsDXR (per-view -// display binding chained on XrViewState, multi-screen M3); 218-219 are free. +// Type values 1004999214-1004999217 (display_info's 210-219 decade, see +// README.md): 214-216 per-display enumeration (multi-screen M1), 217 the +// per-view display bindings chained on XrViewState (multi-screen M3); +// 218-219 are free. #define XR_TYPE_DISPLAY_DXR ((XrStructureType)1004999214) #define XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR ((XrStructureType)1004999215) #define XR_TYPE_SESSION_DISPLAY_BINDING_DXR ((XrStructureType)1004999216) +#define XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR ((XrStructureType)1004999217) //! Size of XrDisplayDXR::vendorPluginId, in bytes, including the NUL. #define XR_MAX_DISPLAY_VENDOR_PLUGIN_ID_SIZE_DXR 64 @@ -821,6 +823,70 @@ typedef struct XrSessionDisplayBindingDXR { uint64_t displayId; //!< 0 = runtime decides } XrSessionDisplayBindingDXR; +/*! + * @brief Which views of a locate belong to which display (v22, multi-screen M3). + * + * One entry of XrViewDisplayBindingsDXR::bindings: the contiguous view range + * [firstView, firstView + viewCount) was located for the part of the window + * that lands on display @ref displayId — the window SEGMENT @ref segmentRect. + */ +typedef struct XrViewDisplayBindingDXR { + //! The display these views are for (an id from xrEnumerateDisplaysDXR). + uint64_t displayId; + /*! + * The segment: the part of the window's canvas on that display, in WINDOW + * pixels (the client area, origin top-left). Render these views into a + * tile of `segmentRect.extent * the active mode's view scale`; the + * runtime places that tile at the segment's position in each view tile. + */ + XrRect2Di segmentRect; + //! First view of the range (index into the XrView array xrLocateViews filled). + uint32_t firstView; + //! Number of views in the range (the active rendering mode's view count). + uint32_t viewCount; +} XrViewDisplayBindingDXR; + +/*! + * @brief Per-view display bindings of a locate (v22, multi-screen M3). + * + * Chained to XrViewState in xrLocateViews; standard two-call idiom on the + * binding array (`bindingCapacityInput == 0` returns the count in + * `bindingCountOutput` and writes nothing else; a capacity smaller than the + * count writes the count and no bindings — xrLocateViews itself still + * succeeds, the views are always located). + * + * A window that spans displays is woven per display. Under + * XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR each 3D display the window + * covers then gets its OWN views: the active rendering mode's view count per + * display, located from that display's eyes with that display's segment as + * the Kooima canvas, laid out CONTIGUOUSLY per display in the order of this + * array (left to right on the desktop). XrViewActivityStateDXR::activeViewCount + * is the sum. The inactive tail aliases the last active view, exactly as in + * the single-display case. + * + * bindingCountOutput is 1 for a window on one display (the whole canvas, + * views [0, activeViewCount)) and 0 when the runtime cannot attribute views to + * a display (no window yet, an IPC/service session, a session that began + * XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO: a stereo session keeps its two + * views on the display holding most of the window, and the other display shows + * a flat 2D copy). An app that never chains this struct still renders + * correctly: render every active view, each into its subImage. + * + * View poses of every display are expressed in ONE frame: the display holding + * most of the window. Each other display's segment is placed beside it the way + * the window's pixels continue across the seam, so a scene spanning the seam + * stays continuous. + * + * @extends XrViewState + */ +typedef struct XrViewDisplayBindingsDXR { + XrStructureType type; //!< Must be XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR + void* XR_MAY_ALIAS next; + uint32_t bindingCapacityInput; //!< Capacity of @ref bindings, or 0 to query the count + uint32_t bindingCountOutput; //!< Number of bindings (or the required capacity) + XrViewDisplayBindingDXR* bindings; //!< Array of bindingCapacityInput entries; may be NULL when the capacity is 0 +} XrViewDisplayBindingsDXR; + #ifndef XR_NO_PROTOTYPES XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateDisplaysDXR( XrInstance instance, diff --git a/src/xrt/include/xrt/xrt_display_metrics.h b/src/xrt/include/xrt/xrt_display_metrics.h index 3f93ee279..d7c61f0f1 100644 --- a/src/xrt/include/xrt/xrt_display_metrics.h +++ b/src/xrt/include/xrt/xrt_display_metrics.h @@ -122,6 +122,99 @@ struct xrt_window_metrics bool valid; //!< True if all metrics are valid }; +/*! + * Max window segments that get their own views (multi-screen M3, ADR-047 D3). + * + * A `PRIMARY_MULTIVIEW_DXR` session advertises `device max views x + * XRT_MAX_SEGMENTS` views (capped at XRT_MAX_VIEWS), so a window spanning up to + * this many screens can carry one view set per screen. The plan's eventual cap + * is 4; 2 covers every two-monitor layout and keeps the 8-view sim quad mode + * inside XRT_MAX_VIEWS. A window covering more screens keeps one view set for + * the whole window (the M2 crop path). + * + * @ingroup xrt_iface + */ +#define XRT_MAX_SEGMENTS 2 + +/*! + * One window segment — the part of a window's canvas on one screen — as the + * view math needs it (multi-screen M3). Produced by an in-process compositor + * that segments its window (Vulkan native, desktop Linux). + * + * @ingroup xrt_iface + */ +struct xrt_segment_metric +{ + uint64_t screen_id; //!< The screen (registry monitor id; `XrDisplayDXR::displayId`) + + struct xrt_rect window_rect; //!< The segment in WINDOW pixels (client area, top-left origin) + struct xrt_rect screen_rect; //!< The segment in its SCREEN's pixels (relative to the screen's desktop origin) + + int32_t screen_desktop_left; //!< The screen's desktop rect (the space window positions are read in) + int32_t screen_desktop_top; + uint32_t screen_desktop_width; + uint32_t screen_desktop_height; + float screen_width_m; //!< The screen's physical size, metres (0 = unknown) + float screen_height_m; + float nominal_viewer_x_m; //!< The screen's nominal viewer, screen-centre relative, metres + float nominal_viewer_y_m; + float nominal_viewer_z_m; + + bool is_primary; //!< On the screen the session's own DP weaves + bool has_dp; //!< A display processor exists for the screen right now + bool tolerates_resample; //!< That DP's output survives a resample (`XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE`) + bool woven; //!< The compositor weaves it in 3D (else a flat 2D copy) + + //! The screen DP's predicted eyes, in THAT screen's display space (filled per query). + struct xrt_eye_positions eyes; + bool have_eyes; //!< @ref eyes is valid (the DP reported them) +}; + +/*! + * The segment table of a window, for per-segment views (multi-screen M3). + * + * @ref count is 0 when the window is not segmented (on the primary screen only, + * no window yet, segmentation off, an IPC/service session): the caller then + * keeps the single view set. When non-zero, the compositor weaves the window + * per screen and every segment is listed, left to right. + * + * @ingroup xrt_iface + */ +struct xrt_segment_metrics +{ + uint32_t count; + struct xrt_segment_metric seg[XRT_MAX_SEGMENTS]; + + struct xrt_rect canvas; //!< The canvas the table was cut from, window px + int32_t window_screen_left; //!< The window's client-area origin, desktop coordinates + int32_t window_screen_top; + uint32_t window_pixel_width; + uint32_t window_pixel_height; + + //! Bumped whenever the table's geometry changes (a move, a resize, a screen change). + uint64_t generation; +}; + +/*! + * Where each segment's views go, handed from the state tracker to the + * compositor with the frame they were located for (multi-screen M3). + * + * Views `[first_view[i], first_view[i] + view_count[i])` of the projection + * layer belong to segment @ref rect[i] (window px). @ref count 0 = the frame + * carries one view set for the whole window (nothing to route). + * + * @ingroup xrt_iface + */ +struct xrt_segment_view_routing +{ + uint32_t count; + uint64_t screen_id[XRT_MAX_SEGMENTS]; + struct xrt_rect rect[XRT_MAX_SEGMENTS]; + uint32_t first_view[XRT_MAX_SEGMENTS]; + uint32_t view_count[XRT_MAX_SEGMENTS]; + struct xrt_rect canvas; //!< The canvas the rects are relative to, window px +}; + #ifdef __cplusplus } #endif From 283f3dbf08c2653535a8c0c2ef2e347af729f0a9 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:56:23 -0700 Subject: [PATCH 02/21] feat(oxr): per-segment views for PRIMARY_MULTIVIEW_DXR (multi-screen M3) - PRIMARY_MULTIVIEW_DXR reports device max x XRT_MAX_SEGMENTS views (capped at XRT_MAX_VIEWS); a one-segment window locates exactly as before and aliases the longer tail. The #1499 mode floor asks one segment's share. - xrLocateViews: when the compositor weaves the window per segment, each segment gets the active mode's views, located from that display's eyes (DP eyes when tracking, else its nominal viewer) with the segment as the Kooima canvas, contiguous, in one frame (the majority display's), seam- continuous. activeViewCount is the sum; XrViewDisplayBindingsDXR filled. - PRIMARY_STEREO keeps 2 views, framed from the majority display. - Camera rigs, zone-scoped locates, >2 segments keep one view set. - The routing rides to the compositor at xrEndFrame. - oxr_segment_views.{h,c}: the pure geometry, unit-tested. Co-Authored-By: Claude Fable 5.1 --- src/xrt/state_trackers/oxr/CMakeLists.txt | 1 + src/xrt/state_trackers/oxr/oxr_api_session.c | 7 +- src/xrt/state_trackers/oxr/oxr_objects.h | 20 +- .../state_trackers/oxr/oxr_segment_views.c | 228 ++++++++ .../state_trackers/oxr/oxr_segment_views.h | 178 ++++++ src/xrt/state_trackers/oxr/oxr_session.c | 508 +++++++++++++++++- .../oxr/oxr_session_frame_end.c | 15 +- src/xrt/state_trackers/oxr/oxr_system.c | 32 +- tests/CMakeLists.txt | 8 + tests/tests_oxr_segment_views.cpp | 264 +++++++++ 10 files changed, 1220 insertions(+), 41 deletions(-) create mode 100644 src/xrt/state_trackers/oxr/oxr_segment_views.c create mode 100644 src/xrt/state_trackers/oxr/oxr_segment_views.h create mode 100644 tests/tests_oxr_segment_views.cpp diff --git a/src/xrt/state_trackers/oxr/CMakeLists.txt b/src/xrt/state_trackers/oxr/CMakeLists.txt index 6c21fc979..ac0386020 100644 --- a/src/xrt/state_trackers/oxr/CMakeLists.txt +++ b/src/xrt/state_trackers/oxr/CMakeLists.txt @@ -49,6 +49,7 @@ add_library( oxr_plugin_stub.c oxr_pretty_print.c oxr_pretty_print.h + oxr_segment_views.c oxr_session.c oxr_session_window_binding.h oxr_session_frame_end.c diff --git a/src/xrt/state_trackers/oxr/oxr_api_session.c b/src/xrt/state_trackers/oxr/oxr_api_session.c index 7c8f5c8b3..8dae41cc5 100644 --- a/src/xrt/state_trackers/oxr/oxr_api_session.c +++ b/src/xrt/state_trackers/oxr/oxr_api_session.c @@ -21,6 +21,7 @@ #include "util/u_debug.h" #include "util/u_trace_marker.h" +#include "oxr_segment_views.h" #include "oxr_frame_sync.h" #include "oxr_objects.h" #include "oxr_logger.h" @@ -1577,7 +1578,11 @@ oxr_xrRequestDisplayRenderingModeDXR(XrSession session, uint32_t modeIndex) // Same fallback as the floor: a session that somehow has no view // count yet is a 2-view session, NOT a session that can fill nothing // (passing 0 to oxr_mode_fillable_by would deny every mode). - const uint32_t max_submit = sess->view_config_view_count != 0 ? sess->view_config_view_count : 2; + // Multi-screen M3: a multiview session reports one view set per + // window segment; what fills a mode is ONE segment's share. + const uint32_t max_submit = oxr_segment_views_per_segment_capacity( + sess->view_config_view_count != 0 ? sess->view_config_view_count : 2, + sess->view_config_type == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR, head->hmd->view_count); if (!oxr_mode_fillable_by(&head->rendering_modes[modeIndex], max_submit)) { U_LOG_W( diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index 496ed9027..e25b17e6b 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -2302,13 +2302,17 @@ struct oxr_system * * @ref view_config_view_counts is the count each TYPE reports * (PRIMARY_MONO 1, PRIMARY_STEREO always 2, PRIMARY_MULTIVIEW_DXR the - * device max across rendering modes). It is NOT @ref view_count: that - * one stays the DEVICE max and keeps sizing every internal array, the - * IPC mirror and @ref views. + * device max across rendering modes times XRT_MAX_SEGMENTS — one view set + * per window segment, multi-screen M3). It is NOT @ref view_count: that + * one stays the DEVICE max and keeps sizing every device-facing call and + * the IPC mirror. */ uint32_t view_config_count; //!< 1 or 2. XrViewConfigurationType view_config_types[2]; //!< Advertised types. uint32_t view_config_view_counts[2]; //!< Views reported per type. + //! Multi-screen M3: the largest of @ref view_config_view_counts — how many + //! entries of @ref views (and the #1488 shadow) are filled. + uint32_t view_config_max_count; /*! * #1486: DXR_VIEW_CONFIG_LEGACY=1, latched at @ref oxr_system_fill_in. @@ -2842,6 +2846,16 @@ struct oxr_session bool bound_display_overrides; struct xrt_screen_info bound_display_info; + /*! + * Multi-screen M3 (ADR-047 D3): which views of the app's last + * xrLocateViews belong to which window segment. count 0 = one view set + * for the whole window. Handed to the compositor with the next frame + * (xrEndFrame), whose projection layer carries those views. + */ + struct xrt_segment_view_routing seg_route; + //! Last per-segment view layout logged (one INFO line per change). + struct xrt_segment_view_routing seg_route_logged; + #ifdef XRT_OS_ANDROID //! The last ANativeWindow this session published into `android_globals` //! from XR_DXR_android_surface_binding (#1037). A MARKER, not an owned diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.c b/src/xrt/state_trackers/oxr/oxr_segment_views.c new file mode 100644 index 000000000..eac32d8f2 --- /dev/null +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.c @@ -0,0 +1,228 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-segment views geometry (multi-screen M3) — see oxr_segment_views.h. + * @ingroup oxr_main + */ + +#include "oxr_segment_views.h" + +#include + + +static float +pitch_x(const struct xrt_segment_metric *s) +{ + return s->screen_desktop_width > 0 ? s->screen_width_m / (float)s->screen_desktop_width : 0.0f; +} + +static float +pitch_y(const struct xrt_segment_metric *s) +{ + return s->screen_desktop_height > 0 ? s->screen_height_m / (float)s->screen_desktop_height : 0.0f; +} + +/*! + * Distance along one axis from the majority centre to a segment centre, in + * metres: the majority pitch up to the majority edge, the segment's own pitch + * beyond it. @p d is the centre-to-centre distance in window px, @p half the + * majority segment's half extent in window px. + */ +static float +seam_distance(float d, float half, float pitch_major, float pitch_own) +{ + const float ad = d < 0.0f ? -d : d; + const float sign = d < 0.0f ? -1.0f : 1.0f; + if (ad <= half) { + return d * pitch_major; + } + return sign * (half * pitch_major + (ad - half) * pitch_own); +} + +uint32_t +oxr_segment_views_majority(const struct xrt_segment_metrics *m) +{ + if (m == NULL || m->count == 0) { + return 0; + } + uint32_t best = 0; + uint64_t best_area = 0; + bool best_primary = false; + for (uint32_t i = 0; i < m->count && i < XRT_MAX_SEGMENTS; i++) { + const struct xrt_rect *r = &m->seg[i].window_rect; + const uint64_t area = + (uint64_t)(r->extent.w > 0 ? r->extent.w : 0) * (uint64_t)(r->extent.h > 0 ? r->extent.h : 0); + const bool primary = m->seg[i].is_primary; + // Strictly larger wins; an exact tie goes to the primary screen, else + // keeps the earlier (leftmost) segment. + if (i == 0 || area > best_area || (area == best_area && primary && !best_primary)) { + best = i; + best_area = area; + best_primary = primary; + } + } + return best; +} + +bool +oxr_segment_views_layout(const struct xrt_segment_metrics *m, struct oxr_segment_layout *out) +{ + if (m == NULL || out == NULL) { + return false; + } + memset(out, 0, sizeof(*out)); + if (m->count == 0 || m->count > XRT_MAX_SEGMENTS) { + return false; + } + + for (uint32_t i = 0; i < m->count; i++) { + const struct xrt_segment_metric *s = &m->seg[i]; + if (s->screen_width_m <= 0.0f || s->screen_height_m <= 0.0f || s->screen_desktop_width == 0 || + s->screen_desktop_height == 0 || s->window_rect.extent.w <= 0 || s->window_rect.extent.h <= 0) { + return false; + } + const float px = pitch_x(s); + const float py = pitch_y(s); + const float scx = (float)s->screen_rect.offset.w + (float)s->screen_rect.extent.w * 0.5f; + const float scy = (float)s->screen_rect.offset.h + (float)s->screen_rect.extent.h * 0.5f; + struct oxr_segment_place *p = &out->seg[i]; + p->own_cx = (scx - (float)s->screen_desktop_width * 0.5f) * px; + p->own_cy = -(scy - (float)s->screen_desktop_height * 0.5f) * py; // +Y up + p->w_m = (float)s->window_rect.extent.w * px; + p->h_m = (float)s->window_rect.extent.h * py; + } + + const uint32_t mj = oxr_segment_views_majority(m); + const struct xrt_segment_metric *ms = &m->seg[mj]; + const float mcx = (float)ms->window_rect.offset.w + (float)ms->window_rect.extent.w * 0.5f; + const float mcy = (float)ms->window_rect.offset.h + (float)ms->window_rect.extent.h * 0.5f; + const float half_x = (float)ms->window_rect.extent.w * 0.5f; + const float half_y = (float)ms->window_rect.extent.h * 0.5f; + + float x0 = 0.0f, x1 = 0.0f, y0 = 0.0f, y1 = 0.0f; + for (uint32_t i = 0; i < m->count; i++) { + const struct xrt_segment_metric *s = &m->seg[i]; + struct oxr_segment_place *p = &out->seg[i]; + if (i == mj) { + p->ref_cx = p->own_cx; + p->ref_cy = p->own_cy; + } else { + const float cx = (float)s->window_rect.offset.w + (float)s->window_rect.extent.w * 0.5f; + const float cy = (float)s->window_rect.offset.h + (float)s->window_rect.extent.h * 0.5f; + p->ref_cx = out->seg[mj].own_cx + seam_distance(cx - mcx, half_x, pitch_x(ms), pitch_x(s)); + // Window px grow downward, display space grows up. + p->ref_cy = out->seg[mj].own_cy - seam_distance(cy - mcy, half_y, pitch_y(ms), pitch_y(s)); + } + const float l = p->ref_cx - p->w_m * 0.5f; + const float r = p->ref_cx + p->w_m * 0.5f; + const float b = p->ref_cy - p->h_m * 0.5f; + const float t = p->ref_cy + p->h_m * 0.5f; + if (i == 0 || l < x0) { + x0 = l; + } + if (i == 0 || r > x1) { + x1 = r; + } + if (i == 0 || b < y0) { + y0 = b; + } + if (i == 0 || t > y1) { + y1 = t; + } + } + out->count = m->count; + out->majority = mj; + out->window_ref_cx = (x0 + x1) * 0.5f; + out->window_ref_cy = (y0 + y1) * 0.5f; + return true; +} + +static void +fill_display(const struct xrt_segment_metric *s, struct xrt_window_metrics *out) +{ + out->display_width_m = s->screen_width_m; + out->display_height_m = s->screen_height_m; + out->display_pixel_width = s->screen_desktop_width; + out->display_pixel_height = s->screen_desktop_height; + out->display_screen_left = s->screen_desktop_left; + out->display_screen_top = s->screen_desktop_top; + out->window_orientation = (struct xrt_quat){0.0f, 0.0f, 0.0f, 1.0f}; +} + +void +oxr_segment_views_window_metrics(const struct xrt_segment_metrics *m, + const struct oxr_segment_layout *l, + uint32_t i, + struct xrt_window_metrics *out) +{ + memset(out, 0, sizeof(*out)); + if (m == NULL || l == NULL || i >= m->count || i >= l->count) { + return; + } + const struct xrt_segment_metric *s = &m->seg[i]; + fill_display(s, out); + out->window_pixel_width = (uint32_t)s->window_rect.extent.w; + out->window_pixel_height = (uint32_t)s->window_rect.extent.h; + // The segment's desktop origin: its own screen's origin + its screen rect. + out->window_screen_left = s->screen_desktop_left + s->screen_rect.offset.w; + out->window_screen_top = s->screen_desktop_top + s->screen_rect.offset.h; + out->window_width_m = l->seg[i].w_m; + out->window_height_m = l->seg[i].h_m; + // The canvas centre in the REFERENCE frame: the eye set handed to the + // Kooima block is in that frame too, so eye - centre = the eye relative + // to the segment as it sits on its own screen. + out->window_center_offset_x_m = l->seg[i].ref_cx; + out->window_center_offset_y_m = l->seg[i].ref_cy; + out->window_center_offset_z_m = 0.0f; + out->valid = true; +} + +void +oxr_segment_views_whole_window_metrics(const struct xrt_segment_metrics *m, uint32_t i, struct xrt_window_metrics *out) +{ + memset(out, 0, sizeof(*out)); + if (m == NULL || i >= m->count || m->window_pixel_width == 0 || m->window_pixel_height == 0) { + return; + } + const struct xrt_segment_metric *s = &m->seg[i]; + if (s->screen_desktop_width == 0 || s->screen_desktop_height == 0 || s->screen_width_m <= 0.0f || + s->screen_height_m <= 0.0f) { + return; + } + fill_display(s, out); + const float px = pitch_x(s); + const float py = pitch_y(s); + out->window_pixel_width = m->window_pixel_width; + out->window_pixel_height = m->window_pixel_height; + out->window_screen_left = m->window_screen_left; + out->window_screen_top = m->window_screen_top; + out->window_width_m = (float)m->window_pixel_width * px; + out->window_height_m = (float)m->window_pixel_height * py; + const float wcx = (float)(m->window_screen_left - s->screen_desktop_left) + (float)m->window_pixel_width * 0.5f; + const float wcy = (float)(m->window_screen_top - s->screen_desktop_top) + (float)m->window_pixel_height * 0.5f; + out->window_center_offset_x_m = (wcx - (float)s->screen_desktop_width * 0.5f) * px; + out->window_center_offset_y_m = -(wcy - (float)s->screen_desktop_height * 0.5f) * py; + out->valid = true; +} + +uint32_t +oxr_segment_views_assign(uint32_t segment_count, + uint32_t per_segment, + uint32_t reported_view_count, + uint32_t out_first[XRT_MAX_SEGMENTS], + uint32_t out_count[XRT_MAX_SEGMENTS]) +{ + if (segment_count == 0 || segment_count > XRT_MAX_SEGMENTS || per_segment == 0) { + return 0; + } + const uint32_t total = segment_count * per_segment; + if (total > reported_view_count || total > XRT_MAX_VIEWS) { + return 0; + } + for (uint32_t k = 0; k < segment_count; k++) { + out_first[k] = k * per_segment; + out_count[k] = per_segment; + } + return total; +} diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h new file mode 100644 index 000000000..84f0469d6 --- /dev/null +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -0,0 +1,178 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-segment views: the pure geometry behind xrLocateViews for a + * window that spans screens (multi-screen M3, ADR-047 D3). + * @ingroup oxr_main + * + * A window woven per screen (see docs/architecture/comp-segments.md) gets, + * under `PRIMARY_MULTIVIEW_DXR`, one view set PER SEGMENT: each located from + * that screen's eyes, with that segment (relative to its own screen, in its + * own screen's metres) as the Kooima canvas. Everything here is the arithmetic + * that turns a compositor segment table into those inputs — no session, no + * device, so it is unit-tested headless (`tests/tests_oxr_segment_views.cpp`): + * + * - @ref oxr_segment_views_multiview_count — the advertised view count. + * - @ref oxr_segment_views_majority — the segment holding most of the window. + * - @ref oxr_segment_views_layout — every segment placed in ONE frame (the + * majority screen's display space), continuous across the seam. + * - @ref oxr_segment_views_window_metrics — a segment as the window metrics + * the existing Kooima block consumes. + * - @ref oxr_segment_views_assign — the contiguous view ranges. + */ + +#pragma once + +#include "xrt/xrt_display_metrics.h" +#include "xrt/xrt_limits.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! + * The view count `PRIMARY_MULTIVIEW_DXR` reports: the device max per segment, + * times @ref XRT_MAX_SEGMENTS, capped at @ref XRT_MAX_VIEWS. A session with one + * segment locates exactly what it did before and aliases the larger tail. + */ +static inline uint32_t +oxr_segment_views_multiview_count(uint32_t device_max_views) +{ + if (device_max_views == 0) { + return 0; + } + uint32_t n = device_max_views * XRT_MAX_SEGMENTS; + return n > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : n; +} + +/*! + * How many views ONE segment of a session can carry: the per-segment share of + * the reported count. This is what the #1499 mode floor asks "can this session + * fill that mode?" against — a multiview session fills any mode on every + * segment, a stereo session (which never splits) fills 2-view modes. + */ +static inline uint32_t +oxr_segment_views_per_segment_capacity(uint32_t reported_view_count, bool is_multiview, uint32_t device_max_views) +{ + if (!is_multiview || reported_view_count <= device_max_views) { + return reported_view_count; + } + return device_max_views; +} + +/*! + * The segment holding the largest share of the window (area of its window + * rect). Ties go to the primary screen, then to the leftmost segment. + * Returns 0 for an empty table. + */ +uint32_t +oxr_segment_views_majority(const struct xrt_segment_metrics *m); + +/*! + * One segment placed in the reference frame. + * + * Frames are display spaces: origin at a screen's centre, +X right, +Y up, + * metres. "own" is the segment's own screen, "ref" the majority screen's. + */ +struct oxr_segment_place +{ + float own_cx, own_cy; //!< Segment centre in its own screen's display space + float ref_cx, ref_cy; //!< Segment centre in the reference (majority) display space + float w_m, h_m; //!< Segment physical size, its own screen's metres +}; + +/*! + * Every segment of a table, placed in one frame. + */ +struct oxr_segment_layout +{ + uint32_t count; + uint32_t majority; + struct oxr_segment_place seg[XRT_MAX_SEGMENTS]; + //! Centre of the union of all segments in the reference frame — where the + //! whole window's centre sits, so a window-centred rig (XR_DXR_view_rig's + //! display rig) can be offset per segment. + float window_ref_cx, window_ref_cy; +}; + +/*! + * Place every segment of @p m in the majority screen's display space. + * + * The majority segment keeps its true position. Every other segment is put + * where the window's pixels continue across the seam: along each axis, the + * distance from the majority centre is measured in the majority screen's pixel + * pitch up to the majority segment's edge and in the segment's OWN pitch beyond + * it, so two segments that share a seam in window pixels also share it in + * metres (no gap, no overlap) even when the two panels' pitches differ. + * + * @return false when any segment lacks a physical size or a desktop rect (the + * caller then keeps the single view set). + */ +bool +oxr_segment_views_layout(const struct xrt_segment_metrics *m, struct oxr_segment_layout *out); + +/*! + * Segment @p i as the window metrics the session's Kooima block consumes: + * canvas = the segment in its own screen's metres, its centre at + * oxr_segment_place::ref_cx/ref_cy (the window offset of the reference frame), + * display = segment @p i's own screen (so XrViewDisplayRawDXR::canvasRectPx + * reports the segment relative to its own screen). + */ +void +oxr_segment_views_window_metrics(const struct xrt_segment_metrics *m, + const struct oxr_segment_layout *l, + uint32_t i, + struct xrt_window_metrics *out); + +/*! + * The WHOLE window as window metrics relative to segment @p i's screen — what a + * `PRIMARY_STEREO` session spanning screens locates against: its two views stay + * one set for the whole window (the compositor crops them per segment, M2), + * framed from the majority screen. + */ +void +oxr_segment_views_whole_window_metrics(const struct xrt_segment_metrics *m, uint32_t i, struct xrt_window_metrics *out); + +/*! + * The translation that carries a point in segment @p i's own display space into + * the reference frame (x, y; z is shared). + */ +static inline void +oxr_segment_views_own_to_ref(const struct oxr_segment_layout *l, uint32_t i, float *out_dx, float *out_dy) +{ + *out_dx = l->seg[i].ref_cx - l->seg[i].own_cx; + *out_dy = l->seg[i].ref_cy - l->seg[i].own_cy; +} + +/*! + * Contiguous view ranges: segment k gets `per_segment` views starting at + * `k * per_segment`. + * + * @return the total (= the session's activeViewCount), or 0 when it does not + * fit in @p reported_view_count (the caller keeps one view set). + */ +uint32_t +oxr_segment_views_assign(uint32_t segment_count, + uint32_t per_segment, + uint32_t reported_view_count, + uint32_t out_first[XRT_MAX_SEGMENTS], + uint32_t out_count[XRT_MAX_SEGMENTS]); + +/*! + * Which located view view @p i carries: itself while active, view 0 for the + * inactive tail `[active_view_count, reported)` — the ADR-041 alias, the same + * with one view set or one per segment. + */ +static inline uint32_t +oxr_segment_views_alias_source(uint32_t i, uint32_t active_view_count) +{ + return i < active_view_count ? i : 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 27247b34d..3264f2db9 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -80,6 +80,7 @@ #include "oxr_chain.h" #include "oxr_api_verify.h" #include "oxr_pretty_print.h" +#include "oxr_segment_views.h" #include "oxr_conversions.h" #include "oxr_xret.h" #include "oxr_legacy_mode_rule.h" // #1510/#1499 mode floor @@ -1168,8 +1169,12 @@ oxr_session_begin(struct oxr_logger *log, struct oxr_session *sess, const XrSess * is untouched by construction. */ if (!legacy_session && oxr_session_mode_floor_enabled()) { - const uint32_t max_submit = - sess->view_config_view_count != 0 ? sess->view_config_view_count : 2; + // Multi-screen M3: a multiview session reports one view set + // per segment; what fills a mode is ONE segment's share. + const uint32_t max_submit = oxr_segment_views_per_segment_capacity( + sess->view_config_view_count != 0 ? sess->view_config_view_count : 2, + sess->view_config_type == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR, + head->hmd->view_count); // The two carve-outs, shared verbatim with the denial in // oxr_xrRequestDisplayRenderingModeDXR so they cannot diverge. @@ -2468,14 +2473,51 @@ oxr_session_fill_cursor_depth(struct oxr_session *sess, } #endif // OXR_HAVE_DXR_cursor_depth -XrResult -oxr_session_locate_views(struct oxr_logger *log, - struct oxr_session *sess, - const XrViewLocateInfo *viewLocateInfo, - XrViewState *viewState, - uint32_t viewCapacityInput, - uint32_t *viewCountOutput, - XrView *views) +/*! + * Multi-screen M3: what one segment's locate uses instead of the session's own + * DP + window (ADR-047 D3). NULL everywhere below = the single-screen path, + * byte for byte. + * + * Everything is expressed in the REFERENCE frame — the display space of the + * screen holding most of the window (oxr_segment_views_layout): the eyes are + * already translated into it, and @ref wm puts the segment's canvas centre + * there too, so `eye - canvas centre` is the eye relative to the segment as it + * sits on its own screen (that screen's Kooima). + */ +struct oxr_locate_segment +{ + //! The canvas: this segment (or, for a stereo session, the whole window). + struct xrt_window_metrics wm; + //! The screen DP's eyes, reference frame. have_eyes false = nominal viewer. + struct xrt_eye_positions eyes; + bool have_eyes; + //! The screen's nominal viewer (its own display space). + float nominal_y_m; + float nominal_z_m; + //! Own-screen -> reference-frame translation (applied to the nominal eyes). + float eye_dx; + float eye_dy; + //! This canvas centre minus the whole window's centre, reference metres: + //! a window-centred display rig puts the WINDOW at its pose, so each + //! segment's virtual canvas is offset by m2v times this. + float rig_dx; + float rig_dy; + //! Views this call locates (the per-segment count); 0 = the session's. + uint32_t view_count; + //! Publish session-level side effects (#1502 VIEW offset, #441 tracking + //! edge): only the majority segment's call does. + bool side_effects; +}; + +static XrResult +locate_views_one(struct oxr_logger *log, + struct oxr_session *sess, + const XrViewLocateInfo *viewLocateInfo, + XrViewState *viewState, + uint32_t viewCapacityInput, + uint32_t *viewCountOutput, + XrView *views, + const struct oxr_locate_segment *seg) { struct oxr_sink_logger slog = {0}; bool print = sess->sys->inst->debug_views; @@ -2497,11 +2539,30 @@ oxr_session_locate_views(struct oxr_logger *log, */ uint32_t view_count = xdev->hmd->view_count; uint32_t reported_view_count = sess->view_config_view_count; - if (reported_view_count == 0 || reported_view_count > view_count) { - // Defensive: a session begun before the field was seeded, or a view - // config claiming more views than the device has. + // Multi-screen M3: PRIMARY_MULTIVIEW_DXR legitimately reports MORE views + // than the device has — one set per window segment + // (oxr_segment_views_multiview_count). Every other type is bounded by the + // device. + uint32_t reported_cap = view_count; +#ifdef OXR_HAVE_DXR_display_info + if (sess->view_config_type == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR) { + reported_cap = oxr_segment_views_multiview_count(view_count); + } +#endif + if (reported_view_count == 0) { + // Defensive: a session begun before the field was seeded. reported_view_count = view_count; + } else if (reported_view_count > reported_cap) { + // Defensive: a view config claiming more views than it may. + reported_view_count = reported_cap; + } + if (seg != NULL && seg->view_count != 0 && seg->view_count <= view_count) { + // One segment's locate: exactly that segment's view set. + reported_view_count = seg->view_count; } + // The views the device math below fills: never more than the device has. + // Views past it are the multiview tail and are aliased further down. + const uint32_t located_view_count = reported_view_count < view_count ? reported_view_count : view_count; // Active rendering mode's view count — controls mono vs 3D eye assignment. // view_count is the max across all modes, but active_view_count reflects @@ -2738,7 +2799,13 @@ oxr_session_locate_views(struct oxr_logger *log, // checks xmcc != NULL before casting to multi_compositor, so IPC proxies // are handled safely (returns false). In IPC mode, the server handles // eye tracking via ipc_try_get_sr_view_poses. - bool got_eye_positions = oxr_session_get_predicted_eye_positions(sess, &eye_pos); + bool got_eye_positions = false; + if (seg != NULL) { + eye_pos = seg->eyes; + got_eye_positions = seg->have_eyes; + } else { + got_eye_positions = oxr_session_get_predicted_eye_positions(sess, &eye_pos); + } // One-shot diagnostic: log stereo gate values for first frames { @@ -2900,6 +2967,9 @@ oxr_session_locate_views(struct oxr_logger *log, // or from the bound display (XrSessionDisplayBindingDXR, M1). float nominal_z_m = 0.0f; oxr_session_get_nominal_viewer(sess, NULL, &nominal_z_m); + if (seg != NULL) { + nominal_z_m = seg->nominal_z_m; + } U_LOG_I("Kooima: nominal_z=%.3f", nominal_z_m); if (nominal_z_m > 0.0f) { float ipd_m = sess->ipd_meters; @@ -2915,6 +2985,14 @@ oxr_session_locate_views(struct oxr_logger *log, // (the `nominal` vector below), never to the eye position. adj_eyes[0] = (struct xrt_eye_position){-ipd_m / 2.0f, 0.0f, nominal_z_m}; adj_eyes[1] = (struct xrt_eye_position){ipd_m / 2.0f, 0.0f, nominal_z_m}; + if (seg != NULL) { + // In front of THIS segment's screen, carried into + // the reference frame. + for (uint32_t ei = 0; ei < 2; ei++) { + adj_eyes[ei].x += seg->eye_dx; + adj_eyes[ei].y += seg->eye_dy; + } + } have_eye_positions = true; if (should_log) { U_LOG_I("Nominal eyes: [0]=(%.4f,%.4f,%.4f) [1]=(%.4f,%.4f,%.4f), IPD=%.1fmm", @@ -3002,7 +3080,16 @@ oxr_session_locate_views(struct oxr_logger *log, bool win_has_orientation = false; struct xrt_window_metrics wm = {0}; - bool have_wm = oxr_session_get_window_metrics(sess, &wm); + bool have_wm = false; + if (seg != NULL) { + // Multi-screen M3: the segment (or, stereo, the whole window + // framed from the majority screen) instead of the session + // DP's window metrics. + wm = seg->wm; + have_wm = wm.valid; + } else { + have_wm = oxr_session_get_window_metrics(sess, &wm); + } #ifdef OXR_HAVE_DXR_display_zones // Zone-scoped locate: rebase the window metrics to the @@ -3117,6 +3204,10 @@ oxr_session_locate_views(struct oxr_logger *log, // Nominal viewer for view math (parallax lerp target) struct xrt_vec3 nominal = {0, 0, 0}; oxr_session_get_nominal_viewer(sess, &nominal.y, &nominal.z); + if (seg != NULL) { + nominal.y = seg->nominal_y_m; + nominal.z = seg->nominal_z_m; + } struct xrt_vec3 raw_eyes[XRT_MAX_VIEWS]; // The DP owns multi-view eye fill: it must report one eye @@ -3199,6 +3290,21 @@ oxr_session_locate_views(struct oxr_logger *log, dt.virtual_display_height = screen_height_m; // identity m2v } + // Multi-screen M3: a display rig centres the WINDOW on + // its pose; this segment's canvas sits m2v x its offset + // from the window centre (ref frame -> rig frame). + if (seg != NULL && (seg->rig_dx != 0.0f || seg->rig_dy != 0.0f) && + screen_height_m > 0.0f) { + const float m2v = dt.virtual_display_height / screen_height_m; + const struct xrt_vec3 off = {seg->rig_dx * m2v, seg->rig_dy * m2v, + 0.0f}; + struct xrt_vec3 off_rot; + math_quat_rotate_vec3(&display_pose.orientation, &off, &off_rot); + display_pose.position.x += off_rot.x; + display_pose.position.y += off_rot.y; + display_pose.position.z += off_rot.z; + } + struct dxr_xrt_view disp_views[XRT_MAX_VIEWS]; dxr_xrt_display3d_compute_views(raw_eyes, eye_count, &nominal, &scr, &dt, &display_pose, disp_views); @@ -3535,7 +3641,7 @@ oxr_session_locate_views(struct oxr_logger *log, if (print) { - for (uint32_t i = 0; i < reported_view_count; i++) { + for (uint32_t i = 0; i < located_view_count; i++) { char tmp[32]; snprintf(tmp, 32, "xdev.view[%i]", i); oxr_pp_fov_indented_as_object(&slog, &fovs[i], tmp); @@ -3552,8 +3658,10 @@ oxr_session_locate_views(struct oxr_logger *log, uint32_t base_space_view_count = 0; // #1486: bounded by what the app's view configuration reports, not by the - // device max — views[] is only reported_view_count long. - for (uint32_t i = 0; i < reported_view_count; i++) { + // device max — views[] is only reported_view_count long. Multi-screen M3: + // and by the device, whose math filled only that many; the multiview tail + // past it is aliased below with the other inactive views. + for (uint32_t i = 0; i < located_view_count; i++) { /* * Pose */ @@ -3856,8 +3964,8 @@ oxr_session_locate_views(struct oxr_logger *log, { const enum xrt_space_relation_flags need = XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_ORIENTATION_VALID_BIT; - const bool measurable = !rig_active && active_view_count > 0 && reported_view_count > 0 && - base_space_view_count >= active_view_count && + const bool measurable = (seg == NULL || seg->side_effects) && !rig_active && active_view_count > 0 && + reported_view_count > 0 && base_space_view_count >= active_view_count && (T_base_head.relation_flags & need) == need; if (measurable) { struct xrt_vec3 centroid = {0.0f, 0.0f, 0.0f}; @@ -3945,12 +4053,14 @@ oxr_session_locate_views(struct oxr_logger *log, // sample yet": the first locate establishes the baseline // without firing (apps read the initial state from // XrViewEyeTrackingStateDXR). - if (sess->last_is_tracking >= 0 && (sess->last_is_tracking != 0) != is_tracking) { - oxr_event_push_XrEventDataEyeTrackingStateChanged( - log, sess, is_tracking ? XR_TRUE : XR_FALSE, - (XrEyeTrackingModeDXR)sess->eye_tracking_mode); + if (seg == NULL || seg->side_effects) { + if (sess->last_is_tracking >= 0 && (sess->last_is_tracking != 0) != is_tracking) { + oxr_event_push_XrEventDataEyeTrackingStateChanged( + log, sess, is_tracking ? XR_TRUE : XR_FALSE, + (XrEyeTrackingModeDXR)sess->eye_tracking_mode); + } + sess->last_is_tracking = is_tracking ? 1 : 0; } - sess->last_is_tracking = is_tracking ? 1 : 0; } #endif // OXR_HAVE_DXR_display_info @@ -3963,6 +4073,346 @@ oxr_session_locate_views(struct oxr_logger *log, return oxr_session_success_result(sess); } + +/* + * + * Multi-screen M3 (ADR-047 D3): per-segment views. + * + */ + +//! The compositor's segment table, when this session's compositor segments its +//! window (in-process Vulkan, desktop Linux). Everything else — the other +//! native compositors, IPC/service sessions (the service never segments) — +//! answers false and keeps one view set. +static bool +locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics *out) +{ + memset(out, 0, sizeof(*out)); +#ifdef XRT_HAVE_VK_NATIVE_COMPOSITOR + if (sess->xcn != NULL && sess->is_vk_native_compositor) { + return comp_vk_native_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; + } +#else + (void)sess; +#endif + return false; +} + +/*! + * Can this locate be split per segment? Per-segment views are a display-centric + * notion: each segment is a canvas the eye looks through. A camera-centric + * locate (a chained XrCameraRigDXR, or the qwerty camera synthesis of a + * runtime-window session) frames a fixed-FOV camera on the whole window and has + * no per-canvas meaning, and a zone-scoped locate already names its own canvas; + * both keep one view set (the compositor then crops it per segment, as in M2). + */ +static bool +locate_can_split(struct oxr_session *sess, const XrViewLocateInfo *info) +{ +#ifdef OXR_HAVE_DXR_display_zones + if (sess->sys->inst->extensions.DXR_display_zones && + OXR_GET_INPUT_FROM_CHAIN(info, XR_TYPE_DISPLAY_ZONE_DXR, XrDisplayZoneDXR) != NULL) { + return false; + } +#endif +#ifdef OXR_HAVE_DXR_view_rig + if (sess->sys->inst->extensions.DXR_view_rig) { + if (OXR_GET_INPUT_FROM_CHAIN(info, XR_TYPE_CAMERA_RIG_DXR, XrCameraRigDXR) != NULL) { + return false; + } + if (OXR_GET_INPUT_FROM_CHAIN(info, XR_TYPE_DISPLAY_RIG_DXR, XrDisplayRigDXR) != NULL) { + return true; + } + } +#else + (void)info; +#endif +#ifdef XRT_BUILD_DRIVER_QWERTY + if (!sess->has_external_window) { + struct qwerty_view_state vs = {0}; + if (qwerty_get_view_state(sess->sys->xsysd->xdevs, sess->sys->xsysd->xdev_count, &vs) && + vs.camera_mode) { + return false; + } + } +#endif + return true; +} + +//! Segment @p i of @p m as a locate override (see struct oxr_locate_segment). +static void +locate_segment_override(const struct xrt_segment_metrics *m, + const struct oxr_segment_layout *l, + uint32_t i, + struct oxr_locate_segment *o) +{ + memset(o, 0, sizeof(*o)); + const struct xrt_segment_metric *s = &m->seg[i]; + oxr_segment_views_window_metrics(m, l, i, &o->wm); + + float dx = 0.0f, dy = 0.0f; + oxr_segment_views_own_to_ref(l, i, &dx, &dy); + o->eye_dx = dx; + o->eye_dy = dy; + o->nominal_y_m = s->nominal_viewer_y_m; + o->nominal_z_m = s->nominal_viewer_z_m; + + // The screen's DP eyes when it is tracking a viewer; otherwise the + // screen's nominal viewer (the locate's untracked branch). + if (s->have_eyes && s->eyes.valid && s->eyes.is_tracking && s->eyes.count > 0) { + o->eyes = s->eyes; + for (uint32_t e = 0; e < o->eyes.count && e < XRT_MAX_VIEWS; e++) { + o->eyes.eyes[e].x += dx; + o->eyes.eyes[e].y += dy; + } + o->have_eyes = true; + } + + o->rig_dx = l->seg[i].ref_cx - l->window_ref_cx; + o->rig_dy = l->seg[i].ref_cy - l->window_ref_cy; +} + +static void +locate_fill_bindings(struct oxr_session *sess, XrViewState *viewState, const struct xrt_segment_view_routing *r) +{ +#ifdef OXR_HAVE_DXR_display_info + if (!sess->sys->inst->extensions.DXR_display_info) { + return; + } + XrViewDisplayBindingsDXR *vb = + OXR_GET_OUTPUT_FROM_CHAIN(viewState, XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR, XrViewDisplayBindingsDXR); + if (vb == NULL) { + return; + } + vb->bindingCountOutput = r->count; + if (vb->bindingCapacityInput < r->count || vb->bindings == NULL) { + return; // count query, or too small: the count only (the views are still located) + } + for (uint32_t k = 0; k < r->count; k++) { + XrViewDisplayBindingDXR *b = &vb->bindings[k]; + b->displayId = r->screen_id[k]; + b->segmentRect = + (XrRect2Di){{r->rect[k].offset.w, r->rect[k].offset.h}, {r->rect[k].extent.w, r->rect[k].extent.h}}; + b->firstView = r->first_view[k]; + b->viewCount = r->view_count[k]; + } +#else + (void)sess; + (void)viewState; + (void)r; +#endif +} + +//! One INFO line per change of the per-segment view layout (never per frame). +static void +locate_log_route_change(struct oxr_session *sess) +{ + if (memcmp(&sess->seg_route, &sess->seg_route_logged, sizeof(sess->seg_route)) == 0) { + return; + } + sess->seg_route_logged = sess->seg_route; + const struct xrt_segment_view_routing *r = &sess->seg_route; + if (r->count == 0) { + U_LOG_I("per-segment views: off — one view set for the whole window"); + return; + } + char buf[512]; + int n = 0; + for (uint32_t k = 0; k < r->count && n >= 0 && (size_t)n < sizeof(buf); k++) { + n += snprintf(buf + n, sizeof(buf) - (size_t)n, + "%s[%u] display 0x%016llx views %u..%u canvas %d,%d %dx%d", k > 0 ? "; " : "", k, + (unsigned long long)r->screen_id[k], r->first_view[k], + r->first_view[k] + r->view_count[k] - 1, r->rect[k].offset.w, r->rect[k].offset.h, + r->rect[k].extent.w, r->rect[k].extent.h); + } + U_LOG_I("per-segment views: %u segment(s): %s", r->count, buf); +} + +XrResult +oxr_session_locate_views(struct oxr_logger *log, + struct oxr_session *sess, + const XrViewLocateInfo *viewLocateInfo, + XrViewState *viewState, + uint32_t viewCapacityInput, + uint32_t *viewCountOutput, + XrView *views) +{ + struct xrt_segment_metrics m; + struct xrt_device *xdev = GET_XDEV_BY_ROLE(sess->sys, head); + if (xdev == NULL || xdev->hmd == NULL || !locate_get_segment_metrics(sess, &m)) { + // The window is on one screen (or nothing segments it): the + // single-screen path, byte for byte. + XrResult ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, + viewCountOutput, views, NULL); + if (viewCapacityInput > 0 && ret == XR_SUCCESS) { + sess->seg_route.count = 0; + locate_fill_bindings(sess, viewState, &sess->seg_route); + locate_log_route_change(sess); + } + return ret; + } + + const uint32_t mj = oxr_segment_views_majority(&m); + bool is_multiview = false; +#ifdef OXR_HAVE_DXR_display_info + is_multiview = sess->view_config_type == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR; +#endif + + /* + * PRIMARY_STEREO (and MONO): the app submits a fixed view set, so the + * window keeps ONE — framed from the screen holding most of the window + * (its eyes, the window relative to it). The compositor crops that set + * per segment, each woven by its own screen's DP or flat 2D (M2). When + * the majority is the primary screen this IS the single-screen path. + */ + if (!is_multiview || !locate_can_split(sess, viewLocateInfo)) { + XrResult ret; + if (m.seg[mj].is_primary) { + ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, viewCountOutput, + views, NULL); + } else { + struct oxr_locate_segment o; + memset(&o, 0, sizeof(o)); + oxr_segment_views_whole_window_metrics(&m, mj, &o.wm); + const struct xrt_segment_metric *s = &m.seg[mj]; + o.nominal_y_m = s->nominal_viewer_y_m; + o.nominal_z_m = s->nominal_viewer_z_m; + if (s->have_eyes && s->eyes.valid && s->eyes.is_tracking && s->eyes.count > 0) { + o.eyes = s->eyes; + o.have_eyes = true; + } + o.side_effects = true; + ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, viewCountOutput, + views, o.wm.valid ? &o : NULL); + } + if (viewCapacityInput > 0 && ret == XR_SUCCESS) { + sess->seg_route.count = 0; + locate_fill_bindings(sess, viewState, &sess->seg_route); + locate_log_route_change(sess); + } + return ret; + } + + /* + * PRIMARY_MULTIVIEW_DXR: one view set per segment, contiguous, in table + * order (left to right). Per segment: that screen's eyes, Kooima with the + * segment (relative to its own screen) as the canvas, the active mode's + * view count (the mode is session-wide in M3). + */ + const uint32_t dev_views = xdev->hmd->view_count; + uint32_t active = dev_views; + if (xdev->hmd->active_rendering_mode_index < xdev->rendering_mode_count) { + active = xdev->rendering_modes[xdev->hmd->active_rendering_mode_index].view_count; + } + if (active == 0 || active > dev_views) { + active = dev_views; + } + uint32_t reported = sess->view_config_view_count != 0 ? sess->view_config_view_count : dev_views; + const uint32_t reported_cap = oxr_segment_views_multiview_count(dev_views); + if (reported > reported_cap) { + reported = reported_cap; + } + + struct oxr_segment_layout l; + uint32_t first[XRT_MAX_SEGMENTS] = {0}; + uint32_t cnt[XRT_MAX_SEGMENTS] = {0}; + uint32_t total = 0; + if (m.count <= XRT_MAX_SEGMENTS && oxr_segment_views_layout(&m, &l)) { + total = oxr_segment_views_assign(m.count, active, reported, first, cnt); + } + if (total == 0) { + // More segments than view sets, or a screen of unknown size: one + // view set for the whole window (the M2 crop path). + XrResult ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, + viewCountOutput, views, NULL); + if (viewCapacityInput > 0 && ret == XR_SUCCESS) { + sess->seg_route.count = 0; + locate_fill_bindings(sess, viewState, &sess->seg_route); + locate_log_route_change(sess); + } + return ret; + } + + // Two-call handling: the same count the single path reports. + if (viewCountOutput != NULL) { + *viewCountOutput = reported; + } + if (viewCapacityInput == 0) { + return oxr_session_success_result(sess); + } + if (viewCapacityInput < reported) { + return oxr_error(log, XR_ERROR_SIZE_INSUFFICIENT, "(viewCapacityInput == %u) need %u", + viewCapacityInput, reported); + } + + // Majority last: its locate is the one that writes the app's chained + // outputs (raw block, depth budget, tracking state) and the session-level + // side effects; the others locate into a bare XrViewState. + XrViewStateFlags flags = 0; + bool first_flags = true; + for (uint32_t pass = 0; pass < 2; pass++) { + for (uint32_t k = 0; k < m.count; k++) { + if ((k == mj) != (pass == 1)) { + continue; + } + struct oxr_locate_segment o; + locate_segment_override(&m, &l, k, &o); + o.view_count = cnt[k]; + o.side_effects = (k == mj); + + XrView tmp[XRT_MAX_VIEWS]; + for (uint32_t v = 0; v < XRT_MAX_VIEWS; v++) { + tmp[v] = (XrView){.type = XR_TYPE_VIEW, .next = NULL}; + } + XrViewState bare = {.type = XR_TYPE_VIEW_STATE, .next = NULL}; + XrViewState *vs = (k == mj) ? viewState : &bare; + uint32_t n = 0; + XrResult ret = locate_views_one(log, sess, viewLocateInfo, vs, XRT_MAX_VIEWS, &n, tmp, &o); + if (ret != XR_SUCCESS) { + return ret; + } + for (uint32_t v = 0; v < cnt[k] && first[k] + v < reported; v++) { + views[first[k] + v].pose = tmp[v].pose; + views[first[k] + v].fov = tmp[v].fov; + } + flags = first_flags ? vs->viewStateFlags : (flags & vs->viewStateFlags); + first_flags = false; + } + } + viewState->viewStateFlags = flags; + + // The inactive tail aliases view 0, exactly as the single path does (ADR-041). + for (uint32_t i = total; i < reported; i++) { + const uint32_t src = oxr_segment_views_alias_source(i, total); + views[i].pose = views[src].pose; + views[i].fov = views[src].fov; + } + +#ifdef OXR_HAVE_DXR_display_info + if (sess->sys->inst->extensions.DXR_display_info) { + XrViewActivityStateDXR *act = + OXR_GET_OUTPUT_FROM_CHAIN(viewState, XR_TYPE_VIEW_ACTIVITY_STATE_DXR, XrViewActivityStateDXR); + if (act != NULL) { + act->activeViewCount = total; // the sum over segments + } + } +#endif + + struct xrt_segment_view_routing *r = &sess->seg_route; + memset(r, 0, sizeof(*r)); + r->count = m.count; + r->canvas = m.canvas; + for (uint32_t k = 0; k < m.count; k++) { + r->screen_id[k] = m.seg[k].screen_id; + r->rect[k] = m.seg[k].window_rect; + r->first_view[k] = first[k]; + r->view_count[k] = cnt[k]; + } + locate_fill_bindings(sess, viewState, r); + locate_log_route_change(sess); + return oxr_session_success_result(sess); +} + /* * #1580 — the frame's per-view cameras, for the compositor. * @@ -4088,6 +4538,12 @@ oxr_session_frame_view_cameras(struct oxr_logger *log, views[i] = (XrView){.type = XR_TYPE_VIEW, .next = NULL}; } + // Multi-screen M3: the per-segment view routing the app's own locate + // recorded is what this frame's projection layer was rendered against; + // this internal locate (which chains nothing) must not replace it. + const struct xrt_segment_view_routing route = sess->seg_route; + const struct xrt_segment_view_routing route_logged = sess->seg_route_logged; + uint32_t count = 0; XrResult ret = oxr_session_locate_views(log, sess, &info, &view_state, XRT_MAX_VIEWS, &count, views); @@ -4096,6 +4552,8 @@ oxr_session_frame_view_cameras(struct oxr_logger *log, // app's published value is what handle_space() will read for this frame's // VIEW layers, so the camera must read the same one. oxr_session_set_view_space_offset(sess, &view_offset); + sess->seg_route = route; + sess->seg_route_logged = route_logged; if (ret != XR_SUCCESS) { return ret; diff --git a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c index e4e9f863f..7337882f0 100644 --- a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c +++ b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c @@ -3139,6 +3139,15 @@ oxr_session_frame_end(struct oxr_logger *log, struct oxr_session *sess, const Xr } } +#ifdef XRT_HAVE_VK_NATIVE_COMPOSITOR + // Multi-screen M3: which views of this frame belong to which window + // segment — what the app's last xrLocateViews handed out. The compositor + // routes each segment's tiles to that segment's display processor. + if (sess->is_vk_native_compositor && sess->xcn != NULL) { + comp_vk_native_compositor_set_view_routing(&sess->xcn->base, &sess->seg_route); + } +#endif + xrt_result_t xret; xret = xrt_comp_layer_begin(xc, &data); OXR_CHECK_XRET(log, sess, xret, xrt_comp_layer_begin); @@ -3365,9 +3374,9 @@ oxr_session_frame_end(struct oxr_logger *log, struct oxr_session *sess, const Xr // backlog-free, so flipping the switch back on cannot // produce a burst. struct oxr_views_change_stats vc_stats = {0}; - if (oxr_views_change_update(&sess->sys->views_change, sess->sys->views, sess->sys->view_count, - view_w, view_h, os_monotonic_get_ns(), want_ext_views_change, - &vc_stats) && + if (oxr_views_change_update(&sess->sys->views_change, sess->sys->views, + sess->sys->view_config_max_count, view_w, view_h, + os_monotonic_get_ns(), want_ext_views_change, &vc_stats) && debug_get_bool_option_views_change_event()) { oxr_event_push_XrEventDataViewConfigurationViewsChangedEXT( // #1486: the type the SESSION began (the system advertises two). diff --git a/src/xrt/state_trackers/oxr/oxr_system.c b/src/xrt/state_trackers/oxr/oxr_system.c index 99658bf51..2f0fc1318 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -23,6 +23,7 @@ #include "oxr_objects.h" #include "oxr_logger.h" #include "oxr_two_call.h" +#include "oxr_segment_views.h" #include "oxr_chain.h" #include "oxr_api_verify.h" #include "oxr_conversions.h" @@ -189,11 +190,20 @@ oxr_system_fill_in( #ifdef OXR_HAVE_DXR_display_info if (inst->extensions.DXR_display_info) { sys->view_config_types[1] = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR; - sys->view_config_view_counts[1] = view_count; + // Multi-screen M3 (ADR-047 D3): one view set per window + // segment. A window on one screen locates exactly what it did + // before and aliases the larger tail (ADR-041). + sys->view_config_view_counts[1] = oxr_segment_views_multiview_count(view_count); sys->view_config_count = 2; } #endif } + sys->view_config_max_count = view_count > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : view_count; + for (uint32_t t = 0; t < sys->view_config_count; t++) { + if (sys->view_config_view_counts[t] > sys->view_config_max_count) { + sys->view_config_max_count = sys->view_config_view_counts[t]; + } + } U_LOG_D("sys->view_config_types[0] = %d (%u views), count = %u", sys->view_config_types[0], sys->view_config_view_counts[0], sys->view_config_count); sys->dynamic_roles_cache = (struct xrt_system_roles)XRT_SYSTEM_ROLES_INIT; @@ -361,9 +371,13 @@ oxr_system_fill_in( } #endif // OXR_HAVE_DXR_display_info - for (uint32_t i = 0; i < view_count; ++i) { - uint32_t w_max = info->views[i].max.width_pixels; - uint32_t h_max = info->views[i].max.height_pixels; + // Multi-screen M3: PRIMARY_MULTIVIEW_DXR reports more views than the + // device has (one set per segment). Every view is the same worst-case + // (full canvas x scale) envelope, so the extra entries copy the device's. + for (uint32_t i = 0; i < sys->view_config_max_count; ++i) { + const uint32_t di = i < view_count ? i : 0; + uint32_t w_max = info->views[di].max.width_pixels; + uint32_t h_max = info->views[di].max.height_pixels; uint32_t w, h; if (view_scale_x > 0.0f && view_scale_y > 0.0f && @@ -380,8 +394,8 @@ oxr_system_fill_in( } } else { // Legacy: no display processor, use compositor recommended directly - w = (uint32_t)(info->views[i].recommended.width_pixels * scale); - h = (uint32_t)(info->views[i].recommended.height_pixels * scale); + w = (uint32_t)(info->views[di].recommended.width_pixels * scale); + h = (uint32_t)(info->views[di].recommended.height_pixels * scale); } w = imin(w, w_max); @@ -401,8 +415,8 @@ oxr_system_fill_in( sys->views[i].maxImageRectWidth = w_max; sys->views[i].recommendedImageRectHeight = h; sys->views[i].maxImageRectHeight = h_max; - sys->views[i].recommendedSwapchainSampleCount = info->views[i].recommended.sample_count; - sys->views[i].maxSwapchainSampleCount = info->views[i].max.sample_count; + sys->views[i].recommendedSwapchainSampleCount = info->views[di].recommended.sample_count; + sys->views[i].maxSwapchainSampleCount = info->views[di].max.sample_count; // Store actual recommended dims for legacy apps (first view = per-eye size) if (i == 0 && info->legacy_app_tile_scaling) { @@ -416,7 +430,7 @@ oxr_system_fill_in( // #1488: seed the live-view shadow from the snapshot we just computed. // Leaves views_change.valid false, so xrEnumerateViewConfigurationViews // keeps answering from sys->views until a real change lands. - oxr_views_change_seed(&sys->views_change, sys->views, sys->view_count); + oxr_views_change_seed(&sys->views_change, sys->views, sys->view_config_max_count); /* diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6603a7448..983c98b76 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,7 @@ set(tests tests_oxr_weave_latch tests_oxr_layer_space tests_oxr_view_config_rule + tests_oxr_segment_views tests_oxr_legacy_mode_rule tests_oxr_mode_fillable_rule tests_oxr_tracking_properties_rule @@ -597,6 +598,13 @@ target_sources(tests_comp_segments PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/composi target_include_directories(tests_comp_segments PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/compositor) target_link_libraries(tests_comp_segments PRIVATE xrt-interfaces aux_util) +# Multi-screen M3 — per-segment views: the pure geometry oxr_session_locate_views +# composes (oxr_segment_views.c) plus the shared Kooima core it feeds (aux_math +# carries displayxr-common), so two screens' frusta are asserted end to end. +target_sources(tests_oxr_segment_views PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/state_trackers/oxr/oxr_segment_views.c) +target_include_directories(tests_oxr_segment_views PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/state_trackers/oxr) +target_link_libraries(tests_oxr_segment_views PRIVATE xrt-interfaces aux_math) + if(XRT_BUILD_DRIVER_HANDTRACKING) target_link_libraries( tests_levenbergmarquardt diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp new file mode 100644 index 000000000..104a43503 --- /dev/null +++ b/tests/tests_oxr_segment_views.cpp @@ -0,0 +1,264 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Multi-screen M3: per-segment views — the view-count contract, the + * majority segment, the one-frame placement, the per-segment Kooima + * inputs, the view ranges and the inactive-tail alias. + * + * The locate entry point needs a session, a head device and a segmenting + * compositor; the geometry it composes is pure and pinned here, together with + * the shared Kooima core it feeds (displayxr-common, via aux_math), so "two + * screens give two different, correct frusta" is asserted end to end. + */ + +#include "catch_amalgamated.hpp" + +#include "oxr_segment_views.h" + +#include "displayxr_math_xrt.h" + +#include +#include + +namespace { + +//! Two screens side by side: A 1920x1080 at (0,0), 0.344 x 0.1935 m, nominal +//! z 0.6; B 2560x1440 at (1920,0), 0.60 x 0.3375 m, nominal z 0.8. +//! The window: 800x600 at desktop (1600,200) -> A holds its left 320 px, +//! B its right 480 px. +xrt_segment_metrics +two_screen_table() +{ + xrt_segment_metrics m; + std::memset(&m, 0, sizeof(m)); + m.count = 2; + m.window_screen_left = 1600; + m.window_screen_top = 200; + m.window_pixel_width = 800; + m.window_pixel_height = 600; + m.canvas = {{0, 0}, {800, 600}}; + + xrt_segment_metric &a = m.seg[0]; + a.screen_id = 0xA; + a.window_rect = {{0, 0}, {320, 600}}; + a.screen_rect = {{1600, 200}, {320, 600}}; + a.screen_desktop_left = 0; + a.screen_desktop_top = 0; + a.screen_desktop_width = 1920; + a.screen_desktop_height = 1080; + a.screen_width_m = 0.344f; + a.screen_height_m = 0.1935f; + a.nominal_viewer_z_m = 0.6f; + a.is_primary = true; + a.woven = true; + + xrt_segment_metric &b = m.seg[1]; + b.screen_id = 0xB; + b.window_rect = {{320, 0}, {480, 600}}; + b.screen_rect = {{0, 200}, {480, 600}}; + b.screen_desktop_left = 1920; + b.screen_desktop_top = 0; + b.screen_desktop_width = 2560; + b.screen_desktop_height = 1440; + b.screen_width_m = 0.60f; + b.screen_height_m = 0.3375f; + b.nominal_viewer_z_m = 0.8f; + b.woven = true; + return m; +} + +} // namespace + +TEST_CASE("PRIMARY_MULTIVIEW_DXR reports one view set per segment", "[oxr][segment_views]") +{ + CHECK(oxr_segment_views_multiview_count(0) == 0); + CHECK(oxr_segment_views_multiview_count(1) == 1 * XRT_MAX_SEGMENTS); + CHECK(oxr_segment_views_multiview_count(2) == 4); // Leia + CHECK(oxr_segment_views_multiview_count(4) == 8); // sim_display quad + // Capped at XRT_MAX_VIEWS: a device that already fills it gets no second set. + CHECK(oxr_segment_views_multiview_count(XRT_MAX_VIEWS) == XRT_MAX_VIEWS); +} + +TEST_CASE("the #1499 mode floor asks one segment's share", "[oxr][segment_views]") +{ + // A multiview session fills any mode the device has, on every segment — + // exactly the pre-M3 verdict (device max). + CHECK(oxr_segment_views_per_segment_capacity(8, true, 4) == 4); + CHECK(oxr_segment_views_per_segment_capacity(4, true, 2) == 2); + // Stereo never splits: 2. + CHECK(oxr_segment_views_per_segment_capacity(2, false, 4) == 2); + // A legacy/unsplit count that does not exceed the device is unchanged. + CHECK(oxr_segment_views_per_segment_capacity(4, true, 4) == 4); +} + +TEST_CASE("majority segment: largest area, ties to the primary", "[oxr][segment_views]") +{ + xrt_segment_metrics m = two_screen_table(); + CHECK(oxr_segment_views_majority(&m) == 1); // B holds 480 of 800 px + + m.seg[1].window_rect.extent.w = 320; // exact tie: the primary (A) wins + CHECK(oxr_segment_views_majority(&m) == 0); + m.seg[0].is_primary = false; + m.seg[1].is_primary = true; + CHECK(oxr_segment_views_majority(&m) == 1); + + xrt_segment_metrics empty; + std::memset(&empty, 0, sizeof(empty)); + CHECK(oxr_segment_views_majority(&empty) == 0); +} + +TEST_CASE("segments are placed continuously across the seam in the majority frame", "[oxr][segment_views]") +{ + const xrt_segment_metrics m = two_screen_table(); + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + REQUIRE(l.majority == 1); + + const float pa = 0.344f / 1920.0f; + const float pb = 0.60f / 2560.0f; + + // The majority keeps its true position on its own screen. + CHECK(l.seg[1].ref_cx == Catch::Approx(l.seg[1].own_cx)); + CHECK(l.seg[1].own_cx == Catch::Approx((240.0f - 1280.0f) * pb)); + CHECK(l.seg[1].own_cy == Catch::Approx(-(500.0f - 720.0f) * pb * (0.3375f / 1440.0f) / pb)); + + // A's own position is on ITS screen, at its right edge. + CHECK(l.seg[0].own_cx == Catch::Approx((1760.0f - 960.0f) * pa)); + CHECK(l.seg[0].w_m == Catch::Approx(320.0f * pa)); + CHECK(l.seg[1].w_m == Catch::Approx(480.0f * pb)); + + // No gap, no overlap: A's right edge is B's left edge, in metres, even + // though the two pitches differ. + const float a_right = l.seg[0].ref_cx + l.seg[0].w_m * 0.5f; + const float b_left = l.seg[1].ref_cx - l.seg[1].w_m * 0.5f; + CHECK(a_right == Catch::Approx(b_left).margin(1e-6)); + // Same rows: A sits at B's height. + CHECK(l.seg[0].ref_cy == Catch::Approx(l.seg[1].ref_cy).margin(1e-6)); + + // The window centre is the centre of the union. + CHECK(l.window_ref_cx == Catch::Approx((l.seg[0].ref_cx - l.seg[0].w_m * 0.5f + b_left + l.seg[1].w_m) * 0.5f)); + + // A screen of unknown size cannot be placed. + xrt_segment_metrics bad = m; + bad.seg[0].screen_width_m = 0.0f; + CHECK_FALSE(oxr_segment_views_layout(&bad, &l)); +} + +TEST_CASE("a segment's window metrics: its own screen, its canvas centre in the reference frame", + "[oxr][segment_views]") +{ + const xrt_segment_metrics m = two_screen_table(); + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + + for (uint32_t i = 0; i < 2; i++) { + xrt_window_metrics wm; + oxr_segment_views_window_metrics(&m, &l, i, &wm); + REQUIRE(wm.valid); + // The raw channel's canvasRectPx = window - display: the segment on its own screen. + CHECK(wm.window_screen_left - wm.display_screen_left == m.seg[i].screen_rect.offset.w); + CHECK(wm.window_screen_top - wm.display_screen_top == m.seg[i].screen_rect.offset.h); + CHECK(wm.window_pixel_width == (uint32_t)m.seg[i].window_rect.extent.w); + CHECK(wm.display_width_m == Catch::Approx(m.seg[i].screen_width_m)); + CHECK(wm.window_width_m == Catch::Approx(l.seg[i].w_m)); + CHECK(wm.window_center_offset_x_m == Catch::Approx(l.seg[i].ref_cx)); + CHECK(wm.window_center_offset_y_m == Catch::Approx(l.seg[i].ref_cy)); + } +} + +TEST_CASE("two screens of different size and nominal viewer give two different, correct frusta", "[oxr][segment_views]") +{ + const xrt_segment_metrics m = two_screen_table(); + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + + xrt_fov fov[2][2]; + for (uint32_t i = 0; i < 2; i++) { + // What the locate does: the screen's nominal eyes (untracked), carried + // into the reference frame, then taken relative to the canvas centre. + float dx = 0, dy = 0; + oxr_segment_views_own_to_ref(&l, i, &dx, &dy); + xrt_window_metrics wm; + oxr_segment_views_window_metrics(&m, &l, i, &wm); + const float ipd = 0.063f; + const float z = m.seg[i].nominal_viewer_z_m; + xrt_vec3 raw[2] = { + {-ipd / 2 + dx - wm.window_center_offset_x_m, dy - wm.window_center_offset_y_m, z}, + {ipd / 2 + dx - wm.window_center_offset_x_m, dy - wm.window_center_offset_y_m, z}, + }; + const dxr_screen scr = {wm.window_width_m, wm.window_height_m}; + dxr_display3d_tunables dt = dxr_display3d_default_tunables(); + dt.virtual_display_height = wm.window_height_m; // identity m2v + const xrt_vec3 nominal = {0.0f, 0.0f, z}; + dxr_xrt_view out[2]; + dxr_xrt_display3d_compute_views(raw, 2, &nominal, &scr, &dt, nullptr, out); + + // Kooima, by hand: the eye relative to the SEGMENT as it sits on its + // OWN screen (own screen centre -> segment centre), over the segment. + for (uint32_t e = 0; e < 2; e++) { + const float ex = (e == 0 ? -ipd / 2 : ipd / 2) - l.seg[i].own_cx; + const float ey = -l.seg[i].own_cy; + const float hw = l.seg[i].w_m * 0.5f; + const float hh = l.seg[i].h_m * 0.5f; + CHECK(std::tan(out[e].fov.angle_right) == Catch::Approx((hw - ex) / z).epsilon(1e-3)); + CHECK(std::tan(out[e].fov.angle_left) == Catch::Approx((-hw - ex) / z).epsilon(1e-3)); + CHECK(std::tan(out[e].fov.angle_up) == Catch::Approx((hh - ey) / z).epsilon(1e-3)); + CHECK(std::tan(out[e].fov.angle_down) == Catch::Approx((-hh - ey) / z).epsilon(1e-3)); + fov[i][e] = out[e].fov; + } + } + // Different screens, different frusta. + CHECK(std::fabs(fov[0][0].angle_left - fov[1][0].angle_left) > 0.05f); + CHECK(std::fabs(fov[0][0].angle_up - fov[1][0].angle_up) > 0.01f); + // A sits right of its screen centre: its frustum looks right (both edges > 0 tilt). + CHECK(fov[0][0].angle_right > -fov[0][0].angle_left); + // B sits left of its screen centre: the opposite skew. + CHECK(fov[1][0].angle_right < -fov[1][0].angle_left); +} + +TEST_CASE("a stereo session spanning screens keeps the whole window, framed from the majority screen", + "[oxr][segment_views]") +{ + const xrt_segment_metrics m = two_screen_table(); + xrt_window_metrics wm; + oxr_segment_views_whole_window_metrics(&m, 1, &wm); + REQUIRE(wm.valid); + const float pb = 0.60f / 2560.0f; + CHECK(wm.window_pixel_width == 800); + CHECK(wm.window_width_m == Catch::Approx(800.0f * pb)); + // Window centre at desktop x 2000 = B-relative 80; B centre 1280. + CHECK(wm.window_center_offset_x_m == Catch::Approx((80.0f - 1280.0f) * pb)); + CHECK(wm.display_screen_left == 1920); +} + +TEST_CASE("views are contiguous per segment and the tail aliases view 0", "[oxr][segment_views]") +{ + uint32_t first[XRT_MAX_SEGMENTS] = {}; + uint32_t count[XRT_MAX_SEGMENTS] = {}; + + // sim anaglyph (2 views) on two screens, MULTIVIEW reports 8. + REQUIRE(oxr_segment_views_assign(2, 2, 8, first, count) == 4); + CHECK(first[0] == 0); + CHECK(count[0] == 2); + CHECK(first[1] == 2); + CHECK(count[1] == 2); + + // Quad on two screens fills all 8. + REQUIRE(oxr_segment_views_assign(2, 4, 8, first, count) == 8); + CHECK(first[1] == 4); + + // One segment (a window wholly on a secondary screen). + REQUIRE(oxr_segment_views_assign(1, 2, 8, first, count) == 2); + + // Does not fit / too many segments / nothing per segment: one view set. + CHECK(oxr_segment_views_assign(2, 4, 4, first, count) == 0); + CHECK(oxr_segment_views_assign(XRT_MAX_SEGMENTS + 1, 1, 8, first, count) == 0); + CHECK(oxr_segment_views_assign(2, 0, 8, first, count) == 0); + + // ADR-041 alias: active views are themselves, the tail is view 0. + CHECK(oxr_segment_views_alias_source(0, 4) == 0); + CHECK(oxr_segment_views_alias_source(3, 4) == 3); + CHECK(oxr_segment_views_alias_source(4, 4) == 0); + CHECK(oxr_segment_views_alias_source(7, 4) == 0); +} From 5256b45b4317f1e674ce2f55a05b626197961749 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:56:23 -0700 Subject: [PATCH 03/21] feat(vk_native): route each segment's own views to its DP; per-screen plug-in DPs (multi-screen M3) - The weave publishes its segment table (geometry, each screen's size and nominal viewer, woven or flat) for xrLocateViews; eyes are predicted per query, segment DPs guarded against the weave. - The renderer builds a mosaic atlas: segment k's local view j lands at the segment's rect inside tile j (blit and compose paths), using the same comp_segments_tile_rect mapping the M2 crop reads with, so each segment DP's input is its own views. Routed frames never zero-copy. - Mixed vendors (closes the M4 runtime gap): a segment DP is created from ITS screen's registry entry (owning iface/instance, ABI-checked factory), whichever plug-in won that screen; one INFO line for a mixed table. - Every segment DP follows the session's 2D/3D mode (request_display_mode). Co-Authored-By: Claude Fable 5.1 --- src/xrt/compositor/util/comp_segments.c | 41 +++ src/xrt/compositor/util/comp_segments.h | 21 ++ .../vk_native/comp_vk_native_compositor.c | 254 +++++++++++++++++- .../vk_native/comp_vk_native_compositor.h | 28 ++ .../vk_native/comp_vk_native_renderer.c | 113 +++++++- .../vk_native/comp_vk_native_renderer.h | 19 ++ .../vk_native/comp_vk_native_segments.c | 248 +++++++++++++---- .../vk_native/comp_vk_native_segments.h | 39 +++ tests/tests_comp_segments.cpp | 29 ++ 9 files changed, 718 insertions(+), 74 deletions(-) diff --git a/src/xrt/compositor/util/comp_segments.c b/src/xrt/compositor/util/comp_segments.c index 19250c996..8d4381257 100644 --- a/src/xrt/compositor/util/comp_segments.c +++ b/src/xrt/compositor/util/comp_segments.c @@ -511,3 +511,44 @@ comp_segments_retire_take(struct comp_segments_retire *r, r->count -= n; return n; } +static int32_t +seg_scale_round(int64_t v, int64_t num, int64_t den) +{ + // round(v * num / den), v/num/den >= 0 + return den > 0 ? (int32_t)((v * num + den / 2) / den) : 0; +} + +bool +comp_segments_tile_rect(const struct comp_seg_rect *seg, + const struct comp_seg_rect *canvas, + uint32_t tile_w, + uint32_t tile_h, + struct comp_seg_rect *out) +{ + if (seg == NULL || canvas == NULL || out == NULL || canvas->w == 0 || canvas->h == 0) { + return false; + } + const int64_t rx = (int64_t)seg->x - canvas->x; + const int64_t ry = (int64_t)seg->y - canvas->y; + if (rx < 0 || ry < 0) { + return false; + } + const int32_t ax = seg_scale_round(rx, tile_w, canvas->w); + const int32_t ay = seg_scale_round(ry, tile_h, canvas->h); + int32_t bx = seg_scale_round(rx + seg->w, tile_w, canvas->w); + int32_t by = seg_scale_round(ry + seg->h, tile_h, canvas->h); + if (bx > (int32_t)tile_w) { + bx = (int32_t)tile_w; + } + if (by > (int32_t)tile_h) { + by = (int32_t)tile_h; + } + if (ax < 0 || ay < 0 || bx <= ax || by <= ay) { + return false; + } + out->x = ax; + out->y = ay; + out->w = (uint32_t)(bx - ax); + out->h = (uint32_t)(by - ay); + return true; +} diff --git a/src/xrt/compositor/util/comp_segments.h b/src/xrt/compositor/util/comp_segments.h index 5f9ae23e3..1351ffc59 100644 --- a/src/xrt/compositor/util/comp_segments.h +++ b/src/xrt/compositor/util/comp_segments.h @@ -220,6 +220,27 @@ comp_segments_uncovered(const struct comp_segment_table *t, struct comp_seg_rect *out, uint32_t cap); +/*! + * A segment's rect in VIEW-TILE pixels: the atlas holds the canvas at view + * resolution, so a segment (window px) maps into every tile at this rect. + * Edges are rounded independently, so two segments sharing a seam share the + * tile column too: no gap, no overlap. The ONE mapping both the per-segment + * crop (M2) and the per-segment view routing (M3) use, so a segment's views + * are always placed exactly where its crop reads them. + * + * @param seg The segment, window px. + * @param canvas The canvas the atlas holds, window px. + * @param tile_w/tile_h One view tile, px. + * @param[out] out The rect inside a tile. + * @return false when the segment is outside the canvas or thinner than a tile pixel. + */ +bool +comp_segments_tile_rect(const struct comp_seg_rect *seg, + const struct comp_seg_rect *canvas, + uint32_t tile_w, + uint32_t tile_h, + struct comp_seg_rect *out); + /*! * Equal geometry and screens (used to log a table change once). */ diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c index f73390285..618a88850 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -726,6 +726,20 @@ struct comp_vk_native_compositor //! set_screens() handed over a new list; the manager is rebuilt on the //! next weave with no fill in flight. bool seg_rebuild; + + /*! + * Multi-screen M3: per-segment views. @ref seg_pub is the segment table + * the weave last took (count 0 = not split) — what xrLocateViews + * frames each segment's views from; @ref seg_route is which of the + * committed frame's views belong to which segment (set by the state + * tracker at xrEndFrame). Both cross threads (app thread vs weave), so + * they — and the @ref segments pointer when it is created or destroyed — + * are guarded by this leaf lock. + */ + struct os_mutex seg_pub_mutex; + struct xrt_segment_metrics seg_pub; + struct xrt_segment_view_routing seg_route; + struct comp_vk_native_eff_layout seg_route_logged; #endif #ifdef XRT_OS_ANDROID @@ -5051,9 +5065,17 @@ vk_log_view_dims_on_change(struct comp_vk_native_compositor *c, } #endif +#ifdef XRT_OS_LINUX_DESKTOP +static void +vk_route_effective_layout(struct comp_vk_native_compositor *c, uint32_t layer_view_count); +#endif + static void vk_compute_effective_layout(struct comp_vk_native_compositor *c) { + // Multi-screen M3: no per-segment routing unless set at the end below. + c->eff_layout.route_count = 0; + uint32_t mode_cols = 1, mode_rows = 1; uint32_t view_w = 0, view_h = 0; comp_vk_native_renderer_get_tile_layout(c->renderer, &mode_cols, &mode_rows); @@ -5099,11 +5121,15 @@ vk_compute_effective_layout(struct comp_vk_native_compositor *c) #endif uint32_t views = mode_tiles; + uint32_t proj_layer_views = 0; // M3: the projection layer's full view count (0 = none) for (uint32_t i = 0; i < c->layer_accum.layer_count; i++) { if (c->layer_accum.layers[i].data.type == XRT_LAYER_PROJECTION || c->layer_accum.layers[i].data.type == XRT_LAYER_PROJECTION_DEPTH || c->layer_accum.layers[i].data.type == XRT_LAYER_ZONE_3D) { views = c->layer_accum.layers[i].data.view_count; + if (c->layer_accum.layers[i].data.type != XRT_LAYER_ZONE_3D) { + proj_layer_views = views; + } break; } } @@ -5129,6 +5155,13 @@ vk_compute_effective_layout(struct comp_vk_native_compositor *c) c->eff_layout.tile_w = view_w; c->eff_layout.tile_h = view_h; } +#ifdef XRT_OS_LINUX_DESKTOP + if (proj_layer_views > 0) { + vk_route_effective_layout(c, proj_layer_views); + } +#else + (void)proj_layer_views; +#endif } /*! @@ -5231,11 +5264,11 @@ vk_dp_canvas_rect(struct comp_vk_native_compositor *c) * - no screens handed over, one screen, a pinned session, mixed vendors. */ static bool -vk_segments_frame_update(struct comp_vk_native_compositor *c, - const struct xrt_rect *dp_canvas, - bool zero_copy, - bool dp_self_submits, - VkRenderPass dp_render_pass) +vk_segments_frame_update_split(struct comp_vk_native_compositor *c, + const struct xrt_rect *dp_canvas, + bool zero_copy, + bool dp_self_submits, + VkRenderPass dp_render_pass) { if (c->seg_screens == NULL || c->display_processor == NULL || c->cmd_pool == VK_NULL_HANDLE || !c->have_seg_window) { @@ -5249,19 +5282,25 @@ vk_segments_frame_update(struct comp_vk_native_compositor *c, // A new screen list (set_screens) rebuilds the manager — but never under // a parked fill, whose command buffer may still use its DPs and images. if (c->seg_rebuild && !c->fill_parked) { + os_mutex_lock(&c->seg_pub_mutex); // the app thread reads c->segments (M3) comp_vk_native_segments_destroy(&c->segments); + memset(&c->seg_pub, 0, sizeof(c->seg_pub)); + os_mutex_unlock(&c->seg_pub_mutex); c->seg_rebuild = false; } if (c->segments != NULL && c->seg_rebuild) { return false; // old manager, new list: sit this frame out } if (c->segments == NULL) { - c->segments = comp_vk_native_segments_create(&c->vk, c->cmd_pool, c->repaint_queue); - if (c->segments == NULL) { + struct comp_vk_native_segments *segs = + comp_vk_native_segments_create(&c->vk, c->cmd_pool, c->repaint_queue); + if (segs == NULL) { return false; } - comp_vk_native_segments_set_screens(c->segments, c->seg_screens, &c->sys_info, - c->seg_pinned_display_id); + comp_vk_native_segments_set_screens(segs, c->seg_screens, &c->sys_info, c->seg_pinned_display_id); + os_mutex_lock(&c->seg_pub_mutex); + c->segments = segs; + os_mutex_unlock(&c->seg_pub_mutex); } if (!comp_vk_native_segments_enabled(c->segments) || zero_copy || dp_self_submits || dp_render_pass == VK_NULL_HANDLE) { @@ -5273,11 +5312,123 @@ vk_segments_frame_update(struct comp_vk_native_compositor *c, (uint32_t)(dp_canvas->extent.w > 0 ? dp_canvas->extent.w : 0), (uint32_t)(dp_canvas->extent.h > 0 ? dp_canvas->extent.h : 0), }; - return comp_vk_native_segments_update(c->segments, &c->seg_window, &canvas, - (int32_t)VK_FORMAT_B8G8R8A8_UNORM, !c->fill_parked, - (c->xdev != NULL && c->xdev->hmd != NULL) - ? c->xdev->hmd->active_rendering_mode_index - : 0); + const bool split = comp_vk_native_segments_update( + c->segments, &c->seg_window, &canvas, (int32_t)VK_FORMAT_B8G8R8A8_UNORM, !c->fill_parked, + (c->xdev != NULL && c->xdev->hmd != NULL) ? c->xdev->hmd->active_rendering_mode_index : 0); + // Every segment DP follows the session's 2D/3D mode, like the primary + // (a DP created by the update above gets it here, before it weaves). + comp_vk_native_segments_set_display_mode(c->segments, c->hardware_display_3d); + return split; +} + +static bool +vk_segments_frame_update(struct comp_vk_native_compositor *c, + const struct xrt_rect *dp_canvas, + bool zero_copy, + bool dp_self_submits, + VkRenderPass dp_render_pass) +{ + const bool split = vk_segments_frame_update_split(c, dp_canvas, zero_copy, dp_self_submits, dp_render_pass); + + /* + * Multi-screen M3: publish the table this weave took, for the next + * xrLocateViews to frame per-segment views from. Nothing is published + * (count 0 = one view set) unless the window really is woven per + * segment — and not while the session is collapsed to flat 2D for a + * resampled surface (#1595/#1831), whose single tile cannot hold a + * per-segment mosaic. + */ + struct xrt_segment_metrics m; + memset(&m, 0, sizeof(m)); + if (split && !c->linux_surface_not_1to1) { + const struct comp_seg_rect canvas = { + dp_canvas->offset.w, + dp_canvas->offset.h, + (uint32_t)(dp_canvas->extent.w > 0 ? dp_canvas->extent.w : 0), + (uint32_t)(dp_canvas->extent.h > 0 ? dp_canvas->extent.h : 0), + }; + (void)comp_vk_native_segments_get_metrics(c->segments, &c->seg_window, &canvas, + c->display_processor != NULL, &m); + } + os_mutex_lock(&c->seg_pub_mutex); + m.generation = c->seg_pub.generation; + struct xrt_segment_metrics prev = c->seg_pub; + prev.generation = m.generation; + if (memcmp(&prev, &m, sizeof(m)) != 0) { + m.generation++; + } + c->seg_pub = m; + os_mutex_unlock(&c->seg_pub_mutex); + return split; +} + +/*! + * Multi-screen M3: turn the committed frame's per-segment view routing into + * the renderer's mosaic (comp_vk_native_eff_layout::route). Each segment's + * rect inside a tile comes from comp_segments_tile_rect — the mapping the + * per-segment crop reads with — so a segment DP's cropped input is exactly + * that segment's own views. + * + * Anything inconsistent (the mode changed between the locate and this commit + * so a segment's view count no longer equals the tile count, a short + * submission, a degenerate rect) routes nothing: the frame then shows its + * first view set across the window for that one frame. + */ +static void +vk_route_effective_layout(struct comp_vk_native_compositor *c, uint32_t layer_view_count) +{ + struct xrt_segment_view_routing r; + os_mutex_lock(&c->seg_pub_mutex); + r = c->seg_route; + os_mutex_unlock(&c->seg_pub_mutex); + + struct comp_vk_native_eff_layout *L = &c->eff_layout; + L->route_count = 0; + if (r.count == 0 || r.count > XRT_MAX_SEGMENTS || L->views == 0 || L->tile_w == 0 || L->tile_h == 0) { + return; + } + const struct comp_seg_rect canvas = { + r.canvas.offset.w, + r.canvas.offset.h, + (uint32_t)(r.canvas.extent.w > 0 ? r.canvas.extent.w : 0), + (uint32_t)(r.canvas.extent.h > 0 ? r.canvas.extent.h : 0), + }; + for (uint32_t k = 0; k < r.count; k++) { + if (r.view_count[k] != L->views || r.first_view[k] + r.view_count[k] > layer_view_count) { + return; + } + const struct comp_seg_rect seg = { + r.rect[k].offset.w, + r.rect[k].offset.h, + (uint32_t)(r.rect[k].extent.w > 0 ? r.rect[k].extent.w : 0), + (uint32_t)(r.rect[k].extent.h > 0 ? r.rect[k].extent.h : 0), + }; + struct comp_seg_rect tr; + if (!comp_segments_tile_rect(&seg, &canvas, L->tile_w, L->tile_h, &tr)) { + return; + } + L->route[k].first_view = r.first_view[k]; + L->route[k].view_count = r.view_count[k]; + L->route[k].x = tr.x; + L->route[k].y = tr.y; + L->route[k].w = tr.w; + L->route[k].h = tr.h; + } + L->route_count = r.count; + + // One INFO line per routing change (a move, a resize, a mode switch). + if (memcmp(L->route, c->seg_route_logged.route, sizeof(L->route)) != 0 || + L->route_count != c->seg_route_logged.route_count || L->tile_w != c->seg_route_logged.tile_w || + L->tile_h != c->seg_route_logged.tile_h) { + c->seg_route_logged = *L; + U_LOG_I( + "segments: per-segment views routed — %u segment(s) x %u view(s), tile %ux%u; [0] views %u.. " + "at %d,%d %ux%u; [1] views %u.. at %d,%d %ux%u", + L->route_count, L->views, L->tile_w, L->tile_h, L->route[0].first_view, L->route[0].x, + L->route[0].y, L->route[0].w, L->route[0].h, L->route_count > 1 ? L->route[1].first_view : 0, + L->route_count > 1 ? L->route[1].x : 0, L->route_count > 1 ? L->route[1].y : 0, + L->route_count > 1 ? L->route[1].w : 0, L->route_count > 1 ? L->route[1].h : 0); + } } #endif @@ -8308,6 +8459,18 @@ vk_compositor_layer_commit_locked(struct xrt_compositor *xc, } #endif +#ifdef XRT_OS_LINUX_DESKTOP + /* + * Multi-screen M3: a frame carrying one view set per segment is a mosaic + * the renderer builds in the atlas; the app's swapchain is not it. Same + * shape as the guards above: a fact applied to the RESULT of + * `u_tiling_can_zero_copy()` (ADR-030), never a second gate inside it. + */ + if (c->eff_layout.route_count > 0) { + zero_copy = false; + } +#endif + // Record the frame's effective capture source = exactly what the DP will // receive (renderer atlas, or the zero-copy app swapchain). The atlas // capture reads this so it shows the DP's input regardless of render mode @@ -9211,7 +9374,10 @@ vk_compositor_destroy(struct xrt_compositor *xc) } #ifdef XRT_OS_LINUX_DESKTOP // Multi-screen M2: the segment DPs go first (they share the DP pool). + os_mutex_lock(&c->seg_pub_mutex); comp_vk_native_segments_destroy(&c->segments); + memset(&c->seg_pub, 0, sizeof(c->seg_pub)); + os_mutex_unlock(&c->seg_pub_mutex); free(c->seg_screens); c->seg_screens = NULL; #endif @@ -9292,6 +9458,9 @@ vk_compositor_destroy(struct xrt_compositor *xc) os_cond_destroy(&c->weave_hand.cond); os_mutex_destroy(&c->weave_hand.mutex); os_mutex_destroy(&c->ws_dead.mutex); +#ifdef XRT_OS_LINUX_DESKTOP + os_mutex_destroy(&c->seg_pub_mutex); +#endif // XR_DXR_depth_budget: the runner owns a mutex. comp_rear_budget_fini(&c->rear_budget); @@ -9859,6 +10028,9 @@ comp_vk_native_compositor_create(struct xrt_device *xdev, os_mutex_init(&c->weave_hand.mutex); os_cond_init(&c->weave_hand.cond); os_mutex_init(&c->ws_dead.mutex); // #1782 +#ifdef XRT_OS_LINUX_DESKTOP + os_mutex_init(&c->seg_pub_mutex); // multi-screen M3 +#endif os_thread_helper_init(&c->repaint_thread); // XR_DXR_depth_budget: the policy exists from the first frame so that a @@ -12351,6 +12523,56 @@ comp_vk_native_compositor_set_system_devices(struct xrt_compositor *xc, #endif } +bool +comp_vk_native_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out) +{ + if (out == NULL) { + return false; + } + memset(out, 0, sizeof(*out)); +#ifdef XRT_OS_LINUX_DESKTOP + if (xc == NULL) { + return false; + } + struct comp_vk_native_compositor *c = vk_comp(xc); + os_mutex_lock(&c->seg_pub_mutex); + *out = c->seg_pub; + // The eyes are predicted NOW, per segment: the primary from the + // session's own DP, every other screen from its segment DP (guarded + // against the weave destroying it). + for (uint32_t k = 0; k < out->count && k < XRT_MAX_SEGMENTS; k++) { + struct xrt_segment_metric *s = &out->seg[k]; + if (s->is_primary) { + s->have_eyes = vk_dp_predicted_eyes(c, &s->eyes) && s->eyes.valid; + } else { + s->have_eyes = comp_vk_native_segments_get_eyes(c->segments, s->screen_id, &s->eyes); + } + } + os_mutex_unlock(&c->seg_pub_mutex); + return out->count > 0; +#else + (void)xc; + return false; +#endif +} + +void +comp_vk_native_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing) +{ +#ifdef XRT_OS_LINUX_DESKTOP + if (xc == NULL || routing == NULL) { + return; + } + struct comp_vk_native_compositor *c = vk_comp(xc); + os_mutex_lock(&c->seg_pub_mutex); + c->seg_route = *routing; + os_mutex_unlock(&c->seg_pub_mutex); +#else + (void)xc; + (void)routing; +#endif +} + void comp_vk_native_compositor_set_screens(struct xrt_compositor *xc, const struct xrt_screen_list *list, @@ -12376,6 +12598,10 @@ comp_vk_native_compositor_set_screens(struct xrt_compositor *xc, // The segment manager is rebuilt on the next weave with no fill in flight // (destroying it here could free DPs a parked fill is still executing). c->seg_rebuild = true; + // ...and nothing may frame views from the old table meanwhile. + os_mutex_lock(&c->seg_pub_mutex); + memset(&c->seg_pub, 0, sizeof(c->seg_pub)); + os_mutex_unlock(&c->seg_pub_mutex); free(c->seg_screens); c->seg_screens = copy; c->seg_pinned_display_id = pinned_display_id; diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.h b/src/xrt/compositor/vk_native/comp_vk_native_compositor.h index 74f5782ef..f5ae80220 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.h @@ -126,6 +126,34 @@ bool comp_vk_native_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_window_metrics *out_metrics); +/*! + * Multi-screen M3 (ADR-047 D3): the window's segment table for per-segment + * views — each segment's geometry, its screen's physical size and nominal + * viewer, whether it is woven, and that screen's DP eyes (predicted now). + * + * Non-zero `count` only while the compositor weaves the window per screen + * (desktop Linux, X11/XWayland, see docs/architecture/comp-segments.md); + * everything else — a window on the primary screen only, one screen, + * segmentation off — returns false, and the caller keeps one view set. + * Thread-safe against the weave. + * + * @ingroup comp_vk_native + */ +bool +comp_vk_native_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out); + +/*! + * Multi-screen M3: which views of the frame about to be committed belong to + * which segment (what the app's last xrLocateViews handed out). The renderer + * places each segment's views at that segment's rect in every tile, so the + * per-segment crop hands each segment DP its OWN views. `count` 0 = one view + * set for the whole window. Call before xrt_comp_layer_begin. + * + * @ingroup comp_vk_native + */ +void +comp_vk_native_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing); + /*! * XR_DXR_depth_budget: whether this session asked for the rear depth budget. * diff --git a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c index f761f9c78..ea67dfab6 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c @@ -350,6 +350,70 @@ compose_layer_source(const struct comp_layer *layer, return *out_xsc != NULL; } +/*! + * Multi-screen M3: does this layer's view set go through the per-segment + * routing (see comp_vk_native_eff_layout::route)? Only projection-class + * layers carry one view set per segment. + */ +static bool +layer_is_routed(const struct comp_layer *layer, const struct comp_vk_native_eff_layout *layout) +{ + return layout->route_count > 0 && + (layer->data.type == XRT_LAYER_PROJECTION || layer->data.type == XRT_LAYER_PROJECTION_DEPTH); +} + +//! Views a routed layer contributes: the end of the last segment's range. +static uint32_t +routed_view_count(const struct comp_layer *layer, const struct comp_vk_native_eff_layout *layout) +{ + uint32_t n = 0; + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t end = layout->route[k].first_view + layout->route[k].view_count; + if (end > n) { + n = end; + } + } + if (n > layer->data.view_count) { + n = layer->data.view_count; + } + return n > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : n; +} + +/*! + * Where a routed layer view goes: segment k's local view j lands in tile j, at + * the segment's rect inside that tile (the mosaic). False = the view belongs to + * no segment (the aliased tail) or to a tile the layout does not have. + */ +static bool +route_eye(const struct comp_vk_native_eff_layout *layout, + uint32_t eye, + uint32_t *out_tile, + int32_t *x0, + int32_t *y0, + int32_t *x1, + int32_t *y1) +{ + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t first = layout->route[k].first_view; + if (eye < first || eye >= first + layout->route[k].view_count) { + continue; + } + const uint32_t j = eye - first; + if (j >= layout->views || layout->cols == 0) { + return false; + } + const int32_t bx = (int32_t)((j % layout->cols) * layout->tile_w); + const int32_t by = (int32_t)((j / layout->cols) * layout->tile_h); + *out_tile = j; + *x0 = bx + layout->route[k].x; + *y0 = by + layout->route[k].y; + *x1 = *x0 + (int32_t)layout->route[k].w; + *y1 = *y0 + (int32_t)layout->route[k].h; + return layout->route[k].w > 0 && layout->route[k].h > 0; + } + return false; +} + /*! * How many views does this layer contribute to? * @@ -363,6 +427,9 @@ compose_layer_view_count(const struct comp_layer *layer, const struct comp_vk_na uint32_t n; if (layer->data.type == XRT_LAYER_QUAD || layer->data.type == XRT_LAYER_EQUIRECT2) { n = layout->views; + } else if (layer_is_routed(layer, layout)) { + // Multi-screen M3: one view set per segment, all of them drawn. + n = routed_view_count(layer, layout); } else { n = layer->data.view_count; if (n > layout->views) { @@ -1734,6 +1801,13 @@ draw_zones_pass(struct comp_vk_native_renderer *r, const bool is_zone = layer->data.type == XRT_LAYER_ZONE_3D; const bool is_quad = layer->data.type == XRT_LAYER_QUAD; const bool is_eq2 = layer->data.type == XRT_LAYER_EQUIRECT2; + const bool routed = layer_is_routed(layer, layout); + // Multi-screen M3: a routed layer draws one view per SEGMENT into + // each tile. The segments are disjoint, so every one of them is the + // layer's first write into its part of the tile: the blend mode is + // decided once per tile and shared by the layer's segments. + bool route_mode_set[XRT_MAX_VIEWS] = {false}; + enum comp_layer_blend_mode route_mode[XRT_MAX_VIEWS]; const uint32_t view_count = compose_layer_view_count(layer, layout); @@ -1757,6 +1831,11 @@ draw_zones_pass(struct comp_vk_native_renderer *r, if (!compose_layer_source(layer, eye, &xsc, &sub)) { continue; } + uint32_t route_tile = 0; + int32_t rx0 = 0, ry0 = 0, rx1 = 0, ry1 = 0; + if (routed && !route_eye(layout, eye, &route_tile, &rx0, &ry0, &rx1, &ry1)) { + continue; // the aliased tail: no segment owns it + } if (is_quad) { // Eye visibility, N-view aware: the parity rule // is right for 2 views and meaningless for a 2x2 @@ -1821,6 +1900,13 @@ draw_zones_pass(struct comp_vk_native_renderer *r, : COMP_LAYER_BLEND_PREMULTIPLIED; } else if (is_quad) { mode = comp_layer_subrect_blend_mode(&tiles[eye], layer->data.flags); + } else if (routed) { + if (!route_mode_set[route_tile]) { + route_mode[route_tile] = + comp_layer_tile_blend_mode(&tiles[route_tile], layer->data.flags); + route_mode_set[route_tile] = true; + } + mode = route_mode[route_tile]; } else { mode = comp_layer_tile_blend_mode(&tiles[eye], layer->data.flags); } @@ -1851,7 +1937,14 @@ draw_zones_pass(struct comp_vk_native_renderer *r, // Tile-place by the effective grid (#542); mono content // spans the full content region. float dx0, dy0, dx1, dy1; - if (layout->views == 1 || view_count == 1) { + if (routed) { + // Multi-screen M3: the segment's rect inside its tile. The + // scissor below clamps to this same box. + dx0 = (float)rx0; + dy0 = (float)ry0; + dx1 = (float)rx1; + dy1 = (float)ry1; + } else if (layout->views == 1 || view_count == 1) { dx0 = 0.0f; dy0 = 0.0f; dx1 = (float)(layout->cols * layout->tile_w); @@ -2314,6 +2407,12 @@ comp_vk_native_renderer_draw(struct comp_vk_native_renderer *r, uint32_t view_count = layer->data.view_count; if (view_count > layout->views) view_count = layout->views; if (view_count == 0) view_count = 1; + // Multi-screen M3: one view set per segment, each into its rect of + // every tile (the mosaic, comp_vk_native_eff_layout::route). + const bool routed = !is_zone && layer_is_routed(layer, layout); + if (routed) { + view_count = routed_view_count(layer, layout); + } static bool blit_logged = false; if (!blit_logged) { @@ -2326,6 +2425,11 @@ comp_vk_native_renderer_draw(struct comp_vk_native_renderer *r, } for (uint32_t eye = 0; eye < view_count; eye++) { + uint32_t route_tile = 0; + int32_t rx0 = 0, ry0 = 0, rx1 = 0, ry1 = 0; + if (routed && !route_eye(layout, eye, &route_tile, &rx0, &ry0, &rx1, &ry1)) { + continue; // the aliased tail: no segment owns it + } struct xrt_swapchain *xsc = layer->sc_array[eye]; if (xsc == NULL) { if (!blit_logged) U_LOG_W("Atlas blit: eye %u swapchain NULL", eye); @@ -2354,7 +2458,12 @@ comp_vk_native_renderer_draw(struct comp_vk_native_renderer *r, int32_t sy1 = sy0 + (int32_t)src_rect->extent.h; int32_t dx0, dy0, dx1, dy1; - if (layout->views == 1 || view_count == 1) { + if (routed) { + dx0 = rx0; + dy0 = ry0; + dx1 = rx1; + dy1 = ry1; + } else if (layout->views == 1 || view_count == 1) { // Mono content: stretch across the full content region dx0 = 0; dy0 = 0; diff --git a/src/xrt/compositor/vk_native/comp_vk_native_renderer.h b/src/xrt/compositor/vk_native/comp_vk_native_renderer.h index 7a55e71ac..bb735fd95 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_renderer.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_renderer.h @@ -10,6 +10,7 @@ #pragma once #include "xrt/xrt_defines.h" +#include "xrt/xrt_display_metrics.h" #include "xrt/xrt_results.h" #include @@ -41,6 +42,24 @@ struct comp_vk_native_eff_layout uint32_t rows; //!< atlas tile rows uint32_t tile_w; //!< per-tile width in pixels uint32_t tile_h; //!< per-tile height in pixels + + /*! + * Multi-screen M3: per-segment views. 0 = one view set fills every tile + * (the only case before M3). Otherwise the projection layer carries one + * view set per window segment and each tile is a MOSAIC: local view j of + * segment k is placed at that segment's rect inside tile j, so cropping + * a segment's rect out of every tile (comp_vk_native_segments) yields + * exactly that segment's own views. Quads, equirects and zones are not + * routed. + */ + uint32_t route_count; + struct + { + uint32_t first_view; //!< first layer view of the segment + uint32_t view_count; //!< its views (== @ref views) + int32_t x, y; //!< the segment's rect inside a tile, tile px + uint32_t w, h; + } route[XRT_MAX_SEGMENTS]; }; /*! diff --git a/src/xrt/compositor/vk_native/comp_vk_native_segments.c b/src/xrt/compositor/vk_native/comp_vk_native_segments.c index d4944361f..9e324a386 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_segments.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_segments.c @@ -15,6 +15,8 @@ #include "vk/vk_helpers.h" +#include "os/os_threading.h" + #include "util/u_logging.h" #include "util/u_misc.h" @@ -70,6 +72,8 @@ struct seg_screen_state //! re-declared on change, like the primary's (#1484, #573). int encoding_latched; int transparent_latched; + //! The session-wide 2D/3D mode last sent to this DP (-1 = never). + int mode_sent; /*! * One DP input image PER FRAME CLASS. The repaint ("fill") parks on its @@ -105,6 +109,8 @@ struct comp_vk_native_segments uint32_t screen_count; struct comp_segments_screen screens[COMP_SEGMENTS_MAX_SCREENS]; struct xrt_screen_binding bindings[COMP_SEGMENTS_MAX_SCREENS]; + //! Display info per screen (physical size, nominal viewer) — M3 views. + struct xrt_screen_info info[COMP_SEGMENTS_MAX_SCREENS]; //! Owning plug-in iface / instance per screen (registry). const struct xrt_plugin_iface *iface[COMP_SEGMENTS_MAX_SCREENS]; struct xrt_plugin_instance *inst[COMP_SEGMENTS_MAX_SCREENS]; @@ -116,6 +122,9 @@ struct comp_vk_native_segments bool have_logged_table; bool logged_split; + //! The session-wide hardware 2D/3D mode every segment DP follows (-1 = not set yet). + int display_mode; + struct seg_capture cap[COMP_SEGMENTS_MAX]; uint32_t cap_count; @@ -129,6 +138,14 @@ struct comp_vk_native_segments //! Rendering-mode index the segment DPs' tolerance was last read at. uint32_t mode_index; + + /*! + * Guards every seg_screen_state::dp pointer swap against + * @ref comp_vk_native_segments_get_eyes, which the state tracker calls + * from the app thread (xrLocateViews) while the weave may create or + * destroy segment DPs. + */ + struct os_mutex dp_mutex; }; @@ -319,8 +336,13 @@ dp_release(struct comp_vk_native_segments *segs, uint32_t i) if (st->dp != NULL) { U_LOG_I("segments: retiring the segment DP for screen 0x%016llx ('%s')", (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name); - release_deferred(segs, SEG_RETIRE_DP, (uint64_t)(uintptr_t)st->dp); + // The app thread reads st->dp for eyes (M3): swap under the lock, + // release (deferred past any in-flight fill) after it. + os_mutex_lock(&segs->dp_mutex); + struct xrt_display_processor *dp = st->dp; st->dp = NULL; + os_mutex_unlock(&segs->dp_mutex); + release_deferred(segs, SEG_RETIRE_DP, (uint64_t)(uintptr_t)dp); } st->configured = false; st->encoding_latched = -1; @@ -329,6 +351,7 @@ dp_release(struct comp_vk_native_segments *segs, uint32_t i) for (uint32_t k = 0; k < SEG_CLASS_COUNT; k++) { crop_retire(segs, &st->crop[k]); } + st->mode_sent = -1; } /*! @@ -351,7 +374,8 @@ dp_create(struct comp_vk_native_segments *segs, uint32_t i, int32_t target_forma xrt_result_t xret = XRT_ERROR_DEVICE_CREATION_FAILED; const char *how = "none"; - if (xrt_plugin_iface_has_create_dp_vk_for_screen(iface)) { + struct xrt_display_processor *dp = NULL; + if (segs->screens[i].has_dp_factory && xrt_plugin_iface_has_create_dp_vk_for_screen(iface)) { how = "create_dp_vk_for_screen"; /* * #868: same queue swap as vk_make_dp_vk — a vendor DP captures @@ -364,13 +388,13 @@ dp_create(struct comp_vk_native_segments *segs, uint32_t i, int32_t target_forma segs->vk->main_queue->queue = segs->dp_queue; } xret = iface->create_dp_vk_for_screen(segs->inst[i], segs->vk, (void *)(uintptr_t)segs->cmd_pool, NULL, - target_format, &segs->bindings[i], &st->dp); + target_format, &segs->bindings[i], &dp); segs->vk->main_queue->queue = saved; } - if (xret != XRT_SUCCESS || st->dp == NULL || st->dp->process_atlas == NULL) { - if (st->dp != NULL) { - xrt_display_processor_destroy(&st->dp); + if (xret != XRT_SUCCESS || dp == NULL || dp->process_atlas == NULL) { + if (dp != NULL) { + xrt_display_processor_destroy(&dp); } U_LOG_W("segments: could not create a DP for screen 0x%016llx ('%s', plug-in '%s') via %s (%d) — " "that segment stays flat 2D", @@ -378,6 +402,9 @@ dp_create(struct comp_vk_native_segments *segs, uint32_t i, int32_t target_forma segs->screens[i].plugin_id, how, (int)xret); return false; } + os_mutex_lock(&segs->dp_mutex); + st->dp = dp; + os_mutex_unlock(&segs->dp_mutex); st->tolerates_resample = xrt_display_processor_vk_tolerates_resample((struct xrt_display_processor_vk *)st->dp); st->configured = false; st->encoding_latched = -1; @@ -389,17 +416,10 @@ dp_create(struct comp_vk_native_segments *segs, uint32_t i, int32_t target_forma return true; } -static int32_t -scale_round(int64_t v, int64_t num, int64_t den) -{ - // round(v * num / den), v/num/den >= 0 - return den > 0 ? (int32_t)((v * num + den / 2) / den) : 0; -} - /*! - * A segment's rect in VIEW-TILE pixels (the atlas holds the canvas at view - * resolution). Edges are rounded independently, so two segments sharing a - * seam share the tile column too: no gap, no overlap. + * A segment's rect in VIEW-TILE pixels — comp_segments_tile_rect, the one + * mapping the M3 view routing shares (so a segment's views land exactly where + * this crop reads them). */ static bool segment_tile_rect(const struct comp_seg_rect *wr, @@ -411,31 +431,14 @@ segment_tile_rect(const struct comp_seg_rect *wr, uint32_t *w, uint32_t *h) { - if (canvas->w == 0 || canvas->h == 0) { + struct comp_seg_rect r; + if (!comp_segments_tile_rect(wr, canvas, view_w, view_h, &r)) { return false; } - const int64_t rx = (int64_t)wr->x - canvas->x; - const int64_t ry = (int64_t)wr->y - canvas->y; - if (rx < 0 || ry < 0) { - return false; - } - const int32_t ax = scale_round(rx, view_w, canvas->w); - const int32_t ay = scale_round(ry, view_h, canvas->h); - int32_t bx = scale_round(rx + wr->w, view_w, canvas->w); - int32_t by = scale_round(ry + wr->h, view_h, canvas->h); - if (bx > (int32_t)view_w) { - bx = (int32_t)view_w; - } - if (by > (int32_t)view_h) { - by = (int32_t)view_h; - } - if (ax < 0 || ay < 0 || bx <= ax || by <= ay) { - return false; - } - *x0 = ax; - *y0 = ay; - *w = (uint32_t)(bx - ax); - *h = (uint32_t)(by - ay); + *x0 = r.x; + *y0 = r.y; + *w = r.w; + *h = r.h; return true; } @@ -481,7 +484,15 @@ comp_vk_native_segments_create(struct vk_bundle *vk, VkCommandPool cmd_pool, VkQ segs->vk = vk; segs->cmd_pool = cmd_pool; segs->dp_queue = dp_queue; + segs->display_mode = -1; + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + segs->st[i].mode_sent = -1; + } comp_segments_lifecycle_init(&segs->lc, 0, 0); + if (os_mutex_init(&segs->dp_mutex) != 0) { + free(segs); + return NULL; + } return segs; } @@ -499,6 +510,7 @@ comp_vk_native_segments_destroy(struct comp_vk_native_segments **segs_ptr) segs->release_safe = true; retire_drain(segs, true); segs->release_safe = false; + os_mutex_destroy(&segs->dp_mutex); free(segs); *segs_ptr = NULL; } @@ -558,6 +570,7 @@ comp_vk_native_segments_set_screens(struct comp_vk_native_segments *segs, uint32_t n = 0; bool mixed = false; + char vendors[256] = {0}; for (uint32_t i = 0; i < list->count && i < XRT_SCREEN_LIST_MAX && n < COMP_SEGMENTS_MAX_SCREENS; i++) { const struct xrt_screen *s = &list->screens[i]; if (s->desktop_width == 0 || s->desktop_height == 0) { @@ -586,18 +599,30 @@ comp_vk_native_segments_set_screens(struct comp_vk_native_segments *segs, segs->iface[n] = NULL; segs->inst[n] = NULL; if (e != NULL && !cs->is_primary) { - const bool same_vendor = strcmp(e->plugin_id, primary_entry->plugin_id) == 0; - if (!same_vendor) { + /* + * Multi-screen M3 (closes the M4 runtime gap): a segment's DP + * comes from ITS screen's registry entry, whichever plug-in + * that is — the active one or a claim source the loader keeps + * resident (M0). `dp_factory_vk` is only set when the plug-in + * passed the vk_bundle ABI check (#1243), so it gates the + * per-screen slot too: a plug-in whose VK factory was refused + * gets no segment DP (that segment is flat 2D). + */ + if (strcmp(e->plugin_id, primary_entry->plugin_id) != 0) { mixed = true; - } else { - segs->iface[n] = (const struct xrt_plugin_iface *)e->owning_iface; - segs->inst[n] = (struct xrt_plugin_instance *)e->owning_instance; - // The per-screen slot, and only it (see dp_create). The - // registry's VK factory being set is what says the claim - // covers Vulkan and the vk_bundle ABI matched (#1243). - cs->has_dp_factory = e->dp_factory_vk != NULL && - xrt_plugin_iface_has_create_dp_vk_for_screen(segs->iface[n]); + if (strstr(vendors, e->plugin_id) == NULL) { + size_t used = strlen(vendors); + snprintf(vendors + used, sizeof(vendors) - used, "%s%s", used > 0 ? ", " : "", + e->plugin_id); + } } + segs->iface[n] = (const struct xrt_plugin_iface *)e->owning_iface; + segs->inst[n] = (struct xrt_plugin_instance *)e->owning_instance; + // The per-screen slot, and only it (see dp_create). The + // registry's VK factory being set is what says the claim + // covers Vulkan and the vk_bundle ABI matched (#1243). + cs->has_dp_factory = + e->dp_factory_vk != NULL && xrt_plugin_iface_has_create_dp_vk_for_screen(segs->iface[n]); } struct xrt_screen_binding *b = &segs->bindings[n]; @@ -617,20 +642,18 @@ comp_vk_native_segments_set_screens(struct comp_vk_native_segments *segs, if (e != NULL) { snprintf(b->serial, sizeof(b->serial), "%s", e->serial); } + segs->info[n] = s->info; n++; } segs->screen_count = n; if (mixed) { - // One-off session setup line. Mixed vendors (a Leia panel next to a - // sim_display laptop panel) are multi-screen M4: the vendor DP's - // behaviour on a sub-rect canvas next to another vendor's segment is - // not established yet, so the window keeps the shipped single-DP - // path rather than risk a broken weave on the 3D panel. - U_LOG_W("segments: the screens belong to different plug-ins (primary '%s') — mixed-vendor segments are " - "multi-screen M4; a window spanning screens keeps the single-DP path", - primary_entry->plugin_id); - return; + // One-off session setup line: each screen is woven by its own + // plug-in's DP (a Leia panel next to a sim_display laptop panel). + U_LOG_I( + "segments: mixed-vendor screen table — primary '%s', other screens by '%s'; each segment DP " + "comes from its own screen's plug-in", + primary_entry->plugin_id, vendors); } segs->enabled = n >= 2; if (segs->enabled) { @@ -994,3 +1017,112 @@ comp_vk_native_segments_get_capture(const struct comp_vk_native_segments *segs, *out_woven = c->woven; return true; } + + +/* + * + * Multi-screen M3: per-segment views. + * + */ + +bool +comp_vk_native_segments_get_metrics(const struct comp_vk_native_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + bool primary_has_dp, + struct xrt_segment_metrics *out) +{ + memset(out, 0, sizeof(*out)); + if (segs == NULL || !segs->enabled || !segs->logged_split || window_desktop == NULL || canvas == NULL) { + return false; + } + const struct comp_segment_table *t = &segs->table; + if (t->count == 0 || t->count > XRT_MAX_SEGMENTS) { + // More screens than view sets: the window keeps one view set (M2). + return false; + } + for (uint32_t k = 0; k < t->count; k++) { + const struct comp_segment *g = &t->seg[k]; + if (g->screen_index >= segs->screen_count) { + return false; + } + const uint32_t i = g->screen_index; + const struct seg_screen_state *st = &segs->st[i]; + struct xrt_segment_metric *m = &out->seg[k]; + m->screen_id = g->screen_id; + m->window_rect = (struct xrt_rect){{g->window_rect.x, g->window_rect.y}, + {(int)g->window_rect.w, (int)g->window_rect.h}}; + m->screen_rect = (struct xrt_rect){{g->screen_rect.x, g->screen_rect.y}, + {(int)g->screen_rect.w, (int)g->screen_rect.h}}; + m->screen_desktop_left = segs->screens[i].desktop.x; + m->screen_desktop_top = segs->screens[i].desktop.y; + m->screen_desktop_width = segs->screens[i].desktop.w; + m->screen_desktop_height = segs->screens[i].desktop.h; + m->screen_width_m = segs->info[i].width_m; + m->screen_height_m = segs->info[i].height_m; + m->nominal_viewer_x_m = segs->info[i].nominal_viewer_x_m; + m->nominal_viewer_y_m = segs->info[i].nominal_viewer_y_m; + m->nominal_viewer_z_m = segs->info[i].nominal_viewer_z_m; + m->is_primary = g->is_primary; + if (g->is_primary) { + m->has_dp = primary_has_dp; + m->tolerates_resample = false; // the primary keeps the session-level gates + m->woven = primary_has_dp; + } else { + m->has_dp = st->dp != NULL; + m->tolerates_resample = st->tolerates_resample; + m->woven = comp_segments_decide(m->has_dp, st->tolerates_resample, g->screen_1to1) == + COMP_SEG_RENDER_WEAVE; + } + } + out->count = t->count; + out->canvas = (struct xrt_rect){{canvas->x, canvas->y}, {(int)canvas->w, (int)canvas->h}}; + out->window_screen_left = window_desktop->x; + out->window_screen_top = window_desktop->y; + out->window_pixel_width = window_desktop->w; + out->window_pixel_height = window_desktop->h; + return true; +} + +bool +comp_vk_native_segments_get_eyes(struct comp_vk_native_segments *segs, + uint64_t screen_id, + struct xrt_eye_positions *out) +{ + if (segs == NULL || out == NULL) { + return false; + } + const int i = screen_index_of(segs, screen_id); + if (i < 0) { + return false; + } + bool ok = false; + os_mutex_lock(&segs->dp_mutex); + if (segs->st[i].dp != NULL) { + ok = xrt_display_processor_get_predicted_eye_positions(segs->st[i].dp, out) && out->valid; + } + os_mutex_unlock(&segs->dp_mutex); + return ok; +} + +void +comp_vk_native_segments_set_display_mode(struct comp_vk_native_segments *segs, bool enable_3d) +{ + if (segs == NULL) { + return; + } + const int want = enable_3d ? 1 : 0; + segs->display_mode = want; + for (uint32_t i = 0; i < segs->screen_count; i++) { + struct seg_screen_state *st = &segs->st[i]; + if (st->dp == NULL || st->mode_sent == want) { + continue; + } + st->mode_sent = want; + // A lifecycle event per DP (create, or a session 2D/3D switch). + const bool ok = xrt_display_processor_request_display_mode(st->dp, enable_3d); + U_LOG_I("segments: screen 0x%016llx ('%s') DP follows the session mode: %s -> %d", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, enable_3d ? "3D" : "2D", + (int)ok); + } +} diff --git a/src/xrt/compositor/vk_native/comp_vk_native_segments.h b/src/xrt/compositor/vk_native/comp_vk_native_segments.h index dfcab2454..fc99aca71 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_segments.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_segments.h @@ -30,6 +30,7 @@ #include "xrt/xrt_compiler.h" #include "xrt/xrt_compositor.h" +#include "xrt/xrt_display_metrics.h" #include "xrt/xrt_display_processor.h" #include "xrt/xrt_screen.h" #include "xrt/xrt_vulkan_includes.h" @@ -182,6 +183,44 @@ comp_vk_native_segments_get_capture(const struct comp_vk_native_segments *segs, uint64_t *out_screen_id, bool *out_woven); +/*! + * The session-wide hardware 2D/3D mode. Every segment DP follows it: sent to + * each live DP on a change and to every newly created one (the primary DP + * gets it from the compositor's own request path). Weave thread. + */ +void +comp_vk_native_segments_set_display_mode(struct comp_vk_native_segments *segs, bool enable_3d); + +/*! + * Multi-screen M3: the last update's segment table as per-segment view + * metrics (geometry, each screen's physical size + nominal viewer, whether it + * is woven). Eyes are NOT filled — they are predicted per query + * (@ref comp_vk_native_segments_get_eyes). + * + * @param window_desktop The window rect the table was computed for. + * @param canvas The canvas it was cut from, window px. + * @param primary_has_dp The session's primary DP exists. + * @return false when the last update did not split the window, or split it + * into more than XRT_MAX_SEGMENTS segments (one view set then). + */ +bool +comp_vk_native_segments_get_metrics(const struct comp_vk_native_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + bool primary_has_dp, + struct xrt_segment_metrics *out); + +/*! + * Multi-screen M3: the predicted eyes of screen @p screen_id's segment DP, in + * that screen's display space. Thread-safe against the weave creating or + * destroying segment DPs. False when the screen has no segment DP (the + * primary's eyes come from the session's own DP). + */ +bool +comp_vk_native_segments_get_eyes(struct comp_vk_native_segments *segs, + uint64_t screen_id, + struct xrt_eye_positions *out); + #ifdef __cplusplus } #endif diff --git a/tests/tests_comp_segments.cpp b/tests/tests_comp_segments.cpp index 05a1f726c..4bec2395b 100644 --- a/tests/tests_comp_segments.cpp +++ b/tests/tests_comp_segments.cpp @@ -422,3 +422,32 @@ TEST_CASE("comp_segments: deferred release") CHECK_FALSE(comp_segments_retire_push(&r, 0, 999)); CHECK_FALSE(comp_segments_retire_push(nullptr, 0, 1)); } + +TEST_CASE("tile rects: adjacent segments share the seam column (M3 routing == M2 crop)", "[comp_segments]") +{ + // A 1600x900 canvas into an 800x900 tile (anaglyph 0.5x1 scale), split + // at an odd column so the scaled seam is fractional. + const comp_seg_rect canvas = {0, 0, 1600, 900}; + const comp_seg_rect left = {0, 0, 801, 900}; + const comp_seg_rect right = {801, 0, 799, 900}; + comp_seg_rect a, b; + REQUIRE(comp_segments_tile_rect(&left, &canvas, 800, 900, &a)); + REQUIRE(comp_segments_tile_rect(&right, &canvas, 800, 900, &b)); + CHECK(a.x == 0); + CHECK(a.x + (int32_t)a.w == b.x); // no gap, no overlap + CHECK(b.x + (int32_t)b.w == 800); + CHECK(a.h == 900); + + // Canvas offset inside the window. + const comp_seg_rect canvas2 = {100, 50, 1600, 900}; + const comp_seg_rect seg2 = {100, 50, 800, 900}; + REQUIRE(comp_segments_tile_rect(&seg2, &canvas2, 800, 900, &a)); + CHECK(a.x == 0); + CHECK(a.w == 400); + + // Outside the canvas / degenerate. + const comp_seg_rect outside = {0, 0, 50, 900}; + CHECK_FALSE(comp_segments_tile_rect(&outside, &canvas2, 800, 900, &a)); + const comp_seg_rect zero = {0, 0, 0, 0}; + CHECK_FALSE(comp_segments_tile_rect(&left, &zero, 800, 900, &a)); +} From 8aa8ba27ce51b7b4b9d028a3dffd739b8a879694 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:56:23 -0700 Subject: [PATCH 04/21] feat(cube_handle_vk_linux): multiview, activeViewCount and view display bindings (multi-screen M3) Begins PRIMARY_MULTIVIEW_DXR when offered (DXR_STEREO_FIXED_APP=1 keeps stereo), renders every active view, chains XrViewActivityStateDXR + XrViewDisplayBindingsDXR, renders each display's views into its segment's part of every tile, and logs the bindings once per change. Co-Authored-By: Claude Fable 5.1 --- docs/guides/displayxr-app-rules.md | 10 ++ test_apps/cube_handle_vk_linux/main.cpp | 198 ++++++++++++++++++------ 2 files changed, 164 insertions(+), 44 deletions(-) diff --git a/docs/guides/displayxr-app-rules.md b/docs/guides/displayxr-app-rules.md index aea93b978..2f40048aa 100644 --- a/docs/guides/displayxr-app-rules.md +++ b/docs/guides/displayxr-app-rules.md @@ -416,6 +416,16 @@ re-implementing — see [INV-8.1](#8-app-folder-layout--what-to-include)). `dxr_view_config.h` (`displayxr::rules`) rather than pasting a copy. `activity.activeViewCount` is the runtime's own answer for how many views are live this frame — prefer it to deriving the number from the rendering mode. + Under `PRIMARY_MULTIVIEW_DXR` it can be LARGER than the mode's view count: a window + spanning two displays gets one view set per display (multi-screen M3, `XR_DXR_display_info` + v22), so `activeViewCount` is the sum and the reported count is the device max times + `XRT_MAX_SEGMENTS`. An app that renders `[0, activeViewCount)`, each view into its own + subImage, is correct without knowing why. To render each display's views at that + display's resolution, chain `XrViewDisplayBindingsDXR` beside `XrViewActivityStateDXR`: + each binding names a display, its `segmentRect` (window px) and its contiguous view + range; render local view `j` of a binding into tile `j` at the segment's position + (`cube_handle_vk_linux` does exactly this). INV-3.1 and INV-3.4 already lint the rest + (begin multiview, submit the located count, alias the tail), so there is no separate rule. **Every leg of a multi-platform app needs its own call.** `check_displayxr_app.py` checks INV-3.4 per top-level platform directory (`windows/`, `macos/`, `linux/`, `android/`, …): diff --git a/test_apps/cube_handle_vk_linux/main.cpp b/test_apps/cube_handle_vk_linux/main.cpp index 3145eaa87..75be878b2 100644 --- a/test_apps/cube_handle_vk_linux/main.cpp +++ b/test_apps/cube_handle_vk_linux/main.cpp @@ -1963,6 +1963,9 @@ struct AppXrSession { XrViewConfigurationType viewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; std::vector configViews; + // PRIMARY_MULTIVIEW_DXR begun: render every ACTIVE view (activeViewCount), + // one view set per display when the window spans displays (multi-screen M3). + bool isMultiview = false; XrSessionState sessionState = XR_SESSION_STATE_UNKNOWN; bool sessionRunning = false; @@ -2225,6 +2228,20 @@ static bool InitializeOpenXR(AppXrSession& xr, DxrWindowBackend requestedBackend } } + // Multi-screen M3: begin PRIMARY_MULTIVIEW_DXR when the runtime offers it, + // so a window spanning two displays gets each display's own views + // (XrViewDisplayBindingsDXR). DXR_STEREO_FIXED_APP=1 keeps the legacy + // 2-view PRIMARY_STEREO session (the runtime then frames those two views + // from the display holding most of the window). + { + const char* fixedStereo = getenv("DXR_STEREO_FIXED_APP"); + if (fixedStereo == nullptr || fixedStereo[0] != '1') { + xr.viewConfigType = DxrSelectViewConfigType(xr.instance, xr.systemId); + } + xr.isMultiview = xr.viewConfigType == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR; + LOG_INFO("View configuration: %s", DxrViewConfigTypeName(xr.viewConfigType)); + } + uint32_t viewCount = 0; XR_CHECK(xrEnumerateViewConfigurationViews(xr.instance, xr.systemId, xr.viewConfigType, 0, &viewCount, nullptr)); xr.configViews.resize(viewCount, {XR_TYPE_VIEW_CONFIGURATION_VIEW}); @@ -3177,6 +3194,16 @@ int main(int argc, char** argv) { locateInfo.space = xr.localSpace; XrViewState viewState = {XR_TYPE_VIEW_STATE}; + // ADR-041 + multi-screen M3: how many located views are + // live (the sum over displays when the window spans + // displays), and which of them belong to which display. + XrViewActivityStateDXR activity = {XR_TYPE_VIEW_ACTIVITY_STATE_DXR}; + XrViewDisplayBindingDXR bindingStorage[4] = {}; + XrViewDisplayBindingsDXR bindings = {XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR}; + bindings.bindingCapacityInput = 4; + bindings.bindings = bindingStorage; + activity.next = &bindings; + viewState.next = &activity; // Query view count xrLocateViews(xr.session, &locateInfo, &viewState, 0, &viewCount, nullptr); if (viewCount == 0) viewCount = 2; // fallback @@ -3239,23 +3266,28 @@ int main(int argc, char** argv) { const XrDisplayRenderingModeInfoDXR* mode = ActiveRenderingMode(xr); float scaleX = xr.viewScaleX, scaleY = xr.viewScaleY; uint32_t tileCols = 2, tileRows = 1; - uint32_t eyeCount = 2; + // Views per view SET (one set per display the + // window covers). A stereo session caps at 2 and + // the tail aliases (ADR-041); a multiview session + // renders every view of the active mode. + uint32_t perSet = 2; if (mode != nullptr) { if (mode->viewScaleX > 0.0f) scaleX = mode->viewScaleX; if (mode->viewScaleY > 0.0f) scaleY = mode->viewScaleY; tileCols = mode->tileColumns > 0 ? mode->tileColumns : 1; tileRows = mode->tileRows > 0 ? mode->tileRows : 1; - // DXR_STEREO_FIXED_APP: hard cap at 2 — the - // session stays PRIMARY_STEREO and always - // submits the 2 located views (ADR-041 alias). - eyeCount = (mode->hardwareDisplay3D && mode->viewCount > 1) ? 2 : 1; - if (eyeCount == 1) { + if (!(mode->hardwareDisplay3D && mode->viewCount > 1)) { + perSet = 1; + } else { + perSet = xr.isMultiview ? mode->viewCount : 2; + } + if (perSet == 1) { tileCols = 1; tileRows = 1; } } - if (eyeCount > locatedCount) - eyeCount = locatedCount; + if (perSet > tileCols * tileRows) perSet = tileCols * tileRows; + uint32_t winW = 0, winH = 0; if (!g_window.current_size(&winW, &winH)) { winW = 0; @@ -3275,45 +3307,123 @@ int main(int argc, char** argv) { if (eyeW > xr.swapchain.width / tileCols) eyeW = xr.swapchain.width / tileCols; if (eyeH > xr.swapchain.height / tileRows) eyeH = xr.swapchain.height / tileRows; - EyeRenderParams eyeParams[2]; + /* + * View sets. Without display bindings: one set, + * the whole window, views [0, active). With them + * (multi-screen M3, a window spanning displays): + * one set per display, each rendered into ITS + * segment's part of every tile — the same mosaic + * the runtime composes, so each tile still holds + * the whole window at view resolution. + */ + struct ViewSet { + uint32_t first, count; + int32_t x, y; + uint32_t w, h; // window px + }; + ViewSet sets[4]; + uint32_t setCount = 0; + const uint32_t nb = bindings.bindingCountOutput; + if (xr.isMultiview && nb > 0 && nb <= 4 && winW > 0 && winH > 0) { + for (uint32_t b = 0; b < nb; b++) { + const XrViewDisplayBindingDXR& vb = bindingStorage[b]; + sets[setCount++] = {vb.firstView, vb.viewCount, vb.segmentRect.offset.x, + vb.segmentRect.offset.y, + (uint32_t)vb.segmentRect.extent.width, + (uint32_t)vb.segmentRect.extent.height}; + } + } else { + uint32_t active = perSet; + // A multiview session takes the runtime's + // count; a stereo-capped one keeps its cap. + if (xr.isMultiview && activity.activeViewCount > 0 && + activity.activeViewCount < active) { + active = activity.activeViewCount; + } + sets[setCount++] = {0, active, 0, 0, winW, winH}; + } + + // Log the bindings when they change (one line, never per frame). + { + static std::string lastBindings; + std::string now = "activeViewCount=" + std::to_string(activity.activeViewCount); + for (uint32_t b = 0; b < nb && b < 4; b++) { + char line[192]; + snprintf(line, sizeof(line), + "; display 0x%016llx views %u..%u segment %d,%d %dx%d", + (unsigned long long)bindingStorage[b].displayId, + bindingStorage[b].firstView, + bindingStorage[b].firstView + bindingStorage[b].viewCount - 1, + bindingStorage[b].segmentRect.offset.x, + bindingStorage[b].segmentRect.offset.y, + bindingStorage[b].segmentRect.extent.width, + bindingStorage[b].segmentRect.extent.height); + now += line; + } + if (nb == 0) now += "; no display bindings (one view set)"; + if (now != lastBindings) { + lastBindings = now; + LOG_INFO("Views: %s", now.c_str()); + } + } + + const float fx = winW > 0 ? (float)eyeW / (float)winW : 1.0f; + const float fy = winH > 0 ? (float)eyeH / (float)winH : 1.0f; + EyeRenderParams eyeParams[8]; + uint32_t eyeCount = 0; // views rendered = [0, eyeCount) projectionViews.resize(locatedCount, {}); - for (uint32_t i = 0; i < eyeCount; i++) { - const uint32_t tileX = i % tileCols; - const uint32_t tileY = i / tileCols; - eyeParams[i].viewportX = tileX * eyeW; - eyeParams[i].viewportY = tileY * eyeH; - eyeParams[i].width = eyeW; - eyeParams[i].height = eyeH; - mat4_view_from_xr_pose(eyeParams[i].viewMat, views[i].pose); - // Clip policy stays app-side (fov is clip-independent). - // Display rig: ZDP-anchored near/far so depth precision - // brackets the convergence plane — near = ez - vH, - // far = ez + 1000·vH, ez = rig-local eye Z (= pose Z at - // identity rig). Matches cube_handle_vk_win. - float nearZ = 0.01f, farZ = 100.0f; - if (useDisplayRig) { - float ez = views[i].pose.position.z; - nearZ = (ez - kVirtualDisplayHeight > 0.001f) - ? (ez - kVirtualDisplayHeight) : 0.001f; - farZ = ez + 1000.0f * kVirtualDisplayHeight; + for (uint32_t si = 0; si < setCount; si++) { + const ViewSet& vs = sets[si]; + // Edges rounded independently: two segments + // that share a seam share the tile column. + const int32_t x0 = (int32_t)std::lround(vs.x * fx); + const int32_t y0 = (int32_t)std::lround(vs.y * fy); + const int32_t x1 = (int32_t)std::lround((vs.x + (int32_t)vs.w) * fx); + const int32_t y1 = (int32_t)std::lround((vs.y + (int32_t)vs.h) * fy); + const uint32_t sw = x1 > x0 ? (uint32_t)(x1 - x0) : 1; + const uint32_t sh = y1 > y0 ? (uint32_t)(y1 - y0) : 1; + for (uint32_t j = 0; j < vs.count && j < tileCols * tileRows; j++) { + const uint32_t i = vs.first + j; + if (i >= locatedCount || i >= 8) break; + const uint32_t tileX = j % tileCols; + const uint32_t tileY = j / tileCols; + eyeParams[i].viewportX = tileX * eyeW + (uint32_t)x0; + eyeParams[i].viewportY = tileY * eyeH + (uint32_t)y0; + eyeParams[i].width = sw; + eyeParams[i].height = sh; + mat4_view_from_xr_pose(eyeParams[i].viewMat, views[i].pose); + // Clip policy stays app-side (fov is clip-independent). + // Display rig: ZDP-anchored near/far so depth precision + // brackets the convergence plane — near = ez - vH, + // far = ez + 1000·vH, ez = rig-local eye Z (= pose Z at + // identity rig). Matches cube_handle_vk_win. + float nearZ = 0.01f, farZ = 100.0f; + if (useDisplayRig) { + float ez = views[i].pose.position.z; + nearZ = (ez - kVirtualDisplayHeight > 0.001f) + ? (ez - kVirtualDisplayHeight) : 0.001f; + farZ = ez + 1000.0f * kVirtualDisplayHeight; + } + mat4_from_xr_fov(eyeParams[i].projMat, views[i].fov, nearZ, farZ); + // mat4_from_xr_fov emits GL [-1,1] clip-depth; Vulkan + // clips [0,1], so remap or the cube is near-clipped + // whenever the ZDP near sits close to the content + // (invisible windowed). displayxr-common shared fn, + // exactly like cube_handle_vk_win. + convert_projection_gl_to_zero_to_one(eyeParams[i].projMat); + + projectionViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; + projectionViews[i].subImage.swapchain = xr.swapchain.swapchain; + projectionViews[i].subImage.imageRect.offset = { + (int32_t)eyeParams[i].viewportX, (int32_t)eyeParams[i].viewportY}; + projectionViews[i].subImage.imageRect.extent = {(int32_t)sw, (int32_t)sh}; + projectionViews[i].subImage.imageArrayIndex = 0; + projectionViews[i].pose = views[i].pose; + projectionViews[i].fov = views[i].fov; + if (i + 1 > eyeCount) eyeCount = i + 1; } - mat4_from_xr_fov(eyeParams[i].projMat, views[i].fov, nearZ, farZ); - // mat4_from_xr_fov emits GL [-1,1] clip-depth; Vulkan - // clips [0,1], so remap or the cube is near-clipped - // whenever the ZDP near sits close to the content - // (invisible windowed). displayxr-common shared fn, - // exactly like cube_handle_vk_win. - convert_projection_gl_to_zero_to_one(eyeParams[i].projMat); - - projectionViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; - projectionViews[i].subImage.swapchain = xr.swapchain.swapchain; - projectionViews[i].subImage.imageRect.offset = { - (int32_t)eyeParams[i].viewportX, (int32_t)eyeParams[i].viewportY}; - projectionViews[i].subImage.imageRect.extent = {(int32_t)eyeW, (int32_t)eyeH}; - projectionViews[i].subImage.imageArrayIndex = 0; - projectionViews[i].pose = views[i].pose; - projectionViews[i].fov = views[i].fov; } + if (eyeCount == 0) eyeCount = 1; // never leave view 0 unwritten // ADR-041: fill the inactive tail — own located // pose/fov, view 0's subimage. From cbc02f3e54f700d98e36242fac8e85abe97bc4c8 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:56:23 -0700 Subject: [PATCH 05/21] docs: per-segment views in the view-configuration model and Kooima notes (multi-screen M3) Co-Authored-By: Claude Fable 5.1 --- docs/architecture/kooima-projection.md | 13 +++++++ docs/reference/view-configuration-model.md | 40 +++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/architecture/kooima-projection.md b/docs/architecture/kooima-projection.md index 70416ee61..da335e343 100644 --- a/docs/architecture/kooima-projection.md +++ b/docs/architecture/kooima-projection.md @@ -348,6 +348,19 @@ The `m2v` factor (`virtual_display_height / screen_height_m`) naturally grows as the window shrinks, producing correct perspective scaling. No artificial `vs` viewport-scale multiplication is needed. +### Per-segment canvas (multi-screen M3) + +A window spanning two displays gets one view set per **segment** (the part of the window +on each display). Each set is window-relative Kooima with the **segment** as the canvas: +the segment's size in its own display's metres, the eye that display's (tracked, or its +nominal viewer) taken relative to the segment's centre as it sits on that display. The +sets are expressed in one frame — the display space of the display holding most of the +window — with the other segment placed beside it the way the window's pixels continue +across the seam, so a display rig still centres the whole window on its pose and each +segment's virtual canvas is offset from it by `m2v ×` that segment's offset from the +window centre. Geometry: `src/xrt/state_trackers/oxr/oxr_segment_views.{h,c}`; contract: +`docs/reference/view-configuration-model.md` § *Per-segment views*. + ### Where the adjustment lives | Path | Location | diff --git a/docs/reference/view-configuration-model.md b/docs/reference/view-configuration-model.md index f4103b7e6..d3f20130b 100644 --- a/docs/reference/view-configuration-model.md +++ b/docs/reference/view-configuration-model.md @@ -75,9 +75,9 @@ must handle it needs an explicit `case`. | | `PRIMARY_MONO` | `PRIMARY_STEREO` | `PRIMARY_MULTIVIEW_DXR` | |---|---|---|---| -| `xrEnumerateViewConfigurationViews` count | 1 | **2** | device **max across modes** (4 on sim-display, 2 on Leia) | -| `xrLocateViews` `*viewCountOutput` | 1 | **2** | same max | -| `xrLocateViews` capacity required | 1 | 2 | max (size to `XRT_MAX_VIEWS` = 8) | +| `xrEnumerateViewConfigurationViews` count | 1 | **2** | device **max across modes × `XRT_MAX_SEGMENTS`**, capped at 8 (8 on sim-display, 4 on Leia) — multi-screen M3, see *Per-segment views* below | +| `xrLocateViews` `*viewCountOutput` | 1 | **2** | same count | +| `xrLocateViews` capacity required | 1 | 2 | that count (size to `XRT_MAX_VIEWS` = 8) | | `xrEndFrame` projection `viewCount` accepted | 1 | **exactly 2** — the located count. An `XR_DXR_display_info` app may still submit 1 while a **1-view mode is in play** — the mode active now **or** the one latched at this frame's `xrBeginFrame` (#1528): **deprecated** (ADR-041), accepted, logged once per session | **exactly the located count** (the device max). ADR-041 removed the old "any rendering mode's `viewCount`". The same deprecated 1-view arm as `PRIMARY_STEREO` applies at the default (#1612): 1 view while a 1-view mode is in play, accepted, logged once per session — nothing wider | | Fixed for the instance lifetime? | yes | yes | yes | @@ -340,7 +340,9 @@ sample. The clamp, the 1 Hz throttle and the doorbell all follow from there. ### What is deliberately NOT floored -- **`PRIMARY_MULTIVIEW_DXR` sessions.** `max_views` is the device max, so the +- **`PRIMARY_MULTIVIEW_DXR` sessions.** `max_views` is one segment's share of the + reported count (`oxr_segment_views_per_segment_capacity`) — the device max, exactly + as before multi-screen M3 doubled the reported count — so the pick returns the active index for every mode: no floor, no denial, no warning. An app that wants the device's full width says so, and gets it. This is the invariant the whole change is built around — #1499 must not take back what @@ -527,6 +529,36 @@ weaves it. Per-view display bindings, where views are assigned to displays, arri multi-screen M3 (ADR-047); until then binding to the system default (or not binding) is exactly today's behaviour. +## Per-segment views (multi-screen M3) + +A window that spans two displays is woven per segment (`docs/architecture/comp-segments.md`). +From M3 a `PRIMARY_MULTIVIEW_DXR` session carries one view set per segment, which is why +the type reports `device max × XRT_MAX_SEGMENTS` (`XRT_MAX_SEGMENTS` = 2, in +`xrt_display_metrics.h`; `oxr_segment_views_multiview_count`). The rules: + +- **One segment (the common case):** byte-for-byte the pre-M3 locate — the active + mode's views first, the whole tail (now longer) aliased onto view 0. The swapchain + worst case and the compositor's atlas are unchanged: tiles are per *active* view of a + set, and every set has the mode's count, so the atlas is still `cols × rows` tiles of + `canvas × scale`. Per-view recommended sizes are the same worst-case envelope for every + index (`oxr_system_fill_in` copies the device's entry into the extra slots). +- **Two segments:** views `[0, n)` are the left segment's, `[n, 2n)` the right one's + (`n` = the active mode's view count; the mode is session-wide), each from its display's + eyes with the segment as the Kooima canvas; `XrViewActivityStateDXR::activeViewCount` + = `2n`; `XrViewDisplayBindingsDXR` names the ranges. The compositor builds a mosaic + atlas (each set at its segment's rect inside every tile) and crops each segment's own + views for its DP. +- **`PRIMARY_STEREO` (and `PRIMARY_MONO`) never split:** 2 views, framed from the display + holding most of the window — its eyes, the window relative to it. When that is the + primary display this is exactly the single-display locate; the other display gets its + crop of the same two views (or flat 2D), as shipped in M2. +- **xrEndFrame:** a multiview projection layer still carries the full located count + (ADR-041); views past `activeViewCount` are ignored. + +Implementation: `oxr_session_locate_views` (the per-segment wrapper) over +`locate_views_one` (the pre-M3 body with a per-segment override), geometry in +`oxr_segment_views.{h,c}` (unit tests: `tests/tests_oxr_segment_views.cpp`). + ## History — what the deviation was Before this change the runtime modelled **one** view configuration per system From 68f37bc858bc0af62586cfb852dd1c5396674a18 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:56:55 -0700 Subject: [PATCH 06/21] docs(comp-segments): per-segment views, the mosaic routing, mixed vendors (multi-screen M3) Co-Authored-By: Claude Fable 5.1 --- docs/architecture/comp-segments.md | 72 +++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index cd4e663d7..4eaa5bcd1 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -1,6 +1,7 @@ # Window segments: one window, one DP per screen -*How the compositor weaves a window that spans several screens (multi-screen M2). +*How the compositor weaves a window that spans several screens (multi-screen M2), and +how each screen gets its own views (M3). Companion to ADR-047 (`docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md`, lands with PR #1849) decision D2 and the multi-screen plan (`docs/roadmap/multi-screen.md`, lands with PR #1849). Code: `src/xrt/compositor/util/comp_segments.{h,c}` (backend-agnostic math and @@ -35,10 +36,12 @@ half-open, so a window flush to a seam belongs to exactly one screen. - The **primary screen** is the system-default screen, the one the session's own DP (`comp_vk_native_compositor::display_processor`) was made for. Its segment is - woven by that DP, which also keeps owning everything view-related: eye positions, - window metrics, Kooima. M2 renders **one view pair** for the whole window; per-segment - views are M3. -- Every **other screen** gets a segment DP from that screen's registry factory: + woven by that DP. Its eyes feed that segment's views (M3, below). +- Every **other screen** gets a segment DP from that screen's registry entry — + **whichever plug-in won that screen** (M3 lifted M2's one-vendor rule: the entry carries + the owning plug-in's iface + instance, kept resident by the loader as a claim source, + M0): its ABI-checked factory + `xrt_plugin_iface::create_dp_vk_for_screen` with an `xrt_screen_binding` `xrt_plugin_iface::create_dp_vk_for_screen` with an `xrt_screen_binding` (monitor id, desktop rect, native px, mm, serial) — only that slot: a plug-in without it gets a flat 2D view on its other screens, never a plain `create_dp_vk` @@ -46,7 +49,8 @@ half-open, so a window flush to a seam belongs to exactly one screen. created against the #868 runtime-owned queue, like the primary. It is windowless (NULL window): its phase is `set_present_origin`, fed per frame, ADR-033. A plug-in's DP made for a screen describes that screen - (`get_display_dimensions` / `get_display_pixel_info`). + (`get_display_dimensions` / `get_display_pixel_info`). Every segment DP follows the + session's 2D/3D mode (`request_display_mode`, on creation and on every change). - A window entirely on the primary screen — the common case — never leaves the single-DP path: `comp_segments_table_is_split()` is false and the frame is byte-for-byte what shipped before. @@ -139,20 +143,54 @@ or destroy is one WARN. are desktop-absolute, the same space as the registry's RandR rects. Native Wayland stays primary-only — the geometry payload does not describe the other monitors (`docs/specs/runtime/wayland-window-geometry.md` §5). -- **One vendor**: all screens must belong to the primary screen's plug-in. A - mixed layout (a Leia DS1 next to a sim_display laptop panel) keeps the single-DP - path; mixing vendors is M4. +- **Mixed vendors** (M3): a Leia DS1 next to a sim_display laptop panel is + segmented like any other layout; each segment DP comes from its own screen's plug-in + and the table logs one INFO line naming the vendors. A plug-in whose VK factory was + refused at load (vk_bundle ABI, #1243) gets no segment DP: that segment is flat 2D. - Not segmented: zero-copy frames, a self-submitting DP or one without a render pass, a session pinned with `XrSessionDisplayBindingDXR`, the shared-texture path. - `DXR_SEGMENTS=0` turns it off. - Capture: with the window split, the post-compose atlas capture also writes each segment's DP input as `.seg.png`. -## What M3 adds - -Per-segment **views**: `xrLocateViews` returns a view pair per segment (eyes from -that segment's DP, Kooima from the segment rect relative to its own screen), -`XrViewDisplayBindingsDXR` tells the app which views belong to which segment, and the -compositor routes each segment's tiles to its DP instead of cropping one shared -pair. The segment table, the lifecycle and the per-segment DPs here are what it -builds on. +## Per-segment views (M3) + +Under `PRIMARY_MULTIVIEW_DXR` each segment gets its **own** views instead of a crop of +one shared set (contract: `docs/reference/view-configuration-model.md` § *Per-segment +views*; API: `XrViewDisplayBindingsDXR` in `XR_DXR_display_info` v22). + +1. **Publish.** Each weave that takes the split path publishes its table as + `xrt_segment_metrics` (`comp_vk_native_compositor_get_segment_metrics`): per segment + the screen id, rect in window and screen px, the screen's desktop rect, physical + size and nominal viewer, and whether it is woven. Eyes are predicted at query time + — the primary from the session DP, the others from their segment DPs + (`comp_vk_native_segments_get_eyes`, guarded against the weave destroying them). + Nothing is published while the session is collapsed to flat 2D (#1595/#1831), for + zero-copy frames or when the window is on the primary screen only. +2. **Locate.** `oxr_session_locate_views` locates one view set per segment and records + which views went where (`xrt_segment_view_routing`); `xrEndFrame` hands it to the + compositor (`comp_vk_native_compositor_set_view_routing`) with the frame. +3. **Route — one atlas, per-segment tile ranges, as a mosaic.** The atlas layout does + not change: `cols × rows` tiles of `canvas × scale`, so `u_tiling` and the + worst-case sizing stay honest and a single-segment frame is byte-for-byte the old + one. What changes is what a tile holds: local view `j` of segment `k` is placed at + segment `k`'s rect inside tile `j` (`comp_vk_native_eff_layout::route`, both the + blit and the compose paths). The rect comes from `comp_segments_tile_rect`, the + mapping the crop (step 2 of *The frame*) reads with, so cropping segment `k` out of + every tile yields exactly its own views — the M2 split path runs unchanged and each + DP weaves its own frustum. A routed frame never zero-copies (the mosaic is built by + the renderer, not by the app). Chosen over per-segment sub-atlases because it keeps + one atlas, one crop path, one capture path. +4. **Capture.** `displayxr_atlas.seg.png` is each segment's DP input, i.e. that + segment's own views. + +What stays one view set (cropped per segment, as in M2): `PRIMARY_STEREO`/`MONO` +sessions (framed from the screen holding most of the window), camera-rig and +zone-scoped locates, a window covering more than `XRT_MAX_SEGMENTS` (2) screens. A +frame whose routing no longer matches (the mode changed between the locate and the +commit) is drawn unrouted for that frame. Under per-segment views, canvas that lies on +no screen is not covered by any view set and stays black (M2 painted it from the shared +set). Quads and equirect layers are drawn with the first segment's camera in each tile. + +**IPC.** In-process only: the service never segments, so a service session always sees +an empty table, one view set and no bindings (its own segmentation is M6). From e0404f4d5a0f11871289397b277baaee58ef5acd Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:58:18 -0700 Subject: [PATCH 07/21] test(oxr_view_space): PRIMARY_MULTIVIEW_DXR reports device max x XRT_MAX_SEGMENTS (multi-screen M3) Co-Authored-By: Claude Fable 5.1 --- tests/tests_oxr_view_space.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/tests_oxr_view_space.cpp b/tests/tests_oxr_view_space.cpp index c17eb3eb1..12b0c4fe0 100644 --- a/tests/tests_oxr_view_space.cpp +++ b/tests/tests_oxr_view_space.cpp @@ -94,6 +94,18 @@ namespace { constexpr float kPosTolM = 0.001f; // 1 mm + +/*! + * Multi-screen M3: PRIMARY_MULTIVIEW_DXR reports one view set per window + * segment — the device max x XRT_MAX_SEGMENTS (2), capped at XRT_MAX_VIEWS (8). + * A window on one display locates its active views first and aliases the rest. + */ +uint32_t +multiview_count(uint32_t device_max) +{ + const uint32_t n = device_max * 2; + return n > 8 ? 8 : n; +} constexpr float kAngTolDeg = 0.1f; // 0.1 degree /*! @@ -943,7 +955,7 @@ TEST_CASE("VIEW tracks the reported view array under MULTIVIEW (#1502)", "[oxr][ const uint32_t device_max = rt.device_max_view_count(); INFO("reported views = " << v.size() << ", device max = " << device_max); if (device_max != 0) { - CHECK(v.size() == device_max); + CHECK(v.size() == multiview_count(device_max)); } const XrPosef T_local_view = rt.locate(rt.view, rt.local, t); @@ -992,7 +1004,7 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] { // PRIMARY_STEREO means exactly 2, whatever the device can drive. CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO) == 2); - CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR) == device_max); + CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR) == multiview_count(device_max)); // A valid core type the system does NOT advertise is still refused. CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO) == 0); } @@ -1003,7 +1015,7 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] uint32_t n = 0; CHECK(rt.locate_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR, t, &n) == XR_SUCCESS); - CHECK(n == device_max); + CHECK(n == multiview_count(device_max)); // Same session, the OTHER advertised configuration: valid enum, wrong // session - UNSUPPORTED, not VALIDATION_FAILURE. @@ -1012,7 +1024,7 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] // And the full locate really writes that many views. std::vector v = rt.views(rt.local, t, nullptr, nullptr); - CHECK(v.size() == device_max); + CHECK(v.size() == multiview_count(device_max)); } tear_down(rt); From 9d4fb83f6303dbffefb61c363c03011eae85c022 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 12:01:43 -0700 Subject: [PATCH 08/21] feat(oxr): advertise extra multiview view sets only where a window can be split (multi-screen M3) PRIMARY_MULTIVIEW_DXR now reports device max x view-set capacity, where the capacity is min(XRT_MAX_SEGMENTS, DP-registry screens with a VK DP factory), computed once in oxr_system_fill_in -- and 1 wherever no compositor segments windows (every platform but desktop Linux, any service session). A single- screen box, Windows, macOS and Android keep exactly the pre-M3 count, so existing multiview apps and the CTS see no change; ds1-linux gets device max x 2. The locate never hands out more view sets than the capacity; the aliasing rule is unchanged. Co-Authored-By: Claude Fable 5.1 --- docs/guides/displayxr-app-rules.md | 4 +-- docs/reference/view-configuration-model.md | 13 ++++++-- docs/specs/extensions/XR_DXR_display_info.md | 11 ++++--- src/xrt/include/xrt/xrt_display_metrics.h | 5 +-- src/xrt/state_trackers/oxr/oxr_objects.h | 4 +++ .../state_trackers/oxr/oxr_segment_views.h | 32 ++++++++++++++++--- src/xrt/state_trackers/oxr/oxr_session.c | 9 ++++-- src/xrt/state_trackers/oxr/oxr_system.c | 31 +++++++++++++++++- tests/tests_oxr_segment_views.cpp | 30 +++++++++++++---- tests/tests_oxr_view_space.cpp | 22 +++++++------ 10 files changed, 125 insertions(+), 36 deletions(-) diff --git a/docs/guides/displayxr-app-rules.md b/docs/guides/displayxr-app-rules.md index 2f40048aa..d8db42993 100644 --- a/docs/guides/displayxr-app-rules.md +++ b/docs/guides/displayxr-app-rules.md @@ -418,8 +418,8 @@ re-implementing — see [INV-8.1](#8-app-folder-layout--what-to-include)). live this frame — prefer it to deriving the number from the rendering mode. Under `PRIMARY_MULTIVIEW_DXR` it can be LARGER than the mode's view count: a window spanning two displays gets one view set per display (multi-screen M3, `XR_DXR_display_info` - v22), so `activeViewCount` is the sum and the reported count is the device max times - `XRT_MAX_SEGMENTS`. An app that renders `[0, activeViewCount)`, each view into its own + v22), so `activeViewCount` is the sum and, on a system that can split a window (desktop + Linux with two DP-backed screens), the reported count is the device max times 2. An app that renders `[0, activeViewCount)`, each view into its own subImage, is correct without knowing why. To render each display's views at that display's resolution, chain `XrViewDisplayBindingsDXR` beside `XrViewActivityStateDXR`: each binding names a display, its `segmentRect` (window px) and its contiguous view diff --git a/docs/reference/view-configuration-model.md b/docs/reference/view-configuration-model.md index d3f20130b..9ee5ebf18 100644 --- a/docs/reference/view-configuration-model.md +++ b/docs/reference/view-configuration-model.md @@ -75,7 +75,7 @@ must handle it needs an explicit `case`. | | `PRIMARY_MONO` | `PRIMARY_STEREO` | `PRIMARY_MULTIVIEW_DXR` | |---|---|---|---| -| `xrEnumerateViewConfigurationViews` count | 1 | **2** | device **max across modes × `XRT_MAX_SEGMENTS`**, capped at 8 (8 on sim-display, 4 on Leia) — multi-screen M3, see *Per-segment views* below | +| `xrEnumerateViewConfigurationViews` count | 1 | **2** | device **max across modes** (4 on sim-display, 2 on Leia), **× the system's view-set capacity** — 1 everywhere except desktop Linux with 2+ DP-backed screens, where it is 2 (capped at 8) — multi-screen M3, see *Per-segment views* below | | `xrLocateViews` `*viewCountOutput` | 1 | **2** | same count | | `xrLocateViews` capacity required | 1 | 2 | that count (size to `XRT_MAX_VIEWS` = 8) | | `xrEndFrame` projection `viewCount` accepted | 1 | **exactly 2** — the located count. An `XR_DXR_display_info` app may still submit 1 while a **1-view mode is in play** — the mode active now **or** the one latched at this frame's `xrBeginFrame` (#1528): **deprecated** (ADR-041), accepted, logged once per session | **exactly the located count** (the device max). ADR-041 removed the old "any rendering mode's `viewCount`". The same deprecated 1-view arm as `PRIMARY_STEREO` applies at the default (#1612): 1 view while a 1-view mode is in play, accepted, logged once per session — nothing wider | @@ -533,8 +533,15 @@ exactly today's behaviour. A window that spans two displays is woven per segment (`docs/architecture/comp-segments.md`). From M3 a `PRIMARY_MULTIVIEW_DXR` session carries one view set per segment, which is why -the type reports `device max × XRT_MAX_SEGMENTS` (`XRT_MAX_SEGMENTS` = 2, in -`xrt_display_metrics.h`; `oxr_segment_views_multiview_count`). The rules: +the type reports `device max × capacity`, capped at `XRT_MAX_VIEWS` (8) +(`oxr_segment_views_multiview_count`). The **view-set capacity** is computed once in +`oxr_system_fill_in` (`oxr_segment_views_set_capacity`): `min(XRT_MAX_SEGMENTS, +DP-registry screens with a VK DP factory)`, and 1 wherever no compositor can segment a +window — every platform but desktop Linux, and any service session. So a single-screen +box, Windows, macOS and Android advertise exactly the pre-M3 count (existing multiview +apps and the CTS see no change); `ds1-linux` (eDP + DS1) advertises `device max × 2`. +`XRT_MAX_SEGMENTS` = 2 lives in `xrt_display_metrics.h`. The locate never hands out more +view sets than the capacity. The rules: - **One segment (the common case):** byte-for-byte the pre-M3 locate — the active mode's views first, the whole tail (now longer) aliased onto view 0. The swapchain diff --git a/docs/specs/extensions/XR_DXR_display_info.md b/docs/specs/extensions/XR_DXR_display_info.md index 49f37e90c..c9971a887 100644 --- a/docs/specs/extensions/XR_DXR_display_info.md +++ b/docs/specs/extensions/XR_DXR_display_info.md @@ -1263,10 +1263,13 @@ typedef struct XrViewDisplayBindingsDXR { // chained on XrViewState at xrLocat } XrViewDisplayBindingsDXR; ``` -**The view count.** `PRIMARY_MULTIVIEW_DXR` now reports `device max views × -XRT_MAX_SEGMENTS` (capped at 8; `XRT_MAX_SEGMENTS` is 2 today — the plan's eventual cap is -4): 4 on a 2-view Leia system, 8 on the 4-view sim_display. The count is fixed for the -session (ADR-041). A window on one display locates exactly what it did before — the +**The view count.** `PRIMARY_MULTIVIEW_DXR` reports `device max views × the system's +view-set capacity` (capped at 8). The capacity is `min(XRT_MAX_SEGMENTS, screens with a +display processor)` where a window can be woven per segment at all (desktop Linux, +in-process), else 1 — so a single-screen system and every other platform report exactly +the pre-M3 count; a Linux box with a DS1 next to its laptop panel reports 4 (Leia) or 8 +(sim_display). `XRT_MAX_SEGMENTS` is 2 today (the plan's eventual cap is 4). The count is +fixed for the session (ADR-041). A window on one display locates exactly what it did before — the active mode's views first, the larger tail aliased onto view 0 — so the only visible change for such an app is the bigger count. Per-view recommended image sizes stay the worst case (full canvas × scale). `PRIMARY_STEREO` stays exactly 2. diff --git a/src/xrt/include/xrt/xrt_display_metrics.h b/src/xrt/include/xrt/xrt_display_metrics.h index d7c61f0f1..4ac6c9427 100644 --- a/src/xrt/include/xrt/xrt_display_metrics.h +++ b/src/xrt/include/xrt/xrt_display_metrics.h @@ -126,8 +126,9 @@ struct xrt_window_metrics * Max window segments that get their own views (multi-screen M3, ADR-047 D3). * * A `PRIMARY_MULTIVIEW_DXR` session advertises `device max views x - * XRT_MAX_SEGMENTS` views (capped at XRT_MAX_VIEWS), so a window spanning up to - * this many screens can carry one view set per screen. The plan's eventual cap + * min(XRT_MAX_SEGMENTS, DP-backed screens)` views (capped at XRT_MAX_VIEWS) on a + * system that can segment windows, so a window spanning up to this many screens + * can carry one view set per screen; elsewhere the pre-M3 device max. The plan's eventual cap * is 4; 2 covers every two-monitor layout and keeps the 8-view sim quad mode * inside XRT_MAX_VIEWS. A window covering more screens keeps one view set for * the whole window (the M2 crop path). diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index e25b17e6b..d5c66e11e 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -2313,6 +2313,10 @@ struct oxr_system //! Multi-screen M3: the largest of @ref view_config_view_counts — how many //! entries of @ref views (and the #1488 shadow) are filled. uint32_t view_config_max_count; + //! Multi-screen M3: view sets a PRIMARY_MULTIVIEW_DXR session can carry + //! (oxr_segment_views_set_capacity), computed once at fill-in. 1 = never + //! split (every platform but desktop Linux with 2+ DP-backed screens). + uint32_t multiview_set_capacity; /*! * #1486: DXR_VIEW_CONFIG_LEGACY=1, latched at @ref oxr_system_fill_in. diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 84f0469d6..981f4f299 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -35,17 +35,39 @@ extern "C" { #endif /*! - * The view count `PRIMARY_MULTIVIEW_DXR` reports: the device max per segment, - * times @ref XRT_MAX_SEGMENTS, capped at @ref XRT_MAX_VIEWS. A session with one - * segment locates exactly what it did before and aliases the larger tail. + * How many view sets this system can ever need: @ref XRT_MAX_SEGMENTS, but no + * more than the screens a window could actually be woven on — the DP registry + * entries that carry a display-processor factory — and only where a compositor + * segments windows at all (@p can_segment: the in-process Vulkan compositor on + * desktop Linux, never a service session). Never below 1. */ static inline uint32_t -oxr_segment_views_multiview_count(uint32_t device_max_views) +oxr_segment_views_set_capacity(uint32_t screens_with_dp_factory, bool can_segment) +{ + if (!can_segment || screens_with_dp_factory < 2) { + return 1; + } + return screens_with_dp_factory < XRT_MAX_SEGMENTS ? screens_with_dp_factory : XRT_MAX_SEGMENTS; +} + +/*! + * The view count `PRIMARY_MULTIVIEW_DXR` reports: the device max per view set, + * times the system's set capacity (@ref oxr_segment_views_set_capacity), capped + * at @ref XRT_MAX_VIEWS. On a system that can never split a window (one screen + * with a DP, or no segmenting compositor) this is the device max — exactly the + * pre-M3 count. A session with one segment locates what it did before and + * aliases any larger tail. + */ +static inline uint32_t +oxr_segment_views_multiview_count(uint32_t device_max_views, uint32_t set_capacity) { if (device_max_views == 0) { return 0; } - uint32_t n = device_max_views * XRT_MAX_SEGMENTS; + if (set_capacity == 0) { + set_capacity = 1; + } + uint32_t n = device_max_views * set_capacity; return n > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : n; } diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 3264f2db9..f2318351b 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2546,7 +2546,7 @@ locate_views_one(struct oxr_logger *log, uint32_t reported_cap = view_count; #ifdef OXR_HAVE_DXR_display_info if (sess->view_config_type == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR) { - reported_cap = oxr_segment_views_multiview_count(view_count); + reported_cap = oxr_segment_views_multiview_count(view_count, sess->sys->multiview_set_capacity); } #endif if (reported_view_count == 0) { @@ -4308,7 +4308,7 @@ oxr_session_locate_views(struct oxr_logger *log, active = dev_views; } uint32_t reported = sess->view_config_view_count != 0 ? sess->view_config_view_count : dev_views; - const uint32_t reported_cap = oxr_segment_views_multiview_count(dev_views); + const uint32_t reported_cap = oxr_segment_views_multiview_count(dev_views, sess->sys->multiview_set_capacity); if (reported > reported_cap) { reported = reported_cap; } @@ -4317,7 +4317,10 @@ oxr_session_locate_views(struct oxr_logger *log, uint32_t first[XRT_MAX_SEGMENTS] = {0}; uint32_t cnt[XRT_MAX_SEGMENTS] = {0}; uint32_t total = 0; - if (m.count <= XRT_MAX_SEGMENTS && oxr_segment_views_layout(&m, &l)) { + // The system's set capacity is the contract the app sized for: never + // hand out more view sets than it advertised. + if (m.count <= XRT_MAX_SEGMENTS && m.count <= sess->sys->multiview_set_capacity && + oxr_segment_views_layout(&m, &l)) { total = oxr_segment_views_assign(m.count, active, reported, first, cnt); } if (total == 0) { diff --git a/src/xrt/state_trackers/oxr/oxr_system.c b/src/xrt/state_trackers/oxr/oxr_system.c index 2f0fc1318..167442848 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -168,6 +168,34 @@ oxr_system_fill_in( * extension enabled, not only on devices whose max exceeds 2 - so an app * can select it uniformly and get 2 views where 2 is all there is. */ + /* + * Multi-screen M3: advertise one view set per segment only where a + * window can ever be split — the in-process Vulkan compositor on desktop + * Linux, with at least two registry screens that have a DP factory. A + * single-screen box, a service session and every other platform keep the + * pre-M3 count, so existing multiview apps and the CTS see no change. + */ + { + uint32_t with_factory = 0; + bool can_segment = false; +#ifdef XRT_OS_LINUX_DESKTOP + if (sys->xsysc != NULL && !sys->xsysc->info.is_service_mode) { + can_segment = true; + const struct xrt_dp_factory_registry *reg = &sys->xsysc->info.dp_registry; + for (uint32_t e = 0; e < reg->entry_count && e < XRT_DP_REGISTRY_MAX_ENTRIES; e++) { + if (reg->entries[e].dp_factory_vk != NULL) { + with_factory++; + } + } + } +#endif + sys->multiview_set_capacity = oxr_segment_views_set_capacity(with_factory, can_segment); + if (sys->multiview_set_capacity > 1) { + U_LOG_I("multi-screen: %u DP-backed screens — PRIMARY_MULTIVIEW_DXR carries up to %u view sets", + with_factory, sys->multiview_set_capacity); + } + } + sys->view_config_legacy = debug_get_bool_option_view_config_legacy(); if (sys->view_config_legacy) { sys->view_config_count = 1; @@ -193,7 +221,8 @@ oxr_system_fill_in( // Multi-screen M3 (ADR-047 D3): one view set per window // segment. A window on one screen locates exactly what it did // before and aliases the larger tail (ADR-041). - sys->view_config_view_counts[1] = oxr_segment_views_multiview_count(view_count); + sys->view_config_view_counts[1] = + oxr_segment_views_multiview_count(view_count, sys->multiview_set_capacity); sys->view_config_count = 2; } #endif diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 104a43503..2a4fda466 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -70,14 +70,32 @@ two_screen_table() } // namespace -TEST_CASE("PRIMARY_MULTIVIEW_DXR reports one view set per segment", "[oxr][segment_views]") +TEST_CASE("view-set capacity: only where a window can ever be split", "[oxr][segment_views]") { - CHECK(oxr_segment_views_multiview_count(0) == 0); - CHECK(oxr_segment_views_multiview_count(1) == 1 * XRT_MAX_SEGMENTS); - CHECK(oxr_segment_views_multiview_count(2) == 4); // Leia - CHECK(oxr_segment_views_multiview_count(4) == 8); // sim_display quad + // No segmenting compositor (Windows/macOS/Android, a service session): 1. + CHECK(oxr_segment_views_set_capacity(0, false) == 1); + CHECK(oxr_segment_views_set_capacity(3, false) == 1); + // One DP-backed screen (or none): 1 — the pre-M3 count. + CHECK(oxr_segment_views_set_capacity(0, true) == 1); + CHECK(oxr_segment_views_set_capacity(1, true) == 1); + // Two or more: up to XRT_MAX_SEGMENTS. + CHECK(oxr_segment_views_set_capacity(2, true) == 2); + CHECK(oxr_segment_views_set_capacity(5, true) == XRT_MAX_SEGMENTS); +} + +TEST_CASE("PRIMARY_MULTIVIEW_DXR reports one view set per possible segment", "[oxr][segment_views]") +{ + // Capacity 1: exactly the pre-M3 count (the device max). + CHECK(oxr_segment_views_multiview_count(0, 1) == 0); + CHECK(oxr_segment_views_multiview_count(2, 1) == 2); + CHECK(oxr_segment_views_multiview_count(4, 1) == 4); + CHECK(oxr_segment_views_multiview_count(4, 0) == 4); // 0 treated as 1 + // Capacity 2 (ds1-linux): device max x 2. + CHECK(oxr_segment_views_multiview_count(1, 2) == 2); + CHECK(oxr_segment_views_multiview_count(2, 2) == 4); // Leia + CHECK(oxr_segment_views_multiview_count(4, 2) == 8); // sim_display quad // Capped at XRT_MAX_VIEWS: a device that already fills it gets no second set. - CHECK(oxr_segment_views_multiview_count(XRT_MAX_VIEWS) == XRT_MAX_VIEWS); + CHECK(oxr_segment_views_multiview_count(XRT_MAX_VIEWS, 2) == XRT_MAX_VIEWS); } TEST_CASE("the #1499 mode floor asks one segment's share", "[oxr][segment_views]") diff --git a/tests/tests_oxr_view_space.cpp b/tests/tests_oxr_view_space.cpp index 12b0c4fe0..d5f65c434 100644 --- a/tests/tests_oxr_view_space.cpp +++ b/tests/tests_oxr_view_space.cpp @@ -97,14 +97,16 @@ constexpr float kPosTolM = 0.001f; // 1 mm /*! * Multi-screen M3: PRIMARY_MULTIVIEW_DXR reports one view set per window - * segment — the device max x XRT_MAX_SEGMENTS (2), capped at XRT_MAX_VIEWS (8). - * A window on one display locates its active views first and aliases the rest. + * segment the system can ever weave — the device max on a box that cannot split + * a window (one DP-backed screen, a service session, non-Linux), the device max + * x 2 (capped at XRT_MAX_VIEWS = 8) on desktop Linux with two DP-backed screens. + * Which one this test box is depends on its monitors, so both are accepted. */ -uint32_t -multiview_count(uint32_t device_max) +bool +is_multiview_count(size_t n, uint32_t device_max) { - const uint32_t n = device_max * 2; - return n > 8 ? 8 : n; + const uint32_t two = device_max * 2 > 8 ? 8 : device_max * 2; + return n == device_max || n == two; } constexpr float kAngTolDeg = 0.1f; // 0.1 degree @@ -955,7 +957,7 @@ TEST_CASE("VIEW tracks the reported view array under MULTIVIEW (#1502)", "[oxr][ const uint32_t device_max = rt.device_max_view_count(); INFO("reported views = " << v.size() << ", device max = " << device_max); if (device_max != 0) { - CHECK(v.size() == multiview_count(device_max)); + CHECK(is_multiview_count(v.size(), device_max)); } const XrPosef T_local_view = rt.locate(rt.view, rt.local, t); @@ -1004,7 +1006,7 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] { // PRIMARY_STEREO means exactly 2, whatever the device can drive. CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO) == 2); - CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR) == multiview_count(device_max)); + CHECK(is_multiview_count(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR), device_max)); // A valid core type the system does NOT advertise is still refused. CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO) == 0); } @@ -1015,7 +1017,7 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] uint32_t n = 0; CHECK(rt.locate_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR, t, &n) == XR_SUCCESS); - CHECK(n == multiview_count(device_max)); + CHECK(is_multiview_count(n, device_max)); // Same session, the OTHER advertised configuration: valid enum, wrong // session - UNSUPPORTED, not VALIDATION_FAILURE. @@ -1024,7 +1026,7 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] // And the full locate really writes that many views. std::vector v = rt.views(rt.local, t, nullptr, nullptr); - CHECK(v.size() == multiview_count(device_max)); + CHECK(is_multiview_count(v.size(), device_max)); } tear_down(rt); From 35221c345be94f3cea690f721eb56ffb6d111911 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 12:05:45 -0700 Subject: [PATCH 09/21] fix(vk_native): VIEW_DIMS says 'mosaic', not 'upscaled', under per-segment views (multi-screen M3) Under per-segment views each view covers only its segment's part of a tile, so a submission narrower than the tile is the layout; the lifecycle WARN now names the segment count instead of claiming the compose pass upscales it. Co-Authored-By: Claude Fable 5.1 --- .../vk_native/comp_vk_native_compositor.c | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c index 618a88850..be4a054de 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -630,7 +630,7 @@ struct comp_vk_native_compositor //! Last canvas / view / submitted-tile sizes logged by the handle-app //! VIEW_DIMS line in layer_commit — logged on change only, never per frame. - uint32_t view_dims_logged[6]; + uint32_t view_dims_logged[7]; #ifdef XRT_OS_LINUX_DESKTOP //! One-shot guard for the X11 present-origin refusal WARN (see @@ -5052,12 +5052,28 @@ vk_log_view_dims_on_change(struct comp_vk_native_compositor *c, break; } } - const uint32_t now[6] = { - c->settings.preferred.width, c->settings.preferred.height, view_w, view_h, sub_w, sub_h}; + // Multi-screen M3: under per-segment views each view covers only its + // segment's part of a tile (the mosaic), so a submission narrower than the + // tile is the layout, not an upscale. + uint32_t segments = 0; +#ifdef XRT_OS_LINUX_DESKTOP + os_mutex_lock(&c->seg_pub_mutex); + segments = c->seg_route.count; + os_mutex_unlock(&c->seg_pub_mutex); +#endif + const uint32_t now[7] = { + c->settings.preferred.width, c->settings.preferred.height, view_w, view_h, sub_w, sub_h, segments}; if (memcmp(now, c->view_dims_logged, sizeof(now)) == 0) { return; } memcpy(c->view_dims_logged, now, sizeof(now)); + if (segments > 0) { + U_LOG_W("VIEW_DIMS: canvas %ux%u x mode scale %.3fx%.3f -> tile %ux%u; mosaic: %u segment(s), view 0 " + "submits %ux%u into its segment of the tile (per-segment views, not an upscale)", + now[0], now[1], (double)mode->view_scale_x, (double)mode->view_scale_y, view_w, view_h, + segments, sub_w, sub_h); + return; + } const bool upscaled = sub_w > 0 && sub_h > 0 && (sub_w < view_w || sub_h < view_h); U_LOG_W("VIEW_DIMS: canvas %ux%u x mode scale %.3fx%.3f -> tile %ux%u; app submits %ux%u per view%s", now[0], now[1], (double)mode->view_scale_x, (double)mode->view_scale_y, view_w, view_h, sub_w, sub_h, From a9c94000accd663cd741e401a9e5ad76db1067df Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 12:14:55 -0700 Subject: [PATCH 10/21] =?UTF-8?q?feat(plugin=5Floader):=20DXR=5FSCREEN=5FP?= =?UTF-8?q?LUGIN=20=E2=80=94=20pin=20one=20monitor=20to=20a=20plug-in=20(m?= =?UTF-8?q?ulti-screen=20M4,=20#793=20phase=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DXR_SCREEN_PLUGIN==[,...], = RandR output, DRM connector (case-insensitive) or monitor id hex. For that monitor only a pin outranks the PreferredPlugin override (#791) and the active-plug-in rule (#1521), provided the pinned plug-in claims it; everything else resolves as before. So XRT_PREFERRED_PLUGIN_ID=sim-display DXR_SCREEN_PLUGIN=HDMI-1=leia-sr keeps the DS1 woven by leia-sr. One INFO per applied pin; WARN for a pin whose plug-in has no claim or no DP factory there, an unknown monitor, a malformed entry. The per-monitor winner rule is now target_screen_pick() (pin > preferred > active > confidence, ties to the lower ProbeOrder) in target_screen_pin.{h,c}, unit-tested with the parser by tests_target_screen_pin. Documented in plugin-discovery.md section 3.4. Co-Authored-By: Claude Fable 5.1 --- docs/specs/runtime/plugin-discovery.md | 36 ++- src/xrt/targets/common/CMakeLists.txt | 1 + src/xrt/targets/common/target_plugin_loader.c | 196 ++++++++-------- src/xrt/targets/common/target_screen_pin.c | 214 ++++++++++++++++++ src/xrt/targets/common/target_screen_pin.h | 115 ++++++++++ tests/CMakeLists.txt | 6 + tests/tests_target_screen_pin.cpp | 127 +++++++++++ 7 files changed, 603 insertions(+), 92 deletions(-) create mode 100644 src/xrt/targets/common/target_screen_pin.c create mode 100644 src/xrt/targets/common/target_screen_pin.h create mode 100644 tests/tests_target_screen_pin.cpp diff --git a/docs/specs/runtime/plugin-discovery.md b/docs/specs/runtime/plugin-discovery.md index 9b3300ea4..bf30f78d9 100644 --- a/docs/specs/runtime/plugin-discovery.md +++ b/docs/specs/runtime/plugin-discovery.md @@ -531,7 +531,41 @@ monitor … 3840x2160 @ (3456,0) ACR 0001 344x193 mm output=HDMI-1 → lei On Linux nothing in the compositors reads the registry yet. The Vulkan compositor uses the scalar `dp_factory_*`, so this milestone changes no -weaving. macOS and Android still enumerate no monitors. +weaving. macOS and Android still enumerate no monitors. (Since multi-screen M2/M3 +the Vulkan compositor does read it for a window spanning screens: each segment's DP +comes from that screen's registry entry, mixed vendors included — +`docs/architecture/comp-segments.md`.) + +**The winner per monitor**, in order (`target_screen_pick`, +`src/xrt/targets/common/target_screen_pin.{h,c}`, unit-tested by +`tests/tests_target_screen_pin.cpp`): + +1. a **per-screen pin** (`DXR_SCREEN_PLUGIN`, below) whose plug-in claims the monitor; +2. the **PreferredPlugin** override (#791, §2.1/§3.3), if that plug-in claims it; +3. the **active** plug-in, if it claims it (#1521); +4. the highest **confidence**, ties to the lower ProbeOrder. + +**`DXR_SCREEN_PLUGIN` — pin one monitor to a plug-in** (multi-screen M4, #793 phase 3). +`DXR_SCREEN_PLUGIN==[,=…]`, where `` is +the monitor's RandR output name (`HDMI-1`), its DRM connector (`HDMI-A-1`) — both +case-insensitive — or its monitor id in hex (`0x886e4475353b22b9`, `0x` optional). +For that monitor only, the pin outranks rules 2–4; every other monitor resolves +exactly as before. The global PreferredPlugin pin makes sim-display win every +monitor it claims — on `ds1-linux` that includes the DS1, which sim claims at +FALLBACK while leia-sr claims it VERIFIED — so a sim-display session with the DS1 +still woven by leia-sr is: + +``` +XRT_PREFERRED_PLUGIN_ID=sim-display DXR_SCREEN_PLUGIN=HDMI-1=leia-sr +``` + +Logging: one INFO line per applied pin (`… → 'leia-sr' by DXR_SCREEN_PLUGIN`); a +WARN when a pin names a plug-in with no claim on that monitor (the pin is ignored +there), when the pinned plug-in offers no display-processor factory for it, when an +entry names no known monitor, and for malformed entries. Read at every registry +resolve (session/system create), like the other loader env overrides. The pinned +plug-in must be loaded as a claim source, so `DXR_PLUGIN_EXCLUSIVE` naming another +plug-in defeats it. --- diff --git a/src/xrt/targets/common/CMakeLists.txt b/src/xrt/targets/common/CMakeLists.txt index e9e8731a6..ca001bd72 100644 --- a/src/xrt/targets/common/CMakeLists.txt +++ b/src/xrt/targets/common/CMakeLists.txt @@ -14,6 +14,7 @@ add_library( target_input_plugin_loader.c target_input_plugin_loader.h target_plugin_loader.c target_plugin_loader.h target_screens.c target_screens.h + target_screen_pin.c target_screen_pin.h target_plugin_preload_sanitize.c target_plugin_preload_sanitize.h target_plugin_path_guard.c target_plugin_path_guard.h target_rig_composer.c target_rig_composer.h diff --git a/src/xrt/targets/common/target_plugin_loader.c b/src/xrt/targets/common/target_plugin_loader.c index 4c802d760..8a49ffff0 100644 --- a/src/xrt/targets/common/target_plugin_loader.c +++ b/src/xrt/targets/common/target_plugin_loader.c @@ -21,6 +21,7 @@ */ #include "target_plugin_loader.h" +#include "target_screen_pin.h" #include "xrt/xrt_config_have.h" #ifdef XRT_HAVE_VULKAN @@ -3835,112 +3836,111 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, char preferred_id[64] = {0}; bool have_preferred = target_plugin_get_preferred(preferred_id, sizeof(preferred_id)); - // Resolve per monitor: highest confidence wins; ties → lower ProbeOrder. - // Sources are already in ascending ProbeOrder, so a strict `>` keeps the - // first (lowest-order) source at any given confidence. + /* + * Multi-screen M4 (#793 phase 3): DXR_SCREEN_PLUGIN pins ONE monitor to a + * plug-in, outranking the global rules below for that monitor only. Read + * per resolve (it is a test/bring-up knob, and resolve is rare). + */ + struct target_screen_pins pins; + uint32_t pins_malformed = 0; + target_screen_pin_parse(getenv("DXR_SCREEN_PLUGIN"), &pins, &pins_malformed); + bool pin_used[TARGET_SCREEN_PIN_MAX] = {false}; + if (pins_malformed > 0) { + U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN has %u malformed entr%s (expected " + "=[,...]) — ignored", + pins_malformed, pins_malformed == 1 ? "y" : "ies"); + } + + /* + * The winner per monitor (target_screen_pick, unit-tested): a per-screen + * pin, then PreferredPlugin (#791), then the active plug-in (#1521), then + * highest confidence with ties to the lower ProbeOrder (sources are in + * ascending ProbeOrder). + */ for (uint32_t d = 0; d < dn; d++) { const struct xrt_display_descriptor *desc = &descriptors[d]; const struct plugin_display_source *best_src = NULL; const struct xrt_display_claim *best_claim = NULL; - // Preferred-plugin override (#791): if the pinned source claims this - // monitor, it wins regardless of confidence / ProbeOrder. - if (have_preferred) { - for (int s = 0; s < g_display_source_count && best_claim == NULL; s++) { - const struct xrt_plugin_iface *sif = g_display_sources[s].iface; - const char *sid = (sif != NULL && sif->id != NULL) ? sif->id : NULL; - if (sid == NULL || strcmp(sid, preferred_id) != 0) { + // Every source's claim on this monitor, in source order. + struct target_screen_candidate cands[TARGET_PLUGIN_MAX_SOURCES]; + const struct plugin_display_source *cand_src[TARGET_PLUGIN_MAX_SOURCES]; + const struct xrt_display_claim *cand_claim[TARGET_PLUGIN_MAX_SOURCES]; + uint32_t nc = 0; + for (int s = 0; s < g_display_source_count && nc < TARGET_PLUGIN_MAX_SOURCES; s++) { + for (uint32_t c = 0; c < src_claim_count[s]; c++) { + if (src_claims[s][c].monitor_id != desc->monitor_id) { continue; } - for (uint32_t c = 0; c < src_claim_count[s]; c++) { - if (src_claims[s][c].monitor_id == desc->monitor_id) { - best_src = &g_display_sources[s]; - best_claim = &src_claims[s][c]; - break; - } - } - } - if (best_claim != NULL) { - U_LOG_W("plugin loader: monitor 0x%016llx → PreferredPlugin override '%s' " - "(forced over EDID confidence)", - (unsigned long long)desc->monitor_id, preferred_id); + const struct xrt_plugin_iface *sif = g_display_sources[s].iface; + cands[nc].plugin_id = (sif != NULL && sif->id != NULL) ? sif->id : ""; + cands[nc].confidence = (uint32_t)src_claims[s][c].confidence; + cands[nc].is_active = g_active_iface != NULL && sif == g_active_iface; + cand_src[nc] = &g_display_sources[s]; + cand_claim[nc] = &src_claims[s][c]; + nc++; + break; // one claim per source per monitor } } - // #1521: the ACTIVE plug-in wins any monitor it claims. ProbeOrder - // forcing (`scripts\run_cts.ps1 -Plugin `, `register_dev_plugin.bat`) - // steers only discover_active_plugin, and the scalar `dp_factory_*` follow - // that active plug-in — so without this the registry could name a DIFFERENT - // vendor and the registry-routed APIs (in-process GL, the D3D11 service - // compositor; both via comp_dp_factory_for_window) would weave with a - // different DP than the scalar-routed ones (in-process D3D11/D3D12/VK/Metal). - // That is exactly the sim-forced-active-but-Leia-wins-on-EDID-confidence - // case. On a normal box this is a no-op: the active plug-in IS the lowest- - // ProbeOrder vendor, which is also the confidence winner on the monitors it - // claims. A vendor does not claim monitors that are not its own, so those - // still fall through to confidence below — sim's FALLBACK backstop and the - // Phase 3b multi-vendor intent are preserved. PreferredPlugin (#791) is - // resolved first and still outranks this. - if (best_claim == NULL) { - // What confidence alone would pick: highest confidence wins; ties → - // lower ProbeOrder. Sources are already in ascending ProbeOrder, so a - // strict `>` keeps the first (lowest-order) source at any confidence. - const struct plugin_display_source *conf_src = NULL; - const struct xrt_display_claim *conf_claim = NULL; - for (int s = 0; s < g_display_source_count; s++) { - for (uint32_t c = 0; c < src_claim_count[s]; c++) { - const struct xrt_display_claim *cl = &src_claims[s][c]; - if (cl->monitor_id != desc->monitor_id) { - continue; - } - if (conf_claim == NULL || cl->confidence > conf_claim->confidence) { - conf_claim = cl; - conf_src = &g_display_sources[s]; - } - } + // This monitor's names, for the pin match (the side table is + // guarded by g_refresh_mutex, which this function holds). + const char *out_name = ""; + const char *conn_name = ""; + for (uint32_t i = 0; i < g_monitor_side_count; i++) { + if (g_monitor_side[i].monitor_id == desc->monitor_id) { + out_name = g_monitor_side[i].mon.output_name; + conn_name = g_monitor_side[i].mon.connector; + break; } + } + const int pin_idx = target_screen_pin_find(&pins, desc->monitor_id, out_name, conn_name); + const char *pin_plugin = pin_idx >= 0 ? pins.pin[pin_idx].plugin_id : NULL; + if (pin_idx >= 0) { + pin_used[pin_idx] = true; + } - // The active plug-in's own claim for this monitor, if it has one. - const struct plugin_display_source *act_src = NULL; - const struct xrt_display_claim *act_claim = NULL; - if (g_active_iface != NULL) { - for (int s = 0; s < g_display_source_count && act_claim == NULL; s++) { - if (g_display_sources[s].iface != g_active_iface) { - continue; - } - for (uint32_t c = 0; c < src_claim_count[s]; c++) { - if (src_claims[s][c].monitor_id == desc->monitor_id) { - act_src = &g_display_sources[s]; - act_claim = &src_claims[s][c]; - break; - } - } - } - } + enum target_screen_pick_reason reason = TARGET_SCREEN_PICK_NONE; + bool pin_unclaimed = false; + const int pick = + target_screen_pick(cands, nc, pin_plugin, have_preferred ? preferred_id : NULL, &reason, &pin_unclaimed); + if (pin_unclaimed) { + U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN pins monitor 0x%016llx ('%s'/'%s') to '%s', which has no " + "claim on it — the pin is ignored for this monitor", + (unsigned long long)desc->monitor_id, out_name, conn_name, pin_plugin); + } + if (pick >= 0) { + best_src = cand_src[pick]; + best_claim = cand_claim[pick]; + } - if (act_claim != NULL) { - best_src = act_src; - best_claim = act_claim; - // One-off per monitor, and only when the override actually - // changes the outcome. - if (conf_claim != NULL && conf_claim != act_claim) { - const struct xrt_plugin_iface *aif = act_src->iface; - const struct xrt_plugin_iface *cif = conf_src->iface; - const char *aid = (aif != NULL && aif->id != NULL) ? aif->id : "?"; - const char *cid = (cif != NULL && cif->id != NULL) ? cif->id : "?"; - U_LOG_W( - "plugin loader: monitor 0x%016llx → ACTIVE plug-in '%s' " - "(confidence=%u, ProbeOrder=%u) forced over '%s' (confidence=%u) — " - "the weaving DP follows the active plug-in so registry-routed APIs " - "(GL, service D3D11) match the scalar-routed ones (#1521)", - (unsigned long long)desc->monitor_id, aid, (unsigned)act_claim->confidence, - act_src->probe_order, cid, (unsigned)conf_claim->confidence); - } - } else { - best_src = conf_src; - best_claim = conf_claim; + // What confidence alone would pick, for the override logs. + int conf_pick = -1; + for (uint32_t i = 0; i < nc; i++) { + if (conf_pick < 0 || cands[i].confidence > cands[conf_pick].confidence) { + conf_pick = (int)i; } } + + if (reason == TARGET_SCREEN_PICK_PIN) { + U_LOG_I("plugin loader: monitor 0x%016llx ('%s'/'%s') → '%s' by DXR_SCREEN_PLUGIN (confidence=%u; " + "outranks PreferredPlugin and the active plug-in for this monitor)", + (unsigned long long)desc->monitor_id, out_name, conn_name, cands[pick].plugin_id, + cands[pick].confidence); + } else if (reason == TARGET_SCREEN_PICK_PREFERRED) { + U_LOG_W("plugin loader: monitor 0x%016llx → PreferredPlugin override '%s' " + "(forced over EDID confidence)", + (unsigned long long)desc->monitor_id, preferred_id); + } else if (reason == TARGET_SCREEN_PICK_ACTIVE && conf_pick >= 0 && conf_pick != pick) { + // One-off per monitor, and only when the override changes the outcome. + U_LOG_W( + "plugin loader: monitor 0x%016llx → ACTIVE plug-in '%s' " + "(confidence=%u, ProbeOrder=%u) forced over '%s' (confidence=%u) — " + "the weaving DP follows the active plug-in so registry-routed APIs " + "(GL, service D3D11) match the scalar-routed ones (#1521)", + (unsigned long long)desc->monitor_id, cands[pick].plugin_id, cands[pick].confidence, + cand_src[pick]->probe_order, cands[conf_pick].plugin_id, cands[conf_pick].confidence); + } if (best_claim == NULL) { continue; // no plug-in claimed this monitor } @@ -3948,6 +3948,20 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, fill_registry_entry(e, desc, best_src, best_claim); U_LOG_I("plugin loader: monitor 0x%016llx → plug-in '%s' (confidence=%u)", (unsigned long long)e->monitor_id, e->plugin_id, e->confidence); + if (reason == TARGET_SCREEN_PICK_PIN && e->dp_factory_vk == NULL && e->dp_factory_gl == NULL && + e->dp_factory_d3d11 == NULL && e->dp_factory_d3d12 == NULL && e->dp_factory_metal == NULL) { + U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN pinned monitor 0x%016llx to '%s', but it offers no " + "display-processor factory for it — that screen will not be woven", + (unsigned long long)e->monitor_id, e->plugin_id); + } + } + + for (uint32_t i = 0; i < pins.count; i++) { + if (!pin_used[i]) { + U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN entry '%s=%s' names no known monitor (match an output " + "or connector name, e.g. HDMI-1 / HDMI-A-1, or a monitor id in hex)", + pins.pin[i].match, pins.pin[i].plugin_id); + } } if (g_refresh_mutex_initialized) { diff --git a/src/xrt/targets/common/target_screen_pin.c b/src/xrt/targets/common/target_screen_pin.c new file mode 100644 index 000000000..0a085af48 --- /dev/null +++ b/src/xrt/targets/common/target_screen_pin.c @@ -0,0 +1,214 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen plug-in pin + per-monitor winner rule — see target_screen_pin.h. + * @ingroup targets_common + */ + +#include "target_screen_pin.h" + +#include +#include +#include +#include + + +static void +copy_trimmed(char *dst, size_t cap, const char *b, const char *e) +{ + while (b < e && isspace((unsigned char)*b)) { + b++; + } + while (e > b && isspace((unsigned char)e[-1])) { + e--; + } + size_t n = (size_t)(e - b); + if (n >= cap) { + n = cap - 1; + } + memcpy(dst, b, n); + dst[n] = '\0'; +} + +uint32_t +target_screen_pin_parse(const char *spec, struct target_screen_pins *out, uint32_t *out_malformed) +{ + uint32_t bad = 0; + if (out != NULL) { + memset(out, 0, sizeof(*out)); + } + if (spec == NULL || out == NULL) { + if (out_malformed != NULL) { + *out_malformed = 0; + } + return 0; + } + const char *p = spec; + while (*p != '\0') { + const char *end = strchr(p, ','); + if (end == NULL) { + end = p + strlen(p); + } + const char *eq = memchr(p, '=', (size_t)(end - p)); + char match[TARGET_SCREEN_PIN_STR]; + char id[TARGET_SCREEN_PIN_STR]; + match[0] = id[0] = '\0'; + if (eq != NULL) { + copy_trimmed(match, sizeof(match), p, eq); + copy_trimmed(id, sizeof(id), eq + 1, end); + } + bool blank = true; + for (const char *q = p; q < end; q++) { + if (!isspace((unsigned char)*q)) { + blank = false; + break; + } + } + if (!blank) { + if (eq == NULL || match[0] == '\0' || id[0] == '\0' || out->count >= TARGET_SCREEN_PIN_MAX) { + bad++; + } else { + struct target_screen_pin *pin = &out->pin[out->count++]; + snprintf(pin->match, sizeof(pin->match), "%s", match); + snprintf(pin->plugin_id, sizeof(pin->plugin_id), "%s", id); + } + } + p = (*end == ',') ? end + 1 : end; + } + if (out_malformed != NULL) { + *out_malformed = bad; + } + return out->count; +} + +static bool +name_eq(const char *a, const char *b) +{ + if (a == NULL || b == NULL || a[0] == '\0' || b[0] == '\0') { + return false; + } + for (; *a != '\0' && *b != '\0'; a++, b++) { + if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) { + return false; + } + } + return *a == '\0' && *b == '\0'; +} + +bool +target_screen_pin_matches(const struct target_screen_pin *pin, + uint64_t monitor_id, + const char *output_name, + const char *connector) +{ + if (pin == NULL || pin->match[0] == '\0') { + return false; + } + if (name_eq(pin->match, output_name) || name_eq(pin->match, connector)) { + return true; + } + // A monitor id in hex, `0x` optional; the whole string must be hex. + const char *h = pin->match; + if (h[0] == '0' && (h[1] == 'x' || h[1] == 'X')) { + h += 2; + } + if (*h == '\0' || strlen(h) > 16) { + return false; + } + for (const char *q = h; *q != '\0'; q++) { + if (!isxdigit((unsigned char)*q)) { + return false; + } + } + return strtoull(h, NULL, 16) == monitor_id && monitor_id != 0; +} + +int +target_screen_pin_find(const struct target_screen_pins *pins, + uint64_t monitor_id, + const char *output_name, + const char *connector) +{ + if (pins == NULL) { + return -1; + } + for (uint32_t i = 0; i < pins->count && i < TARGET_SCREEN_PIN_MAX; i++) { + if (target_screen_pin_matches(&pins->pin[i], monitor_id, output_name, connector)) { + return (int)i; + } + } + return -1; +} + +static int +first_with_id(const struct target_screen_candidate *c, uint32_t n, const char *id) +{ + if (id == NULL || id[0] == '\0') { + return -1; + } + for (uint32_t i = 0; i < n; i++) { + if (c[i].plugin_id != NULL && strcmp(c[i].plugin_id, id) == 0) { + return (int)i; + } + } + return -1; +} + +int +target_screen_pick(const struct target_screen_candidate *cands, + uint32_t count, + const char *pin_plugin, + const char *preferred, + enum target_screen_pick_reason *out_reason, + bool *out_pin_unclaimed) +{ + enum target_screen_pick_reason reason = TARGET_SCREEN_PICK_NONE; + int pick = -1; + bool pin_unclaimed = false; + + if (cands != NULL && count > 0) { + if (pin_plugin != NULL && pin_plugin[0] != '\0') { + pick = first_with_id(cands, count, pin_plugin); + if (pick >= 0) { + reason = TARGET_SCREEN_PICK_PIN; + } else { + pin_unclaimed = true; + } + } + if (pick < 0) { + pick = first_with_id(cands, count, preferred); + if (pick >= 0) { + reason = TARGET_SCREEN_PICK_PREFERRED; + } + } + if (pick < 0) { + for (uint32_t i = 0; i < count; i++) { + if (cands[i].is_active) { + pick = (int)i; + reason = TARGET_SCREEN_PICK_ACTIVE; + break; + } + } + } + if (pick < 0) { + for (uint32_t i = 0; i < count; i++) { + // Strict '>': a tie keeps the earlier (lower ProbeOrder) source. + if (pick < 0 || cands[i].confidence > cands[pick].confidence) { + pick = (int)i; + } + } + reason = TARGET_SCREEN_PICK_CONFIDENCE; + } + } else if (pin_plugin != NULL && pin_plugin[0] != '\0') { + pin_unclaimed = true; + } + + if (out_reason != NULL) { + *out_reason = reason; + } + if (out_pin_unclaimed != NULL) { + *out_pin_unclaimed = pin_unclaimed; + } + return pick; +} diff --git a/src/xrt/targets/common/target_screen_pin.h b/src/xrt/targets/common/target_screen_pin.h new file mode 100644 index 000000000..dcdf2156e --- /dev/null +++ b/src/xrt/targets/common/target_screen_pin.h @@ -0,0 +1,115 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen plug-in pin (`DXR_SCREEN_PLUGIN`) and the per-monitor + * winner rule of the DP registry (multi-screen M0/M4, #793 phase 3). + * @ingroup targets_common + * + * `DXR_SCREEN_PLUGIN==[,=...]` pins ONE + * monitor to a plug-in. `` is an OS output / connector name ("HDMI-1", + * "HDMI-A-1", "eDP-1", case-insensitive) or a monitor id in hex ("0x886e…" or + * bare). For that monitor only, a pin outranks every global rule — the + * PreferredPlugin override (#791) and the active-plug-in rule (#1521) — as long + * as the pinned plug-in claims the monitor; everything else resolves exactly as + * before. That is what lets a sim-display session (`XRT_PREFERRED_PLUGIN_ID= + * sim-display`) keep the DS1 woven by leia-sr: `DXR_SCREEN_PLUGIN=HDMI-1=leia-sr`. + * + * Pure functions (no loader state), unit-tested by + * `tests/tests_target_screen_pin.cpp`; target_plugin_resolve_displays() uses them. + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define TARGET_SCREEN_PIN_MAX 8 +#define TARGET_SCREEN_PIN_STR 64 + +struct target_screen_pin +{ + char match[TARGET_SCREEN_PIN_STR]; + char plugin_id[TARGET_SCREEN_PIN_STR]; +}; + +struct target_screen_pins +{ + uint32_t count; + struct target_screen_pin pin[TARGET_SCREEN_PIN_MAX]; +}; + +/*! + * Parse a `DXR_SCREEN_PLUGIN` value. Entries are comma-separated `match=id`; + * whitespace around either side is trimmed. An entry without `=`, with an empty + * side, or past @ref TARGET_SCREEN_PIN_MAX is skipped and counted in + * @p out_malformed (may be NULL). NULL / empty spec = no pins. + * + * @return the number of pins parsed. + */ +uint32_t +target_screen_pin_parse(const char *spec, struct target_screen_pins *out, uint32_t *out_malformed); + +/*! + * Does @p pin name this monitor? By output name or connector (either may be + * NULL/""), case-insensitive, or by its id in hex (`0x` optional). + */ +bool +target_screen_pin_matches(const struct target_screen_pin *pin, + uint64_t monitor_id, + const char *output_name, + const char *connector); + +/*! + * The first pin naming this monitor, or -1. + */ +int +target_screen_pin_find(const struct target_screen_pins *pins, + uint64_t monitor_id, + const char *output_name, + const char *connector); + +/*! + * One plug-in's claim on one monitor, as the winner rule sees it. Candidates + * are listed in source order (ascending ProbeOrder). + */ +struct target_screen_candidate +{ + const char *plugin_id; + uint32_t confidence; + bool is_active; +}; + +enum target_screen_pick_reason +{ + TARGET_SCREEN_PICK_NONE = 0, //!< No candidate. + TARGET_SCREEN_PICK_PIN = 1, //!< `DXR_SCREEN_PLUGIN` pin for this monitor. + TARGET_SCREEN_PICK_PREFERRED = 2, //!< PreferredPlugin override (#791). + TARGET_SCREEN_PICK_ACTIVE = 3, //!< The active plug-in claims it (#1521). + TARGET_SCREEN_PICK_CONFIDENCE = 4, //!< Highest confidence, ties to the lower ProbeOrder. +}; + +/*! + * The per-monitor winner: a pin (if its plug-in claims the monitor), then the + * preferred plug-in, then the active one, then confidence. @p pin_plugin and + * @p preferred may be NULL. + * + * @param[out] out_pin_unclaimed Set when a pin was given but its plug-in has + * no claim on this monitor (the caller WARNs). + * @return the winning candidate index, or -1. + */ +int +target_screen_pick(const struct target_screen_candidate *cands, + uint32_t count, + const char *pin_plugin, + const char *preferred, + enum target_screen_pick_reason *out_reason, + bool *out_pin_unclaimed); + +#ifdef __cplusplus +} +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 983c98b76..343ca4e91 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,6 +63,7 @@ set(tests tests_oxr_layer_space tests_oxr_view_config_rule tests_oxr_segment_views + tests_target_screen_pin tests_oxr_legacy_mode_rule tests_oxr_mode_fillable_rule tests_oxr_tracking_properties_rule @@ -605,6 +606,11 @@ target_sources(tests_oxr_segment_views PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/sta target_include_directories(tests_oxr_segment_views PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/state_trackers/oxr) target_link_libraries(tests_oxr_segment_views PRIVATE xrt-interfaces aux_math) +# Multi-screen M4 — DXR_SCREEN_PLUGIN (per-screen plug-in pin) and the per-monitor +# winner rule of the DP registry, pure functions of target_screen_pin.c. +target_sources(tests_target_screen_pin PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/targets/common/target_screen_pin.c) +target_include_directories(tests_target_screen_pin PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/targets/common) + if(XRT_BUILD_DRIVER_HANDTRACKING) target_link_libraries( tests_levenbergmarquardt diff --git a/tests/tests_target_screen_pin.cpp b/tests/tests_target_screen_pin.cpp new file mode 100644 index 000000000..3db2fe629 --- /dev/null +++ b/tests/tests_target_screen_pin.cpp @@ -0,0 +1,127 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief DXR_SCREEN_PLUGIN (per-screen plug-in pin) parser and the + * per-monitor winner rule of the DP registry (multi-screen M4). + * + * target_plugin_resolve_displays() needs loaded plug-ins and monitors; the + * decision it takes per monitor is target_screen_pick(), pinned here together + * with the pin parser and matcher. + */ + +#include "catch_amalgamated.hpp" + +#include "target_screen_pin.h" + +#include + +TEST_CASE("DXR_SCREEN_PLUGIN parses match=id pairs", "[target][screen_pin]") +{ + target_screen_pins p; + uint32_t bad = 99; + + CHECK(target_screen_pin_parse(nullptr, &p, &bad) == 0); + CHECK(bad == 0); + CHECK(target_screen_pin_parse("", &p, &bad) == 0); + CHECK(bad == 0); + + REQUIRE(target_screen_pin_parse("HDMI-1=leia-sr", &p, &bad) == 1); + CHECK(std::strcmp(p.pin[0].match, "HDMI-1") == 0); + CHECK(std::strcmp(p.pin[0].plugin_id, "leia-sr") == 0); + CHECK(bad == 0); + + REQUIRE(target_screen_pin_parse(" HDMI-1 = leia-sr , eDP-1=sim-display,", &p, &bad) == 2); + CHECK(std::strcmp(p.pin[0].plugin_id, "leia-sr") == 0); + CHECK(std::strcmp(p.pin[1].match, "eDP-1") == 0); + CHECK(std::strcmp(p.pin[1].plugin_id, "sim-display") == 0); + CHECK(bad == 0); + + // Malformed entries are skipped and counted; good ones survive. + REQUIRE(target_screen_pin_parse("HDMI-1,=leia-sr,eDP-1=,DP-2=sim-display", &p, &bad) == 1); + CHECK(std::strcmp(p.pin[0].match, "DP-2") == 0); + CHECK(bad == 3); +} + +TEST_CASE("a pin matches by output, connector or monitor id", "[target][screen_pin]") +{ + target_screen_pin pin; + std::memset(&pin, 0, sizeof(pin)); + std::strcpy(pin.plugin_id, "leia-sr"); + + std::strcpy(pin.match, "HDMI-1"); + CHECK(target_screen_pin_matches(&pin, 0x1234, "HDMI-1", "HDMI-A-1")); + CHECK(target_screen_pin_matches(&pin, 0x1234, "hdmi-1", "")); + CHECK_FALSE(target_screen_pin_matches(&pin, 0x1234, "HDMI-2", "HDMI-A-1")); + CHECK_FALSE(target_screen_pin_matches(&pin, 0x1234, nullptr, nullptr)); + + std::strcpy(pin.match, "HDMI-A-1"); + CHECK(target_screen_pin_matches(&pin, 0x1234, "HDMI-1", "HDMI-A-1")); + + std::strcpy(pin.match, "0x886e4475353b22b9"); + CHECK(target_screen_pin_matches(&pin, 0x886e4475353b22b9ULL, "", "")); + std::strcpy(pin.match, "886E4475353B22B9"); + CHECK(target_screen_pin_matches(&pin, 0x886e4475353b22b9ULL, "", "")); + CHECK_FALSE(target_screen_pin_matches(&pin, 0x03fdab50663276e4ULL, "", "")); + std::strcpy(pin.match, "eDP-1"); // not hex: never an id match + CHECK_FALSE(target_screen_pin_matches(&pin, 0xed1, "", "")); + + target_screen_pins pins; + REQUIRE(target_screen_pin_parse("eDP-1=sim-display,HDMI-1=leia-sr", &pins, nullptr) == 2); + CHECK(target_screen_pin_find(&pins, 1, "HDMI-1", "HDMI-A-1") == 1); + CHECK(target_screen_pin_find(&pins, 2, "eDP-1", "eDP-1") == 0); + CHECK(target_screen_pin_find(&pins, 3, "DP-3", "DP-3") == -1); +} + +TEST_CASE("winner rule: pin > PreferredPlugin > active > confidence", "[target][screen_pin]") +{ + // The DS1 on HDMI-1 as ds1-linux sees it: sim claims it at FALLBACK (lower + // ProbeOrder here), leia-sr claims it VERIFIED. + const target_screen_candidate hdmi[] = { + {"sim-display", 10, true}, + {"leia-sr", 100, false}, + }; + target_screen_pick_reason why; + bool unclaimed = true; + + // No pin, sim preferred (mode B): sim wins — the #791 rule, unchanged. + CHECK(target_screen_pick(hdmi, 2, nullptr, "sim-display", &why, &unclaimed) == 0); + CHECK(why == TARGET_SCREEN_PICK_PREFERRED); + CHECK_FALSE(unclaimed); + + // The pin outranks the preferred plug-in for THIS monitor. + CHECK(target_screen_pick(hdmi, 2, "leia-sr", "sim-display", &why, &unclaimed) == 1); + CHECK(why == TARGET_SCREEN_PICK_PIN); + CHECK_FALSE(unclaimed); + + // ...and the active one. + CHECK(target_screen_pick(hdmi, 2, "leia-sr", nullptr, &why, &unclaimed) == 1); + CHECK(why == TARGET_SCREEN_PICK_PIN); + + // A pin naming a plug-in without a claim is ignored (and reported). + CHECK(target_screen_pick(hdmi, 2, "other-vendor", "sim-display", &why, &unclaimed) == 0); + CHECK(why == TARGET_SCREEN_PICK_PREFERRED); + CHECK(unclaimed); + + // No pin, no preferred: the active plug-in (#1521). + CHECK(target_screen_pick(hdmi, 2, nullptr, nullptr, &why, &unclaimed) == 0); + CHECK(why == TARGET_SCREEN_PICK_ACTIVE); + + // Nobody active or preferred: confidence; a tie keeps the earlier source. + const target_screen_candidate plain[] = { + {"a", 50, false}, + {"b", 100, false}, + {"c", 100, false}, + }; + CHECK(target_screen_pick(plain, 3, nullptr, nullptr, &why, &unclaimed) == 1); + CHECK(why == TARGET_SCREEN_PICK_CONFIDENCE); + + // A preferred plug-in without a claim falls through. + CHECK(target_screen_pick(plain, 3, nullptr, "zzz", &why, &unclaimed) == 1); + CHECK(why == TARGET_SCREEN_PICK_CONFIDENCE); + + // No candidates at all. + CHECK(target_screen_pick(nullptr, 0, "leia-sr", nullptr, &why, &unclaimed) == -1); + CHECK(why == TARGET_SCREEN_PICK_NONE); + CHECK(unclaimed); +} From f69cda02450715978784d9314b4e73c598e003fd Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:52:04 -0700 Subject: [PATCH 11/21] fix(oxr): segments take their DP's untracked eyes; nominal eyes fill every active view (multi-screen M3 review 1) A segment accepted its DP's eyes only while tracking, unlike the single-screen path, so an untracked DP (sim_display) was replaced by the registry nominal viewer and the primary panel's views jumped when the window crossed the seam. And the nominal branch always produced 2 eyes, so a 4-view mode left views 2..3 with zero FOVs (the save/restore then wrote them into every segment). Now: any valid DP eye set counts (oxr_segment_views_accept_eyes), and the nominal branch fills one eye per active view (the pair repeated; 2 views = exactly the old pair). Quad-mode test added. Co-Authored-By: Claude Fable 5.1 --- .../state_trackers/oxr/oxr_segment_views.h | 37 ++++++++++++ src/xrt/state_trackers/oxr/oxr_session.c | 26 ++++----- tests/tests_oxr_segment_views.cpp | 56 +++++++++++++++++++ 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 981f4f299..3d4a766bd 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -184,6 +184,43 @@ oxr_segment_views_assign(uint32_t segment_count, uint32_t out_first[XRT_MAX_SEGMENTS], uint32_t out_count[XRT_MAX_SEGMENTS]); +/*! + * Does a screen's DP eye set count? Exactly the single-screen rule: any VALID + * set with at least one eye, tracked or not (an untracked DP still reports its + * nominal-viewer eyes — sim_display does, N of them for a Quad mode). Only a + * screen with no DP eyes at all falls back to its registry nominal viewer. + */ +static inline bool +oxr_segment_views_accept_eyes(bool have_eyes, bool valid, uint32_t count) +{ + return have_eyes && valid && count > 0; +} + +/*! + * The untracked eye set: one eye per active view (at least 2), in front of the + * screen centre at @p z, translated by (@p dx, @p dy) into the caller's frame. + * The pair is (-ipd/2, +ipd/2); views past 2 repeat it column by column (even + * views left, odd views right), so a 4-view mode gets a valid frustum for every + * view instead of zero FOVs. Two views = exactly the pre-M3 nominal pair. + * + * @return the number of eyes written (<= XRT_MAX_VIEWS). + */ +static inline uint32_t +oxr_segment_views_nominal_eyes( + float ipd, float z, uint32_t active_view_count, float dx, float dy, struct xrt_eye_position *out) +{ + uint32_t n = active_view_count < 2 ? 2 : active_view_count; + if (n > XRT_MAX_VIEWS) { + n = XRT_MAX_VIEWS; + } + for (uint32_t i = 0; i < n; i++) { + out[i].x = ((i % 2) == 0 ? -ipd / 2.0f : ipd / 2.0f) + dx; + out[i].y = dy; + out[i].z = z; + } + return n; +} + /*! * Which located view view @p i carries: itself while active, view 0 for the * inactive tail `[active_view_count, reported)` — the ADR-041 alias, the same diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index f2318351b..11bdb5242 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2973,7 +2973,6 @@ locate_views_one(struct oxr_logger *log, U_LOG_I("Kooima: nominal_z=%.3f", nominal_z_m); if (nominal_z_m > 0.0f) { float ipd_m = sess->ipd_meters; - eye_count = 2; // Untracked → the eye sits at the REFERENCE: directly in front // of the display centre at the nominal distance, i.e. (0,0,Nz). // nominal_y must NOT leak into the eye: an eye lateral offset @@ -2983,16 +2982,13 @@ locate_views_one(struct oxr_logger *log, // reproduce, so the camera/display rigs would disagree. // nominal_y belongs only to the parallax / zero-parallax pivot // (the `nominal` vector below), never to the eye position. - adj_eyes[0] = (struct xrt_eye_position){-ipd_m / 2.0f, 0.0f, nominal_z_m}; - adj_eyes[1] = (struct xrt_eye_position){ipd_m / 2.0f, 0.0f, nominal_z_m}; - if (seg != NULL) { - // In front of THIS segment's screen, carried into - // the reference frame. - for (uint32_t ei = 0; ei < 2; ei++) { - adj_eyes[ei].x += seg->eye_dx; - adj_eyes[ei].y += seg->eye_dy; - } - } + // One eye per active view (a 2-view mode: exactly the + // pair this always was), so an N-view mode never leaves + // views with zero FOVs. For a segment: in front of THAT + // screen, carried into the reference frame. + eye_count = oxr_segment_views_nominal_eyes(ipd_m, nominal_z_m, active_view_count, + seg != NULL ? seg->eye_dx : 0.0f, + seg != NULL ? seg->eye_dy : 0.0f, adj_eyes); have_eye_positions = true; if (should_log) { U_LOG_I("Nominal eyes: [0]=(%.4f,%.4f,%.4f) [1]=(%.4f,%.4f,%.4f), IPD=%.1fmm", @@ -4157,9 +4153,9 @@ locate_segment_override(const struct xrt_segment_metrics *m, o->nominal_y_m = s->nominal_viewer_y_m; o->nominal_z_m = s->nominal_viewer_z_m; - // The screen's DP eyes when it is tracking a viewer; otherwise the - // screen's nominal viewer (the locate's untracked branch). - if (s->have_eyes && s->eyes.valid && s->eyes.is_tracking && s->eyes.count > 0) { + // The screen's DP eyes whenever it reports a valid set (tracked or not, + // exactly like the single-screen path); else the screen's nominal viewer. + if (oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count)) { o->eyes = s->eyes; for (uint32_t e = 0; e < o->eyes.count && e < XRT_MAX_VIEWS; e++) { o->eyes.eyes[e].x += dx; @@ -4277,7 +4273,7 @@ oxr_session_locate_views(struct oxr_logger *log, const struct xrt_segment_metric *s = &m.seg[mj]; o.nominal_y_m = s->nominal_viewer_y_m; o.nominal_z_m = s->nominal_viewer_z_m; - if (s->have_eyes && s->eyes.valid && s->eyes.is_tracking && s->eyes.count > 0) { + if (oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count)) { o.eyes = s->eyes; o.have_eyes = true; } diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 2a4fda466..87adbd9cd 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -280,3 +280,59 @@ TEST_CASE("views are contiguous per segment and the tail aliases view 0", "[oxr] CHECK(oxr_segment_views_alias_source(4, 4) == 0); CHECK(oxr_segment_views_alias_source(7, 4) == 0); } + +TEST_CASE("a segment takes its DP's eyes tracked or not, like the single-screen path", "[oxr][segment_views]") +{ + // Untracked but valid (sim_display): accepted — not replaced by the + // registry nominal viewer, so the primary panel's views do not jump when + // the window crosses the seam. + CHECK(oxr_segment_views_accept_eyes(true, true, 4)); + CHECK(oxr_segment_views_accept_eyes(true, true, 2)); + CHECK_FALSE(oxr_segment_views_accept_eyes(false, true, 2)); + CHECK_FALSE(oxr_segment_views_accept_eyes(true, false, 2)); + CHECK_FALSE(oxr_segment_views_accept_eyes(true, true, 0)); +} + +TEST_CASE("Quad mode on two segments: every view gets a real frustum", "[oxr][segment_views]") +{ + // The nominal branch must fill one eye per active view: a 4-view mode + // with only 2 eyes left views 2..3 with zero FOVs on every segment. + xrt_eye_position eyes[XRT_MAX_VIEWS]; + REQUIRE(oxr_segment_views_nominal_eyes(0.063f, 0.6f, 2, 0.0f, 0.0f, eyes) == 2); + CHECK(eyes[0].x == Catch::Approx(-0.0315f)); + CHECK(eyes[1].x == Catch::Approx(0.0315f)); + REQUIRE(oxr_segment_views_nominal_eyes(0.063f, 0.6f, 1, 0.0f, 0.0f, eyes) == 2); // mono: the pair, centred later + + const xrt_segment_metrics m = two_screen_table(); + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + uint32_t first[XRT_MAX_SEGMENTS] = {}; + uint32_t count[XRT_MAX_SEGMENTS] = {}; + REQUIRE(oxr_segment_views_assign(2, 4, 8, first, count) == 8); + + for (uint32_t i = 0; i < 2; i++) { + float dx = 0, dy = 0; + oxr_segment_views_own_to_ref(&l, i, &dx, &dy); + xrt_window_metrics wm; + oxr_segment_views_window_metrics(&m, &l, i, &wm); + const uint32_t n = + oxr_segment_views_nominal_eyes(0.063f, m.seg[i].nominal_viewer_z_m, count[i], dx, dy, eyes); + REQUIRE(n == 4); + xrt_vec3 raw[4]; + for (uint32_t e = 0; e < 4; e++) { + raw[e] = {eyes[e].x - wm.window_center_offset_x_m, eyes[e].y - wm.window_center_offset_y_m, + eyes[e].z}; + } + const dxr_screen scr = {wm.window_width_m, wm.window_height_m}; + dxr_display3d_tunables dt = dxr_display3d_default_tunables(); + dt.virtual_display_height = wm.window_height_m; + const xrt_vec3 nominal = {0.0f, 0.0f, m.seg[i].nominal_viewer_z_m}; + dxr_xrt_view out[4]; + dxr_xrt_display3d_compute_views(raw, 4, &nominal, &scr, &dt, nullptr, out); + for (uint32_t e = 0; e < 4; e++) { + INFO("segment " << i << " view " << e); + CHECK(out[e].fov.angle_right - out[e].fov.angle_left > 0.05f); + CHECK(out[e].fov.angle_up - out[e].fov.angle_down > 0.05f); + } + } +} From 8d07774629f49d8ac987a529842a5ea60b7734d5 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:52:46 -0700 Subject: [PATCH 12/21] fix(oxr): only a splitting locate records the frame's segment routing (multi-screen M3 review 2) Every non-splitting locate used to zero the routing, so an app that located its projection per segment and then did a zone-scoped or camera-rig locate in the same frame lost the routing: segment 0's subimage stretched over every tile. The routing is now a per-frame record: a splitting locate writes it, other locates leave it, and xrEndFrame takes it (and logs a change) and resets it for the next frame. Test added. Co-Authored-By: Claude Fable 5.1 --- src/xrt/state_trackers/oxr/oxr_objects.h | 8 +++ .../state_trackers/oxr/oxr_segment_views.h | 21 +++++++ src/xrt/state_trackers/oxr/oxr_session.c | 55 +++++++++++-------- .../oxr/oxr_session_frame_end.c | 4 +- tests/tests_oxr_segment_views.cpp | 25 +++++++++ 5 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index d5c66e11e..97ed9d15e 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -1024,6 +1024,14 @@ oxr_session_set_workspace_view_rig(struct oxr_logger *log, struct oxr_session *s XRT_CHECK_RESULT XrResult oxr_session_poll(struct oxr_logger *log, struct oxr_session *sess); +/*! + * Multi-screen M3: take this frame's per-segment view routing (what the last + * SPLITTING xrLocateViews of the frame handed out; count 0 when none split) + * and reset the record for the next frame. Called once per xrEndFrame. + */ +void +oxr_session_take_segment_view_routing(struct oxr_session *sess, struct xrt_segment_view_routing *out); + XrResult oxr_session_locate_views(struct oxr_logger *log, struct oxr_session *sess, diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 3d4a766bd..15514fdd5 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -29,6 +29,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -221,6 +222,26 @@ oxr_segment_views_nominal_eyes( return n; } +/*! + * The per-frame routing record (xrLocateViews -> xrEndFrame). Only a locate + * that SPLIT the views records; every other locate in the frame (a zone-scoped + * or camera-rig locate, a locate that kept one view set) leaves the record as + * it is, so it cannot erase the routing of the projection the app rendered per + * segment. xrEndFrame takes the record and resets it for the next frame. + */ +static inline void +oxr_segment_views_route_record(struct xrt_segment_view_routing *frame, const struct xrt_segment_view_routing *r) +{ + *frame = *r; +} + +static inline void +oxr_segment_views_route_take(struct xrt_segment_view_routing *frame, struct xrt_segment_view_routing *out) +{ + *out = *frame; + memset(frame, 0, sizeof(*frame)); +} + /*! * Which located view view @p i carries: itself while active, view 0 for the * inactive tail `[active_view_count, reported)` — the ADR-041 alias, the same diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 11bdb5242..e2686219d 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -4201,13 +4201,12 @@ locate_fill_bindings(struct oxr_session *sess, XrViewState *viewState, const str //! One INFO line per change of the per-segment view layout (never per frame). static void -locate_log_route_change(struct oxr_session *sess) +locate_log_route_change(struct oxr_session *sess, const struct xrt_segment_view_routing *r) { - if (memcmp(&sess->seg_route, &sess->seg_route_logged, sizeof(sess->seg_route)) == 0) { + if (memcmp(r, &sess->seg_route_logged, sizeof(*r)) == 0) { return; } - sess->seg_route_logged = sess->seg_route; - const struct xrt_segment_view_routing *r = &sess->seg_route; + sess->seg_route_logged = *r; if (r->count == 0) { U_LOG_I("per-segment views: off — one view set for the whole window"); return; @@ -4224,6 +4223,13 @@ locate_log_route_change(struct oxr_session *sess) U_LOG_I("per-segment views: %u segment(s): %s", r->count, buf); } +void +oxr_session_take_segment_view_routing(struct oxr_session *sess, struct xrt_segment_view_routing *out) +{ + oxr_segment_views_route_take(&sess->seg_route, out); + locate_log_route_change(sess, out); +} + XrResult oxr_session_locate_views(struct oxr_logger *log, struct oxr_session *sess, @@ -4241,9 +4247,10 @@ oxr_session_locate_views(struct oxr_logger *log, XrResult ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, viewCountOutput, views, NULL); if (viewCapacityInput > 0 && ret == XR_SUCCESS) { - sess->seg_route.count = 0; - locate_fill_bindings(sess, viewState, &sess->seg_route); - locate_log_route_change(sess); + // One view set: no bindings. The frame's routing record is + // NOT touched — only a splitting locate writes it. + const struct xrt_segment_view_routing none = {0}; + locate_fill_bindings(sess, viewState, &none); } return ret; } @@ -4282,9 +4289,10 @@ oxr_session_locate_views(struct oxr_logger *log, views, o.wm.valid ? &o : NULL); } if (viewCapacityInput > 0 && ret == XR_SUCCESS) { - sess->seg_route.count = 0; - locate_fill_bindings(sess, viewState, &sess->seg_route); - locate_log_route_change(sess); + // One view set: no bindings. The frame's routing record is + // NOT touched — only a splitting locate writes it. + const struct xrt_segment_view_routing none = {0}; + locate_fill_bindings(sess, viewState, &none); } return ret; } @@ -4325,9 +4333,10 @@ oxr_session_locate_views(struct oxr_logger *log, XrResult ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, viewCountOutput, views, NULL); if (viewCapacityInput > 0 && ret == XR_SUCCESS) { - sess->seg_route.count = 0; - locate_fill_bindings(sess, viewState, &sess->seg_route); - locate_log_route_change(sess); + // One view set: no bindings. The frame's routing record is + // NOT touched — only a splitting locate writes it. + const struct xrt_segment_view_routing none = {0}; + locate_fill_bindings(sess, viewState, &none); } return ret; } @@ -4397,18 +4406,18 @@ oxr_session_locate_views(struct oxr_logger *log, } #endif - struct xrt_segment_view_routing *r = &sess->seg_route; - memset(r, 0, sizeof(*r)); - r->count = m.count; - r->canvas = m.canvas; + struct xrt_segment_view_routing route; + memset(&route, 0, sizeof(route)); + route.count = m.count; + route.canvas = m.canvas; for (uint32_t k = 0; k < m.count; k++) { - r->screen_id[k] = m.seg[k].screen_id; - r->rect[k] = m.seg[k].window_rect; - r->first_view[k] = first[k]; - r->view_count[k] = cnt[k]; + route.screen_id[k] = m.seg[k].screen_id; + route.rect[k] = m.seg[k].window_rect; + route.first_view[k] = first[k]; + route.view_count[k] = cnt[k]; } - locate_fill_bindings(sess, viewState, r); - locate_log_route_change(sess); + oxr_segment_views_route_record(&sess->seg_route, &route); + locate_fill_bindings(sess, viewState, &route); return oxr_session_success_result(sess); } diff --git a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c index 7337882f0..724945626 100644 --- a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c +++ b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c @@ -3144,7 +3144,9 @@ oxr_session_frame_end(struct oxr_logger *log, struct oxr_session *sess, const Xr // segment — what the app's last xrLocateViews handed out. The compositor // routes each segment's tiles to that segment's display processor. if (sess->is_vk_native_compositor && sess->xcn != NULL) { - comp_vk_native_compositor_set_view_routing(&sess->xcn->base, &sess->seg_route); + struct xrt_segment_view_routing route; + oxr_session_take_segment_view_routing(sess, &route); + comp_vk_native_compositor_set_view_routing(&sess->xcn->base, &route); } #endif diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 87adbd9cd..49d977b12 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -336,3 +336,28 @@ TEST_CASE("Quad mode on two segments: every view gets a real frustum", "[oxr][se } } } + +TEST_CASE("the frame's routing survives a later non-splitting locate", "[oxr][segment_views]") +{ + xrt_segment_view_routing frame; + std::memset(&frame, 0, sizeof(frame)); + + // The projection locate split the views... + xrt_segment_view_routing split; + std::memset(&split, 0, sizeof(split)); + split.count = 2; + split.first_view[1] = 2; + split.view_count[0] = split.view_count[1] = 2; + oxr_segment_views_route_record(&frame, &split); + + // ...then a zone-scoped / camera-rig locate keeps one view set: it does + // not record anything, so xrEndFrame still routes per segment. + xrt_segment_view_routing out; + oxr_segment_views_route_take(&frame, &out); + CHECK(out.count == 2); + CHECK(out.first_view[1] == 2); + + // The record is per frame: the next frame starts empty. + oxr_segment_views_route_take(&frame, &out); + CHECK(out.count == 0); +} From b29c5ebfb07675995add40f112a7888c6f51216a Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:53:29 -0700 Subject: [PATCH 13/21] fix(vk_native): draw quads and equirects per segment with that segment's camera (multi-screen M3 review 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under per-segment views a mosaic tile holds every segment's view j, but a quad or equirect2 was drawn once per tile with projection view j's camera — segment 0's frustum across the whole tile, so a world-locked quad (a HUD) landed mis-scaled and mis-placed on every segment. Routed frames now draw each such layer once per routed view (first[k]+j) with that view's camera, viewport and scissor confined to the segment's rect in tile j; visibility and the painter's tile state use the local view index. Co-Authored-By: Claude Fable 5.1 --- docs/architecture/comp-segments.md | 4 +- .../vk_native/comp_vk_native_renderer.c | 43 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index 4eaa5bcd1..e7de6ca2c 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -190,7 +190,9 @@ zone-scoped locates, a window covering more than `XRT_MAX_SEGMENTS` (2) screens. frame whose routing no longer matches (the mode changed between the locate and the commit) is drawn unrouted for that frame. Under per-segment views, canvas that lies on no screen is not covered by any view set and stays black (M2 painted it from the shared -set). Quads and equirect layers are drawn with the first segment's camera in each tile. +set). Quads and equirect2 layers are drawn once per segment in each tile, with that +segment's view camera, viewport and scissor confined to the segment's rect (the compose +pass; the blit fallback draws no quads at all). **IPC.** In-process only: the service never segments, so a service session always sees an empty table, one view set and no bindings (its own segmentation is M6). diff --git a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c index ea67dfab6..5375b9074 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c @@ -352,14 +352,18 @@ compose_layer_source(const struct comp_layer *layer, /*! * Multi-screen M3: does this layer's view set go through the per-segment - * routing (see comp_vk_native_eff_layout::route)? Only projection-class - * layers carry one view set per segment. + * routing (see comp_vk_native_eff_layout::route)? Projection-class layers + * carry one view set per segment; quads and equirect2 layers are drawn once per + * segment too, each with THAT segment's view camera, confined to its rect — + * one camera across the whole mosaic tile would mis-place them on every + * segment but the first. Zones are never routed. */ static bool layer_is_routed(const struct comp_layer *layer, const struct comp_vk_native_eff_layout *layout) { - return layout->route_count > 0 && - (layer->data.type == XRT_LAYER_PROJECTION || layer->data.type == XRT_LAYER_PROJECTION_DEPTH); + const enum xrt_layer_type t = layer->data.type; + return layout->route_count > 0 && (t == XRT_LAYER_PROJECTION || t == XRT_LAYER_PROJECTION_DEPTH || + t == XRT_LAYER_QUAD || t == XRT_LAYER_EQUIRECT2); } //! Views a routed layer contributes: the end of the last segment's range. @@ -373,7 +377,10 @@ routed_view_count(const struct comp_layer *layer, const struct comp_vk_native_ef n = end; } } - if (n > layer->data.view_count) { + // A projection layer is bounded by what it submitted; a quad / equirect2 + // has one sub-image shown in every routed view. + const bool per_view = layer->data.type == XRT_LAYER_PROJECTION || layer->data.type == XRT_LAYER_PROJECTION_DEPTH; + if (per_view && n > layer->data.view_count) { n = layer->data.view_count; } return n > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : n; @@ -425,11 +432,11 @@ static uint32_t compose_layer_view_count(const struct comp_layer *layer, const struct comp_vk_native_eff_layout *layout) { uint32_t n; - if (layer->data.type == XRT_LAYER_QUAD || layer->data.type == XRT_LAYER_EQUIRECT2) { - n = layout->views; - } else if (layer_is_routed(layer, layout)) { - // Multi-screen M3: one view set per segment, all of them drawn. + if (layer_is_routed(layer, layout)) { + // Multi-screen M3: one draw per routed view (per segment). n = routed_view_count(layer, layout); + } else if (layer->data.type == XRT_LAYER_QUAD || layer->data.type == XRT_LAYER_EQUIRECT2) { + n = layout->views; } else { n = layer->data.view_count; if (n > layout->views) { @@ -1775,7 +1782,16 @@ draw_zones_pass(struct comp_vk_native_renderer *r, */ struct comp_layer_view_camera cams[XRT_MAX_VIEWS]; const uint32_t tile_count = layout->views > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : layout->views; - for (uint32_t v = 0; v < tile_count; v++) { + // Multi-screen M3: under routing a quad is drawn per routed view, with + // that view's camera — so resolve every routed view's camera. + uint32_t cam_count = tile_count; + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t end = layout->route[k].first_view + layout->route[k].view_count; + if (end > cam_count) { + cam_count = end > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : end; + } + } + for (uint32_t v = 0; v < cam_count; v++) { (void)comp_layer_view_camera_select(layers, v, NULL, 0.0f, 0.0f, &cams[v]); } @@ -1840,7 +1856,7 @@ draw_zones_pass(struct comp_vk_native_renderer *r, // Eye visibility, N-view aware: the parity rule // is right for 2 views and meaningless for a 2x2 // quad mode, where the halves overlap by one. - if (!is_layer_view_visible_n(&layer->data, eye, tile_count)) { + if (!is_layer_view_visible_n(&layer->data, routed ? route_tile : eye, tile_count)) { continue; } /* @@ -1889,7 +1905,8 @@ draw_zones_pass(struct comp_vk_native_renderer *r, if (is_eq2) { // Visibility, sub-rect blend mode and the packed // ray constants, all per view (#1602). - if (!compose_equirect2_view(layer, eye, tile_count, &cams[eye], &tiles[eye], + if (!compose_equirect2_view(layer, routed ? route_tile : eye, tile_count, &cams[eye], + &tiles[routed ? route_tile : eye], eq2_constants, &mode)) { continue; } @@ -1899,7 +1916,7 @@ draw_zones_pass(struct comp_vk_native_renderer *r, ? COMP_LAYER_BLEND_STRAIGHT : COMP_LAYER_BLEND_PREMULTIPLIED; } else if (is_quad) { - mode = comp_layer_subrect_blend_mode(&tiles[eye], layer->data.flags); + mode = comp_layer_subrect_blend_mode(&tiles[routed ? route_tile : eye], layer->data.flags); } else if (routed) { if (!route_mode_set[route_tile]) { route_mode[route_tile] = From b026b7318c48b2af79eb77befdbcf7738f8ce371 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:54:09 -0700 Subject: [PATCH 14/21] fix(oxr): one display-rig m2v across segments (multi-screen M3 review 4) m2v was V / h_seg per segment, so vertically stacked screens magnified each half ~2x and side-by-side panels of different pitch got different m2v (a gap or overlap at the seam). A rig's virtual display height now sizes the whole window: each segment takes V * h_seg / union_h (oxr_segment_views_vdh_scale), so m2v = V / union_h for every segment. Tests for both layouts. Co-Authored-By: Claude Fable 5.1 --- docs/architecture/kooima-projection.md | 5 +- .../state_trackers/oxr/oxr_segment_views.c | 2 + .../state_trackers/oxr/oxr_segment_views.h | 21 ++++++++ src/xrt/state_trackers/oxr/oxr_session.c | 9 ++++ tests/tests_oxr_segment_views.cpp | 53 +++++++++++++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/architecture/kooima-projection.md b/docs/architecture/kooima-projection.md index da335e343..81d25f52b 100644 --- a/docs/architecture/kooima-projection.md +++ b/docs/architecture/kooima-projection.md @@ -358,7 +358,10 @@ sets are expressed in one frame — the display space of the display holding mos window — with the other segment placed beside it the way the window's pixels continue across the seam, so a display rig still centres the whole window on its pose and each segment's virtual canvas is offset from it by `m2v ×` that segment's offset from the -window centre. Geometry: `src/xrt/state_trackers/oxr/oxr_segment_views.{h,c}`; contract: +window centre. The rig's `virtualDisplayHeight` sizes the WHOLE window: each segment +gets it scaled by its share of the window's height (`h_seg / union_h`), so `m2v` is one +value for every segment — stacked screens are not magnified per half, and panels of +different pitch meet at the seam. Geometry: `src/xrt/state_trackers/oxr/oxr_segment_views.{h,c}`; contract: `docs/reference/view-configuration-model.md` § *Per-segment views*. ### Where the adjustment lives diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.c b/src/xrt/state_trackers/oxr/oxr_segment_views.c index eac32d8f2..b76d01821 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.c +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.c @@ -135,6 +135,8 @@ oxr_segment_views_layout(const struct xrt_segment_metrics *m, struct oxr_segment out->majority = mj; out->window_ref_cx = (x0 + x1) * 0.5f; out->window_ref_cy = (y0 + y1) * 0.5f; + out->window_ref_w = x1 - x0; + out->window_ref_h = y1 - y0; return true; } diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 15514fdd5..485e3ef31 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -120,6 +120,10 @@ struct oxr_segment_layout //! whole window's centre sits, so a window-centred rig (XR_DXR_view_rig's //! display rig) can be offset per segment. float window_ref_cx, window_ref_cy; + //! Size of that union, reference metres: the whole window's extent. A + //! display rig maps the WINDOW's height to its virtual display height, so + //! each segment gets that height scaled by its share (one m2v for all). + float window_ref_w, window_ref_h; }; /*! @@ -160,6 +164,23 @@ oxr_segment_views_window_metrics(const struct xrt_segment_metrics *m, void oxr_segment_views_whole_window_metrics(const struct xrt_segment_metrics *m, uint32_t i, struct xrt_window_metrics *out); +/*! + * The factor a window-centred display rig's virtual display height takes for + * segment @p i: its height over the whole window's (`h_seg / union_h`), so + * `m2v = vH * factor / h_seg = vH / union_h` is the SAME for every segment — + * stacked screens are not magnified 2x per half, and side-by-side panels of + * different pitch meet at the seam without a gap or an overlap. 1 when the + * union is unknown. + */ +static inline float +oxr_segment_views_vdh_scale(const struct oxr_segment_layout *l, uint32_t i) +{ + if (l == NULL || i >= l->count || l->window_ref_h <= 0.0f) { + return 1.0f; + } + return l->seg[i].h_m / l->window_ref_h; +} + /*! * The translation that carries a point in segment @p i's own display space into * the reference frame (x, y; z is shared). diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index e2686219d..a37f753be 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2502,6 +2502,9 @@ struct oxr_locate_segment //! segment's virtual canvas is offset by m2v times this. float rig_dx; float rig_dy; + //! A display rig's virtual display height factor for this canvas (its + //! share of the window's height, oxr_segment_views_vdh_scale); 0 = 1. + float vdh_scale; //! Views this call locates (the per-segment count); 0 = the session's. uint32_t view_count; //! Publish session-level side effects (#1502 VIEW offset, #441 tracking @@ -3282,6 +3285,11 @@ locate_views_one(struct oxr_logger *log, dt.parallax_factor = active_rig->parallax_factor; dt.perspective_factor = active_rig->perspective_factor; dt.virtual_display_height = active_rig->virtual_display_height; + // Multi-screen M3: the rig sizes the WINDOW; this + // canvas is its share of it (one m2v for all segments). + if (seg != NULL && seg->vdh_scale > 0.0f) { + dt.virtual_display_height *= seg->vdh_scale; + } } else { dt.virtual_display_height = screen_height_m; // identity m2v } @@ -4166,6 +4174,7 @@ locate_segment_override(const struct xrt_segment_metrics *m, o->rig_dx = l->seg[i].ref_cx - l->window_ref_cx; o->rig_dy = l->seg[i].ref_cy - l->window_ref_cy; + o->vdh_scale = oxr_segment_views_vdh_scale(l, i); } static void diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 49d977b12..19b8ff5be 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -361,3 +361,56 @@ TEST_CASE("the frame's routing survives a later non-splitting locate", "[oxr][se oxr_segment_views_route_take(&frame, &out); CHECK(out.count == 0); } + +TEST_CASE("a display rig gets ONE m2v across segments", "[oxr][segment_views]") +{ + const float V = 0.24f; // XrDisplayRigDXR::virtualDisplayHeight + + // Side by side, different pitch (the two-screen table): m2v_k = V * + // scale_k / h_k must be equal, so the seam has no gap and no overlap. + { + const xrt_segment_metrics m = two_screen_table(); + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + const float m2v0 = V * oxr_segment_views_vdh_scale(&l, 0) / l.seg[0].h_m; + const float m2v1 = V * oxr_segment_views_vdh_scale(&l, 1) / l.seg[1].h_m; + CHECK(m2v0 == Catch::Approx(m2v1)); + CHECK(m2v0 == Catch::Approx(V / l.window_ref_h)); + // Seam continuity in VIRTUAL units with that shared m2v. + const float a_right = m2v0 * (l.seg[0].ref_cx + l.seg[0].w_m * 0.5f - l.window_ref_cx); + const float b_left = m2v1 * (l.seg[1].ref_cx - l.seg[1].w_m * 0.5f - l.window_ref_cx); + CHECK(a_right == Catch::Approx(b_left).margin(1e-6)); + } + + // Stacked vertically, same panel: each half is half the window, not a + // full virtual display (which magnified each half 2x). + { + xrt_segment_metrics m; + std::memset(&m, 0, sizeof(m)); + m.count = 2; + m.window_screen_left = 100; + m.window_screen_top = 980; + m.window_pixel_width = 800; + m.window_pixel_height = 200; + for (uint32_t i = 0; i < 2; i++) { + xrt_segment_metric &s = m.seg[i]; + s.screen_id = 1 + i; + s.window_rect = {{0, (int)(i * 100)}, {800, 100}}; + s.screen_rect = {{100, i == 0 ? 980 : 0}, {800, 100}}; + s.screen_desktop_left = 0; + s.screen_desktop_top = (int32_t)(i * 1080); + s.screen_desktop_width = 1920; + s.screen_desktop_height = 1080; + s.screen_width_m = 0.344f; + s.screen_height_m = 0.1935f; + s.nominal_viewer_z_m = 0.6f; + } + m.seg[0].is_primary = true; + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + CHECK(oxr_segment_views_vdh_scale(&l, 0) == Catch::Approx(0.5f)); + CHECK(oxr_segment_views_vdh_scale(&l, 1) == Catch::Approx(0.5f)); + // Bottom of the top segment meets the top of the bottom one. + CHECK(l.seg[0].ref_cy - l.seg[0].h_m * 0.5f == Catch::Approx(l.seg[1].ref_cy + l.seg[1].h_m * 0.5f)); + } +} From 7d7bf6718c45823425710c7d6fcc840fe441e156 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:55:36 -0700 Subject: [PATCH 15/21] fix(oxr): VIEW is the centroid of the ACTIVE views, over every segment (multi-screen M3 review 5+7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #1502 VIEW-space offset averaged the whole reported array, so the longer aliased multiview tail dragged VIEW toward view 0 even when nothing was split; during a split it was measured on the majority segment's temporary array only. Now it is the centroid of the active views — (L+R)/2 unsplit, and over every segment's active views when split (published once by the per-segment wrapper, and for a stereo session framed from a non-primary majority screen). The centroid test checks the active views. view-configuration-model.md documents that DP-backed screens include sim's FALLBACK claim on plain monitors (intended on multi-monitor desktop Linux) and the VIEW rule. Co-Authored-By: Claude Fable 5.1 --- docs/reference/view-configuration-model.md | 17 ++- src/xrt/state_trackers/oxr/oxr_session.c | 115 ++++++++++++++++----- tests/tests_oxr_view_space.cpp | 28 +++-- 3 files changed, 123 insertions(+), 37 deletions(-) diff --git a/docs/reference/view-configuration-model.md b/docs/reference/view-configuration-model.md index 9ee5ebf18..890b8de4c 100644 --- a/docs/reference/view-configuration-model.md +++ b/docs/reference/view-configuration-model.md @@ -541,7 +541,22 @@ window — every platform but desktop Linux, and any service session. So a singl box, Windows, macOS and Android advertise exactly the pre-M3 count (existing multiview apps and the CTS see no change); `ds1-linux` (eDP + DS1) advertises `device max × 2`. `XRT_MAX_SEGMENTS` = 2 lives in `xrt_display_metrics.h`. The locate never hands out more -view sets than the capacity. The rules: +view sets than the capacity. + +**Intended on multi-monitor desktop Linux:** "DP-backed" counts every registry screen +with a VK DP factory, **including sim-display's FALLBACK claim** on an ordinary monitor — +a plain monitor next to the 3D panel is a legitimate segment host (it gets its own views +and a flat or anaglyph weave). So a DS1 plus a plain HDMI monitor advertises `device max +× 2` even if the window never leaves the panel. That costs nothing at render time (the +tail is aliased, ADR-041) and apps already size swapchains for the worst case (ADR-010). +`DXR_SEGMENTS=0` turns it off with segmentation. + +**VIEW space (#1502):** VIEW is the centroid of the **active** views only — never the +aliased tail, which would drag it toward view 0 now that the tail can be longer than the +active set. Unsplit, that is `(L+R)/2` exactly as before; split, it is the centroid of +every segment's active views. + +The rules: - **One segment (the common case):** byte-for-byte the pre-M3 locate — the active mode's views first, the whole tail (now longer) aliased onto view 0. The swapchain diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index a37f753be..ed2dba6f4 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2484,6 +2484,49 @@ oxr_session_fill_cursor_depth(struct oxr_session *sess, * there too, so `eye - canvas centre` is the eye relative to the segment as it * sits on its own screen (that screen's Kooima). */ +//! What a per-segment locate reports back to the wrapper (#1502). +struct oxr_locate_segment_out +{ + //! Every segment's locate could measure the VIEW centroid (AND). + bool measurable; + //! VIEW's pose in the locate's base space (the same for every segment). + struct xrt_space_relation T_base_head; +}; + +/*! + * #1502: publish the VIEW-space offset that makes `xrLocateSpace(VIEW, base)` + * equal the centroid of @p views[0, count) — the ACTIVE views. + */ +static void +locate_publish_view_offset(struct oxr_session *sess, + const XrView *views, + uint32_t count, + const struct xrt_space_relation *T_base_head) +{ + if (count == 0) { + return; + } + struct xrt_vec3 centroid = {0.0f, 0.0f, 0.0f}; + for (uint32_t i = 0; i < count; i++) { + centroid.x += views[i].pose.position.x; + centroid.y += views[i].pose.position.y; + centroid.z += views[i].pose.position.z; + } + const float inv = 1.0f / (float)count; + centroid.x *= inv; + centroid.y *= inv; + centroid.z *= inv; + + struct xrt_vec3 delta = {centroid.x - T_base_head->pose.position.x, centroid.y - T_base_head->pose.position.y, + centroid.z - T_base_head->pose.position.z}; + struct xrt_quat inv_head; + math_quat_invert(&T_base_head->pose.orientation, &inv_head); + + struct xrt_pose view_offset = XRT_POSE_IDENTITY; + math_quat_rotate_vec3(&inv_head, &delta, &view_offset.position); + oxr_session_set_view_space_offset(sess, &view_offset); +} + struct oxr_locate_segment { //! The canvas: this segment (or, for a stereo session, the whole window). @@ -2507,9 +2550,12 @@ struct oxr_locate_segment float vdh_scale; //! Views this call locates (the per-segment count); 0 = the session's. uint32_t view_count; - //! Publish session-level side effects (#1502 VIEW offset, #441 tracking - //! edge): only the majority segment's call does. + //! Publish session-level side effects (#441 tracking edge): only the + //! majority segment's call does. bool side_effects; + //! Filled by each call for the #1502 VIEW offset, which the wrapper + //! publishes once over ALL segments' active views. NULL = not wanted. + struct oxr_locate_segment_out *out; }; static XrResult @@ -3947,10 +3993,14 @@ locate_views_one(struct oxr_logger *log, * Amendment 2), so the difference goes into a VIEW-space offset exactly as * ADR-024 Amendment 1 foresaw, and xrLocateSpace applies it on both legs. * - * MEASURED, not re-derived: the centroid is taken from the poses just - * written into views[] (including the inactive-view duplication above, - * which is what an app - and the CTS - averages), and VIEW's own pose in - * the same base is T_base_head. So the offset is whatever makes + * MEASURED, not re-derived: the centroid is taken from the ACTIVE poses + * just written into views[] — not the inactive tail, which only aliases + * view 0 and would drag the centroid toward it now that a multiview + * session can report more views than it renders (multi-screen M3) — and + * VIEW's own pose in the same base is T_base_head. With one segment this + * is (L+R)/2 exactly as before; with several, the per-segment wrapper + * publishes it over every segment's active views + * (locate_publish_view_offset). So the offset is whatever makes * `xrLocateSpace(VIEW, base) == centroid` true, for free, in every base. * It is base-independent by the #1370 invariant, which is why publishing it * from a locate in ANY base is sound - including a locate in VIEW itself, @@ -3968,30 +4018,21 @@ locate_views_one(struct oxr_logger *log, { const enum xrt_space_relation_flags need = XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_ORIENTATION_VALID_BIT; - const bool measurable = (seg == NULL || seg->side_effects) && !rig_active && active_view_count > 0 && - reported_view_count > 0 && base_space_view_count >= active_view_count && + const bool measurable = !rig_active && active_view_count > 0 && reported_view_count > 0 && + base_space_view_count >= active_view_count && (T_base_head.relation_flags & need) == need; - if (measurable) { - struct xrt_vec3 centroid = {0.0f, 0.0f, 0.0f}; - for (uint32_t i = 0; i < reported_view_count; i++) { - centroid.x += views[i].pose.position.x; - centroid.y += views[i].pose.position.y; - centroid.z += views[i].pose.position.z; + if (seg != NULL) { + // One segment's locate: report, the wrapper publishes over + // every segment's active views. + if (seg->out != NULL) { + seg->out->measurable = seg->out->measurable && measurable; + seg->out->T_base_head = T_base_head; } - const float inv = 1.0f / (float)reported_view_count; - centroid.x *= inv; - centroid.y *= inv; - centroid.z *= inv; - - struct xrt_vec3 delta = {centroid.x - T_base_head.pose.position.x, - centroid.y - T_base_head.pose.position.y, - centroid.z - T_base_head.pose.position.z}; - struct xrt_quat inv_head; - math_quat_invert(&T_base_head.pose.orientation, &inv_head); - - struct xrt_pose view_offset = XRT_POSE_IDENTITY; - math_quat_rotate_vec3(&inv_head, &delta, &view_offset.position); - oxr_session_set_view_space_offset(sess, &view_offset); + } else if (measurable) { + locate_publish_view_offset(sess, views, + active_view_count < reported_view_count ? active_view_count + : reported_view_count, + &T_base_head); } } @@ -4294,8 +4335,19 @@ oxr_session_locate_views(struct oxr_logger *log, o.have_eyes = true; } o.side_effects = true; + struct oxr_locate_segment_out so = {.measurable = true}; + o.out = &so; ret = locate_views_one(log, sess, viewLocateInfo, viewState, viewCapacityInput, viewCountOutput, views, o.wm.valid ? &o : NULL); + if (ret == XR_SUCCESS && viewCapacityInput > 0 && o.wm.valid && so.measurable) { + // #1502 over the ACTIVE views of this one set. + uint32_t n = sess->view_config_view_count != 0 ? sess->view_config_view_count : 2; + if (xdev->hmd->active_rendering_mode_index < xdev->rendering_mode_count && + xdev->rendering_modes[xdev->hmd->active_rendering_mode_index].view_count < n) { + n = xdev->rendering_modes[xdev->hmd->active_rendering_mode_index].view_count; + } + locate_publish_view_offset(sess, views, n, &so.T_base_head); + } } if (viewCapacityInput > 0 && ret == XR_SUCCESS) { // One view set: no bindings. The frame's routing record is @@ -4367,6 +4419,7 @@ oxr_session_locate_views(struct oxr_logger *log, // side effects; the others locate into a bare XrViewState. XrViewStateFlags flags = 0; bool first_flags = true; + struct oxr_locate_segment_out seg_out = {.measurable = true}; for (uint32_t pass = 0; pass < 2; pass++) { for (uint32_t k = 0; k < m.count; k++) { if ((k == mj) != (pass == 1)) { @@ -4376,6 +4429,7 @@ oxr_session_locate_views(struct oxr_logger *log, locate_segment_override(&m, &l, k, &o); o.view_count = cnt[k]; o.side_effects = (k == mj); + o.out = &seg_out; XrView tmp[XRT_MAX_VIEWS]; for (uint32_t v = 0; v < XRT_MAX_VIEWS; v++) { @@ -4398,6 +4452,11 @@ oxr_session_locate_views(struct oxr_logger *log, } viewState->viewStateFlags = flags; + // #1502: VIEW is the centroid of every segment's ACTIVE views. + if (seg_out.measurable) { + locate_publish_view_offset(sess, views, total, &seg_out.T_base_head); + } + // The inactive tail aliases view 0, exactly as the single path does (ADR-041). for (uint32_t i = total; i < reported; i++) { const uint32_t src = oxr_segment_views_alias_source(i, total); diff --git a/tests/tests_oxr_view_space.cpp b/tests/tests_oxr_view_space.cpp index d5f65c434..5c15f668e 100644 --- a/tests/tests_oxr_view_space.cpp +++ b/tests/tests_oxr_view_space.cpp @@ -269,6 +269,8 @@ struct Runtime XrSpace view = XR_NULL_HANDLE; bool have_view_rig = false; bool have_display_info = false; + //! XrViewActivityStateDXR::activeViewCount of the last views() call (0 = not reported). + uint32_t last_active = 0; //! #1486: the view configuration this session was begun with. Every locate //! below names it, because a session locates in exactly one configuration. @@ -335,6 +337,11 @@ struct Runtime *raw = {XR_TYPE_VIEW_DISPLAY_RAW_DXR}; vs.next = raw; } + XrViewActivityStateDXR act = {XR_TYPE_VIEW_ACTIVITY_STATE_DXR}; + if (have_display_info) { + act.next = const_cast(vs.next); + vs.next = &act; + } uint32_t count = 0; REQUIRE(XR_SUCCEEDED(pfnLocateViews(session, &li, &vs, 0, &count, nullptr))); @@ -343,6 +350,7 @@ struct Runtime REQUIRE(XR_SUCCEEDED(pfnLocateViews(session, &li, &vs, count, &count, out.data()))); const XrViewStateFlags need = XR_VIEW_STATE_POSITION_VALID_BIT | XR_VIEW_STATE_ORIENTATION_VALID_BIT; REQUIRE((vs.viewStateFlags & need) == need); + last_active = have_display_info ? act.activeViewCount : 0; return out; } @@ -916,13 +924,14 @@ TEST_CASE("VIEW is the centroid of the located views (#1502)", "[oxr][view_space } /* - * #1502 + #1486: the centroid is over the REPORTED array, not the eye set. + * #1502 + #1486 + multi-screen M3: the centroid is over the ACTIVE views. * - * Under PRIMARY_MULTIVIEW_DXR the session reports the device max (4 on - * sim_display) while the active rendering mode may be narrower; xrLocateViews - * fills the surplus slots by duplicating view 0. An app - and the CTS, which - * loops over every enumerated view configuration - averages what it was handed, - * so VIEW must be the mean of THAT array, duplicates included. + * Under PRIMARY_MULTIVIEW_DXR the session reports the device max (times the + * view-set capacity, M3) while the active rendering mode may be narrower; + * xrLocateViews fills the surplus slots by duplicating view 0. Those aliases + * carry no content and would drag the centroid toward view 0 (more so now that + * the tail can be 3x the active set), so VIEW is the mean of the ACTIVE views + * — (L+R)/2 for a stereo mode, exactly as a PRIMARY_STEREO session gets. * * This arm only has teeth where the device max exceeds the active mode's view * count; on a box whose sim-display tops out at 2 (Quad not enabled) it is the @@ -961,8 +970,11 @@ TEST_CASE("VIEW tracks the reported view array under MULTIVIEW (#1502)", "[oxr][ } const XrPosef T_local_view = rt.locate(rt.view, rt.local, t); - const XrVector3f c = rt.centroid(v); - INFO("VIEW in LOCAL: " << pstr(T_local_view)); + REQUIRE(rt.last_active > 0); + REQUIRE(rt.last_active <= v.size()); + const std::vector active(v.begin(), v.begin() + rt.last_active); + const XrVector3f c = rt.centroid(active); + INFO("VIEW in LOCAL: " << pstr(T_local_view) << ", active views " << rt.last_active); INFO("centroid = (" << c.x << "," << c.y << "," << c.z << ")"); CHECK(vdist(c, T_local_view.position) < kPosTolM); From f569321facfc36504befc04cb9e550206671461a Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:55:46 -0700 Subject: [PATCH 16/21] docs(display_info): XrViewDisplayBindingsDXR count is 0 for one view set (multi-screen M3 review 6) The header said bindingCountOutput is 1 for a window on one display; the spec and the runtime report 0 whenever the views are one set for the whole window. Fixed the header text (it ships to the public mirror). Co-Authored-By: Claude Fable 5.1 --- .../openxr/XR_DXR_display_info.h | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/external/openxr_includes/openxr/XR_DXR_display_info.h b/src/external/openxr_includes/openxr/XR_DXR_display_info.h index 85f1e691e..ac691a957 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_display_info.h +++ b/src/external/openxr_includes/openxr/XR_DXR_display_info.h @@ -864,13 +864,17 @@ typedef struct XrViewDisplayBindingDXR { * is the sum. The inactive tail aliases the last active view, exactly as in * the single-display case. * - * bindingCountOutput is 1 for a window on one display (the whole canvas, - * views [0, activeViewCount)) and 0 when the runtime cannot attribute views to - * a display (no window yet, an IPC/service session, a session that began - * XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO: a stereo session keeps its two - * views on the display holding most of the window, and the other display shows - * a flat 2D copy). An app that never chains this struct still renders - * correctly: render every active view, each into its subImage. + * bindingCountOutput is the number of per-display view sets of this locate — + * one per display segment while the views are located per display (2 for a + * window spanning two displays; 1 for a window lying wholly on a display other + * than the system default) — and 0 whenever the views are ONE set for the + * whole window: a window on the system-default display only, no window yet, an + * IPC/service session, a camera-rig or zone-scoped locate, or a session that + * began XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO (a stereo session keeps its + * two views, framed from the display holding most of the window, and each other + * display shows its crop of them or a flat 2D copy). An app that never chains + * this struct still renders correctly: render every active view, each into its + * subImage. * * View poses of every display are expressed in ONE frame: the display holding * most of the window. Each other display's segment is placed beside it the way From ceee825103192bb56c9a4c00a19cc2a682b4497f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:56:23 -0700 Subject: [PATCH 17/21] test(oxr_view_space): exact multiview view count; DXR_SEGMENTS=0 keeps the pre-M3 count (multi-screen M3 review 8) is_multiview_count accepted the device max OR x2 on every platform. The test binary now runs with DXR_SEGMENTS=0 and asserts exactly the device max everywhere; the segmentation kill switch now also keeps PRIMARY_MULTIVIEW_DXR at the pre-M3 count (no window can be split). Deriving the Linux x2 from the box is not exact (a screen counts only when its plug-in's VK factory passed the vk_bundle ABI check, invisible to xrEnumerateDisplaysDXR); the x2 arithmetic stays pinned by tests_oxr_segment_views. Co-Authored-By: Claude Fable 5.1 --- docs/architecture/comp-segments.md | 2 +- src/xrt/state_trackers/oxr/oxr_system.c | 6 +++++- tests/tests_oxr_view_space.cpp | 23 +++++++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index e7de6ca2c..f15113958 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -149,7 +149,7 @@ or destroy is one WARN. refused at load (vk_bundle ABI, #1243) gets no segment DP: that segment is flat 2D. - Not segmented: zero-copy frames, a self-submitting DP or one without a render pass, a session pinned with `XrSessionDisplayBindingDXR`, the shared-texture path. -- `DXR_SEGMENTS=0` turns it off. +- `DXR_SEGMENTS=0` turns it off (and keeps `PRIMARY_MULTIVIEW_DXR` at the pre-M3 view count). - Capture: with the window split, the post-compose atlas capture also writes each segment's DP input as `.seg.png`. diff --git a/src/xrt/state_trackers/oxr/oxr_system.c b/src/xrt/state_trackers/oxr/oxr_system.c index 167442848..702448079 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -179,7 +179,11 @@ oxr_system_fill_in( uint32_t with_factory = 0; bool can_segment = false; #ifdef XRT_OS_LINUX_DESKTOP - if (sys->xsysc != NULL && !sys->xsysc->info.is_service_mode) { + // DXR_SEGMENTS=0 (the segmentation kill switch) also keeps the + // pre-M3 count: no window is ever split. + const char *segs_env = getenv("DXR_SEGMENTS"); + const bool segments_off = segs_env != NULL && segs_env[0] == '0'; + if (sys->xsysc != NULL && !sys->xsysc->info.is_service_mode && !segments_off) { can_segment = true; const struct xrt_dp_factory_registry *reg = &sys->xsysc->info.dp_registry; for (uint32_t e = 0; e < reg->entry_count && e < XRT_DP_REGISTRY_MAX_ENTRIES; e++) { diff --git a/tests/tests_oxr_view_space.cpp b/tests/tests_oxr_view_space.cpp index 5c15f668e..f6b56c8aa 100644 --- a/tests/tests_oxr_view_space.cpp +++ b/tests/tests_oxr_view_space.cpp @@ -99,15 +99,30 @@ constexpr float kPosTolM = 0.001f; // 1 mm * Multi-screen M3: PRIMARY_MULTIVIEW_DXR reports one view set per window * segment the system can ever weave — the device max on a box that cannot split * a window (one DP-backed screen, a service session, non-Linux), the device max - * x 2 (capped at XRT_MAX_VIEWS = 8) on desktop Linux with two DP-backed screens. - * Which one this test box is depends on its monitors, so both are accepted. + * x 2 on desktop Linux with two DP-backed screens. + * + * EXACT everywhere: this binary runs with DXR_SEGMENTS=0 (set below, before any + * instance exists), which keeps the pre-M3 count on every platform. Deriving + * the Linux x2 from the box instead is not exact — a screen counts only when its + * plug-in's VK factory passed the vk_bundle ABI check (#1243), which an + * installed plug-in next to a dev runtime does not, and xrEnumerateDisplaysDXR + * cannot see that. The x2 arithmetic itself is pinned by tests_oxr_segment_views. */ bool is_multiview_count(size_t n, uint32_t device_max) { - const uint32_t two = device_max * 2 > 8 ? 8 : device_max * 2; - return n == device_max || n == two; + return n == device_max; } + +[[maybe_unused]] const bool kSegmentsOff = [] { + // Before main(): the runtime reads it at every system fill-in. +#ifdef _WIN32 + _putenv_s("DXR_SEGMENTS", "0"); +#else + setenv("DXR_SEGMENTS", "0", 1); +#endif + return true; +}(); constexpr float kAngTolDeg = 0.1f; // 0.1 degree /*! From 93894744cbae4a91d6955639f9fc139eb20911a2 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:56:45 -0700 Subject: [PATCH 18/21] fix(vk_native): keep per-segment routing across a 2D/3D toggle (multi-screen M3 review, minor) When the mode changed between the locate and the commit, a segment's view count no longer equalled the tile count and the whole frame went unrouted (segment 0's views stretched over the window). Route min(located, tiles) views per segment instead. Co-Authored-By: Claude Fable 5.1 --- .../compositor/vk_native/comp_vk_native_compositor.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c index be4a054de..7e34a5984 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -5410,7 +5410,13 @@ vk_route_effective_layout(struct comp_vk_native_compositor *c, uint32_t layer_vi (uint32_t)(r.canvas.extent.h > 0 ? r.canvas.extent.h : 0), }; for (uint32_t k = 0; k < r.count; k++) { - if (r.view_count[k] != L->views || r.first_view[k] + r.view_count[k] > layer_view_count) { + // The route is what the locate handed out. Across a 2D<->3D toggle + // the mode (tile count) can change between that locate and this + // commit: route the views the tiles can hold (min of the two) rather + // than dropping the routing for the frame — unrouted, segment 0's + // views would stretch over the whole window. + const uint32_t n = r.view_count[k] < L->views ? r.view_count[k] : L->views; + if (n == 0 || r.first_view[k] + n > layer_view_count) { return; } const struct comp_seg_rect seg = { @@ -5424,7 +5430,7 @@ vk_route_effective_layout(struct comp_vk_native_compositor *c, uint32_t layer_vi return; } L->route[k].first_view = r.first_view[k]; - L->route[k].view_count = r.view_count[k]; + L->route[k].view_count = n; L->route[k].x = tr.x; L->route[k].y = tr.y; L->route[k].w = tr.w; From 34be251a39ea1ee7c3de8fd4fbf3578c3e4d083d Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:56:45 -0700 Subject: [PATCH 19/21] fix(plugin_loader): DXR_SCREEN_PLUGIN plug-in ids match case-insensitively (multi-screen M3 review, minor) Like the monitor names a pin is matched against; PreferredPlugin keeps its exact match. Test added. Co-Authored-By: Claude Fable 5.1 --- src/xrt/targets/common/target_screen_pin.c | 13 +++++++++---- tests/tests_target_screen_pin.cpp | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/xrt/targets/common/target_screen_pin.c b/src/xrt/targets/common/target_screen_pin.c index 0a085af48..0c11dc69b 100644 --- a/src/xrt/targets/common/target_screen_pin.c +++ b/src/xrt/targets/common/target_screen_pin.c @@ -142,13 +142,16 @@ target_screen_pin_find(const struct target_screen_pins *pins, } static int -first_with_id(const struct target_screen_candidate *c, uint32_t n, const char *id) +first_with_id(const struct target_screen_candidate *c, uint32_t n, const char *id, bool ignore_case) { if (id == NULL || id[0] == '\0') { return -1; } for (uint32_t i = 0; i < n; i++) { - if (c[i].plugin_id != NULL && strcmp(c[i].plugin_id, id) == 0) { + if (c[i].plugin_id == NULL) { + continue; + } + if (ignore_case ? name_eq(c[i].plugin_id, id) : strcmp(c[i].plugin_id, id) == 0) { return (int)i; } } @@ -169,7 +172,9 @@ target_screen_pick(const struct target_screen_candidate *cands, if (cands != NULL && count > 0) { if (pin_plugin != NULL && pin_plugin[0] != '\0') { - pick = first_with_id(cands, count, pin_plugin); + // A pin is typed by hand: its plug-in id matches + // case-insensitively, like its monitor name. + pick = first_with_id(cands, count, pin_plugin, true); if (pick >= 0) { reason = TARGET_SCREEN_PICK_PIN; } else { @@ -177,7 +182,7 @@ target_screen_pick(const struct target_screen_candidate *cands, } } if (pick < 0) { - pick = first_with_id(cands, count, preferred); + pick = first_with_id(cands, count, preferred, false); if (pick >= 0) { reason = TARGET_SCREEN_PICK_PREFERRED; } diff --git a/tests/tests_target_screen_pin.cpp b/tests/tests_target_screen_pin.cpp index 3db2fe629..be4db3cd9 100644 --- a/tests/tests_target_screen_pin.cpp +++ b/tests/tests_target_screen_pin.cpp @@ -94,6 +94,10 @@ TEST_CASE("winner rule: pin > PreferredPlugin > active > confidence", "[target][ CHECK(why == TARGET_SCREEN_PICK_PIN); CHECK_FALSE(unclaimed); + // The pin's plug-in id is matched case-insensitively (typed by hand). + CHECK(target_screen_pick(hdmi, 2, "LEIA-SR", "sim-display", &why, &unclaimed) == 1); + CHECK(why == TARGET_SCREEN_PICK_PIN); + // ...and the active one. CHECK(target_screen_pick(hdmi, 2, "leia-sr", nullptr, &why, &unclaimed) == 1); CHECK(why == TARGET_SCREEN_PICK_PIN); From 17dc757fa2b6045ebc18f82317e896fee0e39765 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 14:16:04 -0700 Subject: [PATCH 20/21] fix(sim_display,oxr): a segment's views come from its OWN eye pair in mixed-vendor layouts (multi-screen M4 eyeball) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mode A on ds1-linux (Leia primary, sim on eDP): the eDP half looked flat 2D. The sim segment DP reported eyes from the process-wide sim view count, which only the sim head device sets -- absent when leia-sr is active -- so it stayed 1: one eye, duplicated into both views by the #615 coherence guard, an anaglyph of two identical images. - sim_display: a screen-bound (segment) DP reports one eye per view of the atlas it last wove (2 before the first); the session DP is unchanged. - oxr: a segment uses its DP's eyes only when they cover its views (oxr_segment_views_segment_eyes); an under-reporting DP falls back to that screen's nominal viewer, which fills every view. - Logging: every segment of one locate now logs on the same throttled frame ('Window-relative Kooima [segment 0x…]', 'Segment 0x… eyes from its DP' / '…nominal viewer'); before, the per-call counter only ever let the majority segment log, which hid the other segment's (correct) pitch. - The segment mode log says when a DP has no 2D/3D switch instead of '-> 0'. Test: two screens of different pitch, segment 1 uses its own pitch and its own eye pair; an under-reporting DP is not used. Co-Authored-By: Claude Fable 5.1 --- .../vk_native/comp_vk_native_segments.c | 4 +- .../sim_display/sim_display_processor.c | 15 ++++- .../state_trackers/oxr/oxr_segment_views.h | 30 +++++++++ src/xrt/state_trackers/oxr/oxr_session.c | 61 +++++++++++++------ tests/tests_oxr_segment_views.cpp | 49 +++++++++++++++ 5 files changed, 136 insertions(+), 23 deletions(-) diff --git a/src/xrt/compositor/vk_native/comp_vk_native_segments.c b/src/xrt/compositor/vk_native/comp_vk_native_segments.c index 9e324a386..dbcfce097 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_segments.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_segments.c @@ -1121,8 +1121,8 @@ comp_vk_native_segments_set_display_mode(struct comp_vk_native_segments *segs, b st->mode_sent = want; // A lifecycle event per DP (create, or a session 2D/3D switch). const bool ok = xrt_display_processor_request_display_mode(st->dp, enable_3d); - U_LOG_I("segments: screen 0x%016llx ('%s') DP follows the session mode: %s -> %d", + U_LOG_I("segments: screen 0x%016llx ('%s') DP follows the session mode: %s%s", (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, enable_3d ? "3D" : "2D", - (int)ok); + ok ? "" : " (this DP has no 2D/3D switch — nothing to do)"); } } diff --git a/src/xrt/drivers/sim_display/sim_display_processor.c b/src/xrt/drivers/sim_display/sim_display_processor.c index e21b8ea2d..deac88a01 100644 --- a/src/xrt/drivers/sim_display/sim_display_processor.c +++ b/src/xrt/drivers/sim_display/sim_display_processor.c @@ -175,6 +175,12 @@ struct sim_display_processor * pixels outside it (@ref render_pass_preserve). */ bool screen_bound; + //! Views of the last atlas this segment DP wove (tile_columns x + //! tile_rows; 2 before the first). A segment DP's eye count follows THIS, + //! not the process-wide sim view count, which only the sim head device + //! sets — absent when another vendor's plug-in is active (mixed-vendor + //! segments), where it stays 1 and every view came from one eye. + uint32_t last_atlas_views; uint64_t screen_monitor_id; float screen_w_m, screen_h_m; uint32_t screen_px_w, screen_px_h; @@ -265,6 +271,9 @@ sim_dp_process_atlas(struct xrt_display_processor *xdp, (void)atlas_image; // sim_display uses atlas_view via shader sampling (void)target_image; // sim_display uses target_fb via render pass struct sim_display_processor *sdp = sim_display_processor(xdp); + if (sdp->screen_bound && tile_columns * tile_rows > 0) { + sdp->last_atlas_views = tile_columns * tile_rows; + } // #817: report the weave geometry against the declared panel. Diagnostic // only — it never changes what is rendered. @@ -897,7 +906,10 @@ sim_dp_get_predicted_eye_positions(struct xrt_display_processor *xdp, struct xrt { struct sim_display_processor *sdp = sim_display_processor(xdp); float half_ipd = sdp->ipd_m / 2.0f; - uint32_t vc = sim_display_get_view_count(); + // A segment DP (multi-screen) reports one eye per view of the atlas it + // weaves; the session DP keeps the process-wide sim view count. + uint32_t vc = sdp->screen_bound ? (sdp->last_atlas_views > 0 ? sdp->last_atlas_views : 2) + : sim_display_get_view_count(); if (vc == 1) { out->eyes[0] = (struct xrt_eye_position){sdp->nominal_x_m, sdp->nominal_y_m, sdp->nominal_z_m}; @@ -1236,6 +1248,7 @@ sim_dp_bind_screen(struct sim_display_processor *sdp, const struct xrt_screen_bi sim_display_get_panel_metrics(&def_w_m, &def_h_m, &def_px_w, &def_px_h); sdp->screen_bound = true; + sdp->last_atlas_views = 2; sdp->screen_monitor_id = b->monitor_id; sdp->screen_left = b->desktop_left; sdp->screen_top = b->desktop_top; diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 485e3ef31..3cd4e8ba9 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -218,6 +218,36 @@ oxr_segment_views_accept_eyes(bool have_eyes, bool valid, uint32_t count) return have_eyes && valid && count > 0; } +/*! + * Segment @p s's own DP eyes, carried into the reference frame by (@p dx, @p dy). + * + * Used when the screen's DP reported a valid set (tracked or not, like the + * single-screen path) with at least @p views_needed eyes — one per view the + * segment carries. A DP that under-reports (a sim_display DP made for a + * screen while another vendor is active once reported 1 eye: every view of + * that segment then came from the SAME eye and an anaglyph looked flat) is + * not used; the caller falls back to that screen's nominal viewer, which + * fills every view. + * + * @return true when @p out holds the segment's DP eyes. + */ +static inline bool +oxr_segment_views_segment_eyes( + const struct xrt_segment_metric *s, float dx, float dy, uint32_t views_needed, struct xrt_eye_positions *out) +{ + if (s == NULL || out == NULL || + !oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count) || + s->eyes.count < (views_needed > 0 ? views_needed : 1)) { + return false; + } + *out = s->eyes; + for (uint32_t e = 0; e < out->count && e < XRT_MAX_VIEWS; e++) { + out->eyes[e].x += dx; + out->eyes[e].y += dy; + } + return true; +} + /*! * The untracked eye set: one eye per active view (at least 2), in front of the * screen centre at @p z, translated by (@p dx, @p dy) into the caller's frame. diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index ed2dba6f4..2ababae55 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2548,6 +2548,8 @@ struct oxr_locate_segment //! A display rig's virtual display height factor for this canvas (its //! share of the window's height, oxr_segment_views_vdh_scale); 0 = 1. float vdh_scale; + //! The screen this canvas is on (logs only). + uint64_t screen_id; //! Views this call locates (the per-segment count); 0 = the session's. uint32_t view_count; //! Publish session-level side effects (#441 tracking edge): only the @@ -2673,7 +2675,15 @@ locate_views_one(struct oxr_logger *log, // Throttled logging for display/Kooima diagnostics static int log_counter = 0; - bool should_log = (++log_counter % 120) == 1; // Log every ~2 seconds at 60fps + // Log every ~2 seconds at 60fps. Multi-screen M3: one locate runs once + // per segment (the majority last), so only the majority call advances + // the counter and every segment logs on the SAME locate. + bool should_log; + if (seg != NULL && !seg->side_effects) { + should_log = ((log_counter + 1) % 120) == 1; + } else { + should_log = (++log_counter % 120) == 1; + } // The display-plane / camera pose the view math composes the eyes on, in // the head device's TRACKING-ORIGIN space. Declared here so it is @@ -2852,6 +2862,19 @@ locate_views_one(struct oxr_logger *log, if (seg != NULL) { eye_pos = seg->eyes; got_eye_positions = seg->have_eyes; + if (should_log) { + if (seg->have_eyes && seg->eyes.count >= 2) { + U_LOG_I("Segment 0x%016llx eyes from its DP (%u, tracking=%d): [0]=(%.4f,%.4f,%.4f) " + "[1]=(%.4f,%.4f,%.4f) (reference frame)", + (unsigned long long)seg->screen_id, seg->eyes.count, (int)seg->eyes.is_tracking, + seg->eyes.eyes[0].x, seg->eyes.eyes[0].y, seg->eyes.eyes[0].z, seg->eyes.eyes[1].x, + seg->eyes.eyes[1].y, seg->eyes.eyes[1].z); + } else { + U_LOG_I("Segment 0x%016llx eyes: its screen's nominal viewer (no DP eye set covering " + "its views)", + (unsigned long long)seg->screen_id); + } + } } else { got_eye_positions = oxr_session_get_predicted_eye_positions(sess, &eye_pos); } @@ -3175,11 +3198,15 @@ locate_views_one(struct oxr_logger *log, fabsf(win_orient.w - 1.0f) > 0.0001f); if (should_log) { - U_LOG_I("Window-relative Kooima: screen=%.4fx%.4fm, " + char seg_tag[40] = ""; + if (seg != NULL) { + snprintf(seg_tag, sizeof(seg_tag), " [segment 0x%016llx]", + (unsigned long long)seg->screen_id); + } + U_LOG_I("Window-relative Kooima%s: screen=%.4fx%.4fm, " "eye_offset=(%.4f,%.4f,%.4f)m, rotated=%d", - screen_width_m, screen_height_m, - eye_offset_x, eye_offset_y, eye_offset_z, - win_has_orientation); + seg_tag, screen_width_m, screen_height_m, eye_offset_x, eye_offset_y, + eye_offset_z, win_has_orientation); } } else if (oxr_session_get_display_dimensions(sess, &screen_width_m, &screen_height_m) && screen_width_m > 0.0f && screen_height_m > 0.0f) { @@ -4189,6 +4216,7 @@ static void locate_segment_override(const struct xrt_segment_metrics *m, const struct oxr_segment_layout *l, uint32_t i, + uint32_t views_needed, struct oxr_locate_segment *o) { memset(o, 0, sizeof(*o)); @@ -4202,16 +4230,11 @@ locate_segment_override(const struct xrt_segment_metrics *m, o->nominal_y_m = s->nominal_viewer_y_m; o->nominal_z_m = s->nominal_viewer_z_m; - // The screen's DP eyes whenever it reports a valid set (tracked or not, - // exactly like the single-screen path); else the screen's nominal viewer. - if (oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count)) { - o->eyes = s->eyes; - for (uint32_t e = 0; e < o->eyes.count && e < XRT_MAX_VIEWS; e++) { - o->eyes.eyes[e].x += dx; - o->eyes.eyes[e].y += dy; - } - o->have_eyes = true; - } + // The screen's DP eyes whenever it reports a valid set covering the + // segment's views (tracked or not, exactly like the single-screen path); + // else the screen's nominal viewer. + o->have_eyes = oxr_segment_views_segment_eyes(s, dx, dy, views_needed, &o->eyes); + o->screen_id = s->screen_id; o->rig_dx = l->seg[i].ref_cx - l->window_ref_cx; o->rig_dy = l->seg[i].ref_cy - l->window_ref_cy; @@ -4330,10 +4353,8 @@ oxr_session_locate_views(struct oxr_logger *log, const struct xrt_segment_metric *s = &m.seg[mj]; o.nominal_y_m = s->nominal_viewer_y_m; o.nominal_z_m = s->nominal_viewer_z_m; - if (oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count)) { - o.eyes = s->eyes; - o.have_eyes = true; - } + o.have_eyes = oxr_segment_views_segment_eyes(s, 0.0f, 0.0f, 2, &o.eyes); + o.screen_id = s->screen_id; o.side_effects = true; struct oxr_locate_segment_out so = {.measurable = true}; o.out = &so; @@ -4426,7 +4447,7 @@ oxr_session_locate_views(struct oxr_logger *log, continue; } struct oxr_locate_segment o; - locate_segment_override(&m, &l, k, &o); + locate_segment_override(&m, &l, k, cnt[k], &o); o.view_count = cnt[k]; o.side_effects = (k == mj); o.out = &seg_out; diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 19b8ff5be..2877f29cf 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -414,3 +414,52 @@ TEST_CASE("a display rig gets ONE m2v across segments", "[oxr][segment_views]") CHECK(l.seg[0].ref_cy - l.seg[0].h_m * 0.5f == Catch::Approx(l.seg[1].ref_cy + l.seg[1].h_m * 0.5f)); } } + +TEST_CASE("mixed vendors: each segment uses its OWN pitch and its OWN DP eye pair", "[oxr][segment_views]") +{ + // Segment 0 on screen A (the Leia-like primary, its DP's no-face default: + // both eyes at one point); segment 1 on screen B (a sim_display segment + // DP: an eye pair +-32 mm), at a different pixel pitch. + xrt_segment_metrics m = two_screen_table(); + m.seg[0].have_eyes = true; + m.seg[0].eyes.valid = true; + m.seg[0].eyes.count = 2; + m.seg[0].eyes.eyes[0] = {0.0f, 0.10f, 0.60f}; + m.seg[0].eyes.eyes[1] = {0.0f, 0.10f, 0.60f}; + m.seg[1].have_eyes = true; + m.seg[1].eyes.valid = true; + m.seg[1].eyes.count = 2; + m.seg[1].eyes.eyes[0] = {-0.032f, 0.0f, 0.60f}; + m.seg[1].eyes.eyes[1] = {0.032f, 0.0f, 0.60f}; + + oxr_segment_layout l; + REQUIRE(oxr_segment_views_layout(&m, &l)); + + // Its own pitch: B's segment is 480 px x 0.60/2560 m, not A's pitch. + xrt_window_metrics wm0, wm1; + oxr_segment_views_window_metrics(&m, &l, 0, &wm0); + oxr_segment_views_window_metrics(&m, &l, 1, &wm1); + CHECK(wm0.window_width_m == Catch::Approx(320.0f * 0.344f / 1920.0f)); + CHECK(wm1.window_width_m == Catch::Approx(480.0f * 0.60f / 2560.0f)); + CHECK(wm1.display_width_m == Catch::Approx(0.60f)); + + // Its own eye pair, carried into the reference frame — never segment 0's. + float dx = 0, dy = 0; + oxr_segment_views_own_to_ref(&l, 1, &dx, &dy); + xrt_eye_positions e1; + REQUIRE(oxr_segment_views_segment_eyes(&m.seg[1], dx, dy, 2, &e1)); + CHECK(e1.eyes[1].x - e1.eyes[0].x == Catch::Approx(0.064f)); + CHECK(e1.eyes[0].x == Catch::Approx(-0.032f + dx)); + CHECK(e1.eyes[0].y == Catch::Approx(dy)); + // Relative to its own canvas the pair straddles the eye offset by +-32 mm. + CHECK((e1.eyes[0].x - wm1.window_center_offset_x_m) + 0.032f == + Catch::Approx(-l.seg[1].own_cx).margin(1e-5)); + + // A DP that under-reports for the segment's views (one eye for a 2-view + // mode: every view from the SAME eye, an anaglyph that looks flat) is not + // used; the nominal viewer (a real pair) takes over. + xrt_segment_metric under = m.seg[1]; + under.eyes.count = 1; + CHECK_FALSE(oxr_segment_views_segment_eyes(&under, dx, dy, 2, &e1)); + CHECK(oxr_segment_views_segment_eyes(&under, dx, dy, 1, &e1)); // a 1-view mode is covered +} From e7b846422b01b74957fa136e27f157c71c253053 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 15:17:43 -0700 Subject: [PATCH 21/21] style: git clang-format the M3 changes against main's .clang-format Co-Authored-By: Claude Fable 5.1 --- .../vk_native/comp_vk_native_compositor.c | 9 ++-- .../vk_native/comp_vk_native_renderer.c | 9 ++-- .../sim_display/sim_display_processor.c | 4 +- .../state_trackers/oxr/oxr_segment_views.h | 3 +- src/xrt/state_trackers/oxr/oxr_session.c | 27 ++++++----- src/xrt/targets/common/target_plugin_loader.c | 48 +++++++++++-------- tests/tests_oxr_segment_views.cpp | 6 +-- tests/tests_oxr_view_space.cpp | 5 +- 8 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c index 7e34a5984..ec56f61a0 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -5068,10 +5068,11 @@ vk_log_view_dims_on_change(struct comp_vk_native_compositor *c, } memcpy(c->view_dims_logged, now, sizeof(now)); if (segments > 0) { - U_LOG_W("VIEW_DIMS: canvas %ux%u x mode scale %.3fx%.3f -> tile %ux%u; mosaic: %u segment(s), view 0 " - "submits %ux%u into its segment of the tile (per-segment views, not an upscale)", - now[0], now[1], (double)mode->view_scale_x, (double)mode->view_scale_y, view_w, view_h, - segments, sub_w, sub_h); + U_LOG_W( + "VIEW_DIMS: canvas %ux%u x mode scale %.3fx%.3f -> tile %ux%u; mosaic: %u segment(s), view 0 " + "submits %ux%u into its segment of the tile (per-segment views, not an upscale)", + now[0], now[1], (double)mode->view_scale_x, (double)mode->view_scale_y, view_w, view_h, segments, + sub_w, sub_h); return; } const bool upscaled = sub_w > 0 && sub_h > 0 && (sub_w < view_w || sub_h < view_h); diff --git a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c index 5375b9074..68a63a129 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c @@ -379,7 +379,8 @@ routed_view_count(const struct comp_layer *layer, const struct comp_vk_native_ef } // A projection layer is bounded by what it submitted; a quad / equirect2 // has one sub-image shown in every routed view. - const bool per_view = layer->data.type == XRT_LAYER_PROJECTION || layer->data.type == XRT_LAYER_PROJECTION_DEPTH; + const bool per_view = + layer->data.type == XRT_LAYER_PROJECTION || layer->data.type == XRT_LAYER_PROJECTION_DEPTH; if (per_view && n > layer->data.view_count) { n = layer->data.view_count; } @@ -1906,8 +1907,7 @@ draw_zones_pass(struct comp_vk_native_renderer *r, // Visibility, sub-rect blend mode and the packed // ray constants, all per view (#1602). if (!compose_equirect2_view(layer, routed ? route_tile : eye, tile_count, &cams[eye], - &tiles[routed ? route_tile : eye], - eq2_constants, &mode)) { + &tiles[routed ? route_tile : eye], eq2_constants, &mode)) { continue; } } else if (is_zone) { @@ -1916,7 +1916,8 @@ draw_zones_pass(struct comp_vk_native_renderer *r, ? COMP_LAYER_BLEND_STRAIGHT : COMP_LAYER_BLEND_PREMULTIPLIED; } else if (is_quad) { - mode = comp_layer_subrect_blend_mode(&tiles[routed ? route_tile : eye], layer->data.flags); + mode = + comp_layer_subrect_blend_mode(&tiles[routed ? route_tile : eye], layer->data.flags); } else if (routed) { if (!route_mode_set[route_tile]) { route_mode[route_tile] = diff --git a/src/xrt/drivers/sim_display/sim_display_processor.c b/src/xrt/drivers/sim_display/sim_display_processor.c index deac88a01..bc4b6e599 100644 --- a/src/xrt/drivers/sim_display/sim_display_processor.c +++ b/src/xrt/drivers/sim_display/sim_display_processor.c @@ -908,8 +908,8 @@ sim_dp_get_predicted_eye_positions(struct xrt_display_processor *xdp, struct xrt float half_ipd = sdp->ipd_m / 2.0f; // A segment DP (multi-screen) reports one eye per view of the atlas it // weaves; the session DP keeps the process-wide sim view count. - uint32_t vc = sdp->screen_bound ? (sdp->last_atlas_views > 0 ? sdp->last_atlas_views : 2) - : sim_display_get_view_count(); + uint32_t vc = + sdp->screen_bound ? (sdp->last_atlas_views > 0 ? sdp->last_atlas_views : 2) : sim_display_get_view_count(); if (vc == 1) { out->eyes[0] = (struct xrt_eye_position){sdp->nominal_x_m, sdp->nominal_y_m, sdp->nominal_z_m}; diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 3cd4e8ba9..f75f3b26f 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -235,8 +235,7 @@ static inline bool oxr_segment_views_segment_eyes( const struct xrt_segment_metric *s, float dx, float dy, uint32_t views_needed, struct xrt_eye_positions *out) { - if (s == NULL || out == NULL || - !oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count) || + if (s == NULL || out == NULL || !oxr_segment_views_accept_eyes(s->have_eyes, s->eyes.valid, s->eyes.count) || s->eyes.count < (views_needed > 0 ? views_needed : 1)) { return false; } diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 2ababae55..dd106cb3d 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2864,15 +2864,17 @@ locate_views_one(struct oxr_logger *log, got_eye_positions = seg->have_eyes; if (should_log) { if (seg->have_eyes && seg->eyes.count >= 2) { - U_LOG_I("Segment 0x%016llx eyes from its DP (%u, tracking=%d): [0]=(%.4f,%.4f,%.4f) " - "[1]=(%.4f,%.4f,%.4f) (reference frame)", - (unsigned long long)seg->screen_id, seg->eyes.count, (int)seg->eyes.is_tracking, - seg->eyes.eyes[0].x, seg->eyes.eyes[0].y, seg->eyes.eyes[0].z, seg->eyes.eyes[1].x, - seg->eyes.eyes[1].y, seg->eyes.eyes[1].z); + U_LOG_I( + "Segment 0x%016llx eyes from its DP (%u, tracking=%d): [0]=(%.4f,%.4f,%.4f) " + "[1]=(%.4f,%.4f,%.4f) (reference frame)", + (unsigned long long)seg->screen_id, seg->eyes.count, (int)seg->eyes.is_tracking, + seg->eyes.eyes[0].x, seg->eyes.eyes[0].y, seg->eyes.eyes[0].z, seg->eyes.eyes[1].x, + seg->eyes.eyes[1].y, seg->eyes.eyes[1].z); } else { - U_LOG_I("Segment 0x%016llx eyes: its screen's nominal viewer (no DP eye set covering " - "its views)", - (unsigned long long)seg->screen_id); + U_LOG_I( + "Segment 0x%016llx eyes: its screen's nominal viewer (no DP eye set covering " + "its views)", + (unsigned long long)seg->screen_id); } } } else { @@ -3203,10 +3205,11 @@ locate_views_one(struct oxr_logger *log, snprintf(seg_tag, sizeof(seg_tag), " [segment 0x%016llx]", (unsigned long long)seg->screen_id); } - U_LOG_I("Window-relative Kooima%s: screen=%.4fx%.4fm, " - "eye_offset=(%.4f,%.4f,%.4f)m, rotated=%d", - seg_tag, screen_width_m, screen_height_m, eye_offset_x, eye_offset_y, - eye_offset_z, win_has_orientation); + U_LOG_I( + "Window-relative Kooima%s: screen=%.4fx%.4fm, " + "eye_offset=(%.4f,%.4f,%.4f)m, rotated=%d", + seg_tag, screen_width_m, screen_height_m, eye_offset_x, eye_offset_y, + eye_offset_z, win_has_orientation); } } else if (oxr_session_get_display_dimensions(sess, &screen_width_m, &screen_height_m) && screen_width_m > 0.0f && screen_height_m > 0.0f) { diff --git a/src/xrt/targets/common/target_plugin_loader.c b/src/xrt/targets/common/target_plugin_loader.c index 8a49ffff0..3ad2e5c36 100644 --- a/src/xrt/targets/common/target_plugin_loader.c +++ b/src/xrt/targets/common/target_plugin_loader.c @@ -3846,9 +3846,10 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, target_screen_pin_parse(getenv("DXR_SCREEN_PLUGIN"), &pins, &pins_malformed); bool pin_used[TARGET_SCREEN_PIN_MAX] = {false}; if (pins_malformed > 0) { - U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN has %u malformed entr%s (expected " - "=[,...]) — ignored", - pins_malformed, pins_malformed == 1 ? "y" : "ies"); + U_LOG_W( + "plugin loader: DXR_SCREEN_PLUGIN has %u malformed entr%s (expected " + "=[,...]) — ignored", + pins_malformed, pins_malformed == 1 ? "y" : "ies"); } /* @@ -3902,12 +3903,13 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, enum target_screen_pick_reason reason = TARGET_SCREEN_PICK_NONE; bool pin_unclaimed = false; - const int pick = - target_screen_pick(cands, nc, pin_plugin, have_preferred ? preferred_id : NULL, &reason, &pin_unclaimed); + const int pick = target_screen_pick(cands, nc, pin_plugin, have_preferred ? preferred_id : NULL, + &reason, &pin_unclaimed); if (pin_unclaimed) { - U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN pins monitor 0x%016llx ('%s'/'%s') to '%s', which has no " - "claim on it — the pin is ignored for this monitor", - (unsigned long long)desc->monitor_id, out_name, conn_name, pin_plugin); + U_LOG_W( + "plugin loader: DXR_SCREEN_PLUGIN pins monitor 0x%016llx ('%s'/'%s') to '%s', which has no " + "claim on it — the pin is ignored for this monitor", + (unsigned long long)desc->monitor_id, out_name, conn_name, pin_plugin); } if (pick >= 0) { best_src = cand_src[pick]; @@ -3923,14 +3925,16 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, } if (reason == TARGET_SCREEN_PICK_PIN) { - U_LOG_I("plugin loader: monitor 0x%016llx ('%s'/'%s') → '%s' by DXR_SCREEN_PLUGIN (confidence=%u; " - "outranks PreferredPlugin and the active plug-in for this monitor)", - (unsigned long long)desc->monitor_id, out_name, conn_name, cands[pick].plugin_id, - cands[pick].confidence); + U_LOG_I( + "plugin loader: monitor 0x%016llx ('%s'/'%s') → '%s' by DXR_SCREEN_PLUGIN (confidence=%u; " + "outranks PreferredPlugin and the active plug-in for this monitor)", + (unsigned long long)desc->monitor_id, out_name, conn_name, cands[pick].plugin_id, + cands[pick].confidence); } else if (reason == TARGET_SCREEN_PICK_PREFERRED) { - U_LOG_W("plugin loader: monitor 0x%016llx → PreferredPlugin override '%s' " - "(forced over EDID confidence)", - (unsigned long long)desc->monitor_id, preferred_id); + U_LOG_W( + "plugin loader: monitor 0x%016llx → PreferredPlugin override '%s' " + "(forced over EDID confidence)", + (unsigned long long)desc->monitor_id, preferred_id); } else if (reason == TARGET_SCREEN_PICK_ACTIVE && conf_pick >= 0 && conf_pick != pick) { // One-off per monitor, and only when the override changes the outcome. U_LOG_W( @@ -3950,17 +3954,19 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, (unsigned long long)e->monitor_id, e->plugin_id, e->confidence); if (reason == TARGET_SCREEN_PICK_PIN && e->dp_factory_vk == NULL && e->dp_factory_gl == NULL && e->dp_factory_d3d11 == NULL && e->dp_factory_d3d12 == NULL && e->dp_factory_metal == NULL) { - U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN pinned monitor 0x%016llx to '%s', but it offers no " - "display-processor factory for it — that screen will not be woven", - (unsigned long long)e->monitor_id, e->plugin_id); + U_LOG_W( + "plugin loader: DXR_SCREEN_PLUGIN pinned monitor 0x%016llx to '%s', but it offers no " + "display-processor factory for it — that screen will not be woven", + (unsigned long long)e->monitor_id, e->plugin_id); } } for (uint32_t i = 0; i < pins.count; i++) { if (!pin_used[i]) { - U_LOG_W("plugin loader: DXR_SCREEN_PLUGIN entry '%s=%s' names no known monitor (match an output " - "or connector name, e.g. HDMI-1 / HDMI-A-1, or a monitor id in hex)", - pins.pin[i].match, pins.pin[i].plugin_id); + U_LOG_W( + "plugin loader: DXR_SCREEN_PLUGIN entry '%s=%s' names no known monitor (match an output " + "or connector name, e.g. HDMI-1 / HDMI-A-1, or a monitor id in hex)", + pins.pin[i].match, pins.pin[i].plugin_id); } } diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 2877f29cf..8e2ebb577 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -301,7 +301,8 @@ TEST_CASE("Quad mode on two segments: every view gets a real frustum", "[oxr][se REQUIRE(oxr_segment_views_nominal_eyes(0.063f, 0.6f, 2, 0.0f, 0.0f, eyes) == 2); CHECK(eyes[0].x == Catch::Approx(-0.0315f)); CHECK(eyes[1].x == Catch::Approx(0.0315f)); - REQUIRE(oxr_segment_views_nominal_eyes(0.063f, 0.6f, 1, 0.0f, 0.0f, eyes) == 2); // mono: the pair, centred later + REQUIRE(oxr_segment_views_nominal_eyes(0.063f, 0.6f, 1, 0.0f, 0.0f, eyes) == + 2); // mono: the pair, centred later const xrt_segment_metrics m = two_screen_table(); oxr_segment_layout l; @@ -452,8 +453,7 @@ TEST_CASE("mixed vendors: each segment uses its OWN pitch and its OWN DP eye pai CHECK(e1.eyes[0].x == Catch::Approx(-0.032f + dx)); CHECK(e1.eyes[0].y == Catch::Approx(dy)); // Relative to its own canvas the pair straddles the eye offset by +-32 mm. - CHECK((e1.eyes[0].x - wm1.window_center_offset_x_m) + 0.032f == - Catch::Approx(-l.seg[1].own_cx).margin(1e-5)); + CHECK((e1.eyes[0].x - wm1.window_center_offset_x_m) + 0.032f == Catch::Approx(-l.seg[1].own_cx).margin(1e-5)); // A DP that under-reports for the segment's views (one eye for a 2-view // mode: every view from the SAME eye, an anaglyph that looks flat) is not diff --git a/tests/tests_oxr_view_space.cpp b/tests/tests_oxr_view_space.cpp index f6b56c8aa..e5d70ffd2 100644 --- a/tests/tests_oxr_view_space.cpp +++ b/tests/tests_oxr_view_space.cpp @@ -115,7 +115,7 @@ is_multiview_count(size_t n, uint32_t device_max) } [[maybe_unused]] const bool kSegmentsOff = [] { - // Before main(): the runtime reads it at every system fill-in. +// Before main(): the runtime reads it at every system fill-in. #ifdef _WIN32 _putenv_s("DXR_SEGMENTS", "0"); #else @@ -1033,7 +1033,8 @@ TEST_CASE("XR_DXR_display_info advertises PRIMARY_MULTIVIEW_DXR (#1486)", "[oxr] { // PRIMARY_STEREO means exactly 2, whatever the device can drive. CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO) == 2); - CHECK(is_multiview_count(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR), device_max)); + CHECK(is_multiview_count(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR), + device_max)); // A valid core type the system does NOT advertise is still refused. CHECK(rt.config_view_count(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO) == 0); }