Skip to content

feat(vk_native): per-screen segment DPs for spanning windows (multi-screen M2) - #1853

Merged
dfattal merged 12 commits into
mainfrom
feat/multi-screen-m2-segments
Oct 7, 2026
Merged

dfattal merged 12 commits into
mainfrom
feat/multi-screen-m2-segments

Conversation

@dfattal

@dfattal dfattal commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

What / why

Multi-screen M2 — segments, one vendor (ADR-047 D2). A window whose canvas covers several screens is woven per segment (canvas ∩ screen) by the display processor of that screen, each with its own canvas and present origin, composited into the one presented surface. Stacked on #1852 (M1); do not merge before it.

Target demo on ds1-linux: cube_handle_vk_linux across eDP-1 ↔ DS1 with DXR_PLUGIN_EXCLUSIVE=sim-display is anaglyph on both halves — two sim DPs, two canvases, no 2D band.

The segment model (10 lines)

  1. A segment = canvas ∩ screen: window_rect (window px, = the canvas handed to process_atlas), screen_rect (that screen's px), present_origin (window origin relative to that screen; the DP adds the canvas offset, so phase = screen_rect origin).
  2. util/comp_segments.{h,c} — backend-agnostic, pure math: the table (via u_multi_display_compute_slices), the 1:1 policy, uncovered canvas, DP lifecycle hysteresis.
  3. The primary (system-default) screen's segment keeps the session's DP; it still owns eyes / window metrics / Kooima (one view pair — per-segment views are M3).
  4. Every other screen gets a segment DP from its registry plug-in through create_dp_vk_for_screen only (no plain create_dp_vk fallback — a plug-in without the slot gets flat 2D there), windowless, created on the Decouple weave rate from app render rate: re-weave the last atlas at panel rate with fresh eye positions #868 runtime-owned queue, phase via set_present_origin.
  5. Lifecycle: create after 2 metric updates, destroy after 30 (~0.5 s); failed creates are not retried while the segment lasts; a table change logs once at INFO.
  6. Split frame (vk_native/comp_vk_native_segments.c): clear → crop each woven segment's views out of the atlas (crop before the DP) → process_atlas per segment with canvas = segment (primary first, then left to right) → flat 2D view for unweavable segments + uncovered canvas → target left in PRESENT_SRC_KHR, so everything after the weave is unchanged.
  7. Viewport and scissor = segment, set by the DP from the canvas (plan risk 7); a segment DP must not assume it is the first writer.
  8. A window entirely on the primary screen never leaves the single-DP process_atlas call (byte-for-byte).
  9. Scope: in-process Vulkan, desktop Linux X11/XWayland. Native Wayland stays primary-only (payload follow-up documented in wayland-window-geometry.md §5); also off for zero-copy, self-submitting DPs, pinned sessions (XrSessionDisplayBindingDXR), mixed vendors (M4). Kill switch DXR_SEGMENTS=0.
  10. The post-compose atlas capture also writes each segment's DP input as <stem>.seg<i>.png.

ABI (ADR-020 append-only; XRT_PLUGIN_API_VERSION_CURRENT stays 5)

  • xrt_plugin_iface::create_dp_vk_for_screen(inst, vk_bundle, cmd_pool, window_handle, target_format, const struct xrt_screen_binding *, out) appended after get_display_info_for_monitor; XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN + xrt_plugin_iface_has_create_dp_vk_for_screen(). xrt_screen_binding {struct_size, monitor_id, desktop rect, native px, mm, scale, vendor display_id, serial, device_name}, passed by pointer.
  • xrt_dp_scanout_caps::flags carved out of reserved[0] (same size/offsets) with XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE; read via the existing get_scanout_caps slot (no new DP vtable slot). Clear = needs 1:1 = what every existing plug-in wrote there.
  • Leia plug-in untouched (no slot → its other screens are flat 2D; and mixed-vendor layouts don't segment in M2).

The 1:1 policy

Per segment and per DP capability (comp_segments_decide): no DP → flat 2D; DP needs 1:1 on a resampled screen → flat 2D; 1:1 / unknown / resample-tolerant DP → woven ("unknown never degrades"). The session-level gates (X11 #1831, Wayland #1595 resample reasons b/c) no longer degrade a primary DP that declares the flag. sim_display declares it for every output except INTERLACED. Behaviour change: a sim_display anaglyph session on a fractionally scaled panel now weaves anaglyph instead of presenting 2D; lenticular weaves are unaffected.

sim_display

Per-instance screen state seeded from the binding (EDID mm / native px / desktop origin; the SIM_DISPLAY_* panel is the fallback); process_atlas confines viewport + scissor to the canvas (full-target canvas = pre-M2 pass exactly); a screen-bound DP uses a render-pass variant that starts in COLOR_ATTACHMENT_OPTIMAL and clears only its render area; set_present_origin feeds the interlaced phase; get_scanout_caps implemented. Output mode / view count stay session state (one atlas, one rendering mode, 1/2/3 keys) — segment DPs follow it.

Tests

  • tests_comp_segments (new): table math on ds1-linux geometry, seam ownership, canvas sub-rect, uncovered canvas (coverage, gaps, area conservation), the 1:1 policy, lifecycle hysteresis (N/M, one-frame touch, failed create, no-factory, drain).
  • tests_dp_weave_scope: flags default + offsets. tests_stereo_camera: iface now ends with create_dp_vk_for_screen.
  • ./scripts/build_linux.sh --hybrid --apps green + selftest PASSED; ctest: all pass except the pre-existing tests_rig_composer / tests_oxr_view_space* failures on this box.
  • MinGW: this box's toolchain is broken before any of this (Windows.h casing, TIME_UTC in the mcp dep); only comp_segments.c was cross-compiled cleanly by hand.
  • Not yet run on hardware — no windowed app was launched (shared DS1). Gate below.

Gate (hub runs; X11/XWayland, sim-display only, dev plug-in dir)

cd .claude/worktrees/m2-segments
rm -f /tmp/displayxr_atlas*.png /tmp/displayxr_atlas_trigger
# 1600x900 at +2656+400 relative to eDP-1 (0,0): 800 px on eDP-1, 800 px on HDMI-1
DXR_PLUGIN_EXCLUSIVE=sim-display SIM_DISPLAY_OUTPUT=anaglyph DXR_CUBE_WINDOW=1600x900+2656+400 \
  ./build/run_cube_handle_vk_linux.sh --platform=x11 > /tmp/m2_gate.log 2>&1 &
APP=$!; sleep 8; touch /tmp/displayxr_atlas_trigger; sleep 3; kill $APP
grep -E "segments:|Atlas capture|X11 1:1 gate|sim_display: segment DP" /tmp/m2_gate.log
ls -la /tmp/displayxr_atlas*.png   # displayxr_atlas.png + .seg0.png (eDP) + .seg1.png (DS1)

Optional drag across the seam (hysteresis + table log): start at DXR_CUBE_WINDOW=1600x900+800+400 with DXR_X11_TEST_DRAG=2400,0,24.

Review fixes (after the gate)

  • No plain-factory fallback for a segment DP: has_dp_factory requires create_dp_vk_for_screen; otherwise flat 2D. The per-screen call gets the Decouple weave rate from app render rate: re-weave the last atlas at panel rate with fresh eye positions #868 queue swap.
  • No release under a parked fill: the epic: in-process tier parity — panel-rate weave everywhere (VK / D3D12 to match d3d11-hybrid smoothness) #1264 S1 fence-park releases c->mutex; segment DPs/images now go on a retire list (comp_segments_retire, tested) drained only when no fill is parked (c->fill_parked), with a debug assert in the release path. DP input images are per frame class (app / repaint), so the two never write each other's. set_screens defers the rebuild to a weave with no fill parked.
  • sim_display single-monitor behaviour, stated explicitly: the unbound DP keeps the pre-M2 interlace phase (canvas_offset_x); only screen-bound segment DPs take set_present_origin. Viewport/scissor confinement to the canvas is kept for every caller on purpose (it is the process_atlas contract and the split path needs it from the unbound primary). It is identical to before whenever canvas == target, and differs only when canvas < target: the window-shrink transient, the shared-texture path, a comp_multi zone canvas (where sim used to stretch the canvas-sized atlas across the whole target). Documented in comp-segments.md.
  • Tolerance follows the mode: re-read on a head rendering-mode change (never per frame); the X11 Linux: an X11/XWayland app on a fractionally-scaled 3D panel weaves for the wrong display (panel matched by size), and is resampled anyway #1831 one-shot is re-armed and clears its own degrade when the DP becomes tolerant; segment DPs re-read too.
  • Nits: X11 gate waits for the DP create attempt; segment DPs get encoding (DXR_VK_ATLAS_ENCODING) and transparency on change; segment capture honours DXR_ATLAS_CAPTURE_RAW_ALPHA and splits the suffix on the extension's dot; mirrored outputs are de-duplicated (primary wins).
  • Known, pre-existing, not addressed: a DP shared by the app frame and a parked fill (the primary today, segment DPs alike) may update its descriptor set while the fill's command buffer still uses it.

Gate result — PASSED on ds1-linux (2026-10-07)

Run by the hub with the exact gate command above (X11/XWayland, DXR_PLUGIN_EXCLUSIVE=sim-display, anaglyph, window 1600x900+2656+400, i.e. 800 px on eDP-1 and 800 px on HDMI-1).

Log lines:

segments: 2 screens, plug-in 'sim-display'
sim_display: segment DP ... for screen 0x886e4475353b22b9 — 3840x2160 px, 0.344 x 0.193 m, desktop origin (3456, 0)
segments: created a segment DP for screen 0x886e4475353b22b9 ('HDMI-1', plug-in 'sim-display') via create_dp_vk_for_screen — tolerates a resample
Atlas capture: segment 0 (screen 0x03fdab50663276e4) DP input 800x450 -> /tmp/displayxr_atlas.seg0.png
Atlas capture: segment 1 (screen 0x886e4475353b22b9) DP input 800x450 -> /tmp/displayxr_atlas.seg1.png

Captures (described; gh cannot attach images from the CLI):

  • /tmp/displayxr_atlas.png, 1600x450: the whole-window atlas, two 800x450 view tiles (left and right eye).
  • /tmp/displayxr_atlas.seg0.png, 800x450: the eDP-1 segment's DP input, the left 400 px of each view (two 400x450 tiles).
  • /tmp/displayxr_atlas.seg1.png, 800x450: the HDMI-1 (DS1) segment's DP input, the right 400 px of each view.

So the window was split into two segments, each with its own sim DP (the secondary built through create_dp_vk_for_screen, describing the DS1's own geometry and desktop origin), and the per-segment atlas crop is correct: each DP received exactly its half of every view.

Part of #69 (M2). Addresses #546 items 2 (several DPs per compositor) and 4 (DP lifecycle with hysteresis) and the re-scoped item 3 (segments instead of an atlas split) for the in-process Vulkan compositor on Linux X11 — not closed: item 1 (real monitor id on the COMP_DP_PRIMARY_MONITOR call sites), the other APIs and Wayland remain. Refs #1849 #1851 #1852.

🤖 Generated with Claude Code

dfattal and others added 12 commits October 7, 2026 14:57
…i-screen M2)

A segment is canvas ∩ screen: the part of a window's canvas that lands on
one screen, woven by that screen's own display processor (ADR-047 D2).
comp_segments is the backend-agnostic half, pure integer math:

- comp_segments_compute: the segment table (window px canvas rect, screen
  px rect, present origin = window origin relative to the screen, 1:1
  state), left to right, reusing u_multi_display_compute_slices.
- comp_segments_decide: per-segment refuse-rather-than-resample — flat 2D
  when there is no DP, or when the screen is resampled and the DP needs
  1:1; unknown never degrades.
- comp_segments_lifecycle: create a secondary DP after N updates, destroy
  after M empty ones, never retry a failed create while the segment lasts.

tests_comp_segments pins the table math (ds1-linux geometry), the policy
and the hysteresis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ag (multi-screen M2)

- xrt_plugin_iface::create_dp_vk_for_screen, appended after
  get_display_info_for_monitor per ADR-020 (struct_size-gated, ABI stays 5),
  announced by XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN. Takes an
  xrt_screen_binding (monitor id, desktop rect, native px, mm, scale,
  serial, vendor display id) so a plug-in can make one DP per screen a
  window spans. A plug-in without it falls back to create_dp_vk.
- xrt_dp_scanout_caps::flags, carved out of reserved[0] (same size and
  offsets), with XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE: the DP's output
  survives a display-server resample (anaglyph yes, a lenticular weave no).
  Clear = needs 1:1, which is what every existing plug-in wrote there.
  xrt_display_processor_vk_tolerates_resample() reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reen M2)

- create_dp_vk_for_screen: a DP bound to one screen answers
  get_display_dimensions / get_display_pixel_info for that screen (EDID mm,
  native px, desktop origin), seeded from the binding with the
  SIM_DISPLAY_* panel as the fallback. The unbound (primary) DP is
  unchanged.
- process_atlas confines viewport AND scissor to the canvas; a full-target
  canvas is the pre-M2 pass byte for byte. A screen-bound DP with a sub-rect
  canvas uses a render-pass variant that starts in COLOR_ATTACHMENT_OPTIMAL
  and clears only its render area, so sibling segments survive.
- set_present_origin: the interlaced output's phase (present origin; the
  canvas offset proxy stands when none was fed).
- get_scanout_caps: weave scope from SIM_DISPLAY_WEAVE_SCOPE and
  TOLERATES_RESAMPLE for every output except INTERLACED.

Output mode and view count stay session state (one atlas, one rendering
mode, switched by the 1/2/3 keys); segment DPs follow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The part of a window's canvas that lands on no listed screen (a monitor
no plug-in claimed, or off the desktop) gets the same flat 2D view as a
segment that cannot be woven. Disjoint row-band rectangles; tested for
coverage, gaps, a short screen and area conservation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…creen M2)

A window whose canvas covers several screens is woven per segment
(canvas ∩ screen, ADR-047 D2) by the in-process Vulkan compositor on
desktop Linux X11/XWayland:

- comp_vk_native_segments owns the secondary DPs: created from each
  screen's registry plug-in (create_dp_vk_for_screen with an
  xrt_screen_binding, else create_dp_vk), windowless, with comp_segments
  hysteresis (create after 2 updates, destroy after 30).
- Split frame: clear, crop each woven segment's views out of the atlas
  (crop before the DP), process_atlas per segment with canvas = segment
  (the DP sets viewport + scissor from it) and its own present origin,
  primary first, then flat 2D (the #1654 view) for unweavable segments
  and uncovered canvas. Leaves the target in PRESENT_SRC_KHR like the
  single-DP path, so everything after the weave is unchanged.
- The primary DP keeps the system-default screen's segment and everything
  view-related (eyes, metrics) — per-segment views are M3. A window on the
  primary screen only never leaves the single-DP call.
- 1:1: per segment via comp_segments_decide; the X11 (#1831) and Wayland
  resample (#1595 b/c) session gates no longer degrade a DP declaring
  XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE (read once at DP creation).
- Gated off for native Wayland (the geometry payload cannot place the
  other monitor), zero-copy, self-submitting DPs, pinned sessions
  (XrSessionDisplayBindingDXR), and mixed vendors (M4). DXR_SEGMENTS=0.
- The post-compose atlas capture also writes each segment's DP input as
  <stem>.seg<i>.png.
- oxr hands the compositor the screen list after session create.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd follow-up (multi-screen M2)

- docs/architecture/comp-segments.md: what a segment is, who weaves what,
  the split frame, who sets viewport/scissor, the 1:1 policy, lifecycle,
  scope, what M3 adds.
- xrt_plugin_iface.md: create_dp_vk_for_screen and the resample flag.
- linux-support.md: where #1654's 2D band is replaced by a real segment.
- wayland-window-geometry.md §5: the payload change native Wayland needs
  (every monitor's stage rect, device_scale and connector).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 lands (docs-lint)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…een M2 review)

A non-primary screen whose plug-in lacks create_dp_vk_for_screen got an
ordinary create_dp_vk DP: describing the plug-in's one panel, not bound by
the canvas / not-first-writer contract (so free to clear the segment the
primary had just woven), and created on the app queue. has_dp_factory is
now true only with the per-screen slot (and a VK-capable, ABI-matched
registry entry); otherwise that segment is flat 2D. The per-screen
factory call also gets the #868 runtime-owned queue swap vk_make_dp_vk
does. Slot doc, iface reference and design note updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…input images (multi-screen M2 review)

The repaint's #1264 S1 fence-park releases c->mutex across
vkWaitForFences, so an app frame could run the segment metric update and
destroy a segment DP (dp_release) or recreate a crop image (crop_ensure)
that the fill's in-flight command buffer still references; both frame
classes also wrote the same crop image unsynchronised.

- c->fill_parked marks the park (set/cleared under c->mutex).
- Segment DPs and images are retired onto a comp_segments_retire list
  and handed back to Vulkan only from an update with no fill parked (and
  in destroy). release_now() asserts that in debug builds.
- One DP input image per frame class (app / repaint): each class only
  touches its own and waits its own previous submission, so it may
  recreate it immediately; the capture reads the app class's.
- set_screens() no longer destroys the manager under the lock; it marks a
  rebuild that the next weave performs once no fill is parked.
- comp_segments_retire (pure) + test: nothing moves while unsafe, oldest
  first, capped, a full list refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt canvas confinement (multi-screen M2 review)

- The interlace phase uses set_present_origin only on a screen-bound
  (segment) DP. The unbound DP — every pre-M2 caller, including the
  single-monitor path — keeps phase = canvas_offset_x byte for byte: it
  reports its panel at (0,0), so a fed origin relative to a panel
  elsewhere would land the phase on the wrong lattice.
- Viewport/scissor confinement to the canvas is KEPT for every caller and
  now stated as deliberate: it is the process_atlas contract and the
  split path needs it from the unbound primary. It differs from pre-M2 only
  when canvas < target (window-shrink transient, shared-texture path,
  comp_multi zone canvas). Spelled out in the code and in
  docs/architecture/comp-segments.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mable X11 1:1 gate (multi-screen M2 review)

sim_display's TOLERATES_RESAMPLE follows its output mode (INTERLACED needs
1:1), but the compositor read it once at DP creation: switching to
interlaced on a scaled desktop kept weaving through the resample (the
#1595/#1831 double image), and starting interlaced tripped the one-shot
X11 gate with no recovery after switching to anaglyph.

- vk_dp_refresh_resample_caps(): on a head rendering-mode change (never
  per frame) re-query the primary DP; a changed answer re-arms the X11
  one-shot. The Wayland arm already reads the flag per evaluation.
- The X11 gate now also clears its own degrade when the DP has become
  tolerant (vk_linux_apply_not_1to1(false)), and does not re-log while
  already degraded.
- Segment DPs re-read their tolerance on a mode change too.
- Nit: the X11 gate waits for the DP create ATTEMPT, not success, so a
  failed factory no longer leaves it unevaluated forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Segment DPs get the same per-change declarations as the primary: the
  atlas encoding, honouring DXR_VK_ATLAS_ENCODING (off = never call), and
  the session's transparency when it toggles.
- Segment capture honours DXR_ATLAS_CAPTURE_RAW_ALPHA, and the .seg<i>
  suffix splits on the extension's dot, not one in a directory name.
- comp_segments_compute drops mirrored outputs (identical desktop rects):
  one segment per region, the primary winning a mirror; tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/multi-screen-m2-segments branch from d137c45 to 1be22f0 Compare October 7, 2026 21:57
@dfattal
dfattal merged commit 4a521aa into main Oct 7, 2026
40 checks passed
@dfattal
dfattal deleted the feat/multi-screen-m2-segments branch October 7, 2026 22:13
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