Skip to content
Merged
22 changes: 22 additions & 0 deletions docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,25 @@ primary takes its phase from `set_present_origin` like any segment; drag snap, t
anchor and the hosted window's snap provider follow the owner. Eyes and view routing are
unchanged: the primary screen's DP still serves the primary screen. Logic:
`comp_segments_owner_*` (unit-tested); detail: `docs/architecture/comp-segments.md`.

## Amendment 3 (2026-10-09): segments on the service / IPC path

An `_ipc` client is composited by `displayxr-service`, not in its own process, so the
service has to segment its window and the client's `xrLocateViews` has to learn the table
from the other side of the wire. Decisions:

- **The service owns the screens.** The screen registry already lives in the service (its
instance's `enumerate_displays`, which `xrEnumerateDisplaysDXR` reaches over IPC), so a
client sends only its session's display pin (`compositor_segments_enable`); the service
builds the client's segment manager from its own registry, on the device that presents.
- **Only the direct single-client pipeline segments** (`pipeline_default_policy_render`,
an `APP_HWND` presenter). The workspace / compose path (`multi_compositor_render`) keeps
one presenter, the service window on the panel, so it stays single-screen: that is a
separate decision (per-screen presenters for the workspace), not a consequence of this one.
- **The table and the routing cross the wire by value** (`compositor_get_segment_metrics`,
`compositor_set_view_routing`, appended to `proto.json`); per-segment views are then
framed client-side from each segment's eyes exactly as in-process, and the service paints
the routed mosaic into the client's atlas. A client that never opts in — every
single-screen box — sends nothing new and is composited byte for byte as before.

Detail: `docs/architecture/comp-segments.md` § *Service / IPC path*.
47 changes: 44 additions & 3 deletions docs/architecture/comp-segments.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ deferred-release list because the immediate context executes in order). What dif
one WARN when its segment manager is made (`segments: under the #918 output-device split …`).
- **Not segmented on Windows:** zero-copy frames, a zones / Local2D frame (any non-whole-window
canvas), the shared-texture path, a pinned session, `DXR_SEGMENTS=0`.
The service / hosted path is not segmented yet (ADR-035 amendment, M6 second half).
The service has its own section below (*Service / IPC path*).
- **sim_display** implements both slots on D3D11 too, so a two-monitor Windows box with no vendor
hardware exercises the split path (anaglyph on both halves).
- **Per-segment views (M3) on Windows** ride the same state-tracker path as Linux: `oxr_system` counts
Expand Down Expand Up @@ -302,5 +302,46 @@ set). Quads and equirect2 layers are drawn once per segment in each tile, with t
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).
**IPC.** See *Service / IPC path* below: the D3D11 service publishes the same table for
a direct client, and the client frames per-segment views from it exactly as above.

## Service / IPC path (Windows, ADR-047 Amendment 3)

An `_ipc` client (`XRT_FORCE_MODE=ipc`, or any app under a running service) is composited by
`displayxr-service`. The same segment manager weaves its window there:

- **Opt-in.** At session create the client sends `compositor_segments_enable(pin)` — only when
its system's set capacity is above 1, i.e. the service's registry has two screens with a
D3D11 DP factory (`oxr_system` counts the by-value copy of the service's registry; the
pointers are only NULL-tested). The service answers with its OWN screen list (its instance,
the one `xrEnumerateDisplaysDXR` reports) and keeps a heap copy per client. A single-screen
box never sends it; a client that did not send it is never segmented.
- **Where it weaves.** Only the direct single-client pipeline (`pipeline_default_policy_render`)
for an `APP_HWND` presenter — the app's own window, the only kind that can sit across a seam.
`pipeline_segments_weave` replaces that frame's single `process_atlas`: the client's manager
(`comp_d3d11_segments`, linked from the `comp_d3d11_segs` library), made on `svc_out_device`
(the output device under the #918 split), with the panel DP — bound to the app's HWND — as
the primary and the client's crop (or the egress slot) as the atlas. The window rect is the
app HWND's client area from `ClientToScreen` in the per-monitor-DPI-aware service, i.e.
device px. One client's segment DPs exist at a time: a focus change releases the outgoing
presenter's, and a table older than 250 ms (the client no longer weaves directly — focus
moved, the workspace took over) is not handed out, so that client locates one view set. A hosted client presents into the service's window on the panel, a
present-owner / client-texture client weaves on its own thread and a zones frame keeps the
single DP: none is segmented.
- **Compose / shell mode** (`multi_compositor_render`) is unchanged: one presenter, the
service window on the panel, so a workspace window is never segmented and its table stays
empty. Per-screen workspace presenters are a separate decision.
- **Per-segment views.** `xrLocateViews` fetches the table with `compositor_get_segment_metrics`
(a round trip per locate, only for an opted-in session): the service fills each segment's
eyes at query time — the primary from the panel DP, the others from their segment DPs. The
client then runs the in-process locate unchanged, with two IPC-only differences: the
segment's client-side Kooima FOVs are kept over the device's single-set answer, and a
segment locate never takes the server rig call (`locate_views_rig`), which knows one view
set. `xrEndFrame` sends the routing with `compositor_set_view_routing`, change-only (the
service keeps the last one). The service lays a routed frame into the client's atlas as the
mosaic (`comp_segments_route_place`, `util/comp_segments_route.h`, unit-tested by
`tests/tests_segments_ipc.cpp`); a routed frame never zero-copies.
- **Not yet on the service path:** quads / equirect / cylinder layers in a routed frame are
drawn once per tile with that tile's camera (in-process draws them per segment), and a
second projection layer is routed only where the per-tile pass visits it (segment 0).

2 changes: 2 additions & 0 deletions docs/architecture/service-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,8 @@ that joined under the shell) fails the `IsWindow` test and logs `swap_chain=NULL
(standalone clients are never in `mc->clients[]`) so the first to leave flattens the panel
for the other (INFERRED).

**Multi-screen segments (ADR-047 Amendment 3).** A window spanning monitors is woven per screen only on the direct path: when the focused presenter is an `APP_HWND` client that opted in (`compositor_segments_enable`, sent by its oxr session on a box with two DP-backed screens), `pipeline_segments_weave` replaces that frame's single `process_atlas` with the client's own segment manager (the in-process `comp_d3d11_segments`, on `svc_out_device`): the panel DP weaves the primary screen's segment, windowless per-screen DPs the others. The service owns the screen registry, so only the session's display pin crosses IPC; the segment table and the per-segment view routing then cross by value (`compositor_get_segment_metrics`, `compositor_set_view_routing`) and the commit paints the routed atlas mosaic. Render thread under `render_mutex`; the client's `seg_mutex` is a leaf guarding the table it reads at `xrLocateViews`. The workspace path (`multi_compositor_render`) stays one presenter on the panel and never segments. Detail: `docs/architecture/comp-segments.md` § *Service / IPC path*.

### 4.4 #925 status (correcting the reference doc)

S1 bounded waits ✔, S2 audit ✔, **S3 command queue ✔ default on**, **S4 eviction ✔** (tier 1
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap/multi-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ multiview + display_info v22. An app may also pin itself to one display
| **M3** Per-segment views | Same window: `xrLocateViews` returns 4 views, each half rendered from its own frustum | oxr: per-segment Kooima + eyes from the segment DP; `XrViewDisplayBindingsDXR`; `activeViewCount`; tile routing by segment; legacy stereo = majority segment + 2D blit | none | cube renders the correct perspective on each half; `check_displayxr_app.py` rule for v22 |
| **M4 = T1** Mixed vendor on `ds1-linux` | **Leia weave + DS1 tracking on the DS1 half, sim anaglyph on the laptop half** | loader: two plug-ins resident, DPs created from each screen's factory; plug-in display info per screen | leia-linux: `window = 0` always (one line, `leia_sr_linux_sdk.c:1470`); per-DP display state instead of `g_ctx.display` / statics; `srLensDisable` right after `srCreateLens` if the lens must start off. Confirmed by the SR side: a windowless Linux Vulkan weaver fed the DS1 segment + present origin weaves exactly that viewport, votes the lens on once, then the DP owns it. Works on today's SDK for **one** Leia panel. With SR-P0 (verified on the DS1 2026-10-07, branch `ST-5481-weaver-external-routing`): check `SrWeaverRoutingCapabilities` (tag 24) chained on `SrRuntimeCapabilities.pNext`, then chain `SrWeaverRoutingInfo{mode=SR_WEAVER_ROUTING_EXTERNAL}` (tag 22) on `SrWeaverCreateInfoVulkan.pNext`, `window = 0`, present origin per frame, lens via `srLensEnable/Disable` | David eyeballs it; screenshot both halves |
| **M5 = T2** Two Leia panels | Each panel tracked by its own camera | per-screen tracker plumbing already in place from M3 | **SR-P1** enumeration + identity, **SR-P2** bind display/weaver/lens/tracker by `displayId`, **SR-P3** N trackers + lens per display (SRService refactor, ST-3160 camera pairing). Plug-in: per-DP SR display + tracker, claims from `srEnumerateDisplays` | needs a second panel on a dev box |
| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). the HWND follows the majority screen (ADR-047 Amendment 2: 20 % margin held 0.5 s, windowless replacement first, rollback on failure; rig-unverified); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed; service/hosted half not started.** | shell with a window straddling two panels |
| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). the HWND follows the majority screen (ADR-047 Amendment 2: 20 % margin held 0.5 s, windowless replacement first, rollback on failure; rig-unverified); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed. Service half: a direct (non-workspace) IPC client's window is segmented by the service's direct pipeline, with per-segment views over IPC (ADR-047 Amendment 3); compose/shell mode and hosted windows stay single-screen.** | shell with a window straddling two panels |

Ordering rationale: M0–M3 are hardware-free beyond "two monitors" and exercise everything
with sim_display, so the model is proven before any SR dependency. M4 is David's stated
Expand Down
26 changes: 22 additions & 4 deletions src/xrt/compositor/d3d11/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@ if(WIN32 AND XRT_HAVE_D3D11)
# The cross-adapter atlas bridge is its own static library and lives in
# ../xbridge — see that directory's CMakeLists.txt for why.

# Multi-screen M6: the per-screen segment manager and the output-device
# composite unit it blits flat 2D through. Their own static library so the
# D3D11 service compositor (../d3d11_service) can segment IPC clients'
# windows without linking the whole in-process compositor — the same reason
# comp_xbridge is split out.
add_library(
comp_d3d11_segs STATIC
../d3d_shared/comp_masked_composite_shaders.h
comp_d3d11_outcomp.cpp
comp_d3d11_outcomp.h
comp_d3d11_segments.cpp
comp_d3d11_segments.h
d3d11_layer_shaders.h
)
target_link_libraries(
comp_d3d11_segs
PUBLIC xrt-interfaces
PRIVATE comp_util aux_util aux_os aux_d3d d3d11 d3dcompiler
)
target_include_directories(comp_d3d11_segs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)

add_library(
comp_d3d11 STATIC
../d3d_shared/comp_equirect2_shaders.h
../d3d_shared/comp_masked_composite_shaders.h
comp_d3d11_compositor.cpp
comp_d3d11_compositor.h
comp_d3d11_compositor_internals.h
comp_d3d11_outcomp.cpp
comp_d3d11_outcomp.h
comp_d3d11_renderer.cpp
comp_d3d11_renderer.h
comp_d3d11_segments.cpp
comp_d3d11_segments.h
comp_d3d11_state_guard.h
comp_d3d11_swapchain.cpp
comp_d3d11_swapchain.h
Expand All @@ -44,6 +61,7 @@ if(WIN32 AND XRT_HAVE_D3D11)
# path at all — see docs/investigations/hybrid-igpu-weave.md).
aux_d3d
comp_xbridge
comp_d3d11_segs
xrt-external-stb
d3d11
d3d12
Expand Down
6 changes: 6 additions & 0 deletions src/xrt/compositor/d3d11_service/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ target_link_libraries(
# guard that builds the bridge is satisfied wherever this library builds at all.
target_link_libraries(comp_d3d11_service PRIVATE comp_xbridge)

# Multi-screen M6 on the service path: a direct (non-workspace) IPC client's
# window spanning monitors is woven per screen by the in-process compositor's
# own segment manager (comp_d3d11_segs, split out of comp_d3d11 for exactly
# this). comp_util carries the backend-agnostic segment math (comp_segments.c).
target_link_libraries(comp_d3d11_service PRIVATE comp_d3d11_segs comp_util)

target_include_directories(comp_d3d11_service PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_SOURCE_DIR}/../d3d11
Expand Down
Loading
Loading