Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Integrate your 3D display hardware into DisplayXR.
- [ADR-045](adr/ADR-045-plugins-always-loadable-and-report-platform-state.md) — Plug-ins are always loadable and report their platform state
- [ADR-046](adr/ADR-046-depth-aware-cursor.md) — Depth-aware cursor — opt-in only; the app knows the depth, the runtime places the cursor
- [ADR-047](adr/ADR-047-multi-screen-segments-and-per-screen-views.md) — Multi-screen — segments and per-screen views
- [ADR-048](adr/ADR-048-lift-viewpoint-policy.md) — Lift viewpoint policy — window-relative viewpoints, rig gains, axis mode and ease-back are runtime policy
<!-- END ADR INDEX -->

---
Expand Down
133 changes: 133 additions & 0 deletions docs/adr/ADR-048-lift-viewpoint-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# ADR-048: Lift viewpoint policy — window-relative viewpoints, rig gains, axis mode and ease-back are runtime policy

**Status:** Accepted (2026-10-08) · Phase 1 implemented · spec:
[XR_DXR_lift.md §4.1](../specs/extensions/XR_DXR_lift.md#41-viewpoint-frame-and-policy-spec-v2-adr-048) ·
extends [ADR-042](ADR-042-vendor-2d3d-conversion-supersedes-default.md) · applies
[ADR-012](ADR-012-window-relative-kooima-projection.md) to lifted content · same split of labour as
[ADR-040](ADR-040-rear-depth-budget.md) and [ADR-046](ADR-046-depth-aware-cursor.md)

## In one paragraph

When the runtime lifts 2D content into 3D (ADR-042), the vendor conversion module synthesizes
views for viewpoints the runtime hands it. Until now those were the tracked eyes as the panel
reports them — relative to the **panel** centre. A lifted video in a window in the corner of the
panel was therefore rendered as if the viewer were looking at it from far off-axis, even when they
faced it squarely. And the look-around was entirely the vendor plug-in's business: a fixed eye
distance, a fixed clamp, vertical parallax switched off by hand, knobs read once per process.
This ADR makes the viewpoints **window-relative** — the runtime rebases them to the centre of the
lifted rect (or the submitting window) — and makes everything that shapes them **runtime policy**:
display-rig ipd and parallax factors, which axes follow the viewer, an offset clamp, and an
**ease-back** that slowly returns a viewer who settles off-axis to the straight-on view. Apps
steer it per stream with `XrLiftViewControlDXR`. The plug-in only translates units.

## Context

### What the module needs

A conversion module renders a frame for N viewpoints. It does not know the panel, the window, or
the viewer — only the numbers it is given. Two things decide whether the result looks right:

1. **The origin.** A viewer facing a lifted rect should get the rect's straight-on views. That
is the window-relative rule ADR-012 set for Kooima projection: subtract the window's centre
offset from the eyes, and use the window's physical size. Lift never applied it; the eyes
reached the module panel-centred.
2. **The shaping.** Raw look-around is a lot of motion parallax for flat content that was never
captured from more than one viewpoint. Too much exposes disocclusions; vertical parallax on a
horizontally-trained model jumps; a viewer who simply sits off-centre gets a permanently
skewed picture. Each of those is a judgment call about the **experience**, the same on every
vendor's panel.

### Where the shaping lived

In the vendor plug-in, as fixed constants and process-wide environment knobs: an assumed 63 mm
eye distance, a fixed clamp, y and z forced to zero. Nothing per stream, nothing an app could
ask for, nothing shared with how the runtime renders real 3D content (the display / camera rigs
of `XR_DXR_view_rig`).

## Decision

### D1. Viewpoints are relative to the lifted region's centre

The lift thread rebases every viewpoint — tracked or explicit — to the centre of the region the
frame came from, in display axes, metres:

- a lifted weave rect: that rect on the weave client's window (rect pixels → window fractions →
the window's metres, from the service's existing window metrics);
- `xrSubmitLiftFrameDXR`: the session's window;
- no window (a headless connection): the panel, i.e. the old behaviour.

This is a **fix**, so it is unconditional. The DP is told the frame (`viewpoint_frame = RECT`)
and the region's physical size (`rect_width_m`, `rect_height_m`).

### D2. The runtime owns the policy; the plug-in owns units

For TRACKED viewpoints the lift thread applies, in order:

1. **ipd factor** — scale each eye's offset from the pair midpoint (display-rig step 1a);
2. **parallax factor** — lerp the midpoint toward the nominal viewer (step 1b);
3. **axis mask** — X (default), XY or XYZ: which midpoint components follow the viewer; the
others are pinned (y = 0, z = the nominal viewing distance);
4. **recenter** — the ease-back filter (D3);
5. **clamp** — the midpoint's x / y offset to `maxOffsetMeters` (0 = none).

The DP receives the processed viewpoints plus `baseline_m` (the resulting eye separation),
`axis_mode` and `max_offset_m`, in fields appended to `xrt_dp_lift_params` behind
`XRT_DP_LIFT_HAS_VIEWPOINT_POLICY` (ADR-020: append-only, no ABI bump). The plug-in normalises by
`baseline_m` instead of a fixed eye distance, honours every component it is sent, and keeps its
own knobs only as defaults. It makes no experience decisions.

The math is a pure, lock-free, clock-free C module (`u_lift_viewpoint`) with unit tests, so the
policy is identical for every vendor and testable without hardware.

### D3. Ease-back: the reference follows the viewer

One filter per stream, frame-rate independent, reset on stream create and on tracking loss:

- **EASE_BACK (default).** The camera eases back to (0, 0, 0), the scene camera origin. If you
move your head you see look-around temporarily; once your offset has stayed beyond a few
millimetres for `recenterHoldSeconds` (default 1 s), the camera returns to centre, with no
look-around, with time constant `recenterTimeConstantSeconds` (default 2 s). Any new head
motion gives immediate, temporary look-around again. Mechanically, a reference viewer position
follows the head after the hold and the rendered offset is the head's offset from it, so it
decays to 0 wherever the viewer settles.
- **OFF.** The plain rect-relative offset.

An "ease to the rect axis" variant (decay the offset, restore look-around only once the viewer
returns to the rect's axis) was considered and rejected: a viewer who settles off-axis would
lose look-around until they moved back.

### D4. Apps steer it per stream, and can see what was used

`XrLiftViewControlDXR` (XR_DXR_lift v2) chains on `XrLiftOptionsDXR` or directly on
`XrWeaveRectLiftDXR`, and is sticky per stream. `XrLiftResultViewpointsDXR` on
`XrLiftResultDXR` echoes the viewpoints a result was synthesized for, with the region's centre
and size — so an app compositing its own 3D over a lifted frame can render for the same eyes.

### D5. Defaults change behaviour, with an A/B switch

Defaults are ipd 1, parallax 1, axis X, no clamp, EASE_BACK, 1 s hold, 2 s time constant.
Existing callers (the browser's lifted media) therefore change in two ways without asking: the
viewpoint is centred on the lifted rect, and an off-axis viewer is eased back after a second.
The first is the fix; the second is the chosen experience. `DXR_LIFT_RECENTER=off` in the
service's environment forces recentering off for every stream, for A/B comparison.

## Consequences

- Lifted content in a window looks straight-on to a viewer facing that window, on every panel.
- Vertical and depth look-around become available to apps that ask (XY / XYZ) once the vendor
plug-in stops discarding those components — a plug-in change, no runtime change.
- A plug-in built before this ADR ignores the appended fields and keeps its fixed eye
distance, but it already receives rect-relative eyes: the rebase takes effect immediately, the
rest of the shaping once the plug-in is updated. A runtime built before this ADR passes a
short `struct_size`, which an updated plug-in reads as DISPLAY-frame (panel-centred) viewpoints.
- The recenter filter needs real eye tracking to do anything: with the vendor's fallback eyes
(tracking lost) it stays reset, and sim_display, which reports no tracking, never eases.

## Later phases (not decided here)

- Accept a full `XrCameraRigDXR` / `XrDisplayRigDXR` on lift options so lifted frames and
app-rendered 3D share one camera computed with the `xrLocateViews` math.
- A metric, off-axis camera input on the vendor module (eye positions in metres relative to a
screen plane of given size plus a depth scale), replacing today's dimensionless viewpoints.
- Metric depth as an auxiliary output aligned to the rigs.
- Lift in-process (D3D12) for engine apps.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@
- [ADR-045](ADR-045-plugins-always-loadable-and-report-platform-state.md) — Plug-ins are always loadable and report their platform state
- [ADR-046](ADR-046-depth-aware-cursor.md) — Depth-aware cursor — opt-in only; the app knows the depth, the runtime places the cursor
- [ADR-047](ADR-047-multi-screen-segments-and-per-screen-views.md) — Multi-screen — segments and per-screen views
- [ADR-048](ADR-048-lift-viewpoint-policy.md) — Lift viewpoint policy — window-relative viewpoints, rig gains, axis mode and ease-back are runtime policy
22 changes: 18 additions & 4 deletions docs/reference/xrt_plugin_iface.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,13 @@ struct xrt_dp_lift_stream_info { uint32_t struct_size; uint32_t mode; uint32_t c
float input_scale; };
struct xrt_dp_lift_params { uint32_t struct_size; float convergence; /* [0,1] relative depth at the glass; <0 AUTO */
float strength; uint32_t inpaint; uint32_t view_count;
float focal_px; /* appended: input focal length in px; <=0 unknown (GAUSSIANS) */ };
float focal_px; /* appended: input focal length in px; <=0 unknown (GAUSSIANS) */
/* appended, XRT_DP_LIFT_HAS_VIEWPOINT_POLICY (ADR-048): */
float rect_width_m, rect_height_m; /* lifted region size; 0 unknown */
float baseline_m; /* first-to-last viewpoint distance; 0 = none */
uint32_t axis_mode; /* 1 X, 2 XY, 3 XYZ: components that follow the viewer */
float max_offset_m; /* x/y clamp the runtime applied; 0 none */
uint32_t viewpoint_frame; /* 0 DISPLAY (panel centre), 1 RECT (lifted rect centre) */ };

/* xrt_display_processor_d3d11, slots 25..29 */
bool (*lift_get_caps)(xdp, struct xrt_dp_lift_caps *out);
Expand Down Expand Up @@ -542,9 +548,17 @@ xrt_dp_factory_d3d11_fn_t create_dp_d3d11_lift; /* a DP that serves ONLY the lif
(`XrLiftPriorityDXR`) and timestamps. A `false` return means "no output this frame"; the
runtime keeps the previous result.
- **Viewpoints are always explicit.** For SBS/N-view the runtime passes `viewpoints_xyz` —
the app's EXPLICIT viewpoints, else the panel DP's predicted tracked eye pair (display space,
metres). Give them precedence over any tracker of your own; the lift DP has none. NULL/0 only
while no eyes are known.
the app's EXPLICIT viewpoints, else the panel DP's predicted tracked eye pair (metres). Give
them precedence over any tracker of your own; the lift DP has none. NULL/0 only while no eyes
are known.
- **Viewpoints are rect-relative and already shaped (ADR-048).** With
`viewpoint_frame == RECT` (every runtime that fills the field) they are relative to the
lifted rect's centre, and the runtime has already applied the viewpoint policy — ipd /
parallax factors, axis mask, ease-back, clamp. Translate units only: normalise by
`baseline_m` (not a fixed eye distance), honour every component you are sent (`axis_mode`
says which ones are live), and use `max_offset_m` + the rect size to map onto your module's
range. A `struct_size` that stops before these fields means panel-centred viewpoints (an
older runtime).
- **Output layout.** SBS = two views side by side; NVIEW = `view_count` views in one row, view 0
leftmost; DEPTH = one channel (any single-channel or RGBA format — say which in
`out_format`). The runtime weaves SBS/NVIEW results itself on the ordinary weave path
Expand Down
70 changes: 66 additions & 4 deletions docs/specs/extensions/XR_DXR_lift.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
| Field | Value |
|---|---|
| **Extension Name** | `XR_DXR_lift` |
| **Spec Version** | 1 |
| **Spec Version** | 2 |
| **Extension Type** | Instance extension (service path — Windows/D3D11; advertised on every desktop platform, where a service without a module reports `supportedModes = 0`) |
| **Header** | `src/external/openxr_includes/openxr/XR_DXR_lift.h` (canonical; auto-syncs to `displayxr-extensions`) |
| **Status** | Provisional (`1004999270–280` block, pending Khronos registry) |
| **Decision record** | [ADR-042](../../adr/ADR-042-vendor-2d3d-conversion-supersedes-default.md) |
| **Status** | Provisional (`1004999270–282` block, pending Khronos registry) |
| **Decision record** | [ADR-042](../../adr/ADR-042-vendor-2d3d-conversion-supersedes-default.md); viewpoint policy: [ADR-048](../../adr/ADR-048-lift-viewpoint-policy.md) |
| **Plug-in contract** | `src/xrt/include/xrt/xrt_dp_lift.h` + the lift slots of `xrt_display_processor_d3d11` (`XRT_DP_D3D11_HAS_LIFT`), [`xrt_plugin_iface.md` § lift](../../reference/xrt_plugin_iface.md#turning-2d-into-3d-the-lift-slots-adr-042-xr_dxr_lift) |

## 1. What it is
Expand Down Expand Up @@ -130,6 +130,67 @@ panel display processor's predicted eyes just before each conversion and hands t
module; a module spreads N views around it. The lift display processor has no tracker session
of its own.

### 4.1 Viewpoint frame and policy (spec v2, ADR-048)

**Frame.** Every viewpoint the module receives is relative to the **centre of the lifted
region**, in display axes (+x right, +y up, +z toward the viewer), metres:

| Path | Region |
|---|---|
| lifted weave rect (§6) | that rect, on the weave client's window |
| `xrSubmitLiftFrameDXR` | the session's window (its window binding / presenter window) |
| `xrSubmitLiftFrameDXR` on a connection with no session window (e.g. `displayxr-cli lift`) | the panel |

So a lifted video in a corner window looks straight-on to a viewer facing that window, not to a
viewer facing the panel centre (the same window-relative rule ADR-012 sets for Kooima). This
**rebase is unconditional** in v2 — it fixes v1, which passed panel-centred eyes. EXPLICIT
viewpoints are given in display space as before and are rebased the same way; nothing else is
applied to them.

**Policy** (`XrLiftViewControlDXR`, TRACKED only). Chain it on `XrLiftOptionsDXR` (submit or
weave rect) or directly on `XrWeaveRectLiftDXR` (the direct chain wins). It is **sticky per
stream**: the last one sent applies until another is sent. Omitted, the defaults apply:

| Field | Default | Meaning |
|---|---|---|
| `ipdFactor` | 1 | scales each eye's offset from the pair midpoint ([0, 1]; 0 = mono) — display-rig step 1a |
| `parallaxFactor` | 1 | lerps the midpoint toward the nominal viewer `(0, 0, nominal distance)`: 1 = follow the viewer, 0 = fixed straight-on — step 1b |
| `axisMode` | X | which midpoint components follow the viewer: X, XY or XYZ. Masked components are pinned (y = 0, z = the nominal distance) |
| `maxOffsetMeters` | 0 (none) | clamp of the midpoint's x / y offset from the rect centre |
| `recenterMode` | EASE_BACK | OFF or EASE_BACK: whether the camera returns to centre after a hold (below) |
| `recenterHoldSeconds` | 1 s | how long the offset must stay beyond ~5 mm before recentering starts (< 0 = default) |
| `recenterTimeConstantSeconds` | 2 s | exponential time constant of the recentering (<= 0 = default) |

Order, per conversion: rebase → ipd factor → parallax factor → axis mask → recenter → clamp.

**Recentering.** One filter per stream, frame-rate independent (`1 - exp(-dt / tau)` per step),
reset when the stream is created and whenever eye tracking is lost.

- `EASE_BACK` (default) — the camera eases back to the scene camera origin. If you move your
head you see look-around temporarily; once your offset has stayed beyond the threshold for the
hold time, the camera returns to centre — no look-around — with the time constant. Any new
head motion gives immediate, temporary look-around again. (Mechanically: a reference viewer
position starts at the straight-on viewer and, after the hold, slides toward the head; the
rendered offset is the head's offset from that reference, so it decays to 0 wherever the
viewer settles.)
- `OFF` — the rendered offset is always the viewer's offset from the rect centre.

**Defaults changed behaviour.** A v1 caller that chains nothing gets the rect-relative rebase
and the EASE_BACK default. `DXR_LIFT_RECENTER=off` (service environment, read once when
the lift module is created) forces recentering OFF for every stream, for A/B.

**Echo.** Chain `XrLiftResultViewpointsDXR` on `XrLiftResultDXR` to receive the viewpoints the
result was synthesized for (after the policy, same frame), the region's centre in display space
and its size — so an app can render matching 3D for the same eyes. `viewpointCountOutput` 0 =
no viewpoints were known (the module used its defaults) or a DEPTH stream.

**What the module is told.** The DP receives the processed viewpoints plus, in appended
`xrt_dp_lift_params` fields (`XRT_DP_LIFT_HAS_VIEWPOINT_POLICY`): `viewpoint_frame` (RECT),
`rect_width_m` / `rect_height_m`, `baseline_m` (first-to-last viewpoint distance, i.e. the eye
separation after the ipd factor), `axis_mode` and `max_offset_m`. The plug-in only translates
units — it normalises by `baseline_m` rather than a fixed eye distance, and honours every
component it is sent.

## 5. Acquiring results

### 5.1 Textures (DEPTH / SBS / NVIEW)
Expand Down Expand Up @@ -328,7 +389,7 @@ Mirrors XR_DXR_weave §4b.
| latest-wins mailbox, drops, frame ids | the model, its backend, licensing, warm-up (reported as ACTIVATING) |
| output ring copies, export textures, fences | an output valid until its next call |
| priority scheduling across streams | nothing about scheduling |
| tracked eyes → explicit viewpoints | honouring explicit viewpoints over any tracker of its own |
| tracked eyes → explicit viewpoints, rebased to the lifted rect, with the viewpoint policy applied (ADR-048) | honouring explicit viewpoints over any tracker of its own; unit translation only (normalise by `baseline_m`, honour every component sent) |
| weaving SBS / N-view results (ADR-007) | never weaving a lift result |
| timestamps, latency, stats | reporting `typical_latency_ns` |

Expand Down Expand Up @@ -378,6 +439,7 @@ order: runtime → extensions auto-sync → consumers.
| Version | Change |
|---|---|
| 1 | Initial: properties + states, streams (DEPTH / SBS / NVIEW / GAUSSIANS), non-blocking latest-wins submit, texture acquire (weave-style handles + fence), blob acquire (two-call latch), weave-rect lift chain, per-stream priority scheduling + stats, `focalPx`. |
| 2 | Viewpoint policy (ADR-048, §4.1): viewpoints relative to the lifted rect / submitting window centre (was: the panel centre), `XrLiftViewControlDXR` (ipd / parallax factors, axis mode, offset clamp, recentering; defaults X + EASE_BACK 1 s / 2 s), `XrLiftResultViewpointsDXR` echo. |

## Probing on a Windows box — gotchas (first N0 run, 2026-09-25)

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/extensions/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"XR_DXR_lift": {
"group": "rendering",
"title": "2D-to-3D Conversion",
"summary": "Asynchronous access to a vendor's 2D-to-3D conversion module (depth, stereo, N-view, or photo-to-Gaussian-splats): submit 2D frames, acquire the latest converted result, or flag a weave rect as 2D so the service lifts it before weaving"
"summary": "Asynchronous access to a vendor's 2D-to-3D conversion module (depth, stereo, N-view, or photo-to-Gaussian-splats): submit 2D frames, acquire the latest converted result, or flag a weave rect as 2D so the service lifts it before weaving; the runtime owns the viewpoint policy (window-relative look-around, rig factors, axis mode, ease-back)"
},
"XR_DXR_win32_window_binding": {
"group": "windowing",
Expand Down
Loading
Loading