Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b455f6c
feat(lift): XR_DXR_lift extension header, type-value block 1004999270…
dfattal Sep 25, 2026
0fb758c
feat(lift): u_lift_mailbox — latest-wins input mailbox + pinned outpu…
dfattal Sep 25, 2026
110719b
feat(lift): D3D11 DP lift slots (XRT_DP_D3D11_HAS_LIFT), lift-only pl…
dfattal Sep 25, 2026
b5599a7
feat(lift): D3D11 service lift thread + mailbox/ring + IPC + XR_DXR_l…
dfattal Sep 25, 2026
5054f7c
feat(lift): weave-rect lift chain, per-stream priority scheduling + s…
dfattal Sep 25, 2026
1e69bcd
feat(lift): displayxr-cli lift caps / lift probe, selftest lift_caps …
dfattal Sep 25, 2026
43388fb
style(lift): clang-format the new lift files; v6 flat-fallback wordin…
dfattal Sep 25, 2026
5fe0bbd
docs(lift): XR_DXR_lift spec, ADR-042, plug-in iface lift section, se…
dfattal Sep 25, 2026
fc33ce3
fix(cli lift probe): stamp submit->acquire BEFORE the PNG/PLY readbac…
dfattal Sep 26, 2026
b110d7e
docs(lift): Windows probing gotchas from the first N0 run (DP selecti…
dfattal Sep 26, 2026
cdc58f0
fix(sim_display): D3D11 anaglyph + fake-lift shaders carry the atlas …
dfattal Sep 26, 2026
a4380db
feat(lift): cap the weave-rect snapshot's long edge before the DP (DX…
dfattal Sep 26, 2026
00e7ae5
feat(lift): letterbox crop — lift only the active area, weave the bar…
dfattal Sep 26, 2026
a73d0e5
fix(lift): letterbox — no bar sliver, no caption flicker, no dark-sce…
dfattal Sep 26, 2026
1231f66
fix(lift): letterbox — no re-crop creep by a few pixels once cropped
dfattal Sep 26, 2026
a2d32d5
fix(lift): letterbox — exact per-row crop, no flat overlap into the p…
dfattal Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Vendor display drivers ship as **plug-in DLLs** from their own repos (ADR-019).
- `XR_DXR_android_surface_binding` — app passes its own Android Surface/`ANativeWindow`; `xrSetAndroidSurfaceDXR` republishes it across background/resume and `xrSetAndroidWindowGeometryDXR` feeds the per-frame window rect (ADR-036 D6). **Required for multi-window on Android** — the runtime-spawned SurfaceView is `_hosted`-fullscreen-only
- `XR_DXR_mcp_tools` — app registers its own MCP tools (agent control surface); event-queue dispatch via `XrEventDataMCPToolCallDXR`
- `XR_DXR_depth_budget` — advisory **rear depth budget**: how far behind the display plane a transparent app may render (`farOffsetVH`, 0 = clip at the ZDP, 1000 = unrestricted), chained on `XrViewState` at `xrLocateViews`. The runtime owns the policy (it measures the background's horizontal-disparity cue), the DP owns pixels, the app owns geometry — ADR-040
- `XR_DXR_lift` — 2D→3D **conversion service**: a vendor plug-in's module (depth / SBS / N-view / photo→splats) exposed generically as async latest-wins streams (IPC-only, D3D11 service lift thread); SBS/N-view results are woven on the ordinary weave path (a weave rect can be flagged "lift me"), and a READY module supersedes the browser/SDK open default — ADR-042

The list above is highlights, not the catalog — **`docs/specs/extensions/index.json` is the catalog**: one hand-written note (group, title, one-line summary) per published extension, joined to the `XR_DXR_*.h` headers by `scripts/gen_extensions_index.py`. It generates the `displayxr-extensions` mirror's `README.md` + machine-readable `extensions.json`, and `displayxr-website` merges its longer editorial prose onto that by name — so adding a header is all it takes for an extension to appear on every public surface. `lint.yml` runs `--check` on every PR, so a header with no note (or a note with no header) fails the build. That guard exists because the mirror's README was a frozen heredoc that documented 5 of 16 extensions for months (displayxr-extensions#2).

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ Integrate your 3D display hardware into DisplayXR.
- [ADR-039](adr/ADR-039-one-fill-engine-for-every-tier.md) — One fill engine for every tier (same-adapter split)
- [ADR-040](adr/ADR-040-rear-depth-budget.md) — Rear depth budget — the runtime owns the policy, the plug-in owns pixels, the app owns geometry
- [ADR-041](adr/ADR-041-fixed-view-count-with-per-frame-activity.md) — Fixed view count with per-frame activity — inactive views alias, they do not disappear
- [ADR-042](adr/ADR-042-vendor-2d3d-conversion-supersedes-default.md) — A vendor 2D→3D conversion module supersedes the open default — the runtime exposes it, weaving stays the DP's
- [ADR-043](adr/ADR-043-stereo-camera-source.md) — A display's stereo camera is a plug-in-provided source, owned by the service and privacy-gated by the runtime
- [ADR-044](adr/ADR-044-colour-contract-per-backend.md) — The colour contract, per backend and swapchain format
<!-- END ADR INDEX -->
Expand Down
137 changes: 137 additions & 0 deletions docs/adr/ADR-042-vendor-2d3d-conversion-supersedes-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# ADR-042: A vendor 2D→3D conversion module supersedes the open default — the runtime exposes it, weaving stays the DP's

**Status:** Accepted (2026-09-25) · introduces
[`XR_DXR_lift`](../specs/extensions/XR_DXR_lift.md) · appends five optional D3D11
display-processor slots and one optional plug-in factory under the
[ADR-020](ADR-020-plugin-abi-compatibility-policy.md) append-at-end rule · related:
[ADR-007](ADR-007-compositor-never-weaves.md),
[ADR-019](ADR-019-vendor-plugin-aux-boundary.md),
[ADR-040](ADR-040-rear-depth-budget.md)

## Context

Turning ordinary 2D content — a video in a page, a photo, a call participant — into something
the panel can show in 3D is done today by **open defaults that live in the consumer**: the
DisplayXR Browser and the web SDK run an open monocular depth estimator plus a view generator
(and, for photos, an open depth + splat generator) in the page, then hand the result to the
weave like any other stereo content.

Display vendors have their own conversion modules — trained and tuned for their optics, often
running on dedicated inference paths the page cannot reach (Leia's NeurD over DirectML/CUDA is
the first; a Leia-owned photo → Gaussian-splat model is next). The vendor plug-in is already the
one component that knows the panel, and it already ships per machine. What was missing is a
generic way for the runtime to expose such a module, and a rule for who wins when both exist.

Three questions have to be settled together:

1. **Who wins?** An app — or the SDK inside it — that has its own converter, on a machine whose
plug-in also has one.
2. **What crosses the boundary?** Woven pixels would make the conversion a second weaver and
break ADR-007; depth alone would push view synthesis back into every consumer.
3. **How does it meet the frame loop?** Models take tens of milliseconds (seconds for photo →
splats). The weave is a ~1 ms synchronous service on the present thread of a browser.

## Decision

### 1. A READY vendor module supersedes the open default

When `xrGetLiftPropertiesDXR` reports `READY` with the needed mode bit, a consumer that also
ships an open converter uses the runtime's. It falls back to its open default only when the
runtime reports `UNAVAILABLE` (no module, a failed one, a non-Windows service, an in-process
session) — and treats `ACTIVATING` as "not yet", polling, never as a permanent fallback.

This is the same shape as weaving: the vendor's calibrated implementation, behind the plug-in,
beats a generic one in the app. The runtime exposes the capability **generically** — modes,
limits, state, an informational backend name — never which model runs.

**Vendor Gaussian modules supersede the SDK's open MoGe + generator lift the same way NeurD
supersedes the open depth (VDA-class) default.** A plug-in advertising `GAUSSIANS` is preferred
for photo → splats; the SDK passes the photo's focal length (`focalPx`, e.g. the MoGe-estimated
`fx`) so the vendor model gets the intrinsics it takes as input.

### 2. The module returns pre-weave views; weaving stays the DP's

SBS and N-view results are ordinary pre-weave views (two / N views side by side, not woven),
DEPTH is a depth map, GAUSSIANS a blob. The runtime weaves SBS/N-view results on the existing
weave path, through the same display processor as everything else (ADR-007). So:

- one weaver per panel, never two; the conversion module is never on the present path;
- a consumer may also take the views or depth itself (effects, look-around re-render) — the
result is useful beyond weaving;
- a vendor module needs no knowledge of windows, phase, or the interlace.

### 3. Asynchronous, one frame behind; geometry from the weave rect

Conversion runs on a runtime-owned **lift thread** with its **own device**, off the weave, render
and IPC threads. A submit is a snapshot into a **latest-wins mailbox** (a slow module lags a
frame; it never builds a backlog); an acquire returns the newest finished result. Nothing on any
latency-sensitive thread waits for the model.

When lifted content is woven, the caller flags the weave rect (`XrWeaveSubmitLiftRectsDXR`); the
service snapshots the rect's 2D content into the stream **and weaves the stream's latest result
at the rect's current position** in the same submit. Drag, resize and scroll therefore stay exact
and real-time — they come from this frame's rect — while only the depth is one conversion
behind. Until the first result the rect is woven flat.

### 4. Scheduling is runtime policy

Multiple streams share one module. The runtime schedules them by a per-stream priority (HIGH
every round, NORMAL round-robin, LOW every 4th round, PAUSED never) and reports each stream's
effective rate. The plug-in converts one frame per call and knows nothing about streams'
relative importance.

### 5. The plug-in contract is minimal and synchronous

Five appended D3D11 DP slots (`lift_get_caps`, `lift_stream_create`, `lift_stream_destroy`,
`lift_convert`, `lift_convert_blob`) and one appended plug-in factory
(`create_dp_d3d11_lift` — a DP that serves only lift: no weaver, no tracker session), all
optional (ADR-020, no ABI bump). The runtime always passes explicit viewpoints (the panel's
predicted tracked eyes, or the app's), so the lift DP needs no tracker. A module's warm-up
(licence, model load) is reported as ACTIVATING and polled.

## Consumer contract (browser + web SDK)

| Situation | Browser / SDK does |
|---|---|
| `READY` + mode bit | use the runtime: `XrWeaveSubmitLiftRectsDXR` for inline 2D video/images lifted in place; `xrSubmitLiftFrameDXR` + `xrAcquireLiftResultDXR` where the page wants views/depth itself; `xrAcquireLiftBlobDXR` for photo → splats |
| `ACTIVATING` | keep the current rendering (flat, or the open default if already running); poll ≤ 2 Hz; switch to the runtime on READY |
| `UNAVAILABLE` / `XR_ERROR_FEATURE_UNSUPPORTED` | open default |
| a lift call returns `XR_ERROR_RUNTIME_FAILURE` | transient: retry next frame, do not fall back |
| `XR_ERROR_INSTANCE_LOST` | the weave §4b recovery (new instance), then re-query properties |

- Draw lifted content into the weave input as 2D (the whole rect on the batch layout; every tile
on the N-view layout) — never pre-convert it when the runtime will.
- Key results on `sourceTime` when the pairing matters; otherwise take the freshest.
- Set stream priority from what the user is looking at (active speaker, focused video);
PAUSED for off-screen content keeps its last result without spending the module.
- Gate the chained structs on the extension being enabled, not on a spec version (v1).

## Consequences

- **+** The best available converter on each machine is used without the app knowing which it is.
- **+** No second weaver; ADR-007 holds. The conversion never touches the present path.
- **+** Geometry of lifted content is exact at weave time; depth latency is bounded by the
module, visible per stream, and shaped by priority.
- **+** Hardware-free end to end: sim_display's env-gated fake module exercises every path in CI;
`displayxr-cli lift probe` measures a real module on a panel box.
- **−** One more thread and one more D3D11 device in the service (lazily created — zero cost on a
machine that never asks), and a second DP instance of the vendor plug-in (lift-only).
- **−** Frames cross devices as keyed-mutex shared textures: one extra copy each way. On a hybrid
box the lift device is on the service's render adapter; a module that runs on another adapter
(its own device) pays its own bridge.
- **−** Windows/D3D11 only in v1. Other platforms advertise the extension and report
`supportedModes = 0`, which the consumer contract already handles.

## Alternatives rejected

- **The module weaves its own output.** Two weavers per panel, calibration duplicated, and a
conversion on the present path — ADR-007 exists to prevent exactly this.
- **Return depth only; consumers synthesize views.** Pushes the vendor-specific part (view
synthesis tuned to the optics) back into every consumer.
- **Synchronous conversion inside `xrWeaveSubmitDXR`.** Turns a ~1 ms service into a tens-of-ms
one on the browser's present thread.
- **Reuse the weaving DP instance for lift.** It is driven on the service's single shared
immediate context under the render lock and is recreated on presenter changes; a conversion
would stall every weave for its whole duration.
- **Priority as a per-frame submit field.** Priority changes when attention changes, not per
frame; a per-stream setter says so and keeps the submit minimal.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
- [ADR-039](ADR-039-one-fill-engine-for-every-tier.md) — One fill engine for every tier (same-adapter split)
- [ADR-040](ADR-040-rear-depth-budget.md) — Rear depth budget — the runtime owns the policy, the plug-in owns pixels, the app owns geometry
- [ADR-041](ADR-041-fixed-view-count-with-per-frame-activity.md) — Fixed view count with per-frame activity — inactive views alias, they do not disappear
- [ADR-042](ADR-042-vendor-2d3d-conversion-supersedes-default.md) — A vendor 2D→3D conversion module supersedes the open default — the runtime exposes it, weaving stays the DP's
- [ADR-043](ADR-043-stereo-camera-source.md) — A display's stereo camera is a plug-in-provided source, owned by the service and privacy-gated by the runtime
- [ADR-044](ADR-044-colour-contract-per-backend.md) — The colour contract, per backend and swapchain format
11 changes: 11 additions & 0 deletions docs/architecture/service-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ closed with **no message to the client**. An evicted-but-alive client (#925 S4)
| window-op worker | `comp_d3d11_service.cpp:1398` | `SetWindowPlacement`-family restores off the render path (#925 rank 7) | try/catch, no restart |
| WinRT capture pool | `d3d11_capture.cpp:285-300` | `on_frame_arrived` → `CopyResource` on the **shared immediate context, outside `render_mutex`** (relies on `SetMultithreadProtected(TRUE)`, `comp_d3d11_service.cpp:14697-14705`) | partial |
| provider threads | provider-owned (`ultraleap_provider.cpp:568` poll thread; net_input hub) | LeapC polling, #941 idle watchdog (a branch of the poll thread) | none |
| lift (ADR-042) | `d3d11_lift.cpp` `d3d11_lift_create`, lazily on the first `XR_DXR_lift` call | the ONLY thread that touches the vendor's lift display processor: brings up a dedicated **lift device** on the service adapter (ID3D11Multithread-protected) + the plug-in's lift-only DP, then loops: priority-scheduled round (`u_lift_sched`) → take a stream's newest pending input → `lift_convert` / `lift_convert_blob` (ms to s) → copy into the output ring. Never takes `render_mutex` or `immediate_ctx_mutex`; calls back for tracked eyes with no lift lock held. Joined first in `system_destroy` | `DXR_LIFT=0` kill switch |

### 3.2 Lock order (as of #964–#966)

Expand Down Expand Up @@ -348,6 +349,15 @@ c->mutex → render_mutex → { ws_snapshot_mutex, active_compositor_mutex,
resume path to maintain: `multi_compositor_register_client` restarts the thread
and the presenter re-bind builds a DP on the first frame. The grace window is
what keeps an app restart or a shell relaunch from recreating the vendor weaver.
- **Lift (ADR-042) adds one leaf and no edge into the panel lock.** `d3d11_lift::mtx`
guards the stream table and every lift mailbox and is never held across a GPU
wait, a keyed-mutex acquire or a vendor call. Where both are taken the order is
`immediate_ctx_mutex → lift mtx` (a lift-flagged weave rect's snapshot, inside
`weave_submit`); the lift thread never takes `immediate_ctx_mutex` (it owns a
device of its own), and the IPC-side lift calls take `immediate_ctx_mutex` alone,
for one blit or one copy. Frames cross between the service device and the lift
device only as keyed-mutex shared textures (two input slots, two output-ring
slots per stream), so neither thread ever waits on the other's GPU queue.
- Never join the render thread under `render_mutex`; never hold
`global_state.lock` across a compositor call.

Expand All @@ -364,6 +374,7 @@ c->mutex → render_mutex → { ws_snapshot_mutex, active_compositor_mutex,
| `hub->mutex` | Ultraleap provider | joint sets | poll thread + every consumer's `get_hand_tracking` |
| `ipc_c->mutex` | per client process | the whole pipe round trip | every RPC |
| `usys->sessions.mutex` | per system | session list; event push (unbounded malloc'd per-session list, `u_session.c:34-42`) | broadcasts |
| `d3d11_lift::mtx` | per service (`d3d11_lift.cpp`), leaf | lift stream table, every stream's `u_lift_mailbox` (input slots, output ring, pins, stats), caps | lift thread between conversions; IPC threads for submit / acquire / stats (ns-µs, never across GPU or vendor work); `weave_submit` for lift-flagged rects (under `immediate_ctx_mutex`) |

**Nesting observed:** `global_state.lock → render_mutex` at ≥ 11 handler sites
(`ipc_server_handler.c:3735, 4079, 4110, 4204, 4370, 4429, 4475, 4696, 4792, 4968, 5106`
Expand Down
Loading
Loading