Skip to content

feat(linux): per-DP screen binding, windowless weaver, SR routing/binding chain (multi-screen M4) - #313

Merged
dfattal merged 9 commits into
mainfrom
feat/linux-per-dp-screen
Oct 7, 2026
Merged

dfattal merged 9 commits into
mainfrom
feat/linux-per-dp-screen

Conversation

@dfattal

@dfattal dfattal commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

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.

  1. The weaver is always windowless (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_origin forwarding is unchanged.
  2. Per-DP screen state. create_dp_vk_for_screen resolves its xrt_screen_binding once, at creation, into a struct leia_lnx_screen the 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_dimensions and get_display_pixel_info answer from that state alone. They never read g_ctx.display or the "first panel" statics. The desktop origin is the binding's.
    • SR's size, nominal viewer, recommended view and refresh are used only for the SR-driven panel. That panel is identified by displayId, or else by SR 1.38's first-panel rule.
    • A segment DP with a sub-rect canvas writes only inside the canvas: the 1×1 blit goes to the canvas, the weave render area is the canvas, and the whole-target alpha-gate is skipped (logged once).
    • get_display_info_for_monitor (M1 slot) uses the same resolution. It returns false for a Leia panel that is not SR-driven.
    • get_scanout_caps is now explicit: scope CANVAS, flags = 0, meaning the weave needs 1:1 pixels.
    • The old create_dp_vk is unchanged: primary/first panel.
  3. SR routing and binding chain (LeiaSR Linux 876620d62, phases A–C). It is compiled only when the SDK headers carry the structs, and used only when the installed runtime reports them through SrWeaverRoutingCapabilities / SrDisplayBindingCapabilities on srGetRuntimeCapabilities (queried once per context).
    • Each weaver create chains SrWeaverRoutingInfo{EXTERNAL, flags 0}. There is no drag-snap bit on Linux.
    • For a known displayId with maxBoundDisplays >= 1, it also chains SrDisplayBindingInfo{displayId}.
    • If the create is refused, the binding is dropped first, then the routing, with one INFO line per step. The fallback ends at exactly the pre-M4 weaver.
    • Lens: an EXTERNAL weaver never votes the lens. The lens owner therefore turns the lens on when one is created (unless the last request was 2D), and releases it when the last one is destroyed. Otherwise the long-lived service would hold an ENABLE forever.
    • The SR instance, tracker, lens and display handle stay per process and unbound. That is correct for one Leia panel (only the FPC-verified display binds, and it is the active one). Two Leia panels wait on LeiaSR phase D. This is documented in the runbook and contract.
  4. Refresh: srDisplayGetRefreshRate replaces 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).
  5. Legacy FPC serial (SR 1.38). srLensGetSerialNumber is a stub on the LeiaSR Linux line: it always returns SR_ERROR_FEATURE_NOT_SUPPORTED, so on 1.38 that call never produces VERIFIED. The plug-in now reads the serial from the SR service's device store instead, the same file the SR runtime's own resolveLinuxPrimaryDeviceSerial reads.
    • The store is <root>/active, a symlink to Devices/<serial>. <root> is $XDG_CACHE_HOME/leiasr, else /var/lib/leiasr/leiasr.
    • VERIFIED on 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.
    • srEnumerateDisplays stays the preferred source when the SDK and runtime have it. srLensGetSerialNumber is kept only as a last resort.
    • The source is logged once at INFO, and so is a miss. The old per-call-site WARN is gone.
  6. Docs: the runbook gets an M4 section, and docs/leia-linux-sdk-contract.md gets §8a with one row per new call. CLAUDE.md gets code-structure rows.

Guards

Guard Kind Off ⇒
XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN runtime header macro create_dp_vk_for_screen slot compiled out
XRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITOR runtime header macro get_display_info_for_monitor compiled out
XRT_DP_VK_HAS_SCANOUT_CAPS runtime header macro (present at the pin) get_scanout_caps left NULL (same answer)
DXR_LEIA_LNX_HAVE_SR_ROUTING CMake type-only compile check no routing/binding chain, no caps query
DXR_LEIA_LNX_HAVE_SR_REFRESH CMake compile + link (check_symbol_exists) 60 Hz
SR caps (externalRouting, displayBinding, maxBoundDisplays) run time nothing chained (the installed 1.38 runtime)
DXR_LEIA_SR_EXTERNAL_ROUTING=0 env, A/B SDK-routed weaver

Runtime pin: the Linux pin (DXR_RUNTIME_GIT_TAG_LINUX, CI RUNTIME_REF) moves from v2.27.0 to v2.29.0, and the .deb now Depends on displayxr-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 pointed DXR_RUNTIME_SOURCE_DIR at a snapshot of runtime feat/multi-screen-m2-segments @ 71d4d2c4d. The real repin happens through runtime-pin-bump.yml once the runtime multi-screen PRs merge. The features track sees the new XRT_PLUGIN_IFACE_HAS_* macros.

Review fixes (stacked on #311's A/B/C)

  • B: find_panel now prefers the runtime binding's device_name over the claim table's connector, so twins paired by list order cannot swap.
  • 1: Linux runtime pin moves to v2.29.0, and the package Depends on 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.
  • 2: the device-store serial is used only while the SR context is READY. Devices/ keeps every device ever seen, and active can be stale with SRService stopped.
  • 3: a segment DP whose screen is not the SR-driven panel, or whose display binding SR refused, presents flat: the left view goes to the canvas, with one WARN. It never weaves with another panel's calibration.
  • 4: the EXTERNAL lens release is recorded even when srLensDisable fails 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.

SDK M2 71d4d2c4d pinned v2.29.0
stub ✅ slots IN; routing/refresh n/a ✅ slots OUT
/opt/leiasr (1.38) ✅ slots IN; routing / refresh / enum compiled out ✅ slots OUT; same
~/.cache/leiasr-sdk-876620d62 ✅ slots IN; routing + binding + refresh + enum ENABLED ✅ slots OUT; SR side enabled

Every cell meets all of the following:

  • 0 plug-in warnings.
  • Single export xrtPluginNegotiate.
  • get_scanout_caps present.
  • Every test passes: test_lens_owner_linux, test_screen_linux, test_sr_routing_linux, test_display_claims_linux, test_stereo_camera_parse and test_compose_color_linux.

test_sr_routing_linux builds the real pNext chains only against the 876620d62 headers. Elsewhere it checks the plan only.

Tests (hardware-free)

  • test_screen_linux covers the binding → screen rules: RandR/DRM/origin join, claim connector, displayId precedence, 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_linux covers 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: sType 22/23/24/26, order, displayId, flags 0, and the untouched create-info.
  • test_display_claims_linux adds the device-store reader against a fake tree under a temp dir. It covers relative and absolute targets, a dangling link, a target outside Devices/, bad characters, a plain directory and the XDG_CACHE_HOME override.
  • test_lens_owner_linux adds 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.sh and build-linux.yml run 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), runtime feat/multi-screen-m3-per-segment-views (#1854). One cube_handle_vk_linux window, 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.

  • Plug-in identity: active plug-in: id=leia-sr … version='dev (feat/linux-per-dp-screen bd60ed4, Debug, SR 1.38)'
  • Claims:
    • 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)
  • Windowless weaver:
    • 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:
    • 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_screen
    • present origin: (-800, 630) = window (2656, 630) 1600x900 - panel (3456, 0) 3840x2160
  • Views:
    • Views: activeViewCount=4 …
    • per-segment views routed — 2 segment(s) x 2 view(s)
    • Atlas capture: segment 0 … 800x450 and segment 1 … 800x450
  • Tracking and lens:
    • OXR EVENT: Eye tracking state changed: isTracking=1
    • SR system event Lens has been enabled
  • SRService side (LeiaSR session): CreateRawEyeTracker, DisableLens at creation, EnableLens, lens switched to 3D, clean teardown on kill, no errors.
  • Not yet judged: weave quality is pending a human eyeball. Nobody was in front of the camera, so the Leia DP reported SR's no-face default eyes (0, 0.10, 0.60).
  • Which DS1 path this exercised: in mode A the DS1 half is woven by the compositor's main DP, built by the plain create_dp_vk. Only the eDP half went through create_dp_vk_for_screen, and that was sim-display's.

Mode B (the Leia DP through create_dp_vk_for_screen) passed. It ran with XRT_PREFERRED_PLUGIN_ID=sim-display plus the runtime M3 per-screen pin DXR_SCREEN_PLUGIN=HDMI-1=leia-sr, so eDP is primary and the DS1 half is this plug-in's segment DP.

  • Per-screen pin: 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: segments: mixed-vendor screen table — primary 'sim-display', other screens by 'leia-sr'
  • Per-DP screen state resolved from the binding: 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 0x0000000000000000
  • Created through the per-screen slot: segments: 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 read get_scanout_caps as flags 0.
  • Weaver: 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.
  • Lens:
    • 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:
    • Views: activeViewCount=4 …
    • per-segment views routed — 2 segment(s) x 2 view(s)
    • Atlas capture: segment 1 (screen 0x886e…) DP input 800x450

Still open:

  • Weave quality needs a human eyeball on the panel, with a viewer in front of the camera.
  • The new-API path is untested: EXTERNAL routing, display binding, the lens on/off ownership, and 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).

RT=<runtime worktree with the M3/M4 runtime half>      # e.g. .claude/worktrees/m3-views
PL=/home/dfattal/Documents/GitHub/displayxr/wt/leia-m4-per-dp-screen
(cd $RT && ./scripts/build_linux.sh --apps)              # runtime + sim-display + cube apps
# Build the plug-in against THE SAME runtime tree (vk_bundle fingerprint is exact-match):
SRSDK_ROOT=/opt/leiasr cmake -S $PL -B $PL/build-gate -G Ninja -DCMAKE_BUILD_TYPE=Debug \
    -DDXR_RUNTIME_SOURCE_DIR=$RT -DDXR_LEIA_LINUX_WEAVER=sdk && cmake --build $PL/build-gate
D=$(mktemp -d)                                           # both DPs, nothing else
cp $RT/build/_plugins/200-sim-display.json $D/
cp $PL/build-gate/src/drv_leia_linux/DisplayXR-LeiaSR.so $D/
printf '{"file_format_version":"1.0","plugin":{"id":"leia-sr","display_name":"Leia SR (M4 dev)","vendor":"Leia Inc.","version":"dev","binary_path":"%s/DisplayXR-LeiaSR.so","probe_order":50}}\n' $D > $D/050-leia-sr.json
sed "s#^export XRT_PLUGIN_SEARCH_PATH=.*#export XRT_PLUGIN_SEARCH_PATH=$D#" $RT/build/run_cube_handle_vk_linux.sh > $D/run.sh
XRT_LOG=info SIM_DISPLAY_OUTPUT=anaglyph bash $D/run.sh   # then drag the window across the eDP|DS1 seam

Use the runtime's build type. build_linux.sh builds the runtime as Debug. struct os_mutex has two fields that exist only when NDEBUG is not defined, and vk_bundle holds 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 with vk_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/plugins leia-sr (same id). Grep for active plug-in / loaded from to confirm.

With the installed 1.38 runtime (the new API falls back):

  • SR multi-display caps … is absent: 1.38 headers compile it out.
  • The weaver line reads 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 panel appears.
  • Runtime: created a segment DP … via create_dp_vk_for_screen — needs 1:1 pixels.
  • Expect: the DS1 half woven and tracked, anaglyph on the eDP half, and the lens behaving as before (the weaver votes).

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.
  • The weaver line reads routing EXTERNAL, display bound to 0x… for the segment DP. The plain DP is active (unbound), because it has no screen binding.
  • lens ON (EXTERNAL-routed weaver created …) at session start, and lens OFF — the last EXTERNAL-routed weaver is gone at teardown.
  • refresh … Hz from srDisplayGetRefreshRate.
  • A/B: run with DXR_LEIA_SR_EXTERNAL_ROUTING=0, or SR's own SR_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-displays was deleted on merging #311); same branch, rebased onto main.

dfattal and others added 9 commits October 7, 2026 14:24
…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>
@dfattal
dfattal merged commit cc249a4 into main Oct 7, 2026
16 checks passed
@dfattal
dfattal deleted the feat/linux-per-dp-screen branch October 7, 2026 22:35
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