Skip to content

feat(lift): XR_DXR_lift — vendor 2D→3D conversion module (ADR-042) - #1781

Merged
dfattal merged 16 commits into
mainfrom
feat/lift-main
Oct 1, 2026
Merged

dfattal merged 16 commits into
mainfrom
feat/lift-main

Conversation

@dfattal

@dfattal dfattal commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

XR_DXR_lift — vendor 2D→3D conversion exposed by the runtime (ADR-042)

Lands the lift series that has been panel-tested on the lift/on-* branches since 2026-09-25. 16 commits, rebased onto main; each is a self-contained step, so this is a rebase-merge.

What it adds

  • XR_DXR_lift extension (header, spec, catalog note; type values 1004999270–280) and its OpenXR entry points.
  • u_lift_mailbox — latest-wins input mailbox + pinned output ring, with unit tests.
  • D3D11 display-processor lift slots (XRT_DP_D3D11_HAS_LIFT, appended at BASE+25..29) and a lift-only plug-in factory (XRT_PLUGIN_IFACE_HAS_D3D11_LIFT_FACTORY). sim_display ships a FAKE lift for hardware-free testing.
  • D3D11 service lift thread, IPC, the weave-rect lift chain (per-stream priority scheduling, snapshot long-edge cap DXR_LIFT_MAX_INPUT_EDGE, letterbox crop).
  • displayxr-cli lift caps / lift probe, and a lift_caps selftest check.

Compatibility

  • Plug-in ABI stays v5. Slots are appended and struct_size-gated: a released plug-in without lift loads unchanged and reports lift UNAVAILABLE; a lift plug-in on an older runtime is never read past slot 24.
  • No behaviour change unless an app enables XR_DXR_lift and the active plug-in fills the slots.
  • Lift is Windows/D3D11-service only; other platforms report UNAVAILABLE.

Verification

Follow-ups (not in this PR)

  • weave_submit creates the crop RTV in the caller's format; an _SRGB-typed weave input would double-encode lifted pixels. No known caller sends one.
  • The Leia plug-in needs its Windows runtime pin moved to the release carrying this.

🤖 Generated with Claude Code

dfattal and others added 16 commits October 1, 2026 14:22
…-279, catalog note (ADR-042)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t ring state machine, with unit tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ug-in factory, sim_display FAKE lift

Five append-only slots on xrt_display_processor_d3d11 (lift_get_caps,
lift_stream_create, lift_stream_destroy, lift_convert, lift_convert_blob) per
ADR-020, plus the optional xrt_plugin_iface::create_dp_d3d11_lift factory so
a plug-in can build a DP that serves lift only (no weaver, no tracker).
sim_display fills the slots only under SIM_DISPLAY_FAKE_LIFT=1: shifted
SBS/N-view, gradient depth, a two-layer 3DGS PLY.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ift entry points

- d3d11_lift.{h,cpp}: one lift thread on a dedicated device (ID3D11Multithread
  protected) with the plug-in's lift-only DP; per-stream latest-wins mailbox and
  N=2 output ring as keyed-mutex shared textures crossing service<->lift device;
  client export texture + fence (weave output pattern); blob latch; tracked eyes
  always passed as explicit viewpoints; DXR_LIFT=0 kill switch.
- comp_d3d11_service: system-level lift API; lift-flagged weave rects woven at
  the rect's current position from the latest result (batch v3 + v6 N-view),
  flat until the first result.
- IPC: lift_* calls in proto.json (connection-owned streams, varlen blob),
  server handlers, ipc_client_lift.{h,c}, xc bridges.
- oxr_lift.c: xrGetLiftPropertiesDXR / xrCreate|DestroyLiftStreamDXR /
  xrSubmitLiftFrameDXR / xrAcquireLiftResultDXR / xrAcquireLiftBlobDXR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tats, focal_px

- oxr_weave.c: XrWeaveSubmitLiftRectsDXR — validated, forwarded as
  lift_weave_rects right before the submit it belongs to.
- XrLiftPriorityDXR + xrSetLiftStreamPriorityDXR / xrGetLiftStreamStatsDXR:
  each lift-thread round converts every HIGH stream with a new frame, one
  NORMAL round-robin, LOW every 4th round, PAUSED never (u_lift_sched,
  unit-tested); effective rate from publish intervals.
- xrt_dp_lift_params.focal_px / XrLiftOptionsDXR::focalPx (appended) for
  photo -> Gaussians modules.
- Spin fix: pending frames only wake the lift thread once the module is READY.
- Tests: scheduler, rate, and the sim_display fake PLY's format.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…check

- lift caps [--json]: connects over IPC as DIAG, polls through ACTIVATING.
- lift probe <image|frames_dir> (Windows): shared keyed-mutex input textures,
  sequential or --pipelined submits, reads results back through the exported
  texture + fence, writes lift_out_<i>.png (depth normalised) / .ply, prints
  per-frame submit->acquire and service latency, stream stats.
- selftest: lift_caps (WARP + lift-only factory); absence never fails,
  malformed caps -> CLI_SELFTEST_BAD_LIFT_CAPS (11).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…g in XR_DXR_lift.h

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rvice threads/locks, CLAUDE.md

- docs/specs/extensions/XR_DXR_lift.md: lifecycle, states, submit mailbox,
  texture + blob acquire, weave-rect lift, priority scheduling + stats,
  timestamps, service threading, error table, vendor contract, probe,
  consumers, version history.
- ADR-042: a READY vendor module supersedes the browser/SDK open default
  (vendor Gaussian modules supersede the open MoGe + generator lift the same
  way); SBS-not-woven; async one frame behind; geometry from the weave rect;
  browser + web SDK consumer contract.
- xrt_plugin_iface.md: the five lift slots + create_dp_d3d11_lift.
- service-architecture.md: the lift thread, its lock, lock order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…k; add --no-write / --write-every N (the readback+encode dominated the reported latency and capped the pipelined rate)
…on, non-elevated CLI, NeurD version gate, --no-write)
…alpha instead of forcing 1.0 — present-owner weaves showed the page as black around the 3D tiles (#1742)
…R_LIFT_MAX_INPUT_EDGE, default 1920)

A lift-flagged weave rect was snapshotted at DEVICE pixels. On a real Leia
8K panel at 300% DPI with the NeurD DirectML module, a fullscreen YouTube
player is a 7680x4319 rect, so the module synthesized a 15360x4319 SBS per
frame: 1080p input converts in 48 ms, 4K in 61 ms, 8K in 132 ms (~6.5 Hz).
The vendor's own autoscaling (inputScale / INPUT_AUTOSCALING) shrinks only
inference, not view synthesis, so the cap has to happen before the DP.

The spec already stretches the newest result into the rect's CURRENT
position, so a smaller snapshot is legal and costs little on the panel.
ADR-042: the runtime owns this policy.

- d3d11_lift: weave-rect (SRV) snapshots are scaled so the long edge is at
  most DXR_LIFT_MAX_INPUT_EDGE (read once at lift-module create in the
  service; default 1920, 0 = off, 1-255 clamp to 256). The long edge becomes
  the cap rounded down to even, the short edge scales by the same factor to
  the nearest even value (min 2). The in-slot is allocated at the scaled
  size, and the mailbox commits the SCALED dims so the module sees its real
  input size. The capped blit uses a new 4-tap bilinear box-filter PS
  (exact up to 4x, taps clamped inside the sub-rect); the uncapped blit keeps
  the 1:1 point path. A focal_px hint is scaled with the input.
- xrSubmitLiftFrameDXR frames (d3d11_lift_submit_handle) are never capped.
- One WARN per stream when the cap engages or its capped size changes.
- u_lift_cap_dims / u_lift_max_input_edge_parse are pure helpers in
  u_lift_mailbox, pinned by tests_lift_mailbox.
- Docs: XR_DXR_lift.md step 1; census row in
  control-panel-performance-settings.md (a "Convert-to-3D quality" tier).

Consumers already sample the result into the rect (lift_weave_rect_batch,
lift_weave_rects_nview: blit_to_atlas_texture with pin.width/height as the
source size and the rect as the destination, linear sampler).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s (and subtitles in them) flat

A lift-flagged weave rect often holds a film with black bars, sometimes
with subtitles in them. Converting the bars wastes module time, the hard
black edge confuses depth, and subtitles get lifted with the picture.

Service side, before the DXR_LIFT_MAX_INPUT_EDGE cap (ADR-042: the runtime
owns the policy):
- measure: a small GPU pass writes each row/column bucket's non-black
  fraction (512x2 R32F), read back through a 3-deep staging ring with
  DO_NOT_WAIT, so the weave never stalls (letterbox_step);
- decide: u_lift_letterbox_update (pure, ctested) — a bar is the run from
  an edge below 25% non-black; a shorter bar is extended to the opposite
  one when the extra rows are only sparsely lit (<60%), so dense
  subtitles stay in the bar; bars grow after a 45-frame settle, shrink at
  once, black frames change nothing, a resize resets;
- crop: submit_locked snapshots only the active area (cap applies to it),
  recording the normalised active rect on the input slot -> output slot
  -> pin, so each result is placed with the crop it was made with;
- recompose: lift_weave_rect_batch stretches the result into the active
  sub-rect and writes the bar strips from the 2D input identically into
  both views (zero disparity); the v6 N-view path keeps the caller's flat
  tiles in the bars.

DXR_LIFT_LETTERBOX (service env, default on, 0 = off); never applied to
an app's own xrSubmitLiftFrameDXR frame. One WARN per stream per crop
change.

Verified on the 8K Leia panel (NeurD 0.4.4 DirectML, DXR browser #6): a
3386x1904 rect holding a 2.39:1 still with a subtitle in the bottom bar
-> "letterbox crop 3386x1904 -> active 3386x1422 (bars top 240 bottom
242)", module input 1920x806 instead of 1920x1080; weave-dump SBS: top
bar and subtitle bar |L-R| = 0.00, active area 2.75.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne pillarbox

Panel feedback on 73a87ce (8K Leia, NeurD 0.4.4, YouTube in DXR browser #6):
- a sliver of the black bar was still woven: the profile resolves a bar
  edge to one bucket (~h/512 rows) and NeurD leaves a seam at the crop
  edge. Recompose now draws the flat bar strips AFTER the result, each
  reaching ceil(size/512)+4 px into the active area;
- subtitles made the bar weave intermittently: the log showed the bottom
  bar flipping 186 -> 76 -> 74 -> 0 while the top held 184 (one caption
  frame shrank the crop at once, regrowing took 45 frames). Shrinking now
  needs picture (>= 25%) to intrude for 6 consecutive frames and by more
  than the jitter tolerance; the symmetry rule also accepts a band
  separated from the picture by a black gap, however dense its text;
- a spurious pillarbox (left 514 right 516) committed during a dark
  stretch: bars now GROW only into truly black buckets (< 3% lit).

u_lift_letterbox keeps two readings of each profile (grow = black,
keep = no picture). Tests: +2 cases (dark edges never grow a bar; 300
frames of flickering dense captions keep the bottom bar), un-crop case
updated to the 6-frame debounce; 23/23 [lift] pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Panel (YouTube, build #7): bars settled at 250, then 254, then 256 —
three re-crops, each re-sizing the module input, within seconds. Once a
crop is in effect, growth now commits only when an edge grows beyond a
deadband of max(8 px, size/256); the recompose's flat bars already reach
~7 px into the active area. First engagement is unchanged. +1 ctest
(4 px thicker = no re-crop; 40 px thicker = re-crops); 24/24 [lift].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…icture

David on the panel: the flat bars' ~7 px overlap into the active area
(4d3ba8c, there to hide one profile bucket of slack + the module's edge
seam) makes the woven picture and the flat picture rows disagree at the
border. The profile is now one bucket per row/column (BINS_MAX 8192, one
64 KB async readback), bar sizes are no longer rounded down to even, and
the recompose tiles the rect exactly: the result fills precisely the
cropped rect it was converted from, the flat bars exactly the rest.

Also: every crop-change WARN carries the deciding row profile (64 bands,
digit = max non-black fraction x10), so an unexpected un-crop in the
field says what was in the bars (captions, player UI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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