From d70cd8df963f6e3866eb9e19545ae627d38c239e Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:39:27 -0700 Subject: [PATCH 1/3] feat(comp/util): window-handle owner hysteresis for segments (ADR-047 Amendment 2) comp_segments_majority (the oxr_segment_views_majority rule over a segment table) and comp_segments_owner_*: which screen's DP holds the window handle. A hand-off is due only when another screen holds the majority, beats the owner by >= 20 % of the on-screen area (a 60/40 dead band between two screens) and has done so continuously for 0.5 s; a failed target is not retried until the majority leaves it; a screen without a DP factory is never a target. Pure and host-tested: dead band, hold, dip-restarts-the-clock, a drag swept across the seam never flaps, hand-back, failure latch, no-owner recovery. Co-Authored-By: Claude Opus 5.5 --- src/xrt/compositor/util/comp_segments.c | 120 +++++++++++++++ src/xrt/compositor/util/comp_segments.h | 81 ++++++++++ tests/tests_comp_segments.cpp | 188 ++++++++++++++++++++++++ 3 files changed, 389 insertions(+) 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{}; From f160199ed01c556798ef00548b8b27140cdf3eac Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:39:27 -0700 Subject: [PATCH 2/3] feat(d3d11): the window handle follows the majority segment (multi-screen M6, ADR-047 Amendment 2) The segment DP of the screen holding the majority of a straddling window now owns the real HWND (the vendor's drag phase-snap, KEEP_DRAG_SNAP on Leia); every other DP is windowless and phased by set_present_origin. Before, the HWND stayed with the primary screen's DP. A weaver is one per HWND and a live DP's window is never re-pointed, so a hand-off (between two weaves, under c->mutex) makes a windowless replacement for the old owner first (abort if that fails), swaps it in and destroys the old owner's DP, then makes the new owner's DP with the HWND through create_dp_d3d11_for_screen, rolling back to the old owner on failure. A frame weaves with the old pair or the new pair, never with a gap. The primary screen keeps a DP as the session's own: the compositor swaps display_processor through a swap_primary hook that re-sends transparency, 2D/3D mode, eye-tracking mode and this weave's timing/backdrop, and guards the exchange against its unlocked app-thread readers with a leaf dp_swap_mutex. A bracket hook detaches the own window's snap provider across the hand-off and re-arms it on the DP that now holds the window. A windowless primary gets its present origin on the split path, which stays engaged until the hand-back. Lifecycle retires never touch the owner's DP. One WARN per flip. Sessions without a real window (shared texture) keep the old rule; a single screen, or a window that never leaves the primary, is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../d3d11/comp_d3d11_compositor.cpp | 112 ++++++- .../compositor/d3d11/comp_d3d11_segments.cpp | 314 ++++++++++++++++-- .../compositor/d3d11/comp_d3d11_segments.h | 61 +++- 3 files changed, 454 insertions(+), 33 deletions(-) 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, From 66eaef02b195d79df5d8cfab27cf5749184ff05c Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:39:27 -0700 Subject: [PATCH 3/3] docs(multi-screen): the HWND follows the majority segment (ADR-047 Amendment 2) Co-Authored-By: Claude Opus 5.5 --- ...ti-screen-segments-and-per-screen-views.md | 19 ++++++++++++++++++ docs/architecture/comp-segments.md | 20 +++++++++++++------ docs/roadmap/multi-screen.md | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) 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