Skip to content

feat(display_info): per-segment views and view display bindings (multi-screen M3) - #1854

Merged
dfattal merged 21 commits into
mainfrom
feat/multi-screen-m3-per-segment-views
Oct 7, 2026
Merged

dfattal merged 21 commits into
mainfrom
feat/multi-screen-m3-per-segment-views

Conversation

@dfattal

@dfattal dfattal commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

What / why

Multi-screen M3: per-segment views (ADR-047 decision D3). A window that spans two screens is already woven per segment by M2, but every segment wove a crop of ONE view pair, so the off-primary half had the wrong perspective. Now, under PRIMARY_MULTIVIEW_DXR, each segment gets its own views: located from that screen's eyes, with that segment (relative to its own screen, in its own metres) as the Kooima canvas, and routed to that segment's DP.

Also closes the M4 runtime gap: M2's one-vendor rule is lifted. A segment DP is created from its screen's registry entry (owning iface/instance + ABI-checked VK factory), whichever plug-in won that screen; one INFO line when a mixed-vendor table is built. Every segment DP follows the session's 2D/3D mode (request_display_mode) and gets set_present_origin per frame. DXR_SEGMENTS=0 remains the kill switch.

The view-count contract

  • PRIMARY_MULTIVIEW_DXR reports device max × view-set capacity (capped at XRT_MAX_VIEWS = 8). Capacity = min(XRT_MAX_SEGMENTS (2), DP-registry screens with a VK DP factory), computed once in oxr_system_fill_in, and 1 wherever no compositor can segment a window (every platform but desktop Linux, any service session). So a single-screen box, Windows, macOS and Android keep exactly the pre-M3 count (no change for existing multiview apps or the CTS); ds1-linux gets device max × 2. Per-view recommended sizes stay the worst-case envelope.
  • A window on one screen locates byte-for-byte as before (active views first), the longer tail aliased onto view 0. Swapchain/atlas sizing unchanged (tiles are per active view of a set).
  • Two segments: views [0,n) left segment, [n,2n) right (n = active mode count; mode is session-wide). activeViewCount = Σ. New XrViewDisplayBindingsDXR (1004999217, chained on XrViewState, two-call) names display / segment rect / view range. XR_DXR_display_info stays v22 (append).
  • The View config: PRIMARY_STEREO session should suppress or deny view_count>2 rendering modes (follow-up to #1486 option B) #1499 mode floor asks one segment's share (oxr_segment_views_per_segment_capacity) — same verdict as pre-M3.
  • PRIMARY_STEREO stays exactly 2.

The legacy-stereo rule

A PRIMARY_STEREO (or MONO) session never splits its views: its 2 views are framed from the segment holding most of the window (that screen's eyes, the whole window relative to it — identical to today's path when the majority is the primary screen), and the compositor crops them per segment as shipped in M2.

Routing / atlas choice

One atlas, per-segment tile ranges, as a mosaic: atlas layout unchanged (cols × rows tiles of canvas × scale), but local view j of segment k is drawn at segment k's rect inside tile j (blit + compose paths). The rect comes from comp_segments_tile_rect — the exact mapping M2's crop reads with — so cropping each segment out of every tile yields that segment's own views and the M2 split path runs unchanged. Routed frames never zero-copy. The routing travels oxr → compositor at xrEndFrame (what the app's last locate handed out).

Keeps one view set (M2 crop): STEREO/MONO, camera-rig and zone-scoped locates, >2 segments, unknown screen size. IPC/service: in-process only; the service never segments, so service sessions see one view set and 0 bindings (documented).

Tests

  • New tests_oxr_segment_views (view-count contract, per-segment capacity, majority selection, seam-continuous placement across different pitches, segment window metrics, two screens of different size/nominal viewer → two different frusta, checked analytically through the shared Kooima core, stereo whole-window framing, binding ranges, tail alias).
  • tests_comp_segments: shared tile-rect mapping (seam column shared).
  • tests_oxr_view_space*: MULTIVIEW count updated to the new contract; remaining failures are the pre-existing ones (identical set on the M2 base: :953 centroid, device_max > 2, mode-floor cases), plus pre-existing tests_rig_composer.
  • ./scripts/build_linux.sh --hybrid --apps + selftest green; check_displayxr_app.py test_apps/cube_handle_vk_linux clean; gen_extensions_index.py --check, check_doc_paths.py green.
  • cube_handle_vk_linux now begins MULTIVIEW (DXR_STEREO_FIXED_APP=1 keeps stereo), renders every active view, chains XrViewActivityStateDXR + XrViewDisplayBindingsDXR, renders each display's views into its segment of each tile, logs bindings once per change.

Gate (hub runs it — nothing was put on the panel)

DXR_PLUGIN_EXCLUSIVE=sim-display SIM_DISPLAY_OUTPUT=anaglyph XRT_LOG=info \
  DXR_CUBE_WINDOW=1600x900+2656+400 ./build/run_cube_handle_vk_linux.sh --platform=x11
# once it renders:
touch "${TMPDIR:-/tmp}/displayxr_atlas_trigger"
# -> ${TMPDIR:-/tmp}/displayxr_atlas.png, displayxr_atlas.seg0.png, displayxr_atlas.seg1.png

Expect: app log Views: activeViewCount=4; display 0x… views 0..1 segment 0,0 …; display 0x… views 2..3 segment …,0 …; runtime INFO per-segment views: 2 segment(s) and segments: per-segment views routed — 2 segment(s) x 2 view(s). Each seg<i>.png holds that segment's OWN two views from its own frustum (the cube continues across the seam, with each half's perspective from its own screen's nominal viewer), not a crop of a shared pair.

Per-screen plug-in pin — DXR_SCREEN_PLUGIN (M4, #793 phase 3)

DXR_SCREEN_PLUGIN=<match>=<plugin-id>[,…] (<match> = RandR output HDMI-1, DRM connector HDMI-A-1, or monitor id hex) pins one monitor to a plug-in. For that monitor only it outranks PreferredPlugin (#791) and the active-plug-in rule (#1521) as long as the pinned plug-in claims it; all other monitors resolve as before. The winner rule is now one pure function (target_screen_pick: pin > preferred > active > confidence) in target_screen_pin.{h,c}, unit-tested with the parser (tests_target_screen_pin). One INFO per applied pin; WARN for no claim / no DP factory / unknown monitor / malformed entry. Documented in docs/specs/runtime/plugin-discovery.md §3.4.

M4 gate mode B: XRT_PREFERRED_PLUGIN_ID=sim-display DXR_SCREEN_PLUGIN=HDMI-1=leia-sr + the M3 gate command.

Gate result — PASSED on ds1-linux

Run by the hub with the command above (sim-display exclusive, anaglyph, X11, window 1600x900+2656+400, straddling eDP-1 / HDMI-1). Log:

[INFO] Views: activeViewCount=4; display 0x03fdab50663276e4 views 0..1 segment 0,0 800x900; display 0x886e4475353b22b9 views 2..3 segment 800,0 800x900
per-segment views: 2 segment(s): [0] display 0x03fd… views 0..1 canvas 0,0 800x900; [1] display 0x886e… views 2..3 canvas 800,0 800x900
segments: per-segment views routed — 2 segment(s) x 2 view(s), tile 800x450; [0] views 0.. at 0,0 400x450; [1] views 2.. at 400,0 400x450
created a segment DP for screen 0x886e… ('HDMI-1', plug-in 'sim-display') via create_dp_vk_for_screen — tolerates a resample
Atlas capture: segment 0 … DP input 800x450
Atlas capture: segment 1 … DP input 800x450

Captures: seg0.png = segment 0's own two views (400x450 each), the cube cut at its right edge; seg1.png = segment 1's own two views, the cube continuing from its left edge — each from its own frustum, not a crop of one shared pair.

Follow-up from the run: the pre-existing VIEW_DIMS WARN claimed the 400x450 per-view submission was "upscaled"; under the mosaic it now reports the segment count instead.

M4 gate result — PASSED on ds1-linux (mixed vendor, both modes)

Run by the hub on ds1-linux (eDP-1 laptop panel + Acer DS1 on HDMI-1), with a Debug dev Leia plug-in matching the runtime build (vk_bundle ABI, #1243).

Mode A — leia-sr active: mixed-vendor screen table built; the eDP segment DP created from sim-display via create_dp_vk_for_screen; the DS1 woven by the Leia main DP; activeViewCount=4, per-segment views routed, seg0/seg1 captures written.

Mode B — XRT_PREFERRED_PLUGIN_ID=sim-display DXR_SCREEN_PLUGIN=HDMI-1=leia-sr:

plugin loader: monitor 0x886e… ('HDMI-1'/'HDMI-A-1') → 'leia-sr' by DXR_SCREEN_PLUGIN (confidence=100; outranks PreferredPlugin and the active plug-in for this monitor)
segments: mixed-vendor screen table — primary 'sim-display', other screens by 'leia-sr'
segments: created a segment DP for screen 0x886e… ('HDMI-1', plug-in 'leia-sr') via create_dp_vk_for_screen — needs 1:1 pixels

Leia windowless weaver created, lens enabled, activeViewCount=4, per-segment views routed, seg0/seg1 captures written.

Human-verified — mode A eyeball on 9c119262e (David at the DS1): PASSED. "M4 fix works, I see anaglyph now laptop side and woven 3D DS1 side." The first eyeball (on 6554af85b) showed the laptop half flat: the sim segment DP reported one eye (sim's process-wide view count is only set by the sim head device, absent with leia-sr active), fixed in 9c119262e. Log lines from the passing run:

Segment 0x03fdab50663276e4 eyes from its DP (2, tracking=0): [0]=(-0.3520,0.1000,0.6000) [1]=(-0.2920,0.1000,0.6000) (reference frame)
Window-relative Kooima [segment 0x03fdab50663276e4]: screen=0.0694x0.0792m, eye_offset=(-0.2067,…)
Window-relative Kooima [segment 0x886e4475353b22b9]: screen=0.0717x0.0804m, eye_offset=(-0.1362,…)
Segment 0x886e… eyes from its DP (2, tracking=1): [0]=(-0.2266,-0.0002,0.5174) [1]=(-0.1799,0.0142,0.5039)

Each segment uses its own pitch (eDP 0.300 m / 3456 px, DS1 0.344 m / 3840 px) and its own eyes (sim's nominal pair on the eDP, the DS1's live tracked eyes). Capture: the eDP segment's two views now differ (mean |v0 − v1| ≈ 1.3 per channel, 0.0 before the fix).

Part of #69 (M3). Refs #1849 #1853.

🤖 Generated with Claude Code

@dfattal
dfattal requested a review from a team as a code owner October 7, 2026 18:58
@dfattal
dfattal force-pushed the feat/multi-screen-m2-segments branch from d137c45 to 1be22f0 Compare October 7, 2026 21:57
Base automatically changed from feat/multi-screen-m2-segments to main October 7, 2026 22:13
dfattal and others added 21 commits October 7, 2026 15:14
…multi-screen M3)

XR_DXR_display_info v22 (append-only, same version): XrViewDisplayBindingsDXR
(1004999217, the reserved value) chained on XrViewState at xrLocateViews --
which views belong to which display, with the segment rect in window px.
xrt_display_metrics.h gains XRT_MAX_SEGMENTS (2), xrt_segment_metrics (the
compositor's segment table for the view math) and xrt_segment_view_routing
(which views of a frame go to which segment).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- PRIMARY_MULTIVIEW_DXR reports device max x XRT_MAX_SEGMENTS views (capped
  at XRT_MAX_VIEWS); a one-segment window locates exactly as before and
  aliases the longer tail. The #1499 mode floor asks one segment's share.
- xrLocateViews: when the compositor weaves the window per segment, each
  segment gets the active mode's views, located from that display's eyes
  (DP eyes when tracking, else its nominal viewer) with the segment as the
  Kooima canvas, contiguous, in one frame (the majority display's), seam-
  continuous. activeViewCount is the sum; XrViewDisplayBindingsDXR filled.
- PRIMARY_STEREO keeps 2 views, framed from the majority display.
- Camera rigs, zone-scoped locates, >2 segments keep one view set.
- The routing rides to the compositor at xrEndFrame.
- oxr_segment_views.{h,c}: the pure geometry, unit-tested.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… plug-in DPs (multi-screen M3)

- The weave publishes its segment table (geometry, each screen's size and
  nominal viewer, woven or flat) for xrLocateViews; eyes are predicted per
  query, segment DPs guarded against the weave.
- The renderer builds a mosaic atlas: segment k's local view j lands at the
  segment's rect inside tile j (blit and compose paths), using the same
  comp_segments_tile_rect mapping the M2 crop reads with, so each segment
  DP's input is its own views. Routed frames never zero-copy.
- Mixed vendors (closes the M4 runtime gap): a segment DP is created from
  ITS screen's registry entry (owning iface/instance, ABI-checked factory),
  whichever plug-in won that screen; one INFO line for a mixed table.
- Every segment DP follows the session's 2D/3D mode (request_display_mode).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ay bindings (multi-screen M3)

Begins PRIMARY_MULTIVIEW_DXR when offered (DXR_STEREO_FIXED_APP=1 keeps
stereo), renders every active view, chains XrViewActivityStateDXR +
XrViewDisplayBindingsDXR, renders each display's views into its segment's
part of every tile, and logs the bindings once per change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tes (multi-screen M3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dors (multi-screen M3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…MAX_SEGMENTS (multi-screen M3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n be split (multi-screen M3)

PRIMARY_MULTIVIEW_DXR now reports device max x view-set capacity, where the
capacity is min(XRT_MAX_SEGMENTS, DP-registry screens with a VK DP factory),
computed once in oxr_system_fill_in -- and 1 wherever no compositor segments
windows (every platform but desktop Linux, any service session). A single-
screen box, Windows, macOS and Android keep exactly the pre-M3 count, so
existing multiview apps and the CTS see no change; ds1-linux gets device max
x 2. The locate never hands out more view sets than the capacity; the
aliasing rule is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gment views (multi-screen M3)

Under per-segment views each view covers only its segment's part of a tile, so
a submission narrower than the tile is the layout; the lifecycle WARN now names
the segment count instead of claiming the compose pass upscales it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… (multi-screen M4, #793 phase 3)

DXR_SCREEN_PLUGIN=<match>=<plugin-id>[,...], <match> = RandR output, DRM
connector (case-insensitive) or monitor id hex. For that monitor only a pin
outranks the PreferredPlugin override (#791) and the active-plug-in rule
(#1521), provided the pinned plug-in claims it; everything else resolves as
before. So XRT_PREFERRED_PLUGIN_ID=sim-display DXR_SCREEN_PLUGIN=HDMI-1=leia-sr
keeps the DS1 woven by leia-sr. One INFO per applied pin; WARN for a pin whose
plug-in has no claim or no DP factory there, an unknown monitor, a malformed
entry.

The per-monitor winner rule is now target_screen_pick() (pin > preferred >
active > confidence, ties to the lower ProbeOrder) in target_screen_pin.{h,c},
unit-tested with the parser by tests_target_screen_pin. Documented in
plugin-discovery.md section 3.4.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…every active view (multi-screen M3 review 1)

A segment accepted its DP's eyes only while tracking, unlike the single-screen
path, so an untracked DP (sim_display) was replaced by the registry nominal
viewer and the primary panel's views jumped when the window crossed the seam.
And the nominal branch always produced 2 eyes, so a 4-view mode left views
2..3 with zero FOVs (the save/restore then wrote them into every segment).
Now: any valid DP eye set counts (oxr_segment_views_accept_eyes), and the
nominal branch fills one eye per active view (the pair repeated; 2 views =
exactly the old pair). Quad-mode test added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… (multi-screen M3 review 2)

Every non-splitting locate used to zero the routing, so an app that located
its projection per segment and then did a zone-scoped or camera-rig locate in
the same frame lost the routing: segment 0's subimage stretched over every
tile. The routing is now a per-frame record: a splitting locate writes it,
other locates leave it, and xrEndFrame takes it (and logs a change) and resets
it for the next frame. Test added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t's camera (multi-screen M3 review 3)

Under per-segment views a mosaic tile holds every segment's view j, but a quad
or equirect2 was drawn once per tile with projection view j's camera — segment
0's frustum across the whole tile, so a world-locked quad (a HUD) landed
mis-scaled and mis-placed on every segment. Routed frames now draw each such
layer once per routed view (first[k]+j) with that view's camera, viewport and
scissor confined to the segment's rect in tile j; visibility and the painter's
tile state use the local view index.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
m2v was V / h_seg per segment, so vertically stacked screens magnified each
half ~2x and side-by-side panels of different pitch got different m2v (a gap
or overlap at the seam). A rig's virtual display height now sizes the whole
window: each segment takes V * h_seg / union_h (oxr_segment_views_vdh_scale),
so m2v = V / union_h for every segment. Tests for both layouts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t (multi-screen M3 review 5+7)

The #1502 VIEW-space offset averaged the whole reported array, so the longer
aliased multiview tail dragged VIEW toward view 0 even when nothing was split;
during a split it was measured on the majority segment's temporary array only.
Now it is the centroid of the active views — (L+R)/2 unsplit, and over every
segment's active views when split (published once by the per-segment wrapper,
and for a stereo session framed from a non-primary majority screen). The
centroid test checks the active views. view-configuration-model.md documents
that DP-backed screens include sim's FALLBACK claim on plain monitors
(intended on multi-monitor desktop Linux) and the VIEW rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…set (multi-screen M3 review 6)

The header said bindingCountOutput is 1 for a window on one display; the spec
and the runtime report 0 whenever the views are one set for the whole window.
Fixed the header text (it ships to the public mirror).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s the pre-M3 count (multi-screen M3 review 8)

is_multiview_count accepted the device max OR x2 on every platform. The test
binary now runs with DXR_SEGMENTS=0 and asserts exactly the device max
everywhere; the segmentation kill switch now also keeps PRIMARY_MULTIVIEW_DXR
at the pre-M3 count (no window can be split). Deriving the Linux x2 from the
box is not exact (a screen counts only when its plug-in's VK factory passed
the vk_bundle ABI check, invisible to xrEnumerateDisplaysDXR); the x2
arithmetic stays pinned by tests_oxr_segment_views.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-screen M3 review, minor)

When the mode changed between the locate and the commit, a segment's view
count no longer equalled the tile count and the whole frame went unrouted
(segment 0's views stretched over the window). Route min(located, tiles)
views per segment instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ively (multi-screen M3 review, minor)

Like the monitor names a pin is matched against; PreferredPlugin keeps its
exact match. Test added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… mixed-vendor layouts (multi-screen M4 eyeball)

Mode A on ds1-linux (Leia primary, sim on eDP): the eDP half looked flat 2D.
The sim segment DP reported eyes from the process-wide sim view count, which
only the sim head device sets -- absent when leia-sr is active -- so it stayed
1: one eye, duplicated into both views by the #615 coherence guard, an
anaglyph of two identical images.

- sim_display: a screen-bound (segment) DP reports one eye per view of the
  atlas it last wove (2 before the first); the session DP is unchanged.
- oxr: a segment uses its DP's eyes only when they cover its views
  (oxr_segment_views_segment_eyes); an under-reporting DP falls back to that
  screen's nominal viewer, which fills every view.
- Logging: every segment of one locate now logs on the same throttled frame
  ('Window-relative Kooima [segment 0x…]', 'Segment 0x… eyes from its DP' /
  '…nominal viewer'); before, the per-call counter only ever let the majority
  segment log, which hid the other segment's (correct) pitch.
- The segment mode log says when a DP has no 2D/3D switch instead of '-> 0'.

Test: two screens of different pitch, segment 1 uses its own pitch and its own
eye pair; an under-reporting DP is not used.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/multi-screen-m3-per-segment-views branch from 9c11926 to e7b8464 Compare October 7, 2026 22:18
@dfattal
dfattal merged commit 785396f into main Oct 7, 2026
40 checks passed
@dfattal
dfattal deleted the feat/multi-screen-m3-per-segment-views branch October 7, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant