diff --git a/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md b/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md index 399d24cbea..f5b099a7f2 100644 --- a/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md +++ b/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md @@ -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`. diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index a803b8a253..79cee5aad7 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -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 '' to '' …` (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`). diff --git a/docs/roadmap/multi-screen.md b/docs/roadmap/multi-screen.md index 97c656208a..fde9cc1d69 100644 --- a/docs/roadmap/multi-screen.md +++ b/docs/roadmap/multi-screen.md @@ -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 diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp index c611d3e172..ab5ebfc6dc 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp @@ -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 @@ -819,11 +834,79 @@ d3d11_compositor_snap_window_rect_cb(void *userdata, int32_t *out_y) { auto *c = static_cast(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(userdata); + struct xrt_display_processor_d3d11 *old = nullptr; + { + std::lock_guard 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(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 @@ -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); } @@ -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); @@ -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 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) && @@ -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 dp_lock(c->dp_swap_mutex); if (c->display_processor != nullptr) { return xrt_display_processor_d3d11_get_display_dimensions( @@ -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 dp_lock(c->dp_swap_mutex); // ADR-047 Amendment 2 swap if (c->display_processor == nullptr || metrics_hwnd == nullptr) { return false; } @@ -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 dp_lock(c->dp_swap_mutex); if (c->display_processor != nullptr) { return xrt_display_processor_d3d11_request_display_mode(c->display_processor, enable_3d); @@ -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 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) && @@ -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 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); } diff --git a/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp b/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp index 07ee20a6ba..fd315bb20d 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp @@ -20,6 +20,7 @@ #include "comp_d3d11_outcomp.h" #include "d3d/d3d_dxgi_formats.h" #include "os/os_threading.h" +#include "os/os_time.h" #include "util/u_logging.h" #include "util/u_misc.h" #include "xrt/xrt_plugin.h" @@ -54,6 +55,7 @@ struct seg_screen_state int transparent_latched; //!< last transparency declared, -1 = none. int mode_sent; //!< last 2D/3D mode sent, -1 = none. bool tolerates_resample; + bool has_hwnd; //!< this DP holds the session's window (the owner). struct seg_crop crop; }; @@ -73,6 +75,20 @@ struct comp_d3d11_segments struct comp_segment_table table; struct comp_segments_lifecycle lc; + /* + * Who holds the window handle (ADR-047 Amendment 2). Off the primary, + * the owner's DP lives in st[owner_index].dp with has_hwnd set, and the + * session's primary DP is a windowless per-screen DP the compositor + * swapped in through hooks.swap_primary. + */ + struct comp_d3d11_segments_hwnd_hooks hooks; + struct comp_segments_owner owner; + uint32_t primary_index; //!< index of the primary screen, UINT32_MAX = none + int owner_index; //!< index of the screen holding the handle, -1 = none + bool primary_for_screen; //!< the primary's plug-in has create_dp_d3d11_for_screen + bool primary_windowless; //!< the session's primary DP is windowless right now + ID3D11DeviceContext *ctx; //!< last update's context (the hand-back at destroy) + //! The app thread reads st[i].dp for eyes; the weave creates/destroys it. struct os_mutex dp_mutex; @@ -178,29 +194,31 @@ dp_release(struct comp_d3d11_segments *segs, uint32_t i) st->transparent_latched = -1; st->mode_sent = -1; st->tolerates_resample = false; + st->has_hwnd = false; crop_destroy(&st->crop); } /*! - * Create screen @p i's segment DP through the plug-in's per-screen factory. - * Windowless (NULL window) on purpose: a segment DP's phase comes from - * set_present_origin; the session's real HWND stays with the primary DP (the - * vendor's drag phase-snap follows it). + * Make (not install) a DP for screen @p i through the plug-in's per-screen + * factory. @p window is NULL for a windowless segment DP (its phase comes from + * set_present_origin) and the session's real HWND only for the screen that + * owns it — the screen holding the majority of the window, whose DP keeps the + * vendor's drag phase-snap (ADR-047 Amendment 2). * * No fallback to the plain `create_dp_d3d11`, for the reason given on * xrt_plugin_iface::create_dp_vk_for_screen: that DP describes the plug-in's * process-wide panel and is not bound by the canvas / not-first-writer * contract, so it could wipe the segment woven before it. */ -static bool -dp_create(struct comp_d3d11_segments *segs, uint32_t i, ID3D11DeviceContext *ctx) +static struct xrt_display_processor_d3d11 * +dp_make(struct comp_d3d11_segments *segs, uint32_t i, ID3D11DeviceContext *ctx, void *window) { - struct seg_screen_state *st = &segs->st[i]; const struct xrt_plugin_iface *iface = segs->iface[i]; + const bool can = segs->screens[i].is_primary ? segs->primary_for_screen : segs->screens[i].has_dp_factory; xrt_result_t xret = XRT_ERROR_DEVICE_CREATION_FAILED; struct xrt_display_processor_d3d11 *dp = nullptr; - if (segs->screens[i].has_dp_factory && xrt_plugin_iface_has_create_dp_d3d11_for_screen(iface)) { - xret = iface->create_dp_d3d11_for_screen(segs->inst[i], segs->device, ctx, nullptr, &segs->bindings[i], + if (can && xrt_plugin_iface_has_create_dp_d3d11_for_screen(iface)) { + xret = iface->create_dp_d3d11_for_screen(segs->inst[i], segs->device, ctx, window, &segs->bindings[i], &dp); } if (xret != XRT_SUCCESS || dp == nullptr || dp->process_atlas == nullptr) { @@ -208,29 +226,207 @@ dp_create(struct comp_d3d11_segments *segs, uint32_t i, ID3D11DeviceContext *ctx xrt_display_processor_d3d11_destroy(&dp); } U_LOG_W( - "segments: could not create a DP for screen 0x%016llx ('%s', plug-in '%s') via " - "create_dp_d3d11_for_screen (%d) — that segment stays flat 2D", - (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, segs->screens[i].plugin_id, - (int)xret); - return false; + "segments: could not create a %s DP for screen 0x%016llx ('%s', plug-in '%s') via " + "create_dp_d3d11_for_screen (%d)", + window != nullptr ? "window-holding" : "windowless", (unsigned long long)segs->screens[i].id, + segs->bindings[i].device_name, segs->screens[i].plugin_id, (int)xret); + return nullptr; } + return dp; +} + +/*! + * Install @p dp (may be NULL) as screen @p i's segment DP, latches reset so + * the next weave configures it. Returns the DP it replaced, for the caller to + * destroy (once this returns, get_eyes can no longer reach it). + */ +static struct xrt_display_processor_d3d11 * +dp_install(struct comp_d3d11_segments *segs, uint32_t i, struct xrt_display_processor_d3d11 *dp, bool has_hwnd) +{ + struct seg_screen_state *st = &segs->st[i]; os_mutex_lock(&segs->dp_mutex); + struct xrt_display_processor_d3d11 *old = st->dp; st->dp = dp; os_mutex_unlock(&segs->dp_mutex); - st->tolerates_resample = xrt_display_processor_d3d11_tolerates_resample(dp); + st->tolerates_resample = dp != nullptr && xrt_display_processor_d3d11_tolerates_resample(dp); + st->has_hwnd = dp != nullptr && has_hwnd; st->configured = false; st->encoding_latched = -1; st->transparent_latched = -1; st->mode_sent = -1; + if (dp == nullptr) { + crop_destroy(&st->crop); + } + return old; +} + +/*! + * The lifecycle's CREATE: a windowless segment DP for screen @p i. A screen + * that already has a DP (it holds the window handle) keeps it. + */ +static bool +dp_create(struct comp_d3d11_segments *segs, uint32_t i, ID3D11DeviceContext *ctx) +{ + if (segs->st[i].dp != nullptr) { + return true; + } + struct xrt_display_processor_d3d11 *dp = dp_make(segs, i, ctx, nullptr); + if (dp == nullptr) { + U_LOG_W("segments: screen 0x%016llx's segment stays flat 2D", (unsigned long long)segs->screens[i].id); + return false; + } + (void)dp_install(segs, i, dp, false); // Lifecycle event (hysteresis-gated), not per frame. U_LOG_W( "segments: created a segment DP for screen 0x%016llx ('%s', plug-in '%s') via " "create_dp_d3d11_for_screen — %s", (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, segs->screens[i].plugin_id, - st->tolerates_resample ? "tolerates a resample" : "needs 1:1 pixels"); + segs->st[i].tolerates_resample ? "tolerates a resample" : "needs 1:1 pixels"); return true; } +static bool +table_has_screen(const struct comp_segment_table *t, uint64_t screen_id) +{ + for (uint32_t k = 0; k < t->count; k++) { + if (t->seg[k].screen_id == screen_id && t->seg[k].window_rect.w > 0 && t->seg[k].window_rect.h > 0) { + return true; + } + } + return false; +} + +static const char * +screen_name(const struct comp_d3d11_segments *segs, int i) +{ + return i >= 0 ? segs->bindings[i].device_name : "(none)"; +} + +/*! + * Put @p dp in screen @p i's slot — as the DP holding the window handle + * (@p has_hwnd) or as a windowless one — and destroy what it replaces. The + * primary's slot is the session's own DP, swapped by the compositor. + */ +static void +slot_replace(struct comp_d3d11_segments *segs, uint32_t i, struct xrt_display_processor_d3d11 *dp, bool has_hwnd) +{ + struct xrt_display_processor_d3d11 *old = nullptr; + if (i == segs->primary_index) { + old = segs->hooks.swap_primary(segs->hooks.userdata, dp); + segs->primary_windowless = !has_hwnd; + } else { + old = dp_install(segs, i, dp, has_hwnd); + if (dp != nullptr) { + // The lifecycle tracks this screen's DP as live: no second create. + comp_segments_lifecycle_set_created(&segs->lc, segs->screens[i].id, true); + } + } + if (old != nullptr) { + xrt_display_processor_d3d11_destroy(&old); + } +} + +/*! + * Move the window handle from the current owner to screen @p t (ADR-047 + * Amendment 2). One weaver per window, and a live DP's window cannot be + * re-pointed, so: + * 1. make a windowless replacement for the old owner (the primary always + * needs one; another screen only while the window still covers it) — + * failing that, abort with nothing changed; + * 2. swap it in and destroy the old owner's DP, which frees the window; + * 3. make the target's DP WITH the window and swap it in — failing that, + * roll back (the old owner takes the window again). + * All of it runs on the weave thread between two weaves (the caller holds the + * compositor lock), so a frame weaves with the old pair or the new pair and + * never with a gap. + */ +static void +hwnd_handoff(struct comp_d3d11_segments *segs, uint32_t t, ID3D11DeviceContext *ctx) +{ + const int o = segs->owner_index; + if (o == (int)t) { + return; + } + const uint64_t t_id = segs->screens[t].id; + const uint64_t t0 = os_monotonic_get_ns(); + + // 1. The old owner's windowless replacement. + struct xrt_display_processor_d3d11 *w = nullptr; + const bool o_needs_dp = + o >= 0 && ((uint32_t)o == segs->primary_index || + (segs->screens[o].has_dp_factory && table_has_screen(&segs->table, segs->screens[o].id))); + if (o_needs_dp) { + w = dp_make(segs, (uint32_t)o, ctx, nullptr); + if (w == nullptr) { + U_LOG_W("segments: the window handle stays with '%s' — no windowless DP to replace it with", + screen_name(segs, o)); + comp_segments_owner_set_result(&segs->owner, t_id, false, segs->screens[o].id); + return; + } + } + + // 2. Free the window. + if (segs->hooks.bracket != nullptr) { + segs->hooks.bracket(segs->hooks.userdata, true, nullptr); + } + if (o >= 0) { + slot_replace(segs, (uint32_t)o, w, false); + } + + // 3. The target takes the window. + struct xrt_display_processor_d3d11 *h = dp_make(segs, t, ctx, segs->hooks.hwnd); + int now_owner = -1; + if (h != nullptr) { + slot_replace(segs, t, h, true); + now_owner = (int)t; + } else if (o >= 0) { + // Roll back: the old owner takes it again. + struct xrt_display_processor_d3d11 *back = dp_make(segs, (uint32_t)o, ctx, segs->hooks.hwnd); + if (back != nullptr) { + slot_replace(segs, (uint32_t)o, back, true); + now_owner = o; + } + } + segs->owner_index = now_owner; + if (segs->hooks.bracket != nullptr) { + // The DP the drag snap asks now; NULL = the session's primary DP. + struct xrt_display_processor_d3d11 *hdp = nullptr; + if (now_owner >= 0 && (uint32_t)now_owner != segs->primary_index) { + hdp = segs->st[now_owner].dp; + } + segs->hooks.bracket(segs->hooks.userdata, false, hdp); + } + comp_segments_owner_set_result(&segs->owner, t_id, h != nullptr, + now_owner >= 0 ? segs->screens[now_owner].id : 0); + + // One line per flip (the flip itself is hysteresis-gated). + const double ms = (double)(os_monotonic_get_ns() - t0) / 1.0e6; + if (h != nullptr) { + U_LOG_W( + "segments: window handle handed from '%s' to '%s' (screen 0x%016llx holds the majority) in " + "%.1f ms — drag snap follows it; '%s' weaves windowless", + screen_name(segs, o), screen_name(segs, (int)t), (unsigned long long)t_id, ms, + screen_name(segs, o)); + } else { + U_LOG_W("segments: window handle hand-off to '%s' FAILED after %.1f ms — %s", screen_name(segs, (int)t), + ms, now_owner >= 0 ? "rolled back to the previous owner" : "no DP holds the window now"); + } +} + +/*! + * Hand the window back to the primary screen's DP (a screen-list rebuild + * while another screen owns it). No-op when the primary owns it. + */ +static void +hwnd_return_to_primary(struct comp_d3d11_segments *segs) +{ + if (segs->hooks.swap_primary == nullptr || segs->primary_index == UINT32_MAX || segs->ctx == nullptr || + (segs->owner_index == (int)segs->primary_index && !segs->primary_windowless)) { + return; + } + hwnd_handoff(segs, segs->primary_index, segs->ctx); +} + static void set_viewport_scissor(ID3D11DeviceContext *ctx, const struct comp_seg_rect *r) { @@ -266,6 +462,8 @@ comp_d3d11_segments_create(void *d3d11_device) os_mutex_init(&segs->dp_mutex); comp_segments_lifecycle_init(&segs->lc, 0, 0); segs->mode_index = UINT32_MAX; + segs->primary_index = UINT32_MAX; + segs->owner_index = -1; for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { segs->st[i].encoding_latched = -1; segs->st[i].transparent_latched = -1; @@ -281,6 +479,11 @@ comp_d3d11_segments_destroy(struct comp_d3d11_segments **segs_ptr) return; } struct comp_d3d11_segments *segs = *segs_ptr; + // Another screen holds the window: hand it back to the session's primary + // DP first (no hooks = compositor teardown, where the primary DP goes too). + // An empty table: the old owner needs no windowless replacement. + segs->table.count = 0; + hwnd_return_to_primary(segs); for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { dp_release(segs, i); } @@ -298,10 +501,15 @@ comp_d3d11_segments_set_screens(struct comp_d3d11_segments *segs, if (segs == nullptr) { return; } + segs->table.count = 0; + hwnd_return_to_primary(segs); for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { dp_release(segs, i); } comp_segments_lifecycle_init(&segs->lc, 0, 0); + segs->primary_index = UINT32_MAX; + segs->owner_index = -1; + segs->primary_for_screen = false; segs->enabled = false; segs->screen_count = 0; segs->have_logged_table = false; @@ -370,7 +578,15 @@ comp_d3d11_segments_set_screens(struct comp_d3d11_segments *segs, segs->iface[n] = nullptr; segs->inst[n] = nullptr; - if (e != nullptr && !cs->is_primary) { + if (cs->is_primary) { + // The primary's own plug-in, for the window-handle hand-off: a + // windowless primary DP while another screen holds the window. + segs->iface[n] = (const struct xrt_plugin_iface *)e->owning_iface; + segs->inst[n] = (struct xrt_plugin_instance *)e->owning_instance; + segs->primary_for_screen = e->dp_factory_d3d11 != nullptr && + xrt_plugin_iface_has_create_dp_d3d11_for_screen(segs->iface[n]); + segs->primary_index = n; + } else if (e != nullptr) { // A segment's DP comes from ITS screen's registry entry, // whichever plug-in that is (mixed-vendor rigs, M4). if (strcmp(e->plugin_id, primary_entry->plugin_id) != 0) { @@ -418,12 +634,37 @@ comp_d3d11_segments_set_screens(struct comp_d3d11_segments *segs, primary_entry->plugin_id, vendors); } segs->enabled = n >= 2; + if (segs->primary_index == UINT32_MAX) { + segs->primary_for_screen = false; // no primary segment: nothing to hand off from + } if (segs->enabled) { + if (segs->primary_index != UINT32_MAX) { + segs->owner_index = (int)segs->primary_index; + comp_segments_owner_init(&segs->owner, segs->screens[segs->primary_index].id, 0, 0); + } + const bool handoff = segs->hooks.swap_primary != nullptr && segs->primary_for_screen; U_LOG_W( "segments: %u screens, plug-in '%s' — a window spanning monitors is woven per monitor " - "(multi-screen M6, D3D11)", - n, primary_entry->plugin_id); + "(multi-screen M6, D3D11); the window handle %s", + n, primary_entry->plugin_id, + handoff ? "follows the majority screen" + : (segs->hooks.swap_primary == nullptr + ? "stays with the primary DP (no window to hand off)" + : "stays with the primary DP (its plug-in has no create_dp_d3d11_for_screen)")); + } +} + +extern "C" void +comp_d3d11_segments_set_hwnd_hooks(struct comp_d3d11_segments *segs, const struct comp_d3d11_segments_hwnd_hooks *hooks) +{ + if (segs == nullptr) { + return; + } + if (hooks == nullptr || hooks->hwnd == nullptr || hooks->swap_primary == nullptr) { + memset(&segs->hooks, 0, sizeof(segs->hooks)); + return; } + segs->hooks = *hooks; } extern "C" bool @@ -456,11 +697,26 @@ comp_d3d11_segments_update(struct comp_d3d11_segments *segs, if (act.items[a].action == COMP_SEG_ACTION_CREATE) { const bool ok = dp_create(segs, (uint32_t)i, ctx); comp_segments_lifecycle_set_created(&segs->lc, act.items[a].screen_id, ok); - } else if (act.items[a].action == COMP_SEG_ACTION_DESTROY) { + } else if (act.items[a].action == COMP_SEG_ACTION_DESTROY && !segs->st[i].has_hwnd) { + // The DP holding the window outlives its segment: it moves + // only by a hand-off (below), never by a retire. dp_release(segs, (uint32_t)i); } } + /* + * ADR-047 Amendment 2: the window handle follows the majority screen, + * hysteresis in comp_segments_owner (a clear margin held for 0.5 s). + */ + segs->ctx = ctx; + if (segs->hooks.swap_primary != nullptr && segs->primary_for_screen) { + const uint64_t target = comp_segments_owner_update(&segs->owner, &segs->table, os_monotonic_get_ns()); + const int ti = target != 0 ? screen_index_of(segs, target) : -1; + if (ti >= 0) { + hwnd_handoff(segs, (uint32_t)ti, ctx); + } + } + const uint32_t cw = canvas != nullptr && canvas->w > 0 ? canvas->w : window_desktop->w; const uint32_t ch = canvas != nullptr && canvas->h > 0 ? canvas->h : window_desktop->h; const bool split = comp_segments_table_is_split(&segs->table, cw, ch); @@ -494,7 +750,10 @@ comp_d3d11_segments_update(struct comp_d3d11_segments *segs, segs->have_logged_table = false; } segs->logged_split = split; - return split; + // A windowless primary DP needs its present origin, which only the split + // path sends: keep it while another screen (or nobody) holds the window, + // even with the window back on the primary alone (until the hand-back). + return split || segs->primary_windowless; } extern "C" bool @@ -604,7 +863,14 @@ comp_d3d11_segments_record(struct comp_d3d11_segments *segs, const struct comp_d } struct xrt_display_processor_d3d11 *dp = dp_of[k]; struct seg_screen_state *st = st_of[k]; - if (!g->is_primary) { + if (g->is_primary) { + // The session feeds its own DP everything else; windowless + // (another screen holds the window), it needs its phase here. + if (segs->primary_windowless && g->screen_1to1 != COMP_SEG_1TO1_NO) { + xrt_display_processor_d3d11_set_present_origin(dp, g->present_origin_x, + g->present_origin_y); + } + } else { if (!st->configured) { st->configured = true; xrt_display_processor_d3d11_set_background_2d(dp, nullptr, 0, 0); @@ -626,7 +892,9 @@ comp_d3d11_segments_record(struct comp_d3d11_segments *segs, const struct comp_d // Phase only where window px ARE device px; a resampled // screen's origin is in the wrong units (and only a // resample-tolerant DP weaves there, which has no phase). - if (g->screen_1to1 != COMP_SEG_1TO1_NO) { + // The DP holding the window phases from it, like the + // primary always has. + if (!st->has_hwnd && g->screen_1to1 != COMP_SEG_1TO1_NO) { xrt_display_processor_d3d11_set_present_origin(dp, g->present_origin_x, g->present_origin_y); } diff --git a/src/xrt/compositor/d3d11/comp_d3d11_segments.h b/src/xrt/compositor/d3d11/comp_d3d11_segments.h index bd47bfab28..af720b3847 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_segments.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_segments.h @@ -15,11 +15,16 @@ * `create_dp_d3d11_for_screen`), or whose DP needs 1:1 pixels on a resampled * screen, gets a flat 2D blit of one view instead. * - * The session's primary DP (`comp_d3d11_compositor::display_processor`, the - * one holding the real HWND and so the vendor's drag phase-snap) keeps weaving - * the primary screen's segment and keeps owning everything view-related. A - * window entirely on the primary screen never enters this module's record - * path: that case stays byte-for-byte the single-DP path. + * The session's primary DP (`comp_d3d11_compositor::display_processor`) keeps + * weaving the primary screen's segment and keeps owning everything + * view-related. The real HWND (and so the vendor's drag phase-snap) follows + * the screen holding the majority of the window (ADR-047 Amendment 2): when + * another screen clearly holds it for 0.5 s, that screen's segment DP is + * recreated WITH the window and the primary DP is swapped for a windowless + * one (through @ref comp_d3d11_segments_hwnd_hooks), and back. A window + * entirely on the primary screen never enters this module's record path while + * the primary holds the window: that case stays byte-for-byte the single-DP + * path. * * Design note: docs/architecture/comp-segments.md (§ Windows / D3D11). */ @@ -54,11 +59,48 @@ comp_d3d11_segments_create(void *d3d11_device); /*! * Tear down every segment DP and crop texture. The caller guarantees no weave - * is in flight (the repaint thread is stopped). + * is in flight (the repaint thread is stopped). With hooks set and another + * screen holding the window, the window is handed back to the primary DP + * first; clear the hooks before a teardown that destroys the primary DP too. */ void comp_d3d11_segments_destroy(struct comp_d3d11_segments **segs_ptr); +/*! + * How the manager moves the session's window between DPs (ADR-047 + * Amendment 2). All callbacks run on the weave thread with the compositor's + * weave lock held. + */ +struct comp_d3d11_segments_hwnd_hooks +{ + //! The session's real window (HWND). NULL disables the hand-off. + void *hwnd; + void *userdata; + /*! + * Install @p dp as the session's primary DP and return the previous one, + * which the manager destroys. The compositor re-sends its session-level + * state (transparency, 2D/3D mode, eye-tracking mode) to @p dp and guards + * the exchange against its other threads. + */ + struct xrt_display_processor_d3d11 *(*swap_primary)(void *userdata, struct xrt_display_processor_d3d11 *dp); + /*! + * Brackets a hand-off: @p begin true before any DP holding the window is + * destroyed; false after, with @p hwnd_dp the DP holding the window now — + * NULL when it is the session's primary DP (or none does). The compositor + * routes its own window's drag snap to it. Optional. + */ + void (*bracket)(void *userdata, bool begin, struct xrt_display_processor_d3d11 *hwnd_dp); +}; + +/*! + * Enable the window-handle hand-off. Call before + * @ref comp_d3d11_segments_set_screens. NULL (or a NULL window) disables it: + * the window stays with the primary DP. + */ +void +comp_d3d11_segments_set_hwnd_hooks(struct comp_d3d11_segments *segs, + const struct comp_d3d11_segments_hwnd_hooks *hooks); + /*! * Hand over the system's screens and DP registry. Segmentation is enabled only * when at least two screens are listed, the registry knows the system-default @@ -85,8 +127,13 @@ comp_d3d11_segments_enabled(const struct comp_d3d11_segments *segs); * @param d3d11_context The context handed to new segment DPs. * @param mode_index The head's active rendering-mode index; a change * re-reads each segment DP's resample tolerance. + * Runs the window-handle hand-off when one is due (two DP creates, between + * two weaves). + * * @return true when this frame must take the split path - * (@ref comp_d3d11_segments_record); false = the single-DP path. + * (@ref comp_d3d11_segments_record) — the window spans screens, or + * the primary DP is windowless (another screen holds the window) and + * needs its present origin; false = the single-DP path. */ bool comp_d3d11_segments_update(struct comp_d3d11_segments *segs, diff --git a/src/xrt/compositor/util/comp_segments.c b/src/xrt/compositor/util/comp_segments.c index 8d43812577..0ee05efa36 100644 --- a/src/xrt/compositor/util/comp_segments.c +++ b/src/xrt/compositor/util/comp_segments.c @@ -471,6 +471,126 @@ comp_segments_lifecycle_drain(struct comp_segments_lifecycle *lc, struct comp_se } +/* + * + * Window-handle ownership. + * + */ + +static uint64_t +seg_area(const struct comp_segment *g) +{ + return (uint64_t)g->window_rect.w * (uint64_t)g->window_rect.h; +} + +uint32_t +comp_segments_majority(const struct comp_segment_table *t) +{ + if (t == NULL || t->count == 0) { + return UINT32_MAX; + } + uint32_t best = 0; + uint64_t best_area = 0; + bool best_primary = false; + for (uint32_t i = 0; i < t->count && i < COMP_SEGMENTS_MAX; i++) { + const uint64_t area = seg_area(&t->seg[i]); + const bool primary = t->seg[i].is_primary; + if (i == 0 || area > best_area || (area == best_area && primary && !best_primary)) { + best = i; + best_area = area; + best_primary = primary; + } + } + return best; +} + +void +comp_segments_owner_init(struct comp_segments_owner *o, uint64_t primary_id, uint64_t hold_ns, uint32_t margin_pct) +{ + if (o == NULL) { + return; + } + memset(o, 0, sizeof(*o)); + o->hold_ns = hold_ns > 0 ? hold_ns : COMP_SEGMENTS_DEFAULT_HANDOFF_HOLD_NS; + o->margin_pct = margin_pct > 0 ? margin_pct : COMP_SEGMENTS_DEFAULT_HANDOFF_MARGIN_PCT; + o->owner_id = primary_id; +} + +static void +owner_reset_candidate(struct comp_segments_owner *o) +{ + o->candidate_id = 0; + o->candidate_since_ns = 0; + o->failed_id = 0; +} + +uint64_t +comp_segments_owner_update(struct comp_segments_owner *o, const struct comp_segment_table *t, uint64_t now_ns) +{ + if (o == NULL) { + return 0; + } + const uint32_t mj = comp_segments_majority(t); + if (mj == UINT32_MAX) { + // On no screen: nothing to follow, and the clock restarts. + owner_reset_candidate(o); + return 0; + } + const struct comp_segment *c = &t->seg[mj]; + if (c->screen_id == o->owner_id || !(c->is_primary || c->has_dp_factory)) { + // The owner holds the majority, or the majority cannot be woven. + owner_reset_candidate(o); + return 0; + } + + uint64_t total = 0; + uint64_t owner_area = 0; + for (uint32_t i = 0; i < t->count && i < COMP_SEGMENTS_MAX; i++) { + const uint64_t a = seg_area(&t->seg[i]); + total += a; + if (t->seg[i].screen_id == o->owner_id) { + owner_area += a; + } + } + const uint64_t cand_area = seg_area(c); + // cand - owner >= margin% of the on-screen area, in integers. + if (cand_area <= owner_area || (cand_area - owner_area) * 100u < total * (uint64_t)o->margin_pct) { + // Inside the dead band: not a clear majority, the clock restarts. + owner_reset_candidate(o); + return 0; + } + + if (c->screen_id != o->candidate_id) { + o->candidate_id = c->screen_id; + o->candidate_since_ns = now_ns; + o->failed_id = 0; + } + if (o->failed_id == c->screen_id) { + return 0; + } + if (now_ns - o->candidate_since_ns < o->hold_ns) { + return 0; + } + return c->screen_id; +} + +void +comp_segments_owner_set_result(struct comp_segments_owner *o, uint64_t target_id, bool ok, uint64_t owner_id) +{ + if (o == NULL) { + return; + } + o->owner_id = owner_id; + if (ok) { + owner_reset_candidate(o); + } else { + // Keep timing the same challenger, but do not retry it until the + // majority leaves it. + o->failed_id = target_id; + } +} + + /* * * Deferred release. diff --git a/src/xrt/compositor/util/comp_segments.h b/src/xrt/compositor/util/comp_segments.h index 1351ffc599..894843bed9 100644 --- a/src/xrt/compositor/util/comp_segments.h +++ b/src/xrt/compositor/util/comp_segments.h @@ -23,6 +23,8 @@ * segment refuse-rather-than-resample policy (weave, or a flat 2D blit). * - @ref comp_segments_lifecycle — hysteresis for creating and destroying a * segment's DP, so a window dragged across a seam never thrashes DPs. + * - @ref comp_segments_owner — hysteresis for which screen's DP holds the + * window handle (it follows the majority segment, ADR-047 Amendment 2). * * The rect math is @ref u_multi_display_compute_slices (half-open, so a seam * belongs to exactly one screen). @@ -352,6 +354,85 @@ comp_segments_lifecycle_drain(struct comp_segments_lifecycle *lc, struct comp_se +/* + * + * Window-handle ownership: the HWND follows the majority segment. + * + */ + +//! Default: how long a new majority must hold before the window handle moves (ns). +#define COMP_SEGMENTS_DEFAULT_HANDOFF_HOLD_NS (500ull * 1000ull * 1000ull) + +//! Default: the margin a challenger's area must beat the owner's by, in percent +//! of the window's on-screen area (20 = a 60/40 split between two screens). +#define COMP_SEGMENTS_DEFAULT_HANDOFF_MARGIN_PCT 20u + +/*! + * The segment holding most of the window: the largest window-px area; an exact + * tie goes to the primary screen, else to the earlier (leftmost) segment. The + * same rule as `oxr_segment_views_majority`, so the screen a single view set + * is framed from and the screen that owns the window handle agree. + * + * @return the index into @p t, or UINT32_MAX when @p t is empty. + */ +uint32_t +comp_segments_majority(const struct comp_segment_table *t); + +/*! + * Which screen's display processor holds the window's real handle (the + * vendor's drag phase-snap and window-scoped behaviour follow it). Every other + * segment's DP is windowless and is phased by `set_present_origin`. + * + * Starts on the primary screen. A hand-off to another screen is due only when + * - that screen holds the majority (@ref comp_segments_majority), + * - its area beats the current owner's by at least `margin_pct` percent of + * the window's on-screen area (an absent owner counts as 0), and + * - both have held continuously for `hold_ns`. + * The margin is a dead band around the seam and the hold rides out a drag, so + * a window dragged back and forth across a seam never flaps the handle (a + * hand-off rebuilds two DPs). A target that could not take the handle is not + * retried until the majority leaves it. A screen with no DP factory is never a + * target (its segment is flat 2D). + */ +struct comp_segments_owner +{ + uint64_t hold_ns; + uint32_t margin_pct; + //! The screen whose DP holds the handle; 0 = none (a failed hand-off). + uint64_t owner_id; + //! The challenger being timed, 0 = none. + uint64_t candidate_id; + uint64_t candidate_since_ns; + //! A target whose hand-off failed; skipped until it stops being the challenger. + uint64_t failed_id; +}; + +/*! + * Initialise with the primary screen as the owner. 0 for either threshold + * selects the default. + */ +void +comp_segments_owner_init(struct comp_segments_owner *o, uint64_t primary_id, uint64_t hold_ns, uint32_t margin_pct); + +/*! + * Advance one metric update with the current table at @p now_ns. + * + * @return the screen id the handle should move to now, or 0 for no change. The + * caller performs the hand-off and reports it with + * @ref comp_segments_owner_set_result. + */ +uint64_t +comp_segments_owner_update(struct comp_segments_owner *o, const struct comp_segment_table *t, uint64_t now_ns); + +/*! + * Report a hand-off's outcome: @p owner_id is the screen whose DP holds the + * handle afterwards (the target on success; the old owner after a rollback; + * 0 when no DP holds it). @p ok false latches @p target_id as failed. + */ +void +comp_segments_owner_set_result(struct comp_segments_owner *o, uint64_t target_id, bool ok, uint64_t owner_id); + + /* * * Deferred release. diff --git a/tests/tests_comp_segments.cpp b/tests/tests_comp_segments.cpp index 2fe54312a9..fdff0ae4ec 100644 --- a/tests/tests_comp_segments.cpp +++ b/tests/tests_comp_segments.cpp @@ -392,6 +392,194 @@ TEST_CASE("comp_segments: lifecycle hysteresis") } } +namespace { + +// The laptop panel (primary, 0x1 on the win box) at the origin and the DS1 to +// its right, both 3840x2160 1:1. A 1000x500 window at x places +// (3840 - x) px on the laptop. +const uint64_t LAPTOP = 0x1; +const uint64_t ACER = 0x2; +const uint64_t MS = 1000ull * 1000ull; + +comp_segment_table +window_at(int32_t x, bool acer_has_factory = true) +{ + comp_segments_screen scr[2] = { + screen(LAPTOP, 0, 0, 3840, 2160, 3840, 2160, true), + screen(ACER, 3840, 0, 3840, 2160, 3840, 2160, false), + }; + scr[1].has_dp_factory = acer_has_factory; + comp_seg_rect win{x, 100, 1000, 500}; + comp_segment_table t{}; + comp_segments_compute(&win, nullptr, scr, 2, &t); + return t; +} + +//! Feed @p t every 16 ms for @p ms; return the first hand-off target (0 = none). +uint64_t +feed(comp_segments_owner &o, const comp_segment_table &t, uint64_t &now, uint64_t ms) +{ + uint64_t target = 0; + for (uint64_t elapsed = 0; elapsed <= ms; elapsed += 16) { + const uint64_t r = comp_segments_owner_update(&o, &t, now); + if (r != 0 && target == 0) { + target = r; + } + now += 16 * MS; + } + return target; +} + +} // namespace + +TEST_CASE("comp_segments: majority rule", "[comp_segments]") +{ + comp_segment_table t = window_at(3840 - 300); // 300 laptop / 700 Acer + REQUIRE(t.count == 2); + CHECK(t.seg[comp_segments_majority(&t)].screen_id == ACER); + t = window_at(3840 - 700); + CHECK(t.seg[comp_segments_majority(&t)].screen_id == LAPTOP); + t = window_at(3840 - 500); // exact tie -> the primary + CHECK(t.seg[comp_segments_majority(&t)].screen_id == LAPTOP); + comp_segment_table empty{}; + CHECK(comp_segments_majority(&empty) == UINT32_MAX); + CHECK(comp_segments_majority(nullptr) == UINT32_MAX); +} + +TEST_CASE("comp_segments: HWND owner hysteresis", "[comp_segments]") +{ + comp_segments_owner o; + comp_segments_owner_init(&o, LAPTOP, 0, 0); + uint64_t now = 1000 * MS; + + SECTION("defaults: the primary owns it, 0.5 s hold, 20 % margin") + { + CHECK(o.owner_id == LAPTOP); + CHECK(o.hold_ns == COMP_SEGMENTS_DEFAULT_HANDOFF_HOLD_NS); + CHECK(o.hold_ns == 500 * MS); + CHECK(o.margin_pct == COMP_SEGMENTS_DEFAULT_HANDOFF_MARGIN_PCT); + } + + SECTION("a window on one screen never hands off") + { + CHECK(feed(o, window_at(100), now, 5000) == 0); + CHECK(o.owner_id == LAPTOP); + CHECK(o.candidate_id == 0); + } + + SECTION("a majority inside the dead band never hands off") + { + // 45 % laptop / 55 % Acer: the Acer holds the majority, but not by 20 %. + CHECK(feed(o, window_at(3840 - 450), now, 5000) == 0); + // 41 / 59: still inside. + CHECK(feed(o, window_at(3840 - 410), now, 5000) == 0); + } + + SECTION("a clear majority hands off only after the hold") + { + const comp_segment_table t = window_at(3840 - 300); // 30 / 70 + CHECK(comp_segments_owner_update(&o, &t, now) == 0); + CHECK(o.candidate_id == ACER); + CHECK(comp_segments_owner_update(&o, &t, now + 499 * MS) == 0); + CHECK(comp_segments_owner_update(&o, &t, now + 500 * MS) == ACER); + comp_segments_owner_set_result(&o, ACER, true, ACER); + CHECK(o.owner_id == ACER); + // Owned: no further hand-off while it holds the majority. + now += 600 * MS; + CHECK(feed(o, t, now, 2000) == 0); + } + + SECTION("exactly the margin (60 / 40) is a clear majority") + { + CHECK(feed(o, window_at(3840 - 400), now, 600) == ACER); + } + + SECTION("a window entirely on the other screen hands off") + { + CHECK(feed(o, window_at(5000), now, 600) == ACER); + } + + SECTION("a dip into the dead band restarts the clock") + { + const comp_segment_table clear = window_at(3840 - 200); + const comp_segment_table band = window_at(3840 - 450); + CHECK(comp_segments_owner_update(&o, &clear, now) == 0); + CHECK(comp_segments_owner_update(&o, &clear, now + 400 * MS) == 0); + CHECK(comp_segments_owner_update(&o, &band, now + 450 * MS) == 0); + CHECK(comp_segments_owner_update(&o, &clear, now + 500 * MS) == 0); + CHECK(comp_segments_owner_update(&o, &clear, now + 999 * MS) == 0); + CHECK(comp_segments_owner_update(&o, &clear, now + 1000 * MS) == ACER); + } + + SECTION("a drag back and forth across the seam never flaps") + { + // Sweep the window across the seam and back every 300 ms: each pass + // spends < 0.5 s on either side of the dead band. + uint64_t handoffs = 0; + for (int pass = 0; pass < 20; pass++) { + for (int step = 0; step < 18; step++) { + const int32_t on_laptop = (pass % 2 == 0) ? 900 - step * 45 : 100 + step * 45; + const comp_segment_table t = window_at(3840 - on_laptop); + const uint64_t r = comp_segments_owner_update(&o, &t, now); + if (r != 0) { + handoffs++; + comp_segments_owner_set_result(&o, r, true, r); + } + now += 16 * MS; + } + } + CHECK(handoffs == 0); + CHECK(o.owner_id == LAPTOP); + } + + SECTION("after a hand-off, the way back needs the same margin and hold") + { + REQUIRE(feed(o, window_at(3840 - 300), now, 600) == ACER); + comp_segments_owner_set_result(&o, ACER, true, ACER); + // 55 % laptop: inside the band from the Acer's side. + CHECK(feed(o, window_at(3840 - 550), now, 5000) == 0); + // Back on the laptop: hand back after the hold. + CHECK(feed(o, window_at(100), now, 600) == LAPTOP); + comp_segments_owner_set_result(&o, LAPTOP, true, LAPTOP); + CHECK(o.owner_id == LAPTOP); + } + + SECTION("a failed target is not retried until the majority leaves it") + { + const comp_segment_table t = window_at(3840 - 300); + REQUIRE(feed(o, t, now, 600) == ACER); + comp_segments_owner_set_result(&o, ACER, false, LAPTOP); // rolled back + CHECK(o.owner_id == LAPTOP); + CHECK(feed(o, t, now, 5000) == 0); + // The majority leaves the Acer, then comes back: one more try. + CHECK(feed(o, window_at(100), now, 100) == 0); + CHECK(feed(o, t, now, 600) == ACER); + } + + SECTION("no owner after a failed rollback: the majority takes it") + { + REQUIRE(feed(o, window_at(3840 - 300), now, 600) == ACER); + comp_segments_owner_set_result(&o, ACER, false, 0); + CHECK(o.owner_id == 0); + // The laptop becomes the majority: it takes the handle after the hold. + CHECK(feed(o, window_at(3840 - 800), now, 600) == LAPTOP); + } + + SECTION("a screen without a DP factory is never a target") + { + CHECK(feed(o, window_at(3840 - 100, false), now, 5000) == 0); + CHECK(o.owner_id == LAPTOP); + } + + SECTION("off every screen: nothing to follow") + { + comp_segment_table empty{}; + CHECK(comp_segments_owner_update(&o, &empty, now) == 0); + CHECK(comp_segments_owner_update(&o, nullptr, now) == 0); + CHECK(o.owner_id == LAPTOP); + } +} + TEST_CASE("comp_segments: deferred release") { comp_segments_retire r{};