diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index cd4e663d71..f151139585 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,56 @@ 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. +- `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`. -## 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 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/docs/architecture/kooima-projection.md b/docs/architecture/kooima-projection.md index 70416ee618..81d25f52bb 100644 --- a/docs/architecture/kooima-projection.md +++ b/docs/architecture/kooima-projection.md @@ -348,6 +348,22 @@ 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. 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 | Path | Location | diff --git a/docs/guides/displayxr-app-rules.md b/docs/guides/displayxr-app-rules.md index aea93b9785..d8db42993a 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, 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 + 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/docs/reference/view-configuration-model.md b/docs/reference/view-configuration-model.md index f4103b7e6f..890b8de4cd 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** (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 | | 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,58 @@ 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 × 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. + +**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 + 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 diff --git a/docs/specs/extensions/XR_DXR_display_info.md b/docs/specs/extensions/XR_DXR_display_info.md index 34cfdeebfb..c9971a887b 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,84 @@ 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` 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. + +**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 +2086,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 +2563,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/docs/specs/runtime/plugin-discovery.md b/docs/specs/runtime/plugin-discovery.md index 9b3300ea43..bf30f78d96 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/external/openxr_includes/openxr/README.md b/src/external/openxr_includes/openxr/README.md index fef24dd590..d2decc59e2 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 931702200d..ac691a9573 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,74 @@ 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 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 + * 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/compositor/util/comp_segments.c b/src/xrt/compositor/util/comp_segments.c index 19250c9963..8d43812577 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 5f9ae23e37..1351ffc599 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 f733902858..ec56f61a03 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 @@ -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 @@ -5038,12 +5052,29 @@ 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, @@ -5051,9 +5082,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 +5138,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 +5172,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 +5281,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 +5299,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 +5329,129 @@ 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++) { + // 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 = { + 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 = n; + 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 +8482,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 +9397,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 +9481,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 +10051,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 +12546,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 +12621,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 74f5782efa..f5ae80220e 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 f761f9c784..68a63a129e 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,78 @@ 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)? 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) +{ + 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. +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; + } + } + // 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; +} + +/*! + * 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? * @@ -361,7 +433,10 @@ 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) { + 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; @@ -1708,7 +1783,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]); } @@ -1734,6 +1818,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,11 +1848,16 @@ 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 // 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; } /* @@ -1810,8 +1906,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], - eq2_constants, &mode)) { + if (!compose_equirect2_view(layer, routed ? route_tile : eye, tile_count, &cams[eye], + &tiles[routed ? route_tile : eye], eq2_constants, &mode)) { continue; } } else if (is_zone) { @@ -1820,7 +1916,15 @@ 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] = + 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 +1955,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 +2425,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 +2443,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 +2476,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 7a55e71ac1..bb735fd952 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 d4944361f7..dbcfce0971 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%s", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, enable_3d ? "3D" : "2D", + ok ? "" : " (this DP has no 2D/3D switch — nothing to do)"); + } +} 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 dfcab24548..fc99aca71e 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/src/xrt/drivers/sim_display/sim_display_processor.c b/src/xrt/drivers/sim_display/sim_display_processor.c index e21b8ea2df..bc4b6e5995 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/include/xrt/xrt_display_metrics.h b/src/xrt/include/xrt/xrt_display_metrics.h index 3f93ee279f..4ac6c94276 100644 --- a/src/xrt/include/xrt/xrt_display_metrics.h +++ b/src/xrt/include/xrt/xrt_display_metrics.h @@ -122,6 +122,100 @@ 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 + * 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). + * + * @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 diff --git a/src/xrt/state_trackers/oxr/CMakeLists.txt b/src/xrt/state_trackers/oxr/CMakeLists.txt index 6c21fc979e..ac0386020d 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 7c8f5c8b36..8dae41cc55 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 496ed90277..97ed9d15e0 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, @@ -2302,13 +2310,21 @@ 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; + //! 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. @@ -2842,6 +2858,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 0000000000..b76d018215 --- /dev/null +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.c @@ -0,0 +1,230 @@ +// 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; + out->window_ref_w = x1 - x0; + out->window_ref_h = y1 - y0; + 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 0000000000..f75f3b26f0 --- /dev/null +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -0,0 +1,308 @@ +// 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 +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! + * 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_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; + } + if (set_capacity == 0) { + set_capacity = 1; + } + uint32_t n = device_max_views * set_capacity; + 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; + //! 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; +}; + +/*! + * 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 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). + */ +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]); + +/*! + * 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; +} + +/*! + * 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. + * 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; +} + +/*! + * 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 + * 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 27247b34da..dd106cb3dd 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,102 @@ 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). + */ +//! 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). + 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; + //! 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 + //! 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 +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 +2590,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, sess->sys->multiview_set_capacity); + } +#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 @@ -2563,7 +2675,15 @@ oxr_session_locate_views(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 @@ -2738,7 +2858,28 @@ 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; + 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); + } // One-shot diagnostic: log stereo gate values for first frames { @@ -2900,10 +3041,12 @@ 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; - 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 @@ -2913,8 +3056,13 @@ oxr_session_locate_views(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}; + // 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", @@ -3002,7 +3150,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 @@ -3043,11 +3200,16 @@ oxr_session_locate_views(struct oxr_logger *log, fabsf(win_orient.w - 1.0f) > 0.0001f); if (should_log) { - U_LOG_I("Window-relative Kooima: 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); + 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", + 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) { @@ -3117,6 +3279,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 @@ -3195,10 +3361,30 @@ oxr_session_locate_views(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 } + // 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 +3721,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 +3738,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 */ @@ -3835,10 +4023,14 @@ oxr_session_locate_views(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, @@ -3859,27 +4051,18 @@ oxr_session_locate_views(struct oxr_logger *log, 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); } } @@ -3945,12 +4128,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 +4148,371 @@ 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, + uint32_t views_needed, + 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 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; + o->vdh_scale = oxr_segment_views_vdh_scale(l, i); +} + +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, const struct xrt_segment_view_routing *r) +{ + if (memcmp(r, &sess->seg_route_logged, sizeof(*r)) == 0) { + return; + } + sess->seg_route_logged = *r; + 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); +} + +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, + 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) { + // 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; + } + + 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; + 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; + 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 + // NOT touched — only a splitting locate writes it. + const struct xrt_segment_view_routing none = {0}; + locate_fill_bindings(sess, viewState, &none); + } + 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, sess->sys->multiview_set_capacity); + 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; + // 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) { + // 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) { + // 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; + } + + // 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; + 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)) { + continue; + } + struct oxr_locate_segment o; + locate_segment_override(&m, &l, k, cnt[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++) { + 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; + + // #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); + 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 route; + memset(&route, 0, sizeof(route)); + route.count = m.count; + route.canvas = m.canvas; + for (uint32_t k = 0; k < m.count; 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]; + } + oxr_segment_views_route_record(&sess->seg_route, &route); + locate_fill_bindings(sess, viewState, &route); + return oxr_session_success_result(sess); +} + /* * #1580 — the frame's per-view cameras, for the compositor. * @@ -4088,6 +4638,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 +4652,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 e4e9f863fc..7249456266 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,17 @@ 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) { + 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 + xrt_result_t xret; xret = xrt_comp_layer_begin(xc, &data); OXR_CHECK_XRET(log, sess, xret, xrt_comp_layer_begin); @@ -3365,9 +3376,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 99658bf51b..7024480796 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" @@ -167,6 +168,38 @@ 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 + // 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++) { + 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; @@ -189,11 +222,21 @@ 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->multiview_set_capacity); 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 +404,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 +427,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 +448,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 +463,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/src/xrt/targets/common/CMakeLists.txt b/src/xrt/targets/common/CMakeLists.txt index e9e8731a60..ca001bd72f 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 4c802d7606..3ad2e5c363 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,115 @@ 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 +3952,22 @@ 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 0000000000..0c11dc69b4 --- /dev/null +++ b/src/xrt/targets/common/target_screen_pin.c @@ -0,0 +1,219 @@ +// 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, 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) { + continue; + } + if (ignore_case ? name_eq(c[i].plugin_id, id) : 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') { + // 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 { + pin_unclaimed = true; + } + } + if (pick < 0) { + pick = first_with_id(cands, count, preferred, false); + 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 0000000000..dcdf2156e0 --- /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/test_apps/cube_handle_vk_linux/main.cpp b/test_apps/cube_handle_vk_linux/main.cpp index 3145eaa87b..75be878b2c 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. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6603a74489..343ca4e912 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,8 @@ set(tests tests_oxr_weave_latch 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 @@ -597,6 +599,18 @@ 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) + +# 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_comp_segments.cpp b/tests/tests_comp_segments.cpp index 05a1f726ce..4bec2395be 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)); +} diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp new file mode 100644 index 0000000000..8e2ebb577e --- /dev/null +++ b/tests/tests_oxr_segment_views.cpp @@ -0,0 +1,465 @@ +// 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("view-set capacity: only where a window can ever be split", "[oxr][segment_views]") +{ + // 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, 2) == 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); +} + +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); + } + } +} + +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); +} + +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)); + } +} + +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 +} diff --git a/tests/tests_oxr_view_space.cpp b/tests/tests_oxr_view_space.cpp index c17eb3eb1c..e5d70ffd2d 100644 --- a/tests/tests_oxr_view_space.cpp +++ b/tests/tests_oxr_view_space.cpp @@ -94,6 +94,35 @@ namespace { 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 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) +{ + 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 /*! @@ -255,6 +284,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. @@ -321,6 +352,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))); @@ -329,6 +365,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; } @@ -902,13 +939,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 @@ -943,12 +981,15 @@ 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(is_multiview_count(v.size(), device_max)); } 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); @@ -992,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(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); } @@ -1003,7 +1045,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(is_multiview_count(n, device_max)); // Same session, the OTHER advertised configuration: valid enum, wrong // session - UNSUPPORTED, not VALIDATION_FAILURE. @@ -1012,7 +1054,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(is_multiview_count(v.size(), device_max)); } tear_down(rt); diff --git a/tests/tests_target_screen_pin.cpp b/tests/tests_target_screen_pin.cpp new file mode 100644 index 0000000000..be4db3cd95 --- /dev/null +++ b/tests/tests_target_screen_pin.cpp @@ -0,0 +1,131 @@ +// 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); + + // 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); + + // 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); +}