Skip to content

feat(plugins): generic platform state + world-event re-probe (install-order epic) - #1808

Merged
dfattal merged 3 commits into
mainfrom
feat/plugin-platform-state
Oct 3, 2026
Merged

dfattal merged 3 commits into
mainfrom
feat/plugin-platform-state

Conversation

@dfattal

@dfattal dfattal commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #1803. Closes #1804, closes #1805.

Was stacked on #1793 (Restart Manager support, now merged); rebased onto main.

What

Implements ADR-045 (new, in this PR). A registered plug-in is a fact. Every plug-in is loadable without its vendor platform, and reports a generic platform state. The service re-evaluates selection on world events, but it never live-swaps a vendor plug-in that is already active.

Commit 1: ABI (append-only, ABI stays v5)

  • xrt_plugin.h adds:
    • enum xrt_plugin_platform_state: UNKNOWN, READY, PLATFORM_ABSENT, PLATFORM_NOT_RUNNING, NO_DISPLAY, INCOMPATIBLE.
    • struct xrt_plugin_platform_status: struct_size, state, flags, a 128-byte UTF-8 hint, and a reserved tail.
    • The flag XRT_PLUGIN_PLATFORM_FLAG_FALLBACK.
    • A new optional last slot, xrt_plugin_iface::get_platform_state(out), gated by struct_size, with the feature macro XRT_PLUGIN_HAS_PLATFORM_STATE.
  • The slot takes no instance, so it can be called before probe(). The loader sequence is load → negotiate → get_platform_state → probe.
  • sim-display implements the slot and always reports READY + FALLBACK.
  • plugin-discovery.md §4.1/§4.2 documents the call sequence, the ~100 ms non-blocking probe() rule, and the no-live-swap re-probe.

Commit 2: loader, service, surfacing

  • Loader records. target_plugin_get_status() keeps one record per registered plug-in: the load outcome plus the reported state and hint. The active plug-in's state is queried live.
    • The outcomes are ACTIVE, LOADED, DECLINED, BINARY_MISSING, DEPENDENCY_MISSING, LOAD_FAILED, PATH_REFUSED, NO_ENTRY_POINT, NEGOTIATE_FAILED, ABI_MISMATCH and PROBE_FAILED.
    • An orphan entry (binary missing) and a missing imported library (err=126 with the binary present) now get separate outcomes.
    • A failure that repeats unchanged logs at INFO after the first WARN. The loading plug-in binary breadcrumb and the Install flow: locked plug-in DLL is silently skipped → registry/disk version skew #461 skew check also WARN only on the first attempt.
  • R-d, no live swap. target_plugin_refresh_active returns immediately unless the active plug-in is a fallback. "Fallback" means the plug-in reports the flag. For a plug-in too old to report state, only the runtime's own sim-display id counts as fallback; a vendor id never does.
  • R-c, world-event re-probe (service only, not in-process apps).
    • A dxr-reprobe worker in ipc_server_process.c takes requests through ipc_server_request_display_reprobe(reason).
    • It is fed by four sources:
      • WM_DISPLAYCHANGE on the hidden session window;
      • WM_DEVICECHANGE / DBT_DEVNODES_CHANGED on the same window;
      • a RegNotifyChangeKeyValue waiter on HKLM\Software\DisplayXR\DisplayProcessors (subtree). It waits on the parent key until the root exists.
      • a 10 s timer while info.active_plugin_is_fallback is set.
    • Requests are debounced: a burst gets one refresh, ≥ 1 s after its last event.
    • The refresh runs on the worker, never on the window thread or the IPC main loop. It does not take global_state.lock, the same as the compositor-create caller: a pre-contract plug-in can block in probe() for seconds, and the main loop takes that lock every tick.
  • Complete adoption.
    • When a refresh swaps plug-ins, info.head_device_stale is set. The weaving DP and the display info switch immediately, as before.
    • The head device cannot switch live (trade-off below). Once no client has been connected for 2 s, the main loop ends and ipc_server_restart_requested() returns true.
    • main.c then starts a successor, displayxr-service.exe --adoption-restart-after-pid <pid> [--workspace]. The successor waits for the old process to exit, then builds its whole system on the new plug-in.
    • A successor runs with allow_adoption_restart=false, so it never restarts again for adoption. A flapping probe cannot loop.
  • R-e, surfacing.
    • displayxr-cli info and selftest (text and --json plugins[]) print one line per registered plug-in. The name and version come from the registration.
    • The vendor_dp note adds the rejected plug-in's platform state. Exit codes are unchanged.
    • The tray tooltip shows the active display processor. When the fallback is active, it adds the better-ranked plug-in's state and hint. When the active plug-in reports NO_DISPLAY, it says so. The tooltip refreshes every 5 s from the loader's records and never triggers discovery.
    • The Control Panel lists the same per-plug-in lines and shows the degraded ones in amber.
  • ADR-045, vendor-neutral. It covers the install-order matrix in abstract terms, and the decisions: registration is a fact, the state vocabulary, no live swap, restart-to-complete, and surfacing.

Trade-off: why adoption completes by restart, not a live head swap

The head device is held by the D3D11 service system compositor (35 sys->xdev uses, including the worst-case atlas sizing at system creation). It is also in xsysd roles, in the qwerty and rig pose binding, and in every client's shared-memory mode-table snapshot. Replacing it under live sessions is not safe.

So a client that is connected when an adoption happens keeps the partial adoption (new weaving DP and geometry, old mode table, no eye tracking) until it reconnects. The service restarts as soon as it is idle. Before this PR, that state lasted until the next manual restart.

Verified (this box, dev build, in-process cli only)

  • scripts\build_windows.bat build succeeds, including service, cli and Control Panel.
  • _package\bin\displayxr-cli.exe selftest returns rc=0, PASS. Against the installed released vendor plug-in (ABI 5, no slot) it now prints:
     :: Registered display plug-ins (ADR-045 platform state)
    	vendor plug-in 'DisplayXR Leia SR' 2.8.3 — UNKNOWN (platform state not reported) (ACTIVE; id=leia-sr, ProbeOrder=50)
    	fallback plug-in 'DisplayXR Sim Display' 2.22.2 — UNKNOWN (platform state not reported) (NOT_ATTEMPTED; id=sim-display, ProbeOrder=200)
    
    The installed sim-display is the released build, so it is UNKNOWN too.
  • info and info --json show the same data (plugins[]; JSON is 7.7 KB, inside the panel's 16 KB buffer).
  • With DXR_PLUGIN_EXCLUSIVE=sim-display, the fallback is ACTIVE and the vendor plug-in is NOT_ATTEMPTED.

Not verified here (owed hardware/service tests)

The service was not run on this box: the box rules forbid touching the installed service and the HKLM registrations. The owed steps are in the implementation report. In short:

  1. Plug-in registered while the service runs → display re-probe (plug-in registration changed).
  2. Display unplug/replug → display re-probe (display change).
  3. Vendor platform service started late → fallback timer adopts, then plug-in adoption: '<sim>' -> '<vendor>' and, once idle, plug-in adoption: no client connected — restarting the service …. The successor logs Started to complete a display plug-in adoption.
  4. Tray tooltip text in each state.
  5. Linux/macOS compile (CI).

🤖 Generated with Claude Code

@dfattal
dfattal requested a review from a team as a code owner October 3, 2026 01:06
dfattal and others added 3 commits October 2, 2026 18:07
…R-020 append-only)

Adds enum xrt_plugin_platform_state (UNKNOWN/READY/PLATFORM_ABSENT/
PLATFORM_NOT_RUNNING/NO_DISPLAY/INCOMPATIBLE), struct
xrt_plugin_platform_status (state + flags + 128-byte UTF-8 hint +
reserved tail), XRT_PLUGIN_PLATFORM_FLAG_FALLBACK, and an optional
get_platform_state slot appended at the END of xrt_plugin_iface, gated
by struct_size. No XRT_PLUGIN_API_VERSION_CURRENT bump; feature macro
XRT_PLUGIN_HAS_PLATFORM_STATE.

The slot takes no instance so it is callable before probe() (a plug-in
that declines can still say why): load -> negotiate -> get_platform_state
-> probe. sim-display implements it (always READY + FALLBACK flag).
plugin-discovery.md documents the call sequence, the ~100 ms non-blocking
probe rule, and the no-live-swap re-probe rule.

Part of #1803 (#1804).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… no live swap, complete adoption

Loader (target_plugin_loader.c), all three platform paths:
- per-registered-plug-in records (target_plugin_get_status): load outcome
  (ACTIVE / LOADED / DECLINED / BINARY_MISSING / DEPENDENCY_MISSING /
  LOAD_FAILED / ABI_MISMATCH / ...) + the platform state and hint the plug-in
  reports through get_platform_state, queried after negotiate and BEFORE
  probe; the active plug-in's state is re-queried live.
- orphan registrations (Binary missing) and err=126 "a library the plug-in
  imports is missing" are told apart; a repeated identical failure logs at
  INFO after the first WARN (re-probes no longer spam).
- no live swap: target_plugin_refresh_active returns the active plug-in
  untouched unless it is the FALLBACK (XRT_PLUGIN_PLATFORM_FLAG_FALLBACK, or
  the runtime's own sim-display id for a plug-in too old to report).

Service:
- world-event re-probe worker in the IPC server, fed by WM_DISPLAYCHANGE and
  WM_DEVICECHANGE(DBT_DEVNODES_CHANGED) on the hidden session window, a
  RegNotifyChangeKeyValue waiter on the DisplayProcessors root, and a 10 s
  timer while the fallback is active; debounced >= 1 s, runs off the window
  thread and off the main loop.
- complete adoption: when a refresh adopts a plug-in under the fallback's
  head device, info.head_device_stale is set; once no client has been
  connected for 2 s the service ends its loop and starts a successor
  (--adoption-restart-after-pid) that builds its system on the new plug-in.
  A successor never restarts itself again.
- tray tooltip: active display processor + degraded reason.

Surfacing: displayxr-cli info/selftest (text + --json) list every registered
plug-in with its state and hint; the vendor_dp note carries the rejected
plug-in's state (exit codes unchanged); the Control Panel shows the same.

ADR-045 + plugin-discovery.md.

Part of #1803 (#1804, #1805).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/plugin-platform-state branch from 5a040cd to 7a72186 Compare October 3, 2026 01:07
@dfattal
dfattal merged commit 07a8d7f into main Oct 3, 2026
40 checks passed
@dfattal
dfattal deleted the feat/plugin-platform-state branch October 3, 2026 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment