diff --git a/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md b/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md index ae57d6320e..8ddfabe7c5 100644 --- a/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md +++ b/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md @@ -471,3 +471,59 @@ Vulkan manager's `set_screens` are in place. Both: `DXR_DISPLAY_HOTPLUG=1` runs there. The service / IPC path: the service's own registry is rebuilt by the sweep, but the generation is not forwarded to clients (macOS IPC sessions do not segment). Detail: `docs/architecture/comp-segments.md` § *Display hotplug*. + +## Amendment 9 (2026-10-12): a zone is framed from its own screen on every segmenting session + +The Amendment 4 addendum re-frames a present-owner's element that lies wholly on a non-primary +screen. A session whose own compositor segments its window (in-process D3D11 / D3D12 / GL / +Vulkan / Metal, or a direct client of the D3D11 service, Amendment 3) had the same gap one layer +up: `oxr_session_locate_views` never splits a zone-scoped locate (`locate_can_split`), and its one +view set was framed from the majority screen. A zone on the other screen of a straddling window +(an `XR_DXR_display_zones` app, e.g. a multi-zone Unity app on the in-process D3D12 compositor) +was therefore rendered from the wrong viewer and framed against the wrong panel, though each +screen's DP wove its pixels (displayxr-browser-pvt#272 is the browser's face of the same gap). +Decisions: + +- **The zone's screen frames it when the zone lies wholly on one screen.** The locate asks + `oxr_segment_views_zone_segment` (the table's segment that holds the whole zone rect, its + screen's sizes known) and frames that screen's whole-window metrics + (`oxr_segment_views_whole_window_metrics`) with that screen's eyes and nominal viewer, then + applies the zone rebase as before. That screen may be the primary: a zone on the primary of a + window mostly on screen 2 now takes the single-screen path (the session DP's own eyes and + window metrics), where it took screen 2's before. The present-owner rule never needs that + case, because a present-owner's single set is already framed from the primary. +- **A straddling zone** is superseded by Amendment 10 (its one set is framed from the screen + holding most of the zone, and its parts can get a view set each). +- **Same answer as the server.** Wherever `u_weave_zone_screen_segment` re-frames, the state + tracker's rule picks the same segment (swept in `tests/tests_weave_segments.cpp`). +- **No routing, no extension revision.** A zone locate still records no + `xrt_segment_view_routing`, so the compositor crops the frame per segment as in M2, and the + zone's pixels, wholly on one screen, are woven by that screen's DP. The locate API is unchanged; + only which screen frames a single-screen zone moves. + +## Amendment 10 (2026-10-12): a straddling zone gets a view set per part + +Design and David's decisions: `docs/roadmap/multi-screen.md` § 8 (displayxr-browser-pvt#272). + +- **One majority rule for a zone's one view set, on every path.** A zone (or inline-3D element) + that straddles a seam is framed from the screen holding the largest part of the ZONE (ties to + the primary, then the leftmost): `u_weave_rect_majority_segment`, used by both + `u_weave_zone_screen_segment` (present-owners, which used the primary for a straddling element) + and `oxr_segment_views_zone_segment` (every other segmenting session, which used the window's + majority). +- **Per-part views: `XrDisplayZonePartsDXR`** (`XR_DXR_display_zones` v4), chained on + `XrViewState` of a zone-scoped locate. The runtime cuts the zone at the seam + (`u_weave_zone_part_table`) and locates every part like a window segment over the parts' own + table (`oxr_segment_views_zone_parts` → `oxr_segment_views_layout`): that screen's eyes carried + into the frame of the zone's majority screen, the part as the Kooima canvas on its own screen, + and, for a display rig, one m2v for the whole zone (`vdh_scale` and the rig offset with + "window" read as "zone"), so the parts meet at the seam. `XrView[]` keeps the one set, so an + unaware caller is unchanged; `PRIMARY_MULTIVIEW_DXR` gets per-part zone views only through + the struct. Camera-centric locates are never split. +- **Budget.** At most `DXR_ZONE_PARTS_MAX` (default 4) zones per display time are split, first + located first served; past it a zone keeps its one set. +- **The seam is accepted** as for windows: two viewers, no blending. +- **Where it runs.** In-process segmenting compositors and direct clients of the D3D11 service + (Amendment 3) locate the parts client-side from the segment table, so no wire change. A + present-owner's element locates are framed server-side (Amendment 4 addendum); its parts, and + the weave crop at the painted partition, are the stacked follow-up. diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index 250d870846..6ed3062d4b 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -493,6 +493,12 @@ views*; API: `XrViewDisplayBindingsDXR` in `XR_DXR_display_info` v22). 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 +zone-scoped locate is framed from the **zone's** screen when the zone lies wholly on one +(`oxr_segment_views_zone_segment`: its eyes, the whole window relative to it, then the zone +rebase; ADR-047 Amendment 9), from the screen holding most of the ZONE when it straddles a +seam, and a caller chaining `XrDisplayZonePartsDXR` gets one view set per part on top +(Amendment 10: the parts are located client-side from this table, the frame stays unrouted and +is cropped at the live seam). 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 @@ -603,5 +609,6 @@ pixels into a handback texture on its own IPC thread. That submit is segmented b latch since nothing is torn down), and falls back to the panel DP's snap. - **Not covered:** the v8 hardware wish is still published to the panel DP only (the primary screen's lens); a window across more than `XRT_MAX_SEGMENTS` screens is woven per screen but - reported unsegmented (one eye set); a straddling element's views stay the primary's - (per-part views need a view set per part, displayxr-browser-pvt#272). + reported unsegmented (one eye set); a straddling element's one view set is framed from the + screen holding most of it (ADR-047 Amendment 10), and its per-part views + (`XrDisplayZonePartsDXR`) are not filled server-side yet (`docs/roadmap/multi-screen.md` § 8). diff --git a/docs/roadmap/control-panel-performance-settings.md b/docs/roadmap/control-panel-performance-settings.md index 788269314c..0e1f7a6548 100644 --- a/docs/roadmap/control-panel-performance-settings.md +++ b/docs/roadmap/control-panel-performance-settings.md @@ -325,7 +325,7 @@ never claim a mode the runtime is not in, and "Custom" falls out for free. ## Appendix A — census Every `DXR_*` name read at runtime under `src/xrt`, with its read site, mechanism, default -and tier. **107 distinct names**; the two `DXR_BG2D_*` knobs reach the environment through +and tier. **108 distinct names**; the two `DXR_BG2D_*` knobs reach the environment through `bg2d_int_knob()` rather than a literal `getenv` at the listed line. Process column: **App** = the runtime DLL, loaded into the OpenXR app's process · @@ -410,6 +410,7 @@ library linked into both · **CLI** = `displayxr-cli.exe` (reporting only, contr | `DXR_LAZY_TRANSPARENCY` | `compositor/vk_native/comp_vk_native_compositor.c` (`vk_lazy_transparency_setup`) | `DEBUG_GET_ONCE_BOOL` | **on** | App | 4 | Lazy transparency (desktop Linux): a transparency-capable session runs the DP's desktop capture, compose-under and alpha-gate only while its content carries alpha < 1. `=0` keeps them on for the whole session (the pre-slot behaviour), for A/B. Only engages when the DP implements `set_transparency_active` | | `DXR_LINUX_DIRECT_SCANOUT` | `compositor/vk_native/comp_vk_native_compositor.c:5845` | `getenv` | off | App | 1 | Opt-in RandR direct-scanout present path on Linux | | `DXR_WINDOW_POS` | `compositor/vk_native/comp_vk_native_compositor.c:5248` | `getenv` | plug-in reported | App | 1 | `"x,y"` override for the self-owned window position (#715) | +| `DXR_ZONE_PARTS_MAX` | `state_trackers/oxr/oxr_session.c` (`DEBUG_GET_ONCE_NUM_OPTION`, read in `locate_zone_parts`) | `DEBUG_GET_ONCE_NUM` | 4 | App | 1 | `XrDisplayZonePartsDXR` budget (multi-screen § 8): how many zones per display time get a view set per part when they straddle a seam; a split zone renders its views once per part, so past the cap a zone keeps its one set. `0` = never split; clamped to 16 | | `DXR_WINDOW_FULLSCREEN` | `compositor/vk_native/comp_vk_native_window_xcb.c:273` | `getenv` | on | App | 1 | `=0` opts out of EWMH fullscreen placement (Linux XCB) | ### Native-Wayland window drag (#1748) diff --git a/docs/roadmap/multi-screen.md b/docs/roadmap/multi-screen.md index a4a7afda9e..e80c5f56cf 100644 --- a/docs/roadmap/multi-screen.md +++ b/docs/roadmap/multi-screen.md @@ -305,3 +305,54 @@ that configuration is accepted; it disappears when every screen is tracked. ST-5598 → SR-P1; ST-3160 → SR-P3. - New: ADR "Multi-screen: segments and per-screen views" (supersedes ADR-015 §3-§4), `XR_DXR_display_info` v22 spec section, `comp_segments` design note. + +## 8. Per-part views for a zone that straddles a seam (displayxr-browser-pvt#272) + +**Decided (David, 2026-10-12).** Runtime record: ADR-047 Amendment 10; API: +`XR_DXR_display_zones.md` § 3.3.2. + +A zone (an `XR_DXR_display_zones` zone, or an inline-3D element, which the browser locates as +one) that straddles a seam is woven per screen, but one view set renders both parts from one +screen's viewer. The fix gives each part its own view set. + +### 8.1 Runtime: the locate returns the parts + +- **Surface.** A zone-scoped `xrLocateViews` returns the parts through + `XrDisplayZonePartsDXR`, chained on `XrViewState` (`XR_DXR_display_zones` v4). The caller does + not locate each part: the browser locates in its browser process and weaves in its GPU process, + so the weave output's parts never reach the code that renders views, and pairing a locate's + views with a weave's parts would mix two tables a frame apart during a drag (the #1883 sliver). +- **Per part:** that screen's eyes, Kooima against that screen with the part as the canvas, and + for a display rig ONE m2v for the whole zone (`vdh_scale` / rig offset with "window" read as + "zone"), so the parts meet at the seam. Implemented as the per-segment math over the parts' own + table (`u_weave_zone_part_table`, `oxr_segment_views_zone_parts`). +- **Unaware callers.** `XrView[]` keeps one set, framed from the screen holding most of the ZONE + on every path (`u_weave_rect_majority_segment`). This changed the present-owner path, which + used the primary, and every other session, which used the window's majority. +- **Seam.** Two viewers can make the zone discontinuous at the seam. Accepted, no blending, as for + windows (decided 2026-10-07). +- **Budget.** At most 4 split zones per display time, first located first served + (`DXR_ZONE_PARTS_MAX`, 0 = never split, max 16). Past it a zone keeps its one set. +- **`PRIMARY_MULTIVIEW_DXR`** gets per-part zone views only through the struct, never through + `XrView[]`. +- **Where it runs today.** In-process segmenting compositors and direct D3D11-service clients: + the parts are located client-side from the segment table, no wire change; those frames stay + unrouted and are cropped at the live seam. +- **Still to land: present-owners.** Their element locates are framed server-side + (`ipc_try_get_sr_view_poses`), so the locate reply gains an appended, fixed-size parts block + (≤ `XRT_MAX_SEGMENTS` parts × the active views of pose + fov), versioned by append-only fields + under the `u_git_tag` gate. The weave then crops each part where it was **painted**: the batch / + v6 submit carries the partition the frame was rendered with (zone-relative rects per screen, + appended to the rect), and `weave_seg_record` crops with it. + +### 8.2 Browser and web API (displayxr-browser-pvt) + +- **Shape:** each part is an extra view pair, every `XRView` carrying a **sub-viewport** (the + part's rect relative to the element, normalised 0..1). DisplayXR-only for now. +- Blink installs the parts with their views per element (today one view pair per element, patch + 0036); `GetLatestViews` grows a parts list. An element with zero or one part keeps the current + shape and code path. +- The page renders each pair into that sub-rect of each eye's half of the element's side-by-side + submission (a part occupies `rect.x / w ÷ 2` inside each half, `XR_DXR_weave.md` §5g item 4). A + page that reads only the first pair renders the element from its majority screen. +- The weave submit is unchanged apart from carrying the painted partition (8.1). diff --git a/docs/specs/extensions/XR_DXR_display_zones.md b/docs/specs/extensions/XR_DXR_display_zones.md index 745c2f80b6..eb677aee3c 100644 --- a/docs/specs/extensions/XR_DXR_display_zones.md +++ b/docs/specs/extensions/XR_DXR_display_zones.md @@ -3,8 +3,8 @@ | Property | Value | |----------|-------| | Extension Name | `XR_DXR_display_zones` | -| Spec Version | 1 | -| Type Values | `1004999150–1004999153` | +| Spec Version | 4 | +| Type Values | `1004999150–1004999155` | | Author | Leia Inc. | | Platform | All (extension-app classes only) | | Requires | `XR_DXR_local_3d_zone` (≥ v3), `XR_DXR_view_rig` (≥ v2) | @@ -60,13 +60,15 @@ physically *switches* (the wish). Full rationale: ADR-027. ```c #define XR_DXR_display_zones 1 -#define XR_DXR_display_zones_SPEC_VERSION 1 +#define XR_DXR_display_zones_SPEC_VERSION 4 #define XR_DXR_DISPLAY_ZONES_EXTENSION_NAME "XR_DXR_display_zones" #define XR_TYPE_DISPLAY_ZONE_CAPABILITIES_DXR ((XrStructureType)1004999150) #define XR_TYPE_DISPLAY_ZONE_DXR ((XrStructureType)1004999151) #define XR_TYPE_DISPLAY_ZONES_FRAME_END_INFO_DXR ((XrStructureType)1004999152) #define XR_TYPE_EVENT_DATA_DISPLAY_ZONE_METRICS_CHANGED_DXR ((XrStructureType)1004999153) +#define XR_TYPE_DISPLAY_ZONE_FEATHER_DXR ((XrStructureType)1004999154) // v3 +#define XR_TYPE_DISPLAY_ZONE_PARTS_DXR ((XrStructureType)1004999155) // v4 typedef XrFlags64 XrDisplayZonesFrameEndFlagsDXR; //! Cross-check zone/locate/mask consistency this frame: zone rects vs wish @@ -127,13 +129,16 @@ typedef struct XrDisplayZoneDXR { } XrDisplayZoneDXR; ``` -**Multi-screen (Windows, `XR_DXR_weave` present-owners).** When the session's -window is woven per screen (ADR-047 Amendment 4), a zone-scoped locate whose -`rect` lies wholly on a non-primary screen is framed from that screen: its -eyes, its nominal viewer, and the zone placed on its physical rectangle -(Amendment 4 addendum). The raw channel then reports that screen's eyes and the -zone relative to that screen. A zone that straddles a seam is located from the -primary screen, as before. +**Multi-screen.** When the session's window is woven per screen (ADR-047), a +zone-scoped locate whose `rect` lies wholly on one screen is framed from that +screen: its eyes, its nominal viewer, and the zone placed on its physical +rectangle. The raw channel then reports that screen's eyes and the zone relative +to that screen. A zone that straddles a seam gets ONE view set in `XrView[]`, +framed from the screen holding most of the zone (ties to the primary), on every +session (spec v4; a present-owner used the primary before, every other session +the screen holding most of the window). `XrView[]` is never split per segment, +under `PRIMARY_MULTIVIEW_DXR` either: a straddling zone's per-part view sets +ride `XrDisplayZonePartsDXR` (§ 3.3.2). `zoneId` exists for locate↔submit pairing (validate mode), debug logs/captures, and as the stable referent for the reserved effective-mask @@ -164,6 +169,74 @@ typedef struct XrDisplayZoneFeatherDXR { } XrDisplayZoneFeatherDXR; ``` +### 3.3.2 XrDisplayZonePartsDXR — per-part views of a straddling zone (spec v4) + +```c +typedef struct XrDisplayZonePartDXR { + uint64_t displayId; // the screen (XrDisplayDXR::displayId) + XrRect2Di rect; // the part, ZONE-relative px, cut at the seam + uint32_t firstView; // its first view in XrDisplayZonePartsDXR::views + uint32_t viewCount; // = the locate's active view count +} XrDisplayZonePartDXR; + +typedef struct XrDisplayZonePartsDXR { + XrStructureType type; // XR_TYPE_DISPLAY_ZONE_PARTS_DXR (1004999155) + void* XR_MAY_ALIAS next; + uint64_t generation; // changes with the screen table the parts are cut from + uint32_t partCapacityInput; + uint32_t partCountOutput; // 0 = not split: use XrView[] as before + XrDisplayZonePartDXR* parts; + uint32_t viewCapacityInput; + uint32_t viewCountOutput; + XrView* views; // every part's set, contiguous +} XrDisplayZonePartsDXR; +``` + +**Chain point.** OUTPUT, on `XrViewState::next` of a zone-scoped `xrLocateViews` +(an `XrDisplayZoneDXR` on `XrViewLocateInfo`). Without a zone it is left at 0. + +**What it returns.** When the session's window is woven per screen and the zone +straddles a seam, one view set per part, left to right: + +- **Each part from its own screen.** That screen's eyes (its nominal viewer when + untracked, ADR-047), Kooima against that screen with the part as the canvas. + The two screens' viewers may disagree, so the zone can be visibly + discontinuous at the seam; that is accepted, as for windows (no blending). +- **One scale per zone.** Under a display rig (`XrDisplayRigDXR`) the rig's + virtual display height maps the ZONE: each part gets its share of it and its + offset from the zone's centre, so the parts meet at the seam at the same + metres-to-virtual scale, also across panels of different pixel pitch. +- **One frame.** Every part's poses are in the locate's base space; the parts are + placed continuously in the display space of the screen holding most of the + zone, the screen that also frames `XrView[]`. + +**How to render.** Part p's view at index v fills `p.rect` (scaled by image size / +zone size) of view v's image of the zone's projection layer; each part's FOV +covers exactly its rect. Submit the zone's projection layer as usual: each +screen's display processor weaves its part. + +**When it is 0** (render `XrView[]`): the zone lies on one screen, the window is +not woven per screen, the locate is camera-centric (`XrCameraRigDXR`), or the +runtime's per-frame budget of split zones is spent. A split zone renders its +views once per part, so at most 4 zones per display time are split, first +located first served (`DXR_ZONE_PARTS_MAX`, 0 = never). Locate the zones that +matter most first. `XrView[]` is always the zone's one set, so an app that +ignores this struct renders as before. + +**Two-call idiom, never an error.** `partCountOutput` and `viewCountOutput` are +always written; `parts` and `views` only when both capacities suffice (0 = count +query). Today at most 2 parts (`XRT_MAX_SEGMENTS`) × the per-set view count. + +**`PRIMARY_MULTIVIEW_DXR`.** A zone-scoped locate is never split into +`XrView[]`; its per-part sets come through this struct only, the same mechanism +for every view configuration. + +**Where it is filled.** In-process segmenting compositors and direct clients of +the D3D11 service (the parts are located client-side from the service's segment +table). For an `XR_DXR_weave` present-owner (the DisplayXR Browser), whose +element locates are framed by the service, `partCountOutput` stays 0 until the +service-side parts land (`docs/roadmap/multi-screen.md` § 8.1). + ### 3.4 XrDisplayZonesFrameEndInfoDXR — per-frame wish reference ```c diff --git a/docs/specs/extensions/index.json b/docs/specs/extensions/index.json index cd42cecf34..00af62a49d 100644 --- a/docs/specs/extensions/index.json +++ b/docs/specs/extensions/index.json @@ -60,7 +60,7 @@ "XR_DXR_display_zones": { "group": "rendering", "title": "Display Zones", - "summary": "A layout of independent 3D zones and flat 2D zones across one display, each 3D zone with its own view rig, plus the wish mask a switchable-lens panel honours; poll xrGetDisplayZoneRecommendedViewSizeDXR for per-zone sizes — XrEventDataDisplayZoneMetricsChangedDXR is deprecated and never emitted" + "summary": "A layout of independent 3D zones and flat 2D zones across one display, each 3D zone with its own view rig, plus the wish mask a switchable-lens panel honours; a zone straddling two screens can get a view set per part (XrDisplayZonePartsDXR); poll xrGetDisplayZoneRecommendedViewSizeDXR for per-zone sizes — XrEventDataDisplayZoneMetricsChangedDXR is deprecated and never emitted" }, "XR_DXR_local_3d_zone": { "group": "rendering", diff --git a/src/external/openxr_includes/openxr/README.md b/src/external/openxr_includes/openxr/README.md index b78d7f9920..9b6d96231e 100644 --- a/src/external/openxr_includes/openxr/README.md +++ b/src/external/openxr_includes/openxr/README.md @@ -34,7 +34,7 @@ Rules: | 1004999120–122 | `XR_DXR_workspace_file_dialog` | 122 is an `XrResult`, not an `XrStructureType` | | 1004999130–132 | `XR_DXR_mcp_tools` | | | 1004999140–142 | `XR_DXR_view_rig` | | -| 1004999150–153 | `XR_DXR_display_zones` | assigned (spec v1) | +| 1004999150–159 | `XR_DXR_display_zones` | 150–153 assigned (spec v1); 154 = `XR_TYPE_DISPLAY_ZONE_FEATHER_DXR` (spec v3); 155 = `XR_TYPE_DISPLAY_ZONE_PARTS_DXR` (per-part views of a zone straddling a screen seam, spec v4, multi-screen § 8); 156–159 free for it | | 1004999160–166 | `XR_DXR_local_3d_zone` | relocated from 130–136 (collided with mcp_tools) — spec v4 | | 1004999170–171 | `XR_DXR_atlas_capture` | relocated from 120–121 (collided with workspace_file_dialog) — spec v3 | | 1004999180 | `XR_DXR_macos_gl_binding` | relocated from 1004999010 (collided with display_info) — spec v2 | diff --git a/src/external/openxr_includes/openxr/XR_DXR_display_zones.h b/src/external/openxr_includes/openxr/XR_DXR_display_zones.h index 4376ab2338..536f1efaa3 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_display_zones.h +++ b/src/external/openxr_includes/openxr/XR_DXR_display_zones.h @@ -66,7 +66,13 @@ extern "C" { // edge feather, OPT-IN. Zone edges are HARD by default (and the published // hardware wish is always binary/un-feathered regardless of this struct); // chain a feather on the zone at xrEndFrame to soften the COMPOSITE only. -#define XR_DXR_display_zones_SPEC_VERSION 3 +// SPEC_VERSION 4 (displayxr-browser-pvt#272, multi-screen § 8): + +// XrDisplayZonePartsDXR — a zone-scoped locate of a zone that straddles the seam +// between two screens can return one view set PER PART (each framed from its own +// screen, one rig scale for the whole zone), chained on XrViewState. A +// straddling zone's one view set (XrView[]) is now framed from the screen +// holding most of the zone, for every session. +#define XR_DXR_display_zones_SPEC_VERSION 4 #define XR_DXR_DISPLAY_ZONES_EXTENSION_NAME "XR_DXR_display_zones" // Extension type-value range (1004999xxx); replace with a Khronos-assigned @@ -83,6 +89,7 @@ extern "C" { // XR_EXT_view_configuration_views_change. #define XR_TYPE_EVENT_DATA_DISPLAY_ZONE_METRICS_CHANGED_DXR ((XrStructureType)1004999153) #define XR_TYPE_DISPLAY_ZONE_FEATHER_DXR ((XrStructureType)1004999154) +#define XR_TYPE_DISPLAY_ZONE_PARTS_DXR ((XrStructureType)1004999155) typedef XrFlags64 XrDisplayZonesFrameEndFlagsDXR; //! Cross-check zone/locate/mask consistency this frame: zone rects vs wish @@ -161,6 +168,66 @@ typedef struct XrDisplayZoneFeatherDXR { float radiusPx; //!< inward ramp width, client-window px; 0 = hard } XrDisplayZoneFeatherDXR; +/*! + * @brief One part of a zone that straddles a seam between screens (spec v4). + * + * The pixels of the zone that land on one screen, and where that part's view + * set sits in XrDisplayZonePartsDXR::views. + */ +typedef struct XrDisplayZonePartDXR { + uint64_t displayId; //!< the screen (XrDisplayDXR::displayId, XR_DXR_display_info) + XrRect2Di rect; //!< the part, ZONE-relative pixels (top-left origin), cut at the seam + uint32_t firstView; //!< its first view in XrDisplayZonePartsDXR::views + uint32_t viewCount; //!< its view count (= the locate's active view count) +} XrDisplayZonePartDXR; + +/*! + * @brief Per-part views of a zone-scoped locate (spec v4, OUTPUT). + * + * Chained on XrViewState::next of an xrLocateViews whose XrViewLocateInfo + * chains an XrDisplayZoneDXR. When the session's window is woven per screen + * and the zone STRADDLES a seam, the runtime cuts the zone at the seam and + * locates each part from its own screen: that screen's eyes (or its nominal + * viewer, untracked), Kooima against that screen with the part as the canvas, + * and, under a display rig, ONE metres-to-virtual scale for the whole zone + * (the rig's virtual display height maps the zone; each part gets its share + * and its offset from the zone centre), so the parts meet at the seam at the + * same scale. Every part's view set is in one frame (the base space of the + * locate), contiguous in @c views. + * + * Render part p's views into p.rect of each of the zone's view images (the + * view at index v of part p fills that sub-rect of view v's image; scale p.rect + * by image size / zone size when they differ). Each part's FOV covers exactly + * its rect. The zone's projection layer is submitted as usual: the runtime + * weaves each part on its own screen. + * + * @c partCountOutput is 0 (render XrView[] as before) when the zone lies on one + * screen, the window is not woven per screen, the locate is camera-centric + * (XrCameraRigDXR), or the runtime's per-frame budget of split zones is spent + * (a split zone renders its views once per part; the default budget is 4 zones + * per display time, first located first served). The locate's own XrView[] + * always carries the zone's ONE view set, framed from the screen holding most + * of the zone, so a caller that ignores this struct renders as before. + * PRIMARY_MULTIVIEW_DXR sessions get per-part zone views through this struct + * only (never through XrView[]). + * + * Two-call idiom per array, but never an error: @c partCountOutput and + * @c viewCountOutput are always written; @c parts and @c views are written only + * when BOTH capacities are large enough (a capacity of 0 queries the counts). + * At most 2 parts and 2 x the session's per-set view count today. + */ +typedef struct XrDisplayZonePartsDXR { + XrStructureType type; //!< XR_TYPE_DISPLAY_ZONE_PARTS_DXR + void* XR_MAY_ALIAS next; + uint64_t generation; //!< changes when the screen table the parts are cut from does + uint32_t partCapacityInput; + uint32_t partCountOutput; //!< 0 = not split: use XrView[] as before + XrDisplayZonePartDXR* parts; + uint32_t viewCapacityInput; + uint32_t viewCountOutput; + XrView* views; //!< every part's set, contiguous (pose + fov written) +} XrDisplayZonePartsDXR; + /*! * @brief Per-frame wish reference. Optional, chained on XrFrameEndInfo::next * in a zones frame. diff --git a/src/xrt/auxiliary/util/u_weave_rect_parts.h b/src/xrt/auxiliary/util/u_weave_rect_parts.h index 2f9a8a3a24..8092704bb4 100644 --- a/src/xrt/auxiliary/util/u_weave_rect_parts.h +++ b/src/xrt/auxiliary/util/u_weave_rect_parts.h @@ -164,6 +164,42 @@ u_weave_rect_single_segment(const struct xrt_segment_metrics *m, const struct xr return found; } +/*! + * The segment of table @p m holding the LARGEST part of @p rect (window px), or + * -1 when the rect lands on no segment. A rect wholly on one segment answers + * that segment (@ref u_weave_rect_single_segment). Ties go to the primary + * screen, then to the earlier (leftmost) segment, the same rule as the window + * majority (oxr_segment_views_majority). + * + * The screen that frames a straddling zone's ONE view set for every caller that + * does not take its parts (multi-screen § 8, decided 2026-10-12): the element + * then looks right on most of its area. + */ +static inline int32_t +u_weave_rect_majority_segment(const struct xrt_segment_metrics *m, const struct xrt_rect *rect) +{ + if (m == NULL || rect == NULL || m->count == 0 || m->count > XRT_MAX_SEGMENTS) { + return -1; + } + int32_t best = -1; + uint64_t best_area = 0; + bool best_primary = false; + for (uint32_t k = 0; k < m->count; k++) { + struct xrt_rect isect; + if (!u_weave_rect_intersect(rect, &m->seg[k].window_rect, &isect)) { + continue; + } + const uint64_t area = (uint64_t)isect.extent.w * (uint64_t)isect.extent.h; + const bool primary = m->seg[k].is_primary; + if (best < 0 || area > best_area || (area == best_area && primary && !best_primary)) { + best = (int32_t)k; + best_area = area; + best_primary = primary; + } + } + return best; +} + /*! * Which screen frames a zone-scoped locate (an inline-3D element) of a * present-owner whose bound window is woven per screen (ADR-047 Amendment 4, @@ -176,12 +212,14 @@ u_weave_rect_single_segment(const struct xrt_segment_metrics *m, const struct xr * window wholly on a non-primary screen); * - it was cut from a window of the live size @p live_w x @p live_h (a resize * in flight keeps the old framing for that locate); - * - the element lies wholly on ONE segment (@ref u_weave_rect_single_segment) - * — an element that straddles a seam keeps the primary's views, because - * rendering it per part needs a second view set the page cannot take yet - * (displayxr-browser-pvt#272); - * - that segment is not the primary screen's (the session's own framing - * already is that screen); + * - the screen holding the LARGEST part of the element + * (@ref u_weave_rect_majority_segment) is not the primary screen's (the + * session's own framing already is that screen). An element wholly on one + * screen is framed from that screen; a straddling element is framed from + * the screen holding most of it, the rule every session shares (multi-screen + * § 8, decided 2026-10-12: this path used the primary for a straddling + * element before). A caller that takes the element's parts + * (XrDisplayZonePartsDXR) gets a view set per part on top; * - its screen's physical and desktop sizes are known (the Kooima needs them). */ static inline int32_t @@ -196,7 +234,7 @@ u_weave_zone_screen_segment(const struct xrt_segment_metrics *m, if (m->window_pixel_width != live_w || m->window_pixel_height != live_h) { return -1; } - const int32_t k = u_weave_rect_single_segment(m, zone); + const int32_t k = u_weave_rect_majority_segment(m, zone); if (k < 0) { return -1; } @@ -208,6 +246,61 @@ u_weave_zone_screen_segment(const struct xrt_segment_metrics *m, return k; } +/*! + * The parts of a zone (window px) as a segment table of their own: one entry + * per segment the zone overlaps, in table order, each the segment's entry with + * its window and screen rects cut down to the zone (the same pixels, so both + * shift by the same offset). Everything else (screen geometry, eyes, the + * window header, the generation) is copied verbatim, so the per-segment view + * math (oxr_segment_views_layout and friends) frames the PARTS exactly as it + * frames a window's segments: one reference frame, continuous across the seam, + * and one rig scale for the whole zone (multi-screen § 8.1). + * + * @p out_zone_rel, when not NULL, receives each part relative to the zone's + * top-left (`XrDisplayZonePartDXR::rect`). + * + * @return the number of parts (= @p out->count): 0 for no table, an empty zone + * or one on no segment; 1 for a zone wholly on one screen; 2+ for a + * zone that straddles a seam. + */ +static inline uint32_t +u_weave_zone_part_table(const struct xrt_segment_metrics *m, + const struct xrt_rect *zone, + struct xrt_segment_metrics *out, + struct xrt_rect *out_zone_rel) +{ + if (out == NULL) { + return 0; + } + memset(out, 0, sizeof(*out)); + if (m == NULL || zone == NULL || m->count == 0 || m->count > XRT_MAX_SEGMENTS) { + return 0; + } + *out = *m; + out->count = 0; + memset(out->seg, 0, sizeof(out->seg)); + for (uint32_t k = 0; k < m->count; k++) { + const struct xrt_segment_metric *s = &m->seg[k]; + struct xrt_rect isect; + if (!u_weave_rect_intersect(zone, &s->window_rect, &isect)) { + continue; + } + struct xrt_segment_metric *p = &out->seg[out->count]; + *p = *s; + p->window_rect = isect; + p->screen_rect.offset.w = s->screen_rect.offset.w + (isect.offset.w - s->window_rect.offset.w); + p->screen_rect.offset.h = s->screen_rect.offset.h + (isect.offset.h - s->window_rect.offset.h); + p->screen_rect.extent = isect.extent; + if (out_zone_rel != NULL) { + out_zone_rel[out->count].offset.w = isect.offset.w - zone->offset.w; + out_zone_rel[out->count].offset.h = isect.offset.h - zone->offset.h; + out_zone_rel[out->count].extent = isect.extent; + } + out->count++; + } + return out->count; +} + /*! * The WHOLE window of table @p m as window metrics relative to segment @p k's * screen: that screen's display rect and physical size, the window's client diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp index 5b4e9265f1..5f2983ca69 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp @@ -29779,7 +29779,8 @@ comp_d3d11_service_get_zone_weave_screen(struct xrt_system_compositor *xsysc, } // The element's screen, by the table's geometry (u_weave_zone_screen_segment): - // a straddling element, one on the primary, a resize in flight -> none. + // the screen holding most of it. One mostly on the primary, a resize in + // flight -> none (the session's own framing). const int32_t k = u_weave_zone_screen_segment(out, zone, live_w, live_h); if (k < 0) { memset(out, 0, sizeof(*out)); diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.h b/src/xrt/compositor/d3d11_service/comp_d3d11_service.h index 655b69d422..6fff03ec27 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.h +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.h @@ -1578,8 +1578,9 @@ comp_d3d11_service_weave_segment_count(struct xrt_compositor *xc); /*! * The screen a zone-scoped locate of client @p xc must be framed against * (ADR-047 Amendment 4, addendum): when the window @p xc frames its locates - * against is woven per screen and the element @p zone (window px) lies wholly - * on a NON-primary screen of it (u_weave_zone_screen_segment, with the live + * against is woven per screen and the screen holding most of the element + * @p zone (window px; the whole element when it lies on one screen) is a + * NON-primary screen of it (u_weave_zone_screen_segment, with the live * client size @p live_w x @p live_h), @p out is that window's per-screen table * and @p out_index the element's segment, whose eyes are predicted now from its * segment DP (the table's own, as of that weave, when the DP cannot answer). diff --git a/src/xrt/ipc/server/ipc_server_handler.c b/src/xrt/ipc/server/ipc_server_handler.c index 28cd310406..d0e2692009 100644 --- a/src/xrt/ipc/server/ipc_server_handler.c +++ b/src/xrt/ipc/server/ipc_server_handler.c @@ -779,15 +779,16 @@ ipc_try_get_sr_view_poses(volatile struct ipc_client_state *ics, // present-owner's bound window can be woven per screen; its locates // were still framed from the primary screen (the session DP's eyes, the // window relative to the primary's display). An element that lies - // WHOLLY on another screen now gets that screen's eyes and is framed - // against that screen's display: swap in the whole window relative to - // it, and the zone rebase below places the element on it exactly as it - // places one on the primary. A straddling element, an element on the - // primary, and every window on one screen keep the framing above, byte - // for byte (no per-screen table exists for a window on one screen: the - // lookup answers false before anything else is touched). Per-part views - // for a straddling element need a second view set per element - // (displayxr-browser-pvt#272). + // wholly or MOSTLY on another screen now gets that screen's eyes and is + // framed against that screen's display: swap in the whole window + // relative to it, and the zone rebase below places the element on it + // exactly as it places one on the primary. A straddling element is + // framed from the screen holding most of it, the rule every session + // shares (multi-screen § 8, decided 2026-10-12; it kept the primary's + // framing before). An element mostly on the primary, and every window + // on one screen, keep the framing above, byte for byte (no per-screen + // table exists for a window on one screen: the lookup answers false + // before anything else is touched). if (have_wm && wm_from_app_window && rig != NULL && rig->zone_valid != 0 && rig->zone_w_px > 0 && rig->zone_h_px > 0) { struct xrt_rect zone_rect; diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index 5d71fdf4bc..f474efa1b7 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -38,6 +38,7 @@ #include "oxr_defines.h" #include "oxr_frame_sync.h" #include "oxr_views_change.h" +#include "oxr_segment_views.h" #if defined(XRT_HAVE_D3D11) || defined(XRT_HAVE_D3D12) #include @@ -3321,6 +3322,11 @@ struct oxr_session bool warned_validate_unlocated; bool warned_validate_rect_mismatch; bool warned_frame_end_info_legacy; + //! XrDisplayZonePartsDXR (spec v4): the zones split per part for + //! the current display time (the per-frame cap, DXR_ZONE_PARTS_MAX). + struct oxr_zone_parts_budget parts_budget; + //! Change-only log latch: the last split decision logged. + uint32_t parts_logged_sig; } display_zones; #endif diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.c b/src/xrt/state_trackers/oxr/oxr_segment_views.c index 955043ff62..faddbc17d9 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.c +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.c @@ -8,6 +8,8 @@ #include "oxr_segment_views.h" +#include "util/u_weave_rect_parts.h" + #include @@ -210,6 +212,42 @@ oxr_segment_views_whole_window_metrics(const struct xrt_segment_metrics *m, uint out->valid = true; } +uint32_t +oxr_segment_views_zone_segment(const struct xrt_segment_metrics *m, const struct xrt_rect *zone, uint32_t fallback) +{ + const int32_t k = u_weave_rect_majority_segment(m, zone); + if (k < 0) { + return fallback; + } + const struct xrt_segment_metric *s = &m->seg[k]; + if (s->screen_width_m <= 0.0f || s->screen_height_m <= 0.0f || s->screen_desktop_width == 0 || + s->screen_desktop_height == 0) { + return fallback; + } + return (uint32_t)k; +} + +uint32_t +oxr_segment_views_zone_parts(const struct xrt_segment_metrics *m, + const struct xrt_rect *zone, + struct xrt_segment_metrics *parts, + struct xrt_rect zone_rel[XRT_MAX_SEGMENTS], + struct oxr_segment_layout *layout) +{ + if (parts == NULL || layout == NULL) { + return 0; + } + const uint32_t n = u_weave_zone_part_table(m, zone, parts, zone_rel); + if (n < 2) { + return 0; + } + // Every part needs its screen's size and desktop rect (the layout checks). + if (!oxr_segment_views_layout(parts, layout)) { + return 0; + } + return n; +} + uint32_t oxr_segment_views_assign(uint32_t segment_count, uint32_t per_segment, diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index 6652e29fc5..befcec3a85 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -20,6 +20,10 @@ * - @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. + * - @ref oxr_segment_views_zone_segment — the screen a zone-scoped locate's + * one view set is framed from (ADR-047 Amendment 9; majority rule, § 8). + * - @ref oxr_segment_views_zone_parts — a straddling zone's parts, one view + * set each (XrDisplayZonePartsDXR, multi-screen § 8.1). */ #pragma once @@ -197,6 +201,104 @@ oxr_segment_views_window_metrics(const struct xrt_segment_metrics *m, void oxr_segment_views_whole_window_metrics(const struct xrt_segment_metrics *m, uint32_t i, struct xrt_window_metrics *out); +/*! + * Which segment's screen frames a zone-scoped locate's ONE view set (an + * `XR_DXR_display_zones` zone, or an inline-3D element) on a window woven per + * screen: the segment holding the LARGEST part of @p zone (window px, the same + * space as xrt_segment_metric::window_rect; ties to the primary, then the + * leftmost), or @p fallback. + * + * A zone wholly on one screen is framed from that screen; a zone that + * straddles a seam from the screen holding most of IT, not of the window + * (multi-screen § 8, decided 2026-10-12: one rule with the present-owner path, + * u_weave_zone_screen_segment). The zone is framed from that screen's eyes and + * against that screen's physical rectangle (@ref + * oxr_segment_views_whole_window_metrics relative to it, then the usual zone + * rebase). A caller that chains `XrDisplayZonePartsDXR` also gets a view set + * per part (@ref oxr_segment_views_zone_parts). + * + * @p fallback (the caller passes the window's majority segment) when the zone + * is empty or lands on no segment (off the window), or when its screen's + * physical or desktop size is unknown (no Kooima against it). + */ +uint32_t +oxr_segment_views_zone_segment(const struct xrt_segment_metrics *m, const struct xrt_rect *zone, uint32_t fallback); + +/*! + * A straddling zone's parts as a segment table of their own + * (u_weave_zone_part_table), placed in ONE frame (@ref oxr_segment_views_layout + * of the parts: the frame is the display space of the screen holding most of + * the zone, and the union is the zone). Each part is then located exactly like + * a window segment: @ref oxr_segment_views_window_metrics for its canvas, its + * screen's eyes, and @ref oxr_segment_views_vdh_scale / the union offset for a + * display rig, so both parts share ONE m2v and meet at the seam + * (multi-screen § 8.1). + * + * @param[out] parts The parts' table. + * @param[out] zone_rel Each part relative to the zone's top-left (may be NULL). + * @param[out] layout The parts placed in one frame. + * @return the number of parts when the zone straddles a seam and every part + * can be framed (2..XRT_MAX_SEGMENTS); 0 otherwise (one view set). + */ +uint32_t +oxr_segment_views_zone_parts(const struct xrt_segment_metrics *m, + const struct xrt_rect *zone, + struct xrt_segment_metrics *parts, + struct xrt_rect zone_rel[XRT_MAX_SEGMENTS], + struct oxr_segment_layout *layout); + +/*! + * Default cap on the zones that get a view set per part in one frame + * (multi-screen § 8.1): a split zone renders its views once per part. + * `DXR_ZONE_PARTS_MAX` overrides it (0 = never split, at most + * @ref OXR_ZONE_PARTS_MAX_CAP). + */ +#define OXR_ZONE_PARTS_DEFAULT_CAP 4 +//! Upper bound of `DXR_ZONE_PARTS_MAX`. +#define OXR_ZONE_PARTS_MAX_CAP 16 + +/*! + * The per-frame budget of split zones: the distinct zones split for one + * display time, first come first served (an app locates its most important + * zones first). A zone located twice in a frame is counted once. + */ +struct oxr_zone_parts_budget +{ + int64_t display_time; + uint32_t count; + uint32_t zone_id[OXR_ZONE_PARTS_MAX_CAP]; +}; + +/*! + * May zone @p zone_id be split for @p display_time under @p cap? Admits it + * (and counts it) when it was already admitted for that time or the budget + * has room. A new display time starts a new frame's budget. + */ +static inline bool +oxr_zone_parts_budget_admit(struct oxr_zone_parts_budget *b, int64_t display_time, uint32_t zone_id, uint32_t cap) +{ + if (b == NULL) { + return false; + } + if (cap > OXR_ZONE_PARTS_MAX_CAP) { + cap = OXR_ZONE_PARTS_MAX_CAP; + } + if (b->display_time != display_time) { + b->display_time = display_time; + b->count = 0; + } + for (uint32_t i = 0; i < b->count; i++) { + if (b->zone_id[i] == zone_id) { + return true; + } + } + if (b->count >= cap) { + return false; + } + b->zone_id[b->count++] = zone_id; + return true; +} + /*! * 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 diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index cc409e9b1d..2032cb05f9 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -230,6 +230,14 @@ DEBUG_GET_ONCE_BOOL_OPTION(hand_tracking_prioritize_conforming, "OXR_HAND_TRACKI */ DEBUG_GET_ONCE_BOOL_OPTION(mode_floor, "DXR_MODE_FLOOR", true) +/* + * XrDisplayZonePartsDXR (XR_DXR_display_zones v4, multi-screen § 8.1): + * how many zones per display time get a view set per part. A split zone renders + * its views once per part, so the budget is capped; beyond it a zone keeps its + * one view set. 0 = never split. Clamped to OXR_ZONE_PARTS_MAX_CAP. + */ +DEBUG_GET_ONCE_NUM_OPTION(zone_parts_max, "DXR_ZONE_PARTS_MAX", OXR_ZONE_PARTS_DEFAULT_CAP) + bool oxr_session_mode_floor_enabled(void) { @@ -3000,6 +3008,9 @@ struct oxr_locate_segment uint64_t screen_id; //! Views this call locates (the per-segment count); 0 = the session's. uint32_t view_count; + //! @ref wm already IS the canvas (one part of a zone, XrDisplayZonePartsDXR): + //! the zone rebase of a zone-scoped locate is skipped. + bool canvas_is_final; //! Publish session-level side effects (#441 tracking edge): only the //! majority segment's call does. bool side_effects; @@ -3620,7 +3631,7 @@ locate_views_one(struct oxr_logger *log, // composes relative to that rect rather than the client // window; zones frames ignore the output rect at the // compositor in P2 (ADR-027 rule 2). - if (zone != NULL && have_wm && wm.valid) { + if (zone != NULL && have_wm && wm.valid && (seg == NULL || !seg->canvas_is_final)) { struct u_canvas_rect zone_canvas = { .valid = true, .x = zone->rect.offset.x, @@ -4687,22 +4698,15 @@ locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics } /*! - * 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). + * Is this locate display-centric? Per-segment (and per-part) 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. */ static bool -locate_can_split(struct oxr_session *sess, const XrViewLocateInfo *info) +locate_is_display_centric(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) { @@ -4727,6 +4731,25 @@ locate_can_split(struct oxr_session *sess, const XrViewLocateInfo *info) return true; } +/*! + * Can this locate be split per segment? Only a display-centric one, and never a + * zone-scoped one: it already names its own canvas and keeps one view set in + * XrView[] (a straddling zone's per-part sets ride XrDisplayZonePartsDXR, + * locate_zone_parts; under PRIMARY_MULTIVIEW_DXR too, multi-screen § 8.1). + * The compositor crops a one-set locate 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 + return locate_is_display_centric(sess, info); +} + /*! * ADR-047 Amendment 7: note, for the side-effects locate, whether any segment * other than the one it takes its eyes from has a DP reporting a viewer. @@ -4844,6 +4867,114 @@ oxr_session_take_segment_view_routing(struct oxr_session *sess, struct xrt_segme locate_log_route_change(sess, out); } +#ifdef OXR_HAVE_DXR_display_zones +//! The XrDisplayZonePartsDXR the app chained on this locate's XrViewState, or NULL. +static XrDisplayZonePartsDXR * +locate_zone_parts_out(struct oxr_session *sess, XrViewState *viewState) +{ + if (!sess->sys->inst->extensions.DXR_display_zones || viewState == NULL) { + return NULL; + } + return OXR_GET_OUTPUT_FROM_CHAIN(viewState, XR_TYPE_DISPLAY_ZONE_PARTS_DXR, XrDisplayZonePartsDXR); +} + +/*! + * XrDisplayZonePartsDXR (XR_DXR_display_zones v4, multi-screen § 8.1): a + * zone-scoped locate of a zone that straddles a seam of this window's segment + * table @p m gets one view set PER PART, each located like a window segment + * (locate_segment_override over the parts' own table): that screen's eyes, the + * part as the canvas on that screen, and one rig scale for the whole zone. The + * app's XrView[] (the zone's one set, already located) is untouched. + * + * Leaves @p out at "not split" (the caller zeroed it) for a zone on one screen, + * a camera-centric locate, a part whose screen cannot be framed, or a spent + * per-frame budget (DXR_ZONE_PARTS_MAX). + */ +static XrResult +locate_zone_parts(struct oxr_logger *log, + struct oxr_session *sess, + const XrViewLocateInfo *viewLocateInfo, + const struct xrt_segment_metrics *m, + const XrDisplayZoneDXR *zone, + uint32_t per_part, + XrDisplayZonePartsDXR *out) +{ + if (out == NULL || zone == NULL || per_part == 0 || per_part > XRT_MAX_VIEWS || + !locate_is_display_centric(sess, viewLocateInfo)) { + return XR_SUCCESS; + } + struct xrt_rect zr; + zr.offset.w = zone->rect.offset.x; // xrt_offset names its fields w/h + zr.offset.h = zone->rect.offset.y; + zr.extent.w = zone->rect.extent.width; + zr.extent.h = zone->rect.extent.height; + + struct xrt_segment_metrics pm; + struct xrt_rect rel[XRT_MAX_SEGMENTS]; + struct oxr_segment_layout pl; + const uint32_t n = oxr_segment_views_zone_parts(m, &zr, &pm, rel, &pl); + if (n < 2) { + return XR_SUCCESS; + } + const uint32_t cap = (uint32_t)debug_get_num_option_zone_parts_max(); + const bool admitted = oxr_zone_parts_budget_admit(&sess->display_zones.parts_budget, + viewLocateInfo->displayTime, zone->zoneId, cap); + { + // Change-only (never per frame): the split decision for the last zone. + const uint32_t sig = 1u + (admitted ? 1u : 0u) + (n << 4) + (per_part << 8) + (cap << 16); + if (sig != sess->display_zones.parts_logged_sig) { + sess->display_zones.parts_logged_sig = sig; + U_LOG_I("zone parts: zone %u straddles %u screens - %s (%u views per part, cap %u per frame)", + zone->zoneId, n, admitted ? "one view set per part" : "budget spent, one view set", + per_part, cap); + } + } + if (!admitted) { + return XR_SUCCESS; + } + + out->generation = m->generation; + out->partCountOutput = n; + out->viewCountOutput = n * per_part; + if (out->partCapacityInput < n || out->viewCapacityInput < n * per_part || out->parts == NULL || + out->views == NULL) { + return XR_SUCCESS; // a count query, or too small: the counts only + } + + for (uint32_t p = 0; p < n; p++) { + struct oxr_locate_segment o; + locate_segment_override(&pm, &pl, p, per_part, &o); + o.view_count = per_part; + o.side_effects = false; + o.canvas_is_final = true; + o.out = NULL; + + 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}; + uint32_t got = 0; + XrResult ret = locate_views_one(log, sess, viewLocateInfo, &bare, XRT_MAX_VIEWS, &got, tmp, &o); + if (ret != XR_SUCCESS) { + out->partCountOutput = 0; + out->viewCountOutput = 0; + return ret; + } + XrDisplayZonePartDXR *dp = &out->parts[p]; + dp->displayId = pm.seg[p].screen_id; + dp->rect = (XrRect2Di){{rel[p].offset.w, rel[p].offset.h}, {rel[p].extent.w, rel[p].extent.h}}; + dp->firstView = p * per_part; + dp->viewCount = per_part; + for (uint32_t v = 0; v < per_part; v++) { + out->views[p * per_part + v].pose = tmp[v].pose; + out->views[p * per_part + v].fov = tmp[v].fov; + } + } + return XR_SUCCESS; +} +#endif // OXR_HAVE_DXR_display_zones + XrResult oxr_session_locate_views(struct oxr_logger *log, struct oxr_session *sess, @@ -4855,6 +4986,15 @@ oxr_session_locate_views(struct oxr_logger *log, { struct xrt_segment_metrics m; struct xrt_device *xdev = GET_XDEV_BY_ROLE(sess->sys, head); +#ifdef OXR_HAVE_DXR_display_zones + // XrDisplayZonePartsDXR: "not split" unless the zone's parts are located below. + XrDisplayZonePartsDXR *zone_parts = locate_zone_parts_out(sess, viewState); + if (zone_parts != NULL) { + zone_parts->generation = 0; + zone_parts->partCountOutput = 0; + zone_parts->viewCountOutput = 0; + } +#endif // ADR-047 Amendment 7: no other presenting DP unless the table says so. sess->locate_others_tracked = false; sess->locate_others_count = 0; @@ -4888,24 +5028,67 @@ oxr_session_locate_views(struct oxr_logger *log, * (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. + * + * A zone-scoped locate (XR_DXR_display_zones; never split, see + * locate_can_split) frames ONE zone, not the window: when that zone lies + * wholly on one screen it is framed from THAT screen (its eyes, the + * window relative to it, then the usual zone rebase), so a zone on the + * second screen of a straddling window looks around from that screen's + * viewer, not the majority's (displayxr-browser-pvt#272; the in-process / + * direct-IPC twin of ADR-047 Amendment 4 addendum). A zone that + * straddles a seam is framed from the screen holding most of the ZONE + * (oxr_segment_views_zone_segment; the same rule as the present-owner + * path, multi-screen § 8), and a caller chaining XrDisplayZonePartsDXR + * also gets one view set per part (locate_zone_parts). */ if (!is_multiview || !locate_can_split(sess, viewLocateInfo)) { + uint32_t fk = mj; // the segment whose screen frames this one view set +#ifdef OXR_HAVE_DXR_display_zones + const XrDisplayZoneDXR *zone = NULL; + if (sess->sys->inst->extensions.DXR_display_zones) { + zone = OXR_GET_INPUT_FROM_CHAIN(viewLocateInfo, XR_TYPE_DISPLAY_ZONE_DXR, XrDisplayZoneDXR); + if (zone != NULL && (zone->rect.extent.width <= 0 || zone->rect.extent.height <= 0)) { + zone = NULL; // ignored, as locate_views_one ignores it + } + if (zone != NULL) { + struct xrt_rect zr; + zr.offset.w = zone->rect.offset.x; // xrt_offset names its fields w/h + zr.offset.h = zone->rect.offset.y; + zr.extent.w = zone->rect.extent.width; + zr.extent.h = zone->rect.extent.height; + fk = oxr_segment_views_zone_segment(&m, &zr, mj); + // Change-only (never per frame): the last screen a zone was + // framed from instead of the majority's. Not logged back: an + // app with zones on both screens would flip it every locate. + static uint64_t s_zone_screen_logged = 0; + if (fk != mj && m.seg[fk].screen_id != s_zone_screen_logged) { + s_zone_screen_logged = m.seg[fk].screen_id; + U_LOG_W( + "multi-screen: zone %u (%d,%d %dx%d) mostly on screen 0x%016llx - views " + "from that screen's eyes and geometry, not the window majority's", + zone->zoneId, zone->rect.offset.x, zone->rect.offset.y, + zone->rect.extent.width, zone->rect.extent.height, + (unsigned long long)m.seg[fk].screen_id); + } + } + } +#endif XrResult ret; - if (m.seg[mj].is_primary) { + if (m.seg[fk].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]; + oxr_segment_views_whole_window_metrics(&m, fk, &o.wm); + const struct xrt_segment_metric *s = &m.seg[fk]; 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; if (o.wm.valid) { - locate_note_others_tracking(sess, &m, (int32_t)mj); + locate_note_others_tracking(sess, &m, (int32_t)fk); } struct oxr_locate_segment_out so = {.measurable = true}; o.out = &so; @@ -4926,6 +5109,23 @@ oxr_session_locate_views(struct oxr_logger *log, // NOT touched — only a splitting locate writes it. const struct xrt_segment_view_routing none = {0}; locate_fill_bindings(sess, viewState, &none); +#ifdef OXR_HAVE_DXR_display_zones + // A straddling zone: its parts, one view set each, beside XrView[] + // (multi-screen § 8.1; PRIMARY_MULTIVIEW_DXR sessions too). + if (zone_parts != NULL && zone != NULL) { + uint32_t per_part = sess->view_config_view_count != 0 ? sess->view_config_view_count + : xdev->hmd->view_count; + if (per_part > xdev->hmd->view_count) { + per_part = xdev->hmd->view_count; + } + const uint32_t mode = xdev->hmd->active_rendering_mode_index; + if (mode < xdev->rendering_mode_count && + xdev->rendering_modes[mode].view_count < per_part) { + per_part = xdev->rendering_modes[mode].view_count; + } + ret = locate_zone_parts(log, sess, viewLocateInfo, &m, zone, per_part, zone_parts); + } +#endif } return ret; } diff --git a/tests/tests_weave_segments.cpp b/tests/tests_weave_segments.cpp index e1cff783e9..ab823aff4c 100644 --- a/tests/tests_weave_segments.cpp +++ b/tests/tests_weave_segments.cpp @@ -10,7 +10,13 @@ * addendum: the screen whose eyes and geometry frame an element's * zone-scoped locate (u_weave_zone_screen_segment + * u_weave_segment_window_metrics), and the submit reply's cheap split - * signal (u_weave_segments_fetch_needed). + * signal (u_weave_segments_fetch_needed). Amendment 9: the in-process / + * direct-IPC twin of that rule (oxr_segment_views_zone_segment). + * Multi-screen § 8 (decided 2026-10-12): a straddling zone's one view + * set is framed from the screen holding most of the ZONE on every path + * (u_weave_rect_majority_segment), and its parts get a view set each + * (u_weave_zone_part_table, oxr_segment_views_zone_parts, XrDisplayZonePartsDXR) + * under a per-frame budget (oxr_zone_parts_budget_admit). */ #include "util/comp_segments.h" @@ -375,7 +381,7 @@ TEST_CASE("element views: an element wholly on screen 2 is framed from screen 2' CHECK(wrong.window_center_offset_x_m - el.window_center_offset_x_m == Catch::Approx(1920.0f * px)); } -TEST_CASE("element views: a straddling element keeps the primary's views") +TEST_CASE("element views: a straddling element is framed from the screen holding most of it") { comp_segments_screen screens[2]; two_screens(screens); @@ -384,19 +390,32 @@ TEST_CASE("element views: a straddling element keeps the primary's views") REQUIRE(m.count == 2); with_geometry(m, screens, 2); - // Window x 400..700 crosses the seam at 520: woven per part, but one view - // set per element until the page can take two (browser-pvt#272). + // Window x 400..700 crosses the seam at 520: 120 px on the primary, 180 on + // screen 2. Its one view set comes from screen 2 (multi-screen § 8, + // decided 2026-10-12: it kept the primary's before). const xrt_rect straddle = xr(400, 50, 300, 200); CHECK(u_weave_rect_single_segment(&m, &straddle) == -1); - CHECK(u_weave_zone_screen_segment(&m, &straddle, win.w, win.h) == -1); + CHECK(u_weave_rect_majority_segment(&m, &straddle) == 1); + CHECK(u_weave_zone_screen_segment(&m, &straddle, win.w, win.h) == 1); - // One pixel across the seam is already straddling... + // Mostly on the primary (one pixel across the seam): the session's own framing. const xrt_rect sliver = xr(420, 50, 101, 200); + CHECK(u_weave_rect_majority_segment(&m, &sliver) == 0); CHECK(u_weave_zone_screen_segment(&m, &sliver, win.w, win.h) == -1); - // ...and flush against it, the element is wholly on the primary. + // Flush against the seam, the element is wholly on the primary. const xrt_rect flush = xr(420, 50, 100, 200); CHECK(u_weave_rect_single_segment(&m, &flush) == 0); CHECK(u_weave_zone_screen_segment(&m, &flush, win.w, win.h) == -1); + // An exact tie goes to the primary. + const xrt_rect tie = xr(420, 50, 200, 200); + CHECK(u_weave_rect_majority_segment(&m, &tie) == 0); + CHECK(u_weave_zone_screen_segment(&m, &tie, win.w, win.h) == -1); + // Off the window, or no table: no screen. + const xrt_rect off = xr(2000, 50, 10, 10); + CHECK(u_weave_rect_majority_segment(&m, &off) == -1); + xrt_segment_metrics none; + std::memset(&none, 0, sizeof(none)); + CHECK(u_weave_rect_majority_segment(&none, &straddle) == -1); } TEST_CASE("element views: an element on the primary screen of a split window keeps the session's framing") @@ -505,6 +524,406 @@ TEST_CASE("element views: the server's window metrics are the per-segment views' } } +/* + * The in-process / direct-IPC twin (displayxr-browser-pvt#272): a zone-scoped + * locate on a session whose OWN compositor segments its window (in-process + * D3D11 / D3D12 / GL / Vulkan, or a direct IPC client of the D3D11 service). + * oxr_session_locate_views keeps one view set for it and asks + * oxr_segment_views_zone_segment which screen frames it: the zone's own screen + * when the zone lies wholly on one, else the majority (the pre-#272 answer). + * Unlike the present-owner rule it may answer the PRIMARY screen: an in-process + * window's one view set is framed from the majority screen, so a zone on the + * primary of a window mostly on screen 2 was framed from screen 2 before. + */ + +TEST_CASE("zone framing: a zone wholly on screen 2 is framed from screen 2, as for a present-owner") +{ + comp_segments_screen screens[2]; + two_screens(screens); + const comp_seg_rect win = {1400, 100, 1000, 600}; // seam at window x 520 + xrt_segment_metrics m = publish(win, screens, 2); + REQUIRE(m.count == 2); + with_geometry(m, screens, 2); + const uint32_t mj = oxr_segment_views_majority(&m); + REQUIRE(m.seg[mj].is_primary); // 520 px on the primary, 480 on screen 2 + + const xrt_rect zone = xr(800, 300, 150, 120); + const uint32_t k = oxr_segment_views_zone_segment(&m, &zone, mj); + REQUIRE(k == 1); + CHECK(m.seg[k].screen_id == SECOND_ID); + // The present-owner rule picks the same screen. + CHECK(u_weave_zone_screen_segment(&m, &zone, win.w, win.h) == (int32_t)k); + + // The canvas the locate frames: the whole window relative to screen 2, + // rebased to the zone - the element where it sits ON screen 2. + xrt_window_metrics whole; + oxr_segment_views_whole_window_metrics(&m, k, &whole); + REQUIRE(whole.valid); + const xrt_window_metrics el = zone_canvas(whole, zone); + u_weave_rect_part part; + REQUIRE(u_weave_rect_parts(&m, &zone, 1, &part, 1) == 1); + CHECK(el.window_screen_left - el.display_screen_left == part.screen_rect.offset.w); + CHECK(el.window_screen_top - el.display_screen_top == part.screen_rect.offset.h); + const float px = PANEL_W_M / 1920.0f; + CHECK(el.window_center_offset_x_m == Catch::Approx((280.0f + 75.0f - 960.0f) * px)); + + // And screen 2's eyes, which oxr_segment_views_segment_eyes accepts + // untranslated (the whole-window canvas is in that screen's own space). + xrt_eye_positions eyes; + REQUIRE(oxr_segment_views_segment_eyes(&m.seg[k], 0.0f, 0.0f, 2, &eyes)); + CHECK(eyes.eyes[0].x == Catch::Approx(0.068f)); + CHECK(eyes.eyes[1].x == Catch::Approx(0.132f)); +} + +TEST_CASE("zone framing: a zone on the primary of a window mostly on screen 2 is framed from the primary") +{ + comp_segments_screen screens[2]; + two_screens(screens); + const comp_seg_rect win = {1700, 100, 1000, 600}; // 220 px on the primary, 780 on screen 2 + xrt_segment_metrics m = publish(win, screens, 2); + REQUIRE(m.count == 2); + with_geometry(m, screens, 2); + const uint32_t mj = oxr_segment_views_majority(&m); + REQUIRE(m.seg[mj].screen_id == SECOND_ID); + + // Wholly on the primary: its screen frames it (the locate's single-screen + // path, the session DP's own eyes and window metrics), not the majority. + const xrt_rect zone = xr(10, 10, 200, 100); + const uint32_t k = oxr_segment_views_zone_segment(&m, &zone, mj); + CHECK(k != mj); + CHECK(m.seg[k].is_primary); + // The present-owner rule leaves this one to the session's framing, which + // for a present-owner already IS the primary's. + CHECK(u_weave_zone_screen_segment(&m, &zone, win.w, win.h) == -1); + + // A zone wholly on the majority screen is framed from it, as before. + const xrt_rect on_major = xr(500, 10, 200, 100); + CHECK(oxr_segment_views_zone_segment(&m, &on_major, mj) == mj); +} + +TEST_CASE("zone framing: a straddling zone is framed from ITS majority; empty or off-window keeps the window's") +{ + comp_segments_screen screens[2]; + two_screens(screens); + const comp_seg_rect win = {1400, 100, 1000, 600}; + xrt_segment_metrics m = publish(win, screens, 2); + REQUIRE(m.count == 2); + with_geometry(m, screens, 2); + const uint32_t mj = oxr_segment_views_majority(&m); + + // A temporary zone lives for the whole call it is passed to. + auto zs = [mj](const xrt_segment_metrics &t, const xrt_rect &z) { + return oxr_segment_views_zone_segment(&t, &z, mj); + }; + // Across the seam at 520: the screen holding most of the ZONE (120 px on + // the primary, 180 on screen 2), not the window's majority (the primary). + REQUIRE(mj == 0); + CHECK(zs(m, xr(400, 50, 300, 200)) == 1); + // One pixel across: mostly on the primary. + CHECK(zs(m, xr(420, 50, 101, 200)) == 0); + // Flush against the seam: wholly on the primary. + CHECK(zs(m, xr(420, 50, 100, 200)) == 0); + // Flush on the other side: wholly on screen 2. + CHECK(zs(m, xr(520, 50, 100, 200)) == 1); + // Empty, and wholly outside the window. + CHECK(zs(m, xr(800, 300, 0, 120)) == mj); + CHECK(zs(m, xr(2000, 300, 50, 50)) == mj); + // No table, or no zone. + xrt_segment_metrics none; + std::memset(&none, 0, sizeof(none)); + CHECK(zs(none, xr(10, 10, 10, 10)) == mj); + CHECK(oxr_segment_views_zone_segment(&m, nullptr, mj) == mj); + + // A screen of unknown size cannot be framed against: majority. + xrt_segment_metrics no_size = m; + no_size.seg[1].screen_height_m = 0.0f; + CHECK(zs(no_size, xr(800, 300, 150, 120)) == mj); + no_size = m; + no_size.seg[1].screen_desktop_width = 0; + CHECK(zs(no_size, xr(800, 300, 150, 120)) == mj); +} + +TEST_CASE("zone framing: one majority rule for the present-owner and every other session") +{ + // Sweep zones across a straddling window in both layouts: the state + // tracker's rule and the server's (u_weave_zone_screen_segment) pick the + // same screen - the one holding most of the zone - wherever the server + // re-frames, and the server keeps the session's own (primary) framing + // exactly when that screen is the primary (multi-screen § 8.1). + comp_segments_screen screens[2]; + two_screens(screens); + for (int layout = 0; layout < 2; layout++) { + if (layout == 1) { + screens[1].desktop = {-1920, 0, 1920, 1080}; + } + const comp_seg_rect win = + layout == 0 ? comp_seg_rect{1400, 100, 1000, 600} : comp_seg_rect{-300, 40, 1000, 500}; + xrt_segment_metrics m = publish(win, screens, 2); + REQUIRE(m.count == 2); + with_geometry(m, screens, 2); + const uint32_t mj = oxr_segment_views_majority(&m); + for (int x = 0; x < 1000; x += 37) { + for (int w = 10; w < 400; w += 61) { + const xrt_rect zone = xr(x, 20, w, 80); + const int32_t server = u_weave_zone_screen_segment(&m, &zone, win.w, win.h); + const int32_t single = u_weave_rect_single_segment(&m, &zone); + const int32_t major = u_weave_rect_majority_segment(&m, &zone); + const uint32_t client = oxr_segment_views_zone_segment(&m, &zone, mj); + REQUIRE(major >= 0); + CHECK(client == (uint32_t)major); + if (single >= 0) { + CHECK(major == single); + } + if (server >= 0) { + CHECK(client == (uint32_t)server); + } else { + CHECK(m.seg[client].is_primary); + } + // The parts' own window majority is the zone's majority: + // the frame the parts are placed in is that screen's. + xrt_segment_metrics pm; + const uint32_t np = u_weave_zone_part_table(&m, &zone, &pm, nullptr); + REQUIRE(np >= 1); + CHECK(pm.seg[oxr_segment_views_majority(&pm)].screen_id == m.seg[client].screen_id); + } + } + } +} + +/* + * Multi-screen § 8.1: a straddling zone's parts, one view set each + * (XrDisplayZonePartsDXR). oxr_session_locate_views locates part p with + * locate_segment_override over the parts' own table: the part as the canvas on + * its own screen, that screen's eyes carried into the frame of the screen + * holding most of the zone, and one rig scale for the whole zone. + */ + +TEST_CASE("zone parts: a straddling zone is cut at the seam, zone-relative and on its screens") +{ + comp_segments_screen screens[2]; + two_screens(screens); + const comp_seg_rect win = {1400, 100, 1000, 600}; // seam at window x 520 + xrt_segment_metrics m = publish(win, screens, 2); + REQUIRE(m.count == 2); + with_geometry(m, screens, 2); + m.generation = 7; + + const xrt_rect zone = xr(400, 50, 300, 200); + xrt_segment_metrics pm; + xrt_rect rel[XRT_MAX_SEGMENTS]; + REQUIRE(u_weave_zone_part_table(&m, &zone, &pm, rel) == 2); + CHECK(pm.count == 2); + CHECK(pm.generation == 7); + CHECK(pm.window_pixel_width == m.window_pixel_width); + // Left part on the primary: window x 400..520, at 1800 on the desktop. + CHECK(pm.seg[0].screen_id == PRIMARY_ID); + check_rect(pm.seg[0].window_rect, 400, 50, 120, 200); + check_rect(pm.seg[0].screen_rect, 1800, 150, 120, 200); + check_rect(rel[0], 0, 0, 120, 200); + // Right part on screen 2: window x 520..700, its screen x 0..180. + CHECK(pm.seg[1].screen_id == SECOND_ID); + check_rect(pm.seg[1].window_rect, 520, 50, 180, 200); + check_rect(pm.seg[1].screen_rect, 0, 150, 180, 200); + check_rect(rel[1], 120, 0, 180, 200); + // The eyes ride along verbatim. + CHECK(pm.seg[1].eyes.eyes[0].x == Catch::Approx(0.068f)); + // The same cut as the weave's rect parts. + u_weave_rect_part wp[2]; + REQUIRE(u_weave_rect_parts(&m, &zone, 1, wp, 2) == 2); + for (uint32_t k = 0; k < 2; k++) { + CHECK(std::memcmp(&wp[k].window_rect, &pm.seg[k].window_rect, sizeof(xrt_rect)) == 0); + CHECK(std::memcmp(&wp[k].screen_rect, &pm.seg[k].screen_rect, sizeof(xrt_rect)) == 0); + CHECK(std::memcmp(&wp[k].rect_rel, &rel[k], sizeof(xrt_rect)) == 0); + } + + // Located as parts: two, in one frame. + xrt_segment_metrics pm2; + oxr_segment_layout l; + CHECK(oxr_segment_views_zone_parts(&m, &zone, &pm2, rel, &l) == 2); + CHECK(l.count == 2); +} + +TEST_CASE("zone parts: a zone on one screen, off the window, or of unknown screen size is not split") +{ + comp_segments_screen screens[2]; + two_screens(screens); + const comp_seg_rect win = {1400, 100, 1000, 600}; + xrt_segment_metrics m = publish(win, screens, 2); + with_geometry(m, screens, 2); + xrt_segment_metrics pm; + xrt_rect rel[XRT_MAX_SEGMENTS]; + oxr_segment_layout l; + auto parts = [&](const xrt_segment_metrics &t, const xrt_rect &z) { + return oxr_segment_views_zone_parts(&t, &z, &pm, rel, &l); + }; + CHECK(parts(m, xr(10, 10, 100, 100)) == 0); // wholly on the primary + CHECK(parts(m, xr(800, 10, 100, 100)) == 0); // wholly on screen 2 + CHECK(parts(m, xr(420, 10, 100, 100)) == 0); // flush against the seam + CHECK(parts(m, xr(2000, 10, 10, 10)) == 0); // off the window + CHECK(parts(m, xr(400, 10, 0, 100)) == 0); // empty + CHECK(parts(m, xr(420, 10, 101, 100)) == 2); // one pixel across IS split + xrt_segment_metrics none; + std::memset(&none, 0, sizeof(none)); + CHECK(parts(none, xr(400, 10, 300, 100)) == 0); // a window on one screen + xrt_segment_metrics no_size = m; + no_size.seg[1].screen_width_m = 0.0f; + CHECK(parts(no_size, xr(400, 10, 300, 100)) == 0); + CHECK(oxr_segment_views_zone_parts(&m, nullptr, &pm, rel, &l) == 0); +} + +namespace { + +/* + * One part as oxr_session_locate_views frames it: the canvas + * (oxr_segment_views_window_metrics), the rig offset and virtual display + * height factor (locate_segment_override), and the canvas' top-left / centre + * on its OWN screen. + */ +struct part_frame +{ + xrt_window_metrics wm; + float rig_dx, rig_dy, vdh_scale; + float own_dx, own_dy; // own -> reference translation +}; + +part_frame +frame_part(const xrt_segment_metrics &pm, const oxr_segment_layout &l, uint32_t p) +{ + part_frame f; + oxr_segment_views_window_metrics(&pm, &l, p, &f.wm); + f.rig_dx = l.seg[p].ref_cx - l.window_ref_cx; + f.rig_dy = l.seg[p].ref_cy - l.window_ref_cy; + f.vdh_scale = oxr_segment_views_vdh_scale(&l, p); + oxr_segment_views_own_to_ref(&l, p, &f.own_dx, &f.own_dy); + return f; +} + +} // namespace + +TEST_CASE("zone parts: each part is its own screen's canvas, one rig scale, meeting at the seam") +{ + comp_segments_screen screens[2]; + two_screens(screens); + // Window mostly on the primary; the ZONE mostly on screen 2. + const comp_seg_rect win = {1400, 100, 1000, 600}; // seam at window x 520 + xrt_segment_metrics m = publish(win, screens, 2); + with_geometry(m, screens, 2); + REQUIRE(m.seg[oxr_segment_views_majority(&m)].is_primary); + + const xrt_rect zone = xr(400, 50, 300, 200); + xrt_segment_metrics pm; + xrt_rect rel[XRT_MAX_SEGMENTS]; + oxr_segment_layout l; + REQUIRE(oxr_segment_views_zone_parts(&m, &zone, &pm, rel, &l) == 2); + // The frame is the zone's majority screen: screen 2. + REQUIRE(pm.seg[l.majority].screen_id == SECOND_ID); + + const float px = PANEL_W_M / 1920.0f; + const float py = PANEL_H_M / 1080.0f; + const part_frame a = frame_part(pm, l, 0); + const part_frame b = frame_part(pm, l, 1); + + // Canvas = the part, on its own screen (what the raw channel reports). + CHECK(a.wm.display_screen_left == 0); + CHECK(a.wm.window_screen_left - a.wm.display_screen_left == 1800); + CHECK(a.wm.window_pixel_width == 120u); + CHECK(a.wm.window_width_m == Catch::Approx(120.0f * px)); + CHECK(b.wm.display_screen_left == 1920); + CHECK(b.wm.window_screen_left - b.wm.display_screen_left == 0); + CHECK(b.wm.window_pixel_width == 180u); + CHECK(b.wm.window_height_m == Catch::Approx(200.0f * py)); + + // Eye minus canvas centre is the eye relative to the part ON ITS OWN + // screen: both are carried into the reference frame by the same + // translation, so the canvas centre minus that translation is the part's + // own-screen centre. + CHECK(b.wm.window_center_offset_x_m - b.own_dx == Catch::Approx((0.0f + 90.0f - 960.0f) * px)); + CHECK(a.wm.window_center_offset_x_m - a.own_dx == Catch::Approx((1800.0f + 60.0f - 960.0f) * px)); + CHECK(a.wm.window_center_offset_y_m - a.own_dy == Catch::Approx(-((150.0f + 100.0f - 540.0f) * py))); + // The majority part's frame is its own. + CHECK(b.own_dx == Catch::Approx(0.0f)); + + // Continuous across the seam in the reference frame: no gap, no overlap. + CHECK(l.seg[0].ref_cx + l.seg[0].w_m * 0.5f == Catch::Approx(l.seg[1].ref_cx - l.seg[1].w_m * 0.5f)); + // The union is the zone. + CHECK(l.window_ref_w == Catch::Approx(300.0f * px)); + CHECK(l.window_ref_h == Catch::Approx(200.0f * py)); + + // One m2v for the whole zone: virtual height x factor / part height is the + // same for both parts (= vH / zone height), and each part's rig offset is + // its centre minus the zone's centre. + const float vH = 1.0f; + const float m2v_a = vH * a.vdh_scale / a.wm.window_height_m; + const float m2v_b = vH * b.vdh_scale / b.wm.window_height_m; + CHECK(m2v_a == Catch::Approx(m2v_b)); + CHECK(m2v_a == Catch::Approx(vH / (200.0f * py))); + CHECK(a.rig_dx == Catch::Approx(-(300.0f * 0.5f - 60.0f) * px)); + CHECK(b.rig_dx == Catch::Approx((300.0f * 0.5f - 90.0f) * px)); + CHECK(a.rig_dy == Catch::Approx(0.0f).margin(1e-6)); + // Scaled by the same m2v, the parts' virtual canvases still abut. + CHECK((a.rig_dx + a.wm.window_width_m * 0.5f) * m2v_a == + Catch::Approx((b.rig_dx - b.wm.window_width_m * 0.5f) * m2v_b).margin(1e-5)); + + // Eyes: screen 2's set in its own frame; the primary's carried over. + xrt_eye_positions ea, eb; + REQUIRE(oxr_segment_views_segment_eyes(&pm.seg[0], a.own_dx, a.own_dy, 2, &ea)); + REQUIRE(oxr_segment_views_segment_eyes(&pm.seg[1], b.own_dx, b.own_dy, 2, &eb)); + CHECK(eb.eyes[0].x == Catch::Approx(0.068f)); + CHECK(ea.eyes[0].x - a.wm.window_center_offset_x_m == Catch::Approx(-0.032f - (1800.0f + 60.0f - 960.0f) * px)); +} + +TEST_CASE("zone parts: panels of different pitch still meet at the seam at one scale") +{ + comp_segments_screen screens[2]; + two_screens(screens); + // Screen 2: the same physical panel at 3840x2160 (half the pitch). + screens[1].desktop = {1920, 0, 3840, 2160}; + screens[1].native_w = 3840; + screens[1].native_h = 2160; + const comp_seg_rect win = {1400, 100, 1000, 600}; + xrt_segment_metrics m = publish(win, screens, 2); + REQUIRE(m.count == 2); + with_geometry(m, screens, 2); + + const xrt_rect zone = xr(300, 100, 400, 300); + xrt_segment_metrics pm; + xrt_rect rel[XRT_MAX_SEGMENTS]; + oxr_segment_layout l; + REQUIRE(oxr_segment_views_zone_parts(&m, &zone, &pm, rel, &l) == 2); + const part_frame a = frame_part(pm, l, 0); + const part_frame b = frame_part(pm, l, 1); + // Same pixel height, different metres. + CHECK(a.wm.window_height_m == Catch::Approx(2.0f * b.wm.window_height_m)); + // The seam is continuous in the reference frame... + CHECK(l.seg[0].ref_cx + l.seg[0].w_m * 0.5f == Catch::Approx(l.seg[1].ref_cx - l.seg[1].w_m * 0.5f)); + // ...and both parts share one metres-to-virtual scale. + CHECK(a.vdh_scale / a.wm.window_height_m == Catch::Approx(b.vdh_scale / b.wm.window_height_m)); +} + +TEST_CASE("zone parts: the per-frame budget admits the first N distinct zones of a display time") +{ + oxr_zone_parts_budget b; + std::memset(&b, 0, sizeof(b)); + const int64_t t0 = 1000; + CHECK(oxr_zone_parts_budget_admit(&b, t0, 10, 2)); + CHECK(oxr_zone_parts_budget_admit(&b, t0, 11, 2)); + // A zone located twice in one frame is counted once. + CHECK(oxr_zone_parts_budget_admit(&b, t0, 10, 2)); + // The third distinct zone keeps one view set. + CHECK_FALSE(oxr_zone_parts_budget_admit(&b, t0, 12, 2)); + // A new display time is a new frame's budget. + CHECK(oxr_zone_parts_budget_admit(&b, t0 + 16, 12, 2)); + CHECK(b.count == 1); + // 0 = never split; the cap is clamped to OXR_ZONE_PARTS_MAX_CAP. + CHECK_FALSE(oxr_zone_parts_budget_admit(&b, t0 + 32, 1, 0)); + for (uint32_t z = 0; z < OXR_ZONE_PARTS_MAX_CAP; z++) { + CHECK(oxr_zone_parts_budget_admit(&b, t0 + 48, z, 1000)); + } + CHECK_FALSE(oxr_zone_parts_budget_admit(&b, t0 + 48, 999, 1000)); + CHECK(OXR_ZONE_PARTS_DEFAULT_CAP == 4); + CHECK_FALSE(oxr_zone_parts_budget_admit(nullptr, t0, 1, 4)); +} + #ifdef DXR_TESTS_HAVE_IPC_PROTOCOL TEST_CASE("element views: weave_submit's reply carries the split signal as a trailing field") {