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
19 changes: 19 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 @@ -85,3 +85,22 @@ context), they weave the output-side copy of the composed atlas (the egress slot
segment keeps its own viewport, scissor and present origin exactly as off the split. The
split is not a separate segment path; the primary screen's DP keeps the real HWND. Detail:
`docs/architecture/comp-segments.md` § *Windows / D3D11*.

## Amendment 2 (2026-10-09): the HWND follows the majority segment

Decision 2's "the real HWND goes to the majority-area segment's DP" ships on Windows D3D11
(it first shipped with the HWND pinned to the primary screen's DP, David 2026-10-07). The
owner is the segment with the largest window-px area, by the same rule that frames a single
view set (`oxr_segment_views_majority` / `comp_segments_majority`). A hand-off is due only
when another screen beats the owner by at least 20 % of the window's on-screen area (a
60/40 dead band between two screens) continuously for 0.5 s, so a drag across a seam never
flaps it; a target that fails is not retried until the majority leaves it. Because a weaver
is one-per-HWND and a live DP's window is never re-pointed, the hand-off creates a
windowless replacement for the old owner (the primary screen always keeps a DP, as the
session's own), destroys the old owner's DP, then creates the new owner's DP with the HWND
through `create_dp_d3d11_for_screen` — rolling back if that fails — all between two weaves,
so a frame weaves with the old pair or the new pair and never with a gap. The windowless
primary takes its phase from `set_present_origin` like any segment; drag snap, the window
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`.
20 changes: 14 additions & 6 deletions docs/architecture/comp-segments.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,20 @@ deferred-release list because the immediate context executes in order). What dif
- **Two appended ABI slots, no version bump** (ADR-020): `xrt_plugin_iface::create_dp_d3d11_for_screen`
(the D3D11 twin of the Vulkan per-screen factory) and `xrt_display_processor_d3d11::set_present_origin`.
A plug-in without them gets flat 2D on its other screens, exactly like Vulkan.
- **The window.** The session's primary DP keeps the real HWND and so the vendor's drag phase-snap;
every other screen's DP is created with a NULL window and phases from `set_present_origin` + the
canvas offset. The vendor weaver reads the current D3D11 viewport **and scissor** and writes only
inside them, so the segment path sets both to the segment before every `process_atlas`, and the
back buffer is re-bound before every DP. (ADR-047's "HWND follows the majority segment" is a
follow-up: today the HWND stays with the primary screen's DP.)
- **The window.** The real HWND, and so the vendor's drag phase-snap, belongs to the DP of the
screen holding the majority of the window (ADR-047 Amendment 2); every other screen's DP is
windowless and phases from `set_present_origin` + the canvas offset. It starts on the primary DP.
`comp_segments_owner_update` (pure, unit-tested) calls a hand-off when another screen beats the
owner by ≥ 20 % of the on-screen area for 0.5 s; `hwnd_handoff` then makes a windowless
replacement for the old owner (the primary is swapped through the compositor's `swap_primary`
hook, which re-sends transparency / mode / eye-tracking mode and guards the exchange with
`dp_swap_mutex`), destroys the old owner's DP, and makes the new owner's with the HWND — rolling
back on failure — between two weaves. While the primary is windowless the split path runs even
for a window back on the primary alone (it needs its present origin) until the hand-back. One
WARN per flip: `segments: window handle handed from '<dev>' to '<dev>' …` (or `… FAILED …`).
Only sessions with a real window hand off (not the shared-texture path). The vendor weaver reads
the current D3D11 viewport **and scissor** and writes only inside them, so the segment path sets
both to the segment before every `process_atlas`, and the back buffer is re-bound before every DP.
- **Units.** The window rect comes from `ClientToScreen` in a per-monitor-DPI-aware process, i.e.
device px — the same space as the registry's monitor rects. A DPI-virtualised origin would weave on
the wrong lattice and look plausible (`docs/reference/dpi-awareness.md`).
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). HWND stays with the primary screen's DP (majority hand-off is a follow-up); 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/hosted half not started.** | 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
112 changes: 109 additions & 3 deletions src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,21 @@ struct comp_d3d11_compositor
//! Generic D3D11 display processor (vendor-agnostic weaving).
struct xrt_display_processor_d3d11 *display_processor;

/*!
* ADR-047 Amendment 2: while another screen holds the window, the segment
* manager swaps display_processor for a windowless DP of the primary
* screen (and back) on the weave thread under c->mutex. The weave's own
* uses need nothing more; the app-thread entry points that read
* display_processor WITHOUT c->mutex take this leaf lock, which the swap
* holds across the pointer exchange.
*/
std::mutex dp_swap_mutex;
//! The DP the own window's drag snap asks (the window's holder); NULL = display_processor.
struct xrt_display_processor_d3d11 *snap_dp;
//! The app's last eye-tracking mode, re-sent to a swapped-in primary DP.
bool eye_tracking_mode_set;
uint32_t eye_tracking_mode;

/*!
* Multi-screen M6 (ADR-047 D2 on Windows): a window spanning monitors is
* woven per segment, each by its own screen's DP, into the one back
Expand Down Expand Up @@ -819,11 +834,79 @@ d3d11_compositor_snap_window_rect_cb(void *userdata,
int32_t *out_y)
{
auto *c = static_cast<struct comp_d3d11_compositor *>(userdata);
if (c == nullptr || c->display_processor == nullptr) {
if (c == nullptr) {
return false;
}
return xrt_display_processor_d3d11_snap_window_rect(c->display_processor, origin_x, origin_y,
target_x, target_y, out_x, out_y);
// The DP holding the window snaps it (ADR-047 Amendment 2: another
// screen's segment DP while that screen holds the majority). Both
// pointers only change with this provider detached.
struct xrt_display_processor_d3d11 *dp = c->snap_dp != nullptr ? c->snap_dp : c->display_processor;
if (dp == nullptr) {
return false;
}
return xrt_display_processor_d3d11_snap_window_rect(dp, origin_x, origin_y, target_x, target_y, out_x, out_y);
}

/*!
* ADR-047 Amendment 2 hook: install @p dp as the session's primary DP (weave
* thread, c->mutex held) and return the previous one for the segment manager
* to destroy. Re-sends the session-level state a new DP has not seen.
*/
static struct xrt_display_processor_d3d11 *
d3d11_segments_swap_primary_cb(void *userdata, struct xrt_display_processor_d3d11 *dp)
{
auto *c = static_cast<struct comp_d3d11_compositor *>(userdata);
struct xrt_display_processor_d3d11 *old = nullptr;
{
std::lock_guard<std::mutex> lock(c->dp_swap_mutex);
old = c->display_processor;
c->display_processor = dp;
}
if (dp != nullptr) {
xrt_display_processor_d3d11_set_transparent_background(dp, c->transparent_background, false);
xrt_display_processor_d3d11_set_shared_texture_present(dp, c->has_shared_texture);
xrt_display_processor_d3d11_request_display_mode(dp, c->hardware_display_3d);
if (c->eye_tracking_mode_set) {
xrt_display_processor_d3d11_set_eye_tracking_mode(dp, c->eye_tracking_mode);
}
// This weave's per-frame inputs, already sent to the old DP. (Under
// the #918 split the backdrop rides the slot: the next weave has it.)
xrt_display_processor_d3d11_set_frame_timing(dp, comp_d3d11_target_get_measured_weave_ns(c->target),
(uint64_t)(U_TIME_1S_IN_NS / c->display_refresh_rate));
xrt_display_processor_d3d11_set_predicted_scanout(
dp, comp_d3d11_target_predict_weave_to_scanout_ns(c->target));
if (!c->split_active) {
xrt_display_processor_d3d11_set_background_2d(dp, c->repaint.backdrop_srv,
c->repaint.backdrop_w, c->repaint.backdrop_h);
}
}
// A zone mask published to the old DP is gone with it; the next publish
// starts fresh instead of clearing a mask the new DP never saw.
c->zone_published = false;
return old;
}

/*!
* ADR-047 Amendment 2 hook: bracket a hand-off. The own window's drag snap is
* detached (the setter's exclusive lock waits out a snap in flight) before
* any DP holding the window dies, and re-armed on the DP holding it after.
*/
static void
d3d11_segments_hwnd_bracket_cb(void *userdata, bool begin, struct xrt_display_processor_d3d11 *hwnd_dp)
{
auto *c = static_cast<struct comp_d3d11_compositor *>(userdata);
const bool snapping = c->owns_window && c->own_window != nullptr;
if (begin) {
if (snapping) {
comp_d3d11_window_set_snap_provider(c->own_window, NULL, NULL);
}
c->snap_dp = nullptr;
return;
}
c->snap_dp = hwnd_dp;
if (snapping) {
comp_d3d11_window_set_snap_provider(c->own_window, d3d11_compositor_snap_window_rect_cb, c);
}
}

// #439 Phase 1 authored zone-mask helpers (XR_DXR_local_3d_zone). Defined
Expand Down Expand Up @@ -2700,6 +2783,18 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint)
if (c->segments == nullptr) {
c->segments = comp_d3d11_segments_create(d3d11_out_device(c));
if (c->segments != nullptr) {
// ADR-047 Amendment 2: the window follows the majority
// screen — sessions with a real window only (a shared-
// texture session's app HWND is a position anchor, #696).
if (c->hwnd != nullptr && !c->has_shared_texture &&
c->display_processor != nullptr) {
struct comp_d3d11_segments_hwnd_hooks hooks = {};
hooks.hwnd = c->hwnd;
hooks.userdata = c;
hooks.swap_primary = d3d11_segments_swap_primary_cb;
hooks.bracket = d3d11_segments_hwnd_bracket_cb;
comp_d3d11_segments_set_hwnd_hooks(c->segments, &hooks);
}
comp_d3d11_segments_set_screens(c->segments, c->seg_screens, c->seg_sys_info,
c->seg_pinned_display_id);
}
Expand Down Expand Up @@ -4241,7 +4336,10 @@ d3d11_compositor_destroy(struct xrt_compositor *xc)

// Multi-screen M6: segment DPs + crop textures, before the primary DP
// (the repaint thread is already stopped, so no weave is in flight).
// No hooks: the window is not handed back to a primary DP about to die.
comp_d3d11_segments_set_hwnd_hooks(c->segments, nullptr);
comp_d3d11_segments_destroy(&c->segments);
c->snap_dp = nullptr;
free(c->seg_screens);
c->seg_screens = nullptr;
free(c->seg_sys_info);
Expand Down Expand Up @@ -8294,6 +8392,7 @@ comp_d3d11_compositor_get_predicted_eye_positions(struct xrt_compositor *xc,
struct xrt_eye_positions *out_eye_pos)
{
struct comp_d3d11_compositor *c = d3d11_comp(xc);
std::lock_guard<std::mutex> dp_lock(c->dp_swap_mutex);

if (c->display_processor != nullptr) {
if (xrt_display_processor_d3d11_get_predicted_eye_positions(c->display_processor, out_eye_pos) &&
Expand All @@ -8311,6 +8410,7 @@ comp_d3d11_compositor_get_display_dimensions(struct xrt_compositor *xc,
float *out_height_m)
{
struct comp_d3d11_compositor *c = d3d11_comp(xc);
std::lock_guard<std::mutex> dp_lock(c->dp_swap_mutex);

if (c->display_processor != nullptr) {
return xrt_display_processor_d3d11_get_display_dimensions(
Expand Down Expand Up @@ -8398,6 +8498,7 @@ comp_d3d11_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_w
// runtime-side Kooima (rig path, raw channel, legacy-2D fovs) ran
// display-scoped (#396 W7).
HWND metrics_hwnd = c->hwnd != nullptr ? c->hwnd : c->app_hwnd;
std::lock_guard<std::mutex> dp_lock(c->dp_swap_mutex); // ADR-047 Amendment 2 swap
if (c->display_processor == nullptr || metrics_hwnd == nullptr) {
return false;
}
Expand Down Expand Up @@ -8499,6 +8600,7 @@ comp_d3d11_compositor_request_display_mode(struct xrt_compositor *xc, bool enabl
}

struct comp_d3d11_compositor *c = d3d11_comp(xc);
std::lock_guard<std::mutex> dp_lock(c->dp_swap_mutex);

if (c->display_processor != nullptr) {
return xrt_display_processor_d3d11_request_display_mode(c->display_processor, enable_3d);
Expand Down Expand Up @@ -8560,6 +8662,7 @@ comp_d3d11_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_
for (uint32_t k = 0; k < out->count && k < XRT_MAX_SEGMENTS; k++) {
struct xrt_segment_metric *s = &out->seg[k];
if (s->is_primary) {
std::lock_guard<std::mutex> dp_lock(c->dp_swap_mutex);
s->have_eyes = c->display_processor != nullptr &&
xrt_display_processor_d3d11_get_predicted_eye_positions(c->display_processor,
&s->eyes) &&
Expand Down Expand Up @@ -8590,7 +8693,10 @@ comp_d3d11_compositor_set_eye_tracking_mode(struct xrt_compositor *xc, uint32_t
}

struct comp_d3d11_compositor *c = d3d11_comp(xc);
std::lock_guard<std::mutex> dp_lock(c->dp_swap_mutex);

c->eye_tracking_mode_set = true;
c->eye_tracking_mode = mode;
if (c->display_processor != nullptr) {
xrt_display_processor_d3d11_set_eye_tracking_mode(c->display_processor, mode);
}
Expand Down
Loading
Loading