Skip to content

feat(win): multi-screen M6 — one D3D11 DP per screen, bound EXTERNAL weaver; nominal viewer, bound lens (D3) and own tracker (D4) on the second panel - #315

Merged
dfattal merged 4 commits into
mainfrom
feat/win-m6-dp-segments
Oct 8, 2026
Merged

dfattal merged 4 commits into
mainfrom
feat/win-m6-dp-segments

Conversation

@dfattal

@dfattal dfattal commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Multi-screen M6 (Windows): one D3D11 DP per screen, bound SR weaver, nominal viewer + bound lens on the untracked panel

Companion of displayxr-runtime #1857 and #1863, both merged and released in runtime v2.30.0. The runtime appended two slots (xrt_plugin_iface::create_dp_d3d11_for_screen, xrt_display_processor_d3d11::set_present_origin, no ABI bump); this PR implements them in the Windows D3D11 DP.

What changes

  • leia_dp_factory_d3d11_for_screen: resolves the screen to an SR display id — the binding's own display_id, else this plug-in's per-monitor claim from probe_displays (M0, leia_win_claims_lookup) — and creates the weaver through leiasr_d3d11_create_for_screen.

  • Per-screen weaver plan (leia_sr_multi_win.h, plain C, host-tested): from srGetRuntimeCapabilities (routing + binding + lens-binding caps) decide SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP iff a real HWND} + SrDisplayBindingInfo{displayId} on SrWeaverCreateInfoDX11.pNext, with the Linux fallback order (drop the binding, then routing) on a refusal. An unbound DP (every pre-M6 caller) gets an empty plan: SDK routing, byte-identical to today.

  • Display query by id: leia_sr_v2_query_display chains the binding on SrDisplayCreateInfo so a bound DP's geometry describes ITS panel; it also reports srDisplayGetIdentifier, which for the unbound query is the runtime's active display.

  • Untracked screen = nominal viewer (David, 2026-10-07): a weaver bound to a display that is not the runtime's active one pins srWeaverSetSimulatedViewer (the SDK's own no-face viewer (0,100,600) mm split by 63 mm), re-applied before every weave; its instance is never srInitialized and late latching stays off (nothing to latch from until SR D4 gives that screen a tracker).

  • Bound lens (SR D3, LeiaSR 2079+): when the runtime reports SrLensBindingCapabilities::lensPerDevice, that same DP creates a lens bound to ITS display and drives it from the runtime's 2D/3D requests — it must, since an EXTERNAL weaver never votes. The bound vote is context-owned and survives srDestroyLens, so it is disabled before destroy (a window leaving that screen hands the panel back to 2D). On a pre-D3 runtime that screen's lens is left alone, as an unbound handle would switch the ACTIVE panel.

  • set_present_origin → srWeaverSetPresentOrigin before every weave on a windowless EXTERNAL weaver (device px relative to its screen; the effective phase is origin + viewport offset, as measured by the SR session). A weaver with the real HWND is left to the SDK.

  • Scissor = canvas in process_atlas, alongside the viewport (the runtime contract for segment DPs; the SR weaver reads both).

  • The per-weave diagnostic in process_atlas moves from process-wide statics to per-DP state: with two DPs alternating viewports every frame it logged a WARN per frame.

  • Tracker per device (SR D4, LeiaSR 2141+): when the runtime reports SrEyeTrackerBindingCapabilities::eyeTrackerPerDevice, a weaver bound to a calibrated non-active display tracks that display's own camera automatically (EXTERNAL included). The screen-bound DP keys on it: tracked ⇒ no simulated viewer (a pin would override tracking), srInitialize on its instance, late latching on — the same as the active display; not tracked (gate tracker_per_device off, older service) ⇒ the untracked-screen path above stands. The bound lens (D3) is independent of tracking. This is what makes a straddling window tracked on both halves once the service gate is on.

  • SDK pin → sr-sdk-v2-1.38.0.2141 (tag 27 lens-binding + tag 28 tracker-binding headers; dispatch table unchanged; the two Linux-only compose slots are placeholders, unused on Windows). CMake gates: DXR_LEIA_HAS_SR_MULTI_WIN, DXR_LEIA_HAS_SR_LENS_BINDING, DXR_LEIA_HAS_SR_TRACKER_BINDING.

  • Per-monitor display info (get_display_info_for_monitor, multi-screen M1): every monitor this plug-in claimed reports its SR-calibrated physical size (the enumeration descriptor's cm, cached, non-blocking), native pixel dims, a nominal viewer scaled from the active panel's, and MANAGED eye tracking. Without it the runtime's per-segment views (#1863) refused the layout — the second screen had no metres on Windows (the EDID reader there never fills mm) — so a straddling window got ONE view set from the active panel's tracker: the DS1 half weaved from its own camera (D4) but looked around with the laptop's.

  • New host test tests/test_sr_multi_win.c in the Linux lane (plan, fallback order, bound-lens gate, tracker gate, simulated-viewer gate).

Verified on the two-panel win rig

LeiaSR 1.38.0+2141, tracker_per_device=on, runtime v2.30.0 (+ the change-gated locate diagnostic of runtime #1870): cube_handle_d3d11_win straddling AL | DS1 — the DS1 DP is created for monitor 0xb72e… -> SR display 0x2 with plan{external=1 keep_drag_snap=0 bind=1}, tracked by its own camera, no simulated viewer, lens BOUND; the locate reports segments=2 … -> total=4; [0] 0.344x0.194 m; [1] 0.340x0.190 m; the pre-weave atlas carries a distinct L/R pair per segment; both halves weave (adjacent-column ratio 1.04 / 1.07, 2D fallback reads ≈0.54); and David confirmed by eye (2026-10-08): "each half weaves and lookaround locally" — hiding the DS1 camera degrades only the DS1 half. The SR session independently verified EXTERNAL + binding + window=0 weaving with per-device calibration on DX11/DX12/GL/VK, a two-weaver pair in one process, and a bound lens driving the DS1's own FPC.

Not in this PR: D3D12 / GL / Vulkan per-screen factories (D3D11 first per David), the service path, per-display trackers (SR D4, tag 28, a further SDK tag).

🤖 Generated with Claude Code

@dfattal dfattal changed the title feat(win): multi-screen M6 — one D3D11 DP per screen, bound EXTERNAL weaver, nominal viewer on the untracked panel feat(win): multi-screen M6 — one D3D11 DP per screen, bound EXTERNAL weaver, nominal viewer + bound lens on the untracked panel Oct 8, 2026
dfattal and others added 3 commits October 8, 2026 11:07
…rigin, nominal viewer on the untracked panel (multi-screen M6)

Implements the runtime's two appended slots: create_dp_d3d11_for_screen resolves the screen to an SR display id (the binding's, else the M0 per-monitor claim) and creates a weaver with SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP iff a real HWND} + SrDisplayBindingInfo{id} chained on the create-info, with the Linux fallback order on a refusal; set_present_origin feeds srWeaverSetPresentOrigin before every weave of a windowless weaver; scissor = canvas alongside the viewport. A weaver bound to a display that is not the runtime's active one pins srWeaverSetSimulatedViewer to the nominal viewer (re-applied per weave), starts no senses, keeps late latching off and creates no lens (David 2026-10-07: an untracked screen renders from its own nominal viewer). The display query chains the binding so a bound DP's geometry describes its panel and reports srDisplayGetIdentifier. The plan is plain C (leia_sr_multi_win.h) with a host test in the Linux lane; an unbound DP gets an empty plan and stays byte-identical. The per-weave diagnostic becomes per-DP state (two alternating DPs made the shared statics WARN every frame). Gated by DXR_LEIA_HAS_SR_MULTI_WIN (SDK headers) and the runtime HAS macros, so the v2.27.0 CI pin still builds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-v2-1.38.0.2083

With LeiaSR 2079 (D3, lens per device) the runtime reports SrLensBindingCapabilities::lensPerDevice; a screen-bound D3D11 DP on a non-active display now creates a lens bound to ITS display (SrDisplayBindingInfo on SrLensCreateInfo) and drives it from the runtime's 2D/3D requests, since its EXTERNAL weaver never votes. The bound vote is context-owned and survives srDestroyLens, so it is disabled before destroy (a window leaving that screen hands the panel back to 2D). Pre-D3 runtimes keep leaving that screen's lens alone (gated on tag 27). Pin moves to sr-sdk-v2-1.38.0.2083 (headers add tag 27; dispatch table unchanged; the two Linux-only compose slots are placeholders, unused on Windows). Host test covers the plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ned + SR SDK pin sr-sdk-v2-1.38.0.2141

With LeiaSR 2141 (D4, tracker per device) the runtime reports SrEyeTrackerBindingCapabilities::eyeTrackerPerDevice; a weaver bound to a calibrated non-active display then tracks that display's own camera automatically (EXTERNAL included). The screen-bound D3D11 DP keys on it: tracked => no simulated viewer (a pin would override tracking), srInitialize on its instance, late latching on — the same as the active display; not tracked (gate off, older service) => the untracked-screen path stays (pinned nominal viewer, no senses). The bound lens (D3) is independent of tracking. Pin moves to sr-sdk-v2-1.38.0.2141 (tag 28 header only; dispatch table unchanged). Host test covers the gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/win-m6-dp-segments branch from fb21141 to 47b6b2a Compare October 8, 2026 18:08
@dfattal dfattal changed the title feat(win): multi-screen M6 — one D3D11 DP per screen, bound EXTERNAL weaver, nominal viewer + bound lens on the untracked panel feat(win): multi-screen M6 — one D3D11 DP per screen, bound EXTERNAL weaver; nominal viewer, bound lens (D3) and own tracker (D4) on the second panel Oct 8, 2026
@dfattal
dfattal force-pushed the feat/win-m6-dp-segments branch from a11cb7b to 1e7b63a Compare October 8, 2026 22:31
…ports its SR-calibrated size (multi-screen M1/M3)

The runtime's per-segment views (runtime #1863, ADR-047 D3) refuse to lay
out view sets while any segment's screen has no physical size, and on
Windows the EDID path never yields millimetres — so a window straddling
AL | DS1 got ONE view set from the AL tracker, and the DS1 half weaved
correctly (D4) but looked around with the laptop's camera (David,
2026-10-08: "lookaround keeps responding using laptop camera").

The Windows arm now implements the per-monitor slot: for a monitor this
plug-in claimed, the size comes from SR's own enumeration descriptor
(physicalWidthCm/HeightCm, cached 2 s, non-blocking — the slot must be
cheap and callable from any thread), pixel dims native, nominal viewer =
the active panel's distance scaled by the height ratio, MANAGED eye
tracking. False for an unclaimed monitor or one SR cannot size, so the
runtime keeps its own defaults there.

Rig (win, LeiaSR 1.38.0+2141, tracker_per_device=on): the locate now
reports segments=2 -> total=4 (DS1 0.340x0.190 m), the atlas carries a
distinct L/R pair per segment, both halves weave (column ratio 1.04/1.07)
and each half looks around from its own camera — confirmed by eye.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/win-m6-dp-segments branch from 1e7b63a to 7db06e7 Compare October 8, 2026 22:31
@dfattal
dfattal merged commit f7c7168 into main Oct 8, 2026
16 checks passed
@dfattal
dfattal deleted the feat/win-m6-dp-segments branch October 8, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant