Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Build apps for 3D displays using the OpenXR standard.
- [XR_DXR_depth_budget](specs/extensions/XR_DXR_depth_budget.md) — rear depth budget: how far behind the display plane a transparent app may render (ADR-040)
- [XR_EXT_view_configuration_views_change](specs/extensions/XR_EXT_view_configuration_views_change.md) — **Khronos** extension: adoption note for the live-`recommendedImageRect*` doorbell (move `subImage.imageRect`, never reallocate; kill switches; the two bespoke zone events it soft-deprecates)
- [Kooima Projection](architecture/kooima-projection.md) — N-view Kooima math and projection pipelines
- [Window Segments](architecture/comp-segments.md) — a window spanning screens, woven per screen by each screen's own DP (multi-screen M2)

---

Expand Down
158 changes: 158 additions & 0 deletions docs/architecture/comp-segments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Window segments: one window, one DP per screen

*How the compositor weaves a window that spans several screens (multi-screen M2).
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
policy, unit-tested by `tests/tests_comp_segments.cpp`) and
`src/xrt/compositor/vk_native/comp_vk_native_segments.{h,c}` (the Vulkan
in-process compositor's use of it).*

## What a segment is

A **screen** is an OS monitor in the per-monitor DP registry; each one is won by
a plug-in, which can make a display processor (DP) for it. A **segment** is the
part of a window's canvas that lands on one screen:

```
segment = canvas ∩ screen
```

Each segment has:

| field | meaning |
|---|---|
| `window_rect` | the segment in **window pixels** — the canvas handed to `process_atlas` |
| `screen_rect` | the segment in **that screen's pixels** — where it lands on the panel |
| `present_origin` | the window's origin relative to that screen (`set_present_origin`; may be negative). The DP adds the canvas offset: `present_origin + window_rect.origin = screen_rect.origin`, the phase input |
| `screen_1to1` | whether window pixels reach that screen unresampled (desktop rect == device mode), or unknown |

The table is ordered left to right by desktop position (the order M3's
per-segment views will use). The rect math is `u_multi_display_compute_slices`,
half-open, so a window flush to a seam belongs to exactly one screen.

## Who weaves what

- 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:
`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`).
- 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.

## The frame (split path)

`comp_vk_native_segments_record()`, called where the single-DP `process_atlas`
was:

1. **Clear** the target (whatever no segment covers would otherwise be undefined).
2. **Crop per segment.** The atlas holds the whole canvas; each woven segment gets
its own image with exactly its views (each tile cut to the segment, edges rounded
independently so neighbours share the seam column). Crop before the DP is the
law (ADR-030): a DP's atlas holds exactly its canvas.
3. **Weave**, primary first, then left to right. Each DP gets `canvas = segment`;
between two DPs the target goes `PRESENT_SRC_KHR → COLOR_ATTACHMENT_OPTIMAL`
with its contents preserved.
4. **Flat 2D** for every segment that cannot be woven and for canvas no segment
covers: one view (the middle one, the left eye of a stereo pair), scaled from the
view tile to the window rect — the same content and seam registration as the
shipped #1654 band.

It leaves the target in `PRESENT_SRC_KHR`, exactly as the single-DP path does, so
the Local2D composite, the HUD and the present after it are unchanged.

## Who sets viewport and scissor

The **DP**, from the canvas. A Vulkan DP records its own render pass, so the
compositor cannot set dynamic state inside it; what it guarantees is the canvas
argument, the cropped atlas and the target layout (`COLOR_ATTACHMENT_OPTIMAL` on
entry). The contract a segment DP must honour (`create_dp_vk_for_screen` doc):

- **render area, viewport AND scissor = the canvas.** A windowless weaver's default
scissor is its panel rect, so a backbuffer wider than the panel is clipped unless
both are set (multi-screen plan, risk 7);
- **not the first writer**: preserve the target outside the canvas.

sim_display implements both (a screen-bound DP uses a render-pass variant starting
in `COLOR_ATTACHMENT_OPTIMAL` with the clear confined to the render area). A DP
built before M2 may still clear its whole target, which is why the primary runs
first.

**sim_display behaviour change for every caller, on purpose.** sim's viewport and
scissor now follow the canvas for the unbound (primary) DP too — the split path
needs it, and it is what the `process_atlas` contract always said. It is identical
to before whenever the canvas is the whole target (every single-screen in-process
frame); it differs when the canvas is smaller — the one-or-two-frame window-shrink
transient before the swapchain is recreated, the shared-texture path, and a
`comp_multi` zone canvas — where sim used to stretch the canvas-sized atlas across
the whole target. The interlace phase does NOT change for the unbound DP: it stays
`canvas_offset_x`; only a screen-bound segment DP takes its phase from
`set_present_origin`.

## The 1:1 policy

A lenticular weave is a ~1-pixel-period pattern: if the display server resamples
the window on its way to the panel, every view leaks into its neighbour
(#1595, #1831 — refuse rather than resample). Anaglyph, side-by-side and blend do
not care. So the gate is per segment and per DP capability
(`comp_segments_decide`):

| DP for the segment | screen 1:1 | rendered |
|---|---|---|
| none | — | flat 2D |
| needs 1:1 | no | flat 2D |
| needs 1:1 | yes / unknown | woven |
| tolerates a resample | any | woven |

"Unknown never degrades." The capability is `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE`
in `xrt_dp_scanout_caps::flags`, read through the existing `get_scanout_caps` slot
(the flags word was `reserved[0]`, so every older plug-in reads as "needs 1:1").
The primary segment keeps the session-level gates, which now also let a tolerant
primary DP weave (the X11 gate of #1831 and the resample reasons of the Wayland
gate of #1595). On `ds1-linux` both monitors are resampled under XWayland, which is
why M2 could not be shown there without this.

## Lifecycle

Segment DPs follow the table with hysteresis (`comp_segments_lifecycle`): created
once a segment has existed for 2 consecutive metric updates, destroyed once it has
been gone for 30 (~0.5 s at 60 Hz), so a drag back and forth across a seam never
rebuilds DPs. A failed create is not retried until the segment goes away. Until a
DP exists the segment is flat 2D. A table change is logged once at INFO
(`segments: window … -> N segment(s); [i] screen … canvas … origin …`); a DP create
or destroy is one WARN.

## Scope and switches

- In-process Vulkan compositor, desktop Linux, **X11/XWayland**: root coordinates
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.
- 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.
- 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.
53 changes: 52 additions & 1 deletion docs/reference/xrt_plugin_iface.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,56 @@ append. Reference implementation: `sim_display_plugin.c` (EDID mm, else its
`SIM_DISPLAY_WIDTH_M/HEIGHT_M` defaults; its usual nominal viewer; `SIM_DISPLAY_FAKE_TRACKING`
semantics unchanged).

### `create_dp_vk_for_screen` (multi-screen M2)

```c
xrt_result_t (*create_dp_vk_for_screen)(struct xrt_plugin_instance *inst,
void *vk_bundle,
void *vk_cmd_pool,
void *window_handle,
int32_t target_format,
const struct xrt_screen_binding *binding,
struct xrt_display_processor **out_xdp);
```

Make a Vulkan DP for ONE screen your plug-in won. A window that spans screens is woven per
**segment** (canvas ∩ screen, [`comp-segments.md`](../architecture/comp-segments.md)); the
session's own DP keeps the system-default screen's segment and this slot makes the DP for each
other screen. Same contract as `create_dp_vk`, plus:

- `binding` names the screen: monitor id, desktop rect, native (device-mode) pixels, EDID mm,
desktop scale, your claim's serial, a vendor display id (0 until SR-P1/P2 supply one), the
output name. Clamp reads to `binding->struct_size`. Answer `get_display_dimensions` /
`get_display_pixel_info` for THAT screen, with the screen origin = the binding's desktop
origin.
- `window_handle` is NULL on desktop Linux: a segment DP is windowless, its phase comes from
`set_present_origin` (ADR-033) — the window's origin relative to THIS screen, to which you add
the canvas offset.
- Each `process_atlas` hands you `canvas = the segment` (a sub-rect of the target) and an atlas
that holds exactly that segment's views (the runtime crops per segment — crop before the DP is
the law). **Confine render area, viewport AND scissor to the canvas** (a windowless weaver's
default scissor is its panel rect — multi-screen plan risk 7), and **do not assume you are the
first writer** to the target this frame: the target arrives in `COLOR_ATTACHMENT_OPTIMAL`
holding other segments' pixels; leave it in `PRESENT_SRC_KHR` like any VK DP.
- Several instances coexist in one process, one per screen.

NULL (or an older `struct_size`) → your screens other than the session's primary one get a
flat 2D view. There is deliberately no fallback to `create_dp_vk`: that DP describes your one
panel, not the screen, and nothing binds it to the canvas / not-first-writer contract above, so
it could clear the segment the primary DP just wove.
Appended after `get_display_info_for_monitor` per ADR-020 (`struct_size`-gated, no
`XRT_PLUGIN_API_VERSION_CURRENT` bump; ABI stays 5); guard with
`#ifdef XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN`. M2 only segments a window whose screens all
belong to the ACTIVE plug-in; mixed vendors are M4. Reference implementation:
`sim_display_dp_factory_vk_for_screen` (`drivers/sim_display/sim_display_processor.c`).

**The 1:1 capability.** Report through the existing `get_scanout_caps` slot whether your output
survives a display-server resample: `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE` in
`xrt_dp_scanout_caps::flags` (carved from the former `reserved[0]`, so an older plug-in reads
as "needs 1:1"). A lenticular weave leaves it clear; the runtime then paints flat 2D on a
segment whose screen is resampled, and the #1595 / #1831 session gates degrade as before.
sim_display sets it for every output but INTERLACED.

### `set_pose_source`

```c
Expand Down Expand Up @@ -684,7 +734,8 @@ Flags (`XRT_PLUGIN_STEREO_CAMERA_*`) mirror the XR bits: `SHARED_WITH_EYE_TRACKI
**Slot order.** The six slots are the LAST members of the iface, appended (ADR-020) after
everything that was on `main` before them: `… vk_bundle_fn_table_offset` → `create_dp_d3d11_lift`
(ADR-042) → `get_platform_state` (ADR-045) → `stereo_camera_enumerate … stereo_camera_close`
→ `get_display_info_for_monitor` (multi-screen M1, now the last member).
→ `get_display_info_for_monitor` (multi-screen M1) → `create_dp_vk_for_screen` (multi-screen M2,
now the last member).
`tests_stereo_camera` pins that order (lift right after the vk fingerprint, platform-state right
after lift, the camera block right after platform-state and ending the struct), so a reorder or a
slot squeezed in between fails on the host, not on a vendor box.
Expand Down
18 changes: 18 additions & 0 deletions docs/roadmap/linux-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,24 @@ source comments and, until this section, in no document at all.
overlay, and a window entirely off the panel skips the weave; kill switch
`DXR_SPAN_2D=0` (either name works on both paths), headless proof
`weave_probe_vk_linux --span2d`.
**Multi-screen M2 (X11/XWayland, in-process Vulkan) replaces the band with a
real segment wherever the other monitor has a display processor.** A window
spanning screens is split into segments (`canvas ∩ screen`,
`util/comp_segments.h`); each segment is woven by its own screen's DP
(created from that screen's registry factory, `create_dp_vk_for_screen`)
with `canvas = segment`, a pre-cropped atlas and its own present origin, and
composited into the one surface (`comp_vk_native_segments.c`). Only a
segment with no DP (a monitor no plug-in claimed, a mixed-vendor layout —
that is M4) or whose DP needs 1:1 pixels on a resampled screen keeps the flat
2D view. sim_display's anaglyph tolerates a resample, so with
`DXR_PLUGIN_EXCLUSIVE=sim-display` a window across eDP-1 and the DS1 is
anaglyph on both halves. The window entirely on the primary screen is the
shipped single-DP path, unchanged. Native Wayland keeps the band until the
geometry service publishes every monitor
(`docs/specs/runtime/wayland-window-geometry.md` §5); the X11 1:1 gate
(#1831) and the Wayland resample reasons of #1595 no longer degrade a DP
that declares `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE`. Kill switch
`DXR_SEGMENTS=0`. Design: `docs/architecture/comp-segments.md`.
- **Flat regions on the weave service are painted flat, not wished (spec v8,
browser#88).** The caller's flat regions — each submit's
`XrWeaveSubmitFlatRegionsDXR` plus the sticky screen-space
Expand Down
34 changes: 34 additions & 0 deletions docs/specs/runtime/wayland-window-geometry.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,40 @@ is what lets any package ship it and any runtime consume it.
- Mutter emits geometry transactionally with its own redraw, so tracking
during interactive drags is expected to be at least as good as the X11
per-frame poll; validate visually (phase lock while dragging).
- **Multi-screen segments need every monitor, not just the window's own
(multi-screen M2 follow-up, ADR-047 D2).** M2 weaves a window that spans
screens per segment — `canvas ∩ screen`, each by that screen's display
processor with its own present origin — on X11/XWayland, where root
coordinates are desktop-absolute and RandR gives every monitor's rect in the
same space (`docs/architecture/comp-segments.md`). Native Wayland stays on
the primary-segment-only behaviour (#1654 bands), because this payload cannot
place the second segment:
- `monitor` describes only the window's OWN monitor. The rect of the part of
the window that lands on another monitor, in THAT monitor's device px,
needs that monitor's stage rect and its own `device_scale` — a
mixed-scale layout (this box: 1.6667 laptop + 2.0 DS1) makes a single
factor wrong for one of the two.
- The runtime's screen registry is keyed by connector / RandR output name,
and the payload names no connector, so even the window's own monitor is
matched only by its device size today (`wr.monitor_width_px ==
panel_px_w`), which is ambiguous for two identical panels.

**Required extension change** (not installed on any dev box; the runtime
keeps refusing until a publisher announces it): add a top-level
`monitors: [{connector, x, y, w, h, scale, device_scale}]` array — every
monitor in stage coordinates, `connector` = the DRM connector / Mutter
monitor connector (`Meta.MonitorManager` → `get_monitor_for_connector`,
e.g. `"HDMI-1"`, which is what the registry's `device_name` holds) — plus
`monitor.connector` on each window. Additive, so the schema stays
`version: 1` and an older consumer ignores it; bump the extension version
so `org.displayxr.WindowGeometry1` capability probing can tell. The consumer
then intersects the window's `buffer` rect with each monitor in stage
coordinates, converts each piece to that monitor's device px with that
monitor's `device_scale`, and hands `comp_segments` a segment table in
which every rect is already per screen. Note that Mutter paints a straddling
surface at ONE buffer scale (the window's main monitor's), so the part on
the other monitor is resampled: the per-segment 1:1 policy applies (a
resample-tolerant DP weaves it, a lenticular weave gets flat 2D).

## 6. Capture exclusion — `org.displayxr.CaptureExclusion1` (extension version 2)

Expand Down
2 changes: 2 additions & 0 deletions src/xrt/compositor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ if(XRT_HAVE_VULKAN)
util/comp_render_helpers.h
util/comp_scratch.c
util/comp_scratch.h
util/comp_segments.c
util/comp_segments.h
util/comp_semaphore.h
util/comp_semaphore.c
util/comp_swapchain.h
Expand Down
Loading
Loading