Repository navigation
feat(linux): per-DP screen binding, windowless weaver, SR routing/binding chain (multi-screen M4) - #313
Merged
Conversation
…SR refresh rate (multi-screen M4)
The SR side of multi-screen M4, all behind the leia_sr_linux.h seam:
- The Vulkan weaver is created with window = 0 on every path. The runtime
supplies the phase origin per frame (set_present_origin ->
srWeaverSetPresentOrigin, ADR-033); the Linux SDK never used the window for
anything else, and an X11 id made it refuse a resampled panel, forcing 2D
under a fractionally scaled XWayland desktop.
- On SDK headers that carry them (LeiaSR Linux 876620d62, CMake
DXR_LEIA_LNX_HAVE_SR_ROUTING, a type-only check), each weaver create chains
SrWeaverRoutingInfo{EXTERNAL} and, for a known SR displayId,
SrDisplayBindingInfo — gated at run time on SrWeaverRoutingCapabilities /
SrDisplayBindingCapabilities (queried once per context). A refused create
drops the binding, then the routing, one INFO line each: the end state is
the pre-M4 weaver. The installed 1.38 runtime reports neither, so nothing
is chained there. DXR_LEIA_SR_EXTERNAL_ROUTING=0 opts out.
- An EXTERNAL weaver never votes the lens, so the lens owner now turns the
lens on when one is created (unless the last request was 2D), sends every
3D request while one is alive, and releases the lens when the last one is
destroyed — recorded as "nothing to re-apply", not as a 2D wish.
- srDisplayGetRefreshRate (DXR_LEIA_LNX_HAVE_SR_REFRESH, compile + link
probe) replaces the 60 Hz constant when present and plausible (#184).
- The decision (leia_sr_routing_linux.h) is pure C and tested everywhere;
the SR-typed pNext builder (leia_sr_chain_sdk_linux.h) is tested against
SDK headers that carry the structs. New lens-owner cases cover EXTERNAL.
The SR instance, tracker, lens and display handle stay process-wide and
unbound: correct for one Leia panel (maxBoundDisplays = 1); two panels wait
on LeiaSR phase D.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…splay_info_for_monitor (multi-screen M4) Implements the runtime's two appended plug-in slots, each compiled only when the runtime headers announce it (XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN, XRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITOR), so the arm still builds — slots compiled out — against the pinned runtime. - create_dp_vk_for_screen: the DP resolves its xrt_screen_binding ONCE, at creation, into a struct leia_lnx_screen it owns (leia_screen_linux.c: binding + the #311 claim table + the cached EDID panel list + the SR query), and get_display_dimensions / get_display_pixel_info answer from that alone — no process-wide SR display, no "first panel" statics, so two DPs cannot answer for each other. Origin = the binding's desktop rect; SR's size/viewer/recommended view/refresh only for the SR-driven panel (by displayId, else SR 1.38's first-panel rule). The SR displayId (the binding's, else the claim table's) goes to the weaver's display binding. - A segment DP with a sub-rect canvas confines every write to it: 1x1 blit to the canvas, weave render area = the canvas, whole-target alpha-gate skipped (logged once). The plain create_dp_vk is unchanged. - get_display_info_for_monitor answers for a claimed monitor that is the SR-driven panel (same numbers as get_display_info); false otherwise, so the runtime derives EDID defaults with no eye tracking. - get_scanout_caps stated explicitly: weave scope CANVAS, flags = 0 (no TOLERATES_RESAMPLE — the weave needs 1:1 pixels). - test_screen_linux: binding -> screen rules, and two instances never share state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…M4 section; SDK contract rows - scripts/build-linux.sh and build-linux.yml run test_screen_linux and test_sr_routing_linux next to the M0 claims test. - docs/linux-track-b-runbook.md: "One DP per screen, windowless EXTERNAL weaver (multi-screen M4)" — the window = 0 rule, what a DP does with a screen binding, the SR routing/binding chain and its fallbacks, lens ownership under EXTERNAL routing, refresh, what stays per process; the DXR_LEIA_SR_EXTERNAL_ROUTING / SR_WEAVER_ROUTING toggles. - docs/leia-linux-sdk-contract.md §8a: one row per new SR call/struct with its build gate and run-time fallback; R-D1's refresh gap answered. - CLAUDE.md: code-structure rows for the new files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LensGetSerialNumber demoted srLensGetSerialNumber is a stub on the LeiaSR Linux line (always SR_ERROR_FEATURE_NOT_SUPPORTED on 1.38), so a 1.38 claim could never reach VERIFIED. The supported legacy source is the SR service's device store: <root>/active -> Devices/<serial>, <root> = $XDG_CACHE_HOME/leiasr, else /var/lib/leiasr/leiasr — what the SR runtime's resolveLinuxPrimaryDeviceSerial reads. srEnumerateDisplays stays the preferred source when SDK and runtime have it. - leia_lnx_sr_device_store_serial[_at] (claims module, no SDK): readlink, target must be .../Devices/<serial> and exist (dangling = no active device), serial [A-Za-z0-9._-]. Unit-tested against a fake tree under a temp dir, incl. the XDG_CACHE_HOME override. - leiasr_lnx_peek_fpc_serial (sdk backend): device store first, then srLensGetSerialNumber as a last resort; source logged once at INFO, a miss once at INFO (was a WARN from the per-call-site LOG_SR_ONCE). - VERIFIED still requires exactly one Leia panel (the serial is system-wide). - Runbook: the 1.38 claim rule, and why the plug-in must be built with the runtime's build type (os_mutex NDEBUG fields shift vk_bundle by 16 bytes: 2176/784 vs 2192/800, #1243). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rst (review B) find_panel() preferred the probe_displays claim's connector over the runtime binding's device_name. The claim table pairs identical twins by their own rules, while the binding names the output the runtime resolved for THIS monitor, so the binding now wins; the claim's connector and the origin are the fallbacks. test_screen_linux covers the disagreeing case. (Also: the leia_lnx_edid_panels_cached doc now matches the TTL'd shared cache from the #311 review.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 2.27.1) (review 1) The windowless weaver (window = 0 on every path) drops the LeiaSR 1.38 SDK's own refusal to weave into a resampled X11 window. The runtime only took that refusal over in v2.27.1 (#1831, X11 on a fractionally scaled panel presents 2D), but the Linux pin was v2.27.0 and the .deb's Depends unversioned — so a scaled-desktop X11 app on an older runtime would weave a double image. - DXR_RUNTIME_GIT_TAG_LINUX / build-linux.yml RUNTIME_REF: v2.27.0 -> v2.29.0 (the current tag; vk_bundle, os_mutex and xrt_plugin.h are unchanged in between, so the loader's layout check is unaffected). - package_deb_leia.sh: Depends: displayxr-runtime (>= 2.27.1); the package description and docs/linux-deb-packaging.md say why. - Runbook: the runtime owns the resampled refusal from 2.27.1, which is why the weaver is windowless — and why the X11 window must not come back (on a scaled XWayland desktop it forces 2D even with an explicit origin, the M4 straddling case). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view 2) The device-store serial was read with no live SR context and no check that the device is connected. Devices/ keeps every device ever seen (here both QI012321D10117 and VD012450ET0116), so a stale `active` link or a stopped SRService plus one Leia panel gave VERIFIED with the wrong serial. leiasr_lnx_peek_fpc_serial now returns false unless the SR context is READY and valid (the service is reachable and driving a device); the claim stays at EDID confidence otherwise. Seam doc, probe_displays comment and runbook updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t (review 3) When SR refuses the display binding (an EDID-only displayId gives SR_ERROR_DEVICE_NOT_AVAILABLE, an unknown one SR_ERROR_DISPLAY_NOT_FOUND), the weaver falls back to the ACTIVE display. A screen-bound DP for a second Leia panel that SR does not drive would then weave with the first panel's calibration and tracking, which is wrong-phase output. The backend now reports a refused binding (leiasr_lnx_display_binding_refused; the stub never binds). A segment DP whose screen is not the SR-driven panel, or whose binding was refused, is flat_only: every frame takes the flat blit (left view to the canvas), the 2D over-layer is declined, and one WARN at creation says why. Plain-factory DPs are unchanged. Runbook updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fails (review 4) An EXTERNAL weaver destroyed after SRService invalidated the SR context sent srLensDisable on the dead lens. The call failed, so the release was never recorded, while external_weavers had already reached 0. last_sent stayed 3D, on_new_context re-applied ENABLE to the replacement context with no weaver alive, and the lens stayed on over the 2D desktop. leiasr_lnx_destroy now records the release whatever the call returns: a dead context's preference died with its connection. A missing lens handle is treated the same way. test_lens_owner_linux covers the failed-release case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
This is the Leia SR plug-in side of multi-screen M4 (mixed vendor on ds1-linux). A window straddling the laptop panel (eDP-1, sim-display, anaglyph) and the Acer DS1 (HDMI-A-1, leia-sr) should be woven on the DS1 half by this plug-in's DP, with DS1 eye tracking. The runtime splits the window into per-screen segments and creates one DP per screen through the appended slot
create_dp_vk_for_screen(runtime #1853, M2). This PR is the plug-in's half of that. It is stacked on #311 (probe_displays, M0) and does not touch the runtime.window = 0) on every path, the plain factory included. The runtime supplies the phase origin each frame (set_present_origin→srWeaverSetPresentOrigin, ADR-033). Passing an X11 id made the SDK refuse a resampled panel, which forced 2D under scaled XWayland (this box).set_present_originforwarding is unchanged.create_dp_vk_for_screenresolves itsxrt_screen_bindingonce, at creation, into astruct leia_lnx_screenthe DP owns (leia_screen_linux.c). It draws on the binding, the feat(linux): implement probe_displays (multi-screen M0) #311 claim table, the cached EDID panel list and the SR query.get_display_dimensionsandget_display_pixel_infoanswer from that state alone. They never readg_ctx.displayor the "first panel" statics. The desktop origin is the binding's.displayId, or else by SR 1.38's first-panel rule.get_display_info_for_monitor(M1 slot) uses the same resolution. It returnsfalsefor a Leia panel that is not SR-driven.get_scanout_capsis now explicit: scopeCANVAS,flags = 0, meaning the weave needs 1:1 pixels.create_dp_vkis unchanged: primary/first panel.SrWeaverRoutingCapabilities/SrDisplayBindingCapabilitiesonsrGetRuntimeCapabilities(queried once per context).SrWeaverRoutingInfo{EXTERNAL, flags 0}. There is no drag-snap bit on Linux.displayIdwithmaxBoundDisplays >= 1, it also chainsSrDisplayBindingInfo{displayId}.srDisplayGetRefreshRatereplaces the 60 Hz constant when it is available and plausible (Panel physical size and refresh disagree between init paths (0.344x0.194 m / 60 Hz on a 27" 165 Hz Odyssey 3D) #184).srLensGetSerialNumberis a stub on the LeiaSR Linux line: it always returnsSR_ERROR_FEATURE_NOT_SUPPORTED, so on 1.38 that call never producesVERIFIED. The plug-in now reads the serial from the SR service's device store instead, the same file the SR runtime's ownresolveLinuxPrimaryDeviceSerialreads.<root>/active, a symlink toDevices/<serial>.<root>is$XDG_CACHE_HOME/leiasr, else/var/lib/leiasr/leiasr.VERIFIEDon 1.38 depends on that store having an active device (a dangling link means none), and on exactly one Leia panel being connected, because the serial is system-wide.srEnumerateDisplaysstays the preferred source when the SDK and runtime have it.srLensGetSerialNumberis kept only as a last resort.docs/leia-linux-sdk-contract.mdgets §8a with one row per new call. CLAUDE.md gets code-structure rows.Guards
XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREENcreate_dp_vk_for_screenslot compiled outXRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITORget_display_info_for_monitorcompiled outXRT_DP_VK_HAS_SCANOUT_CAPSget_scanout_capsleft NULL (same answer)DXR_LEIA_LNX_HAVE_SR_ROUTINGDXR_LEIA_LNX_HAVE_SR_REFRESHcheck_symbol_exists)externalRouting,displayBinding,maxBoundDisplays)DXR_LEIA_SR_EXTERNAL_ROUTING=0Runtime pin: the Linux pin (
DXR_RUNTIME_GIT_TAG_LINUX, CIRUNTIME_REF) moves fromv2.27.0tov2.29.0, and the.debnow Depends ondisplayxr-runtime (>= 2.27.1). The reason is the windowless weaver: the runtime owns the resampled-X11 refusal only from v2.27.1 (#1831). The multi-screen slots still compile out at this pin. The local build pointedDXR_RUNTIME_SOURCE_DIRat a snapshot of runtimefeat/multi-screen-m2-segments@71d4d2c4d. The real repin happens throughruntime-pin-bump.ymlonce the runtime multi-screen PRs merge. Thefeaturestrack sees the newXRT_PLUGIN_IFACE_HAS_*macros.Review fixes (stacked on #311's A/B/C)
find_panelnow prefers the runtime binding'sdevice_nameover the claim table's connector, so twins paired by list order cannot swap.displayxr-runtime (>= 2.27.1). The runbook says the runtime owns the resampled refusal from 2.27.1, which is why the weaver is windowless. The X11 window is not reintroduced.Devices/keeps every device ever seen, andactivecan be stale with SRService stopped.srLensDisablefails on a dead context. Otherwise the next context would re-apply ENABLE with no weaver alive. A unit test covers this.Build matrix (all green, local, Ubuntu 26.04)
Columns are runtime headers; rows are SR SDKs.
71d4d2c4dv2.29.0/opt/leiasr(1.38)~/.cache/leiasr-sdk-876620d62Every cell meets all of the following:
xrtPluginNegotiate.get_scanout_capspresent.test_lens_owner_linux,test_screen_linux,test_sr_routing_linux,test_display_claims_linux,test_stereo_camera_parseandtest_compose_color_linux.test_sr_routing_linuxbuilds the realpNextchains only against the 876620d62 headers. Elsewhere it checks the plan only.Tests (hardware-free)
test_screen_linuxcovers the binding → screen rules: RandR/DRM/origin join, claim connector,displayIdprecedence, SR panel versus a second Leia panel, no SR, forced probe, and incomplete bindings. It also checks that two DP instances never share or disturb state.test_sr_routing_linuxcovers the plan against the caps (new runtime, 1.38, unknown, opt-out) and the fallback order. Against SDK headers that carry the structs it also checks the real chains:sType22/23/24/26, order,displayId, flags 0, and the untouched create-info.test_display_claims_linuxadds the device-store reader against a fake tree under a temp dir. It covers relative and absolute targets, a dangling link, a target outsideDevices/, bad characters, a plain directory and theXDG_CACHE_HOMEoverride.test_lens_owner_linuxadds three cases: an EXTERNAL weaver turns the lens on and releases it, 3D is sent while one is alive, and 2D is respected.scripts/build-linux.shandbuild-linux.ymlrun the two new tests.Not run: nothing touched the panel. No
displayxr-cli, no SR context and no window were used. The DS1 gate is coordinated by the hub.Gate result (ds1-linux, 2026-10-07)
Setup: plug-in at
bd60ed4(Debug,SRSDK_ROOT=/opt/leiasr, installed SR 1.38), runtimefeat/multi-screen-m3-per-segment-views(#1854). Onecube_handle_vk_linuxwindow, 1600×900, straddling eDP-1 (sim-display, anaglyph) and the DS1 on HDMI-1 (leia-sr), with 800 px on each monitor.Mode A (leia-sr active, DS1 primary) ran end to end.
active plug-in: id=leia-sr … version='dev (feat/linux-per-dp-screen bd60ed4, Debug, SR 1.38)'FPC serial 'QI012321D10117' from the SR device store (/var/lib/leiasr/leiasr/active)claim monitor 0x886e4475353b22b9 on HDMI-A-1 confidence=VERIFIED serial='QI012321D10117'monitor 0x886e… → plug-in 'leia-sr' (confidence=100)monitor 0x03fd… → plug-in 'sim-display' (confidence=10)X11 window 0xa00001 not forwarded — the weaver is windowless (phase from set_present_origin)Vulkan weaver created (window=0x0 = windowless, routing SDK, display active (unbound) …), as expected on 1.38.segments: mixed-vendor screen table — primary 'leia-sr', other screens by 'sim-display'created a segment DP for screen 0x03fd… ('eDP-1', plug-in 'sim-display') via create_dp_vk_for_screenpresent origin: (-800, 630) = window (2656, 630) 1600x900 - panel (3456, 0) 3840x2160Views: activeViewCount=4 …per-segment views routed — 2 segment(s) x 2 view(s)Atlas capture: segment 0 … 800x450andsegment 1 … 800x450OXR EVENT: Eye tracking state changed: isTracking=1Lens has been enabledCreateRawEyeTracker,DisableLensat creation,EnableLens, lens switched to 3D, clean teardown on kill, no errors.create_dp_vk. Only the eDP half went throughcreate_dp_vk_for_screen, and that was sim-display's.Mode B (the Leia DP through
create_dp_vk_for_screen) passed. It ran withXRT_PREFERRED_PLUGIN_ID=sim-displayplus the runtime M3 per-screen pinDXR_SCREEN_PLUGIN=HDMI-1=leia-sr, so eDP is primary and the DS1 half is this plug-in's segment DP.monitor 0x886e4475353b22b9 ('HDMI-1'/'HDMI-A-1') → 'leia-sr' by DXR_SCREEN_PLUGIN (confidence=100; outranks PreferredPlugin and the active plug-in for this monitor)segments: mixed-vendor screen table — primary 'sim-display', other screens by 'leia-sr'leia_lnx_dp: segment DP for monitor 0x886e… ('HDMI-1', connector 'HDMI-A-1'): 3840x2160 px at (3456,0), 0.344x0.193 m, the SR-driven panel, SR display 0x0000000000000000segments: created a segment DP for screen 0x886e… ('HDMI-1', plug-in 'leia-sr') via create_dp_vk_for_screen — needs 1:1 pixels. "Needs 1:1 pixels" means the runtime readget_scanout_capsas flags 0.Vulkan weaver created (window=0x0 = windowless, routing SDK, display active (unbound) …). On 1.38 this is the expected answer: no routing or binding is chained, and the SR display id is 0.request_display_mode(3D) left to the weaver. That is the 1.38 SDK-routed lens rule.system event 14: Lens has been enabled.Views: activeViewCount=4 …per-segment views routed — 2 segment(s) x 2 view(s)Atlas capture: segment 1 (screen 0x886e…) DP input 800x450Still open:
srDisplayGetRefreshRate. It needs the 876620d62 test .deb and a rebuild against that SDK.Issue found and fixed during the gate: the first attempt used a RelWithDebInfo plug-in, which the runtime refused with
vk_bundle ABI mismatch(2176/784 vs 2192/800, #1243), so no Leia code ran. The cause and the Debug requirement are in the gate recipe below.Gate recipe (hub)
This needs a runtime that weaves mixed-vendor segments. The M2 head still keeps the single-DP path when the screens belong to different plug-ins (
segments: … mixed-vendor segments are multi-screen M4), so use the runtime branch that lifts that (M3/M4).Use the runtime's build type.
build_linux.shbuilds the runtime as Debug.struct os_mutexhas two fields that exist only when NDEBUG is not defined, andvk_bundleholds two of those mutexes. So a RelWithDebInfo plug-in has the wrong layout: size 2176 with function-table offset 784, against the runtime's 2192/800. The loader then refuses the VK factory withvk_bundle ABI mismatch(#1243), and no Leia code runs. That is what the first gate attempt hit.The dev dir shadows the installed
/usr/lib/displayxr/pluginsleia-sr(same id). Grep foractive plug-in/loaded fromto confirm.With the installed 1.38 runtime (the new API falls back):
SR multi-display caps …is absent: 1.38 headers compile it out.window=0x0 = windowless, routing SDK, display active (unbound).refresh 60.000 Hz HARDCODED…appears.leia_lnx_dp: segment DP for monitor 0x… ('HDMI-1', connector 'HDMI-A-1'): 3840x2160 px at (<x>,0), …, the SR-driven panelappears.created a segment DP … via create_dp_vk_for_screen — needs 1:1 pixels.Once the 876620d62 test .deb is installed: rebuild with
SRSDK_ROOT=~/.cache/leiasr-sdk-876620d62(or the .deb's/opt/leiasr). The changes:SR multi-display caps: externalRouting=1 displayBinding=1 maxBoundDisplays=1.routing EXTERNAL, display bound to 0x…for the segment DP. The plain DP isactive (unbound), because it has no screen binding.lens ON (EXTERNAL-routed weaver created …)at session start, andlens OFF — the last EXTERNAL-routed weaver is goneat teardown.refresh … Hz from srDisplayGetRefreshRate.DXR_LEIA_SR_EXTERNAL_ROUTING=0, or SR's ownSR_WEAVER_ROUTING=sdk|external.Refs #43, #184; runtime DisplayXR/displayxr-runtime#69 (M4), DisplayXR/displayxr-runtime#1853.
🤖 Generated with Claude Code
Supersedes #312 (auto-closed when its stacked base
feat/linux-probe-displayswas deleted on merging #311); same branch, rebased ontomain.