Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d33261f
feat(display_info): XrViewDisplayBindingsDXR + segment metric types (…
dfattal Oct 7, 2026
283f3db
feat(oxr): per-segment views for PRIMARY_MULTIVIEW_DXR (multi-screen M3)
dfattal Oct 7, 2026
5256b45
feat(vk_native): route each segment's own views to its DP; per-screen…
dfattal Oct 7, 2026
8aa8ba2
feat(cube_handle_vk_linux): multiview, activeViewCount and view displ…
dfattal Oct 7, 2026
cbc02f3
docs: per-segment views in the view-configuration model and Kooima no…
dfattal Oct 7, 2026
68f37bc
docs(comp-segments): per-segment views, the mosaic routing, mixed ven…
dfattal Oct 7, 2026
e0404f4
test(oxr_view_space): PRIMARY_MULTIVIEW_DXR reports device max x XRT_…
dfattal Oct 7, 2026
9d4fb83
feat(oxr): advertise extra multiview view sets only where a window ca…
dfattal Oct 7, 2026
35221c3
fix(vk_native): VIEW_DIMS says 'mosaic', not 'upscaled', under per-se…
dfattal Oct 7, 2026
a9c9400
feat(plugin_loader): DXR_SCREEN_PLUGIN — pin one monitor to a plug-in…
dfattal Oct 7, 2026
f69cda0
fix(oxr): segments take their DP's untracked eyes; nominal eyes fill …
dfattal Oct 7, 2026
8d07774
fix(oxr): only a splitting locate records the frame's segment routing…
dfattal Oct 7, 2026
b29c5eb
fix(vk_native): draw quads and equirects per segment with that segmen…
dfattal Oct 7, 2026
b026b73
fix(oxr): one display-rig m2v across segments (multi-screen M3 review 4)
dfattal Oct 7, 2026
7d7bf67
fix(oxr): VIEW is the centroid of the ACTIVE views, over every segmen…
dfattal Oct 7, 2026
f569321
docs(display_info): XrViewDisplayBindingsDXR count is 0 for one view …
dfattal Oct 7, 2026
ceee825
test(oxr_view_space): exact multiview view count; DXR_SEGMENTS=0 keep…
dfattal Oct 7, 2026
9389474
fix(vk_native): keep per-segment routing across a 2D/3D toggle (multi…
dfattal Oct 7, 2026
34be251
fix(plugin_loader): DXR_SCREEN_PLUGIN plug-in ids match case-insensit…
dfattal Oct 7, 2026
17dc757
fix(sim_display,oxr): a segment's views come from its OWN eye pair in…
dfattal Oct 7, 2026
e7b8464
style: git clang-format the M3 changes against main's .clang-format
dfattal Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 58 additions & 18 deletions docs/architecture/comp-segments.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -35,18 +36,21 @@ 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`
DP (which would describe the wrong panel and may clear the whole target). It is
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.
Expand Down Expand Up @@ -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 `<stem>.seg<i>.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<i>.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).
16 changes: 16 additions & 0 deletions docs/architecture/kooima-projection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
10 changes: 10 additions & 0 deletions docs/guides/displayxr-app-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`, …):
Expand Down
62 changes: 58 additions & 4 deletions docs/reference/view-configuration-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading