Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
18 changes: 13 additions & 5 deletions docs/adr/ADR-043-stereo-camera-source.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# ADR-043: A display's stereo camera is a plug-in-provided source, owned by the service and privacy-gated by the runtime

**Status:** Proposed (2026-09-25) — design only, nothing implemented · introduces
**Status:** Proposed (2026-09-25) · **R1 implemented** 2026-09-26 (runtime, hardware-free; maintainer
defaults recorded in the roadmap §G: stereo-only, iface slots, service clients only, no raw frames
to pages) · introduces
[`XR_DXR_stereo_camera`](../specs/extensions/XR_DXR_stereo_camera.md) · appends optional
camera slots to `xrt_plugin_iface` under the [ADR-020](ADR-020-plugin-abi-compatibility-policy.md)
append-at-end rule · related: [ADR-019](ADR-019-vendor-plugin-aux-boundary.md),
[ADR-034](ADR-034-input-provider-plugins.md), [ADR-035](ADR-035-service-owned-arbitration-single-pipeline-isolated-satellites.md),
ADR-042 (vendor 2D→3D conversion; in flight on branch `feat/lift-ext`, not yet on `main`) (the same shape in reverse) ·
[ADR-042](ADR-042-vendor-2d3d-conversion-supersedes-default.md) (vendor 2D→3D conversion — the same shape in reverse) ·
integration + plan: [roadmap/stereo-camera-source.md](../roadmap/stereo-camera-source.md)

## Context
Expand All @@ -30,7 +32,11 @@ machines):
correct design must key calibration by the **active** device; and the SDK's new-frame event is
**auto-reset**, so two readers waiting on it steal each other's wake-ups.
- 3D **tablets** carry a front stereo pair (15 mm and 25 mm baselines on two models, parallel)
owned by the vendor's camera/tracking service on Android.
owned by the vendor's camera/tracking service on Android. Measured 2026-09-26 (roadmap §C.0):
the pair is a hidden Camera2 **logical** camera (front 5, back 4) that delivers two raw 1280×720
NV12 streams at 30 fps. Opening the front one **evicts the vendor tracker, which never
reacquires**. The vendor camera SDK (CNSDK) supplies only calibration and in-app face tracking,
not frames. The DisplayXR Browser cannot see the pair at all (browser-pvt#175).

Everything a correct stereo camera needs — the private frame channel, the device's calibration,
keeping the tracker alive — is vendor knowledge. The runtime's plug-in model (ADR-019) exists
Expand Down Expand Up @@ -87,5 +93,7 @@ exactly so that knowledge lives in one vendor DLL and nowhere else.
defensively until the vendor SDK formalises it (roadmap open questions).
- **−** Frame quality is the tracker's: greyscale, 640×480, tracker-chosen rate/exposure.
- **−** Another service thread, and a CPU copy per frame (≈ 27 MB/s at 1280×480 NV12 30 Hz).
- **−** Android depends on the vendor camera SDK sharing frames at all (open question); until then
Android documents a platform difference rather than breaking isolation.
- **−** On Android the source cannot be a passenger on the tracker: the pair and the tracker cannot
both hold Camera2. So the runtime (the vendor plug-in in the runtime APK, ADR-038) must own the
front pair and run the vendor's in-app tracking from the same capture, on a worker thread
(roadmap §C.1). That is more vendor code inside the runtime APK, and more CPU, than on Windows.
41 changes: 41 additions & 0 deletions docs/reference/xrt_plugin_iface.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,47 @@ the runtime logs one warning and runs unpaced. Either way nothing fails — the
difference is accuracy, not correctness. Application-side guidance:
[INV-5.9](../guides/displayxr-app-rules.md).

## A stereo camera: the `stereo_camera_*` slots (ADR-043)

Optional, appended per ADR-020, announced by `XRT_PLUGIN_IFACE_HAS_STEREO_CAMERA`; the runtime
uses them only when `struct_size` covers `stereo_camera_close` **and** all six are non-NULL
(`xrt_plugin_iface_has_stereo_camera()`). They let a plug-in expose the display's own stereo
camera — usually the one its eye tracker looks through — as a runtime source
([`XR_DXR_stereo_camera`](../specs/extensions/XR_DXR_stereo_camera.md)). The service opens each
camera once and fans frames out to every authorised client; the plug-in only produces frames.

```c
uint32_t (*stereo_camera_enumerate)(inst, capacity, struct xrt_plugin_stereo_camera_info *out);
xrt_result_t (*stereo_camera_get_calibration)(inst, index, struct xrt_plugin_stereo_camera_calibration *out);
xrt_result_t (*stereo_camera_open)(inst, index, struct xrt_plugin_stereo_camera **out_cam);
uint32_t (*stereo_camera_wait_frame)(cam, int64_t timeout_ns, struct xrt_plugin_stereo_camera_frame *out);
void (*stereo_camera_release_frame)(cam);
void (*stereo_camera_close)(cam);
```

| Rule | Why |
|---|---|
| **Never take the device from the tracker.** Read the frames the tracker already publishes (shared memory, a vendor camera service); never open, reconfigure or re-time the device. | The whole point: while tracking runs the device is exclusive (Windows `NotReadableError`), and on Android opening the front pair evicts the tracker (roadmap §C) |
| `stereo_camera_open` is a **tracker keep-alive**: while any camera is open, keep the tracker running. | Frames only exist while tracking runs |
| `wait_frame` is called from **one runtime-owned thread per open camera**, may block up to `timeout_ns`, and returns an `enum xrt_plugin_stereo_camera_wait` (`OK` / `TIMEOUT` / `SUSPENDED` / `ERROR`). Planes stay valid until `release_frame`. | The service maps `TIMEOUT` → `WAITING` (then `SUSPENDED` after 1 s without frames), `SUSPENDED` → `SUSPENDED`, `ERROR` → close + retry in 1 s |
| **Never wait on a wake object shared with other readers** of the vendor channel — poll it. | An auto-reset event wakes exactly one waiter: two readers steal each other's frames (ADR-043) |
| Decode your transport (JPEG, YUY2, …) to `GRAY8`, `NV12` or `BGRA8` yourself; always deliver **one side-by-side image**, `2·eye_width × eye_height`, left eye left, unmirrored, even extents. | The runtime owns format conversion, decimation, rectification (R2) and transport; one SBS layout for every vendor |
| Calibration is the **ACTIVE** device's — the unit you weave for — keyed by its own identity (serial). RAW, OpenCV convention: `k[eye] = fx fy cx cy`, `distortion_model` + coefficients, and `R`, `T` with **`x_R = R·x_L + T`** (T in mm; a right camera at +x of the left has `T = (−B, 0, 0)`). The runtime converts it to the client's pose-of-the-right-camera form. Return `XRT_ERROR_FEATURE_NOT_SUPPORTED` and do **not** set `CALIBRATED` if you cannot resolve it. | One test box carried eleven calibration folders; "the first one" is the wrong camera |
| `device_identity` is private: the service hashes it per consumer (`persistentId`) and never exports it. | Fingerprinting (spec §7.5) |
| Nothing throws across the boundary. | C ABI |

Flags (`XRT_PLUGIN_STEREO_CAMERA_*`) mirror the XR bits: `SHARED_WITH_EYE_TRACKING`,
`USER_FACING`, `CALIBRATED`, `NATIVELY_RECTIFIED`, `MONOCHROME`. The reference implementation is
`sim_display`'s fake (`drivers/sim_display/sim_display_stereo_camera.c`, enabled with
`SIM_DISPLAY_FAKE_STEREO_CAMERA=1` in the **service** environment).

**Slot order.** The six slots are the LAST members of the iface, appended (ADR-020) after
everything that was on `main` before them: `… vk_bundle_fn_table_offset` → `create_dp_d3d11_lift`
(ADR-042) → `get_platform_state` (ADR-045) → `stereo_camera_enumerate … stereo_camera_close`.
`tests_stereo_camera` pins that order (lift right after the vk fingerprint, platform-state right
after lift, the camera block right after platform-state and ending the struct), so a reorder or a
slot squeezed in between fails on the host, not on a vendor box.

## Aux surface — separate from the iface

Logging, debug-variable tracking, frame metrics, Perfetto tracing, and unique-ID generation are **not** plumbed through this iface. Plug-ins reach them by linking the runtime DLL's import library (`DisplayXRClient.lib`) and getting `__declspec(dllimport)`'d symbols. See [ADR-019](../adr/ADR-019-vendor-plugin-aux-boundary.md) for the rationale.
Expand Down
4 changes: 3 additions & 1 deletion docs/roadmap/control-panel-performance-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ never claim a mode the runtime is not in, and "Custom" falls out for free.
## Appendix A — census

Every `DXR_*` name read at runtime under `src/xrt`, with its read site, mechanism, default
and tier. **100 distinct names**; the two `DXR_BG2D_*` knobs reach the environment through
and tier. **102 distinct names**; the two `DXR_BG2D_*` knobs reach the environment through
`bg2d_int_knob()` rather than a literal `getenv` at the listed line.

Process column: **App** = the runtime DLL, loaded into the OpenXR app's process ·
Expand Down Expand Up @@ -546,6 +546,7 @@ as `docs/specs/vendor/oem-android-platform-requirements.md` §R6.
| `DXR_DEVICE_REMOVED_EXIT_MS` | `compositor/d3d11_service/comp_d3d11_service.cpp:10279` | `getenv`, `static` cached | 2000 | Svc | 2 | Delay before the service exits on an unrecoverable `DEVICE_REMOVED` |
| `DXR_HEALTH_MS` | `ipc/server/ipc_server_process.c:82` | `DEBUG_GET_ONCE_NUM` | 10000 | Svc | 2 | #951 `[HEALTH]` telemetry cadence; 0 disables |
| `DXR_MAX_CLIENTS` | `ipc/server/ipc_server_process.c:87` | `DEBUG_GET_ONCE_NUM`, consumed `:541-551` | 0 = auto | Svc | 1 | #959 admitted-client cap. Read once at service start |
| `DXR_STEREO_CAMERA` | `ipc/server/ipc_server_stereo_camera.c` (`DEBUG_GET_ONCE_BOOL_OPTION`, read at camera-manager create) | on | Svc | 1 | ADR-043 admin/dev kill switch: `0` = the service enumerates zero stereo cameras (indistinguishable from "none", on purpose). The user-facing twin is the planned R3 "Share the 3D camera with apps" toggle — a privacy control, not a performance lever |
| `DXR_ULTRALEAP_SETTLE_MS` | `drivers/ultraleap/ultraleap_provider.cpp:1023` | `getenv` at provider init | `UL_SETTLE_DEFAULT_MS` | Both | 1 | How long to wait for a definitive Leap "attached" answer before role arbitration |

### Test / dev — never exposed
Expand All @@ -570,6 +571,7 @@ as `docs/specs/vendor/oem-android-platform-requirements.md` §R6.
| `DXR_VIEW_CONFIG_LEGACY` | `state_trackers/oxr/oxr_system.c` (`oxr_system_fill_in`, `DEBUG_GET_ONCE_BOOL_OPTION`) | off | App | #1486 kill switch: restores the pre-#1486 single view configuration — `PRIMARY_STEREO` for any `view_count >= 2`, reporting the max across modes (4 on sim-display), and no `PRIMARY_MULTIVIEW_DXR`. Reintroduces the spec deviation and the CTS red on purpose, for one round of field bisection. Logs one `U_LOG_W` when active. 22 characters, so `debug.xrt.DXR_VIEW_CONFIG_LEGACY` is **stranded on Android** by the `PROP_NAME_MAX` trap above — env-only in practice. See [View-Configuration Model](../reference/view-configuration-model.md) |
| `DXR_MODE_FLOOR` | `state_trackers/oxr/oxr_session.c` (`DEBUG_GET_ONCE_BOOL_OPTION`, one TU; `oxr_api_session.c` reads it through `oxr_session_mode_floor_enabled()`) | on | App | #1499 kill switch. `0` restores the pre-#1499 behaviour for EXTENSION sessions: `xrBeginSession` leaves the display in whatever rendering mode it found, and `xrRequestDisplayRenderingModeDXR` honours a request whose `viewCount` exceeds the session's view configuration — so the compositor's under-submit clamp paints the first tiles and the rest stay at the clear colour, which is exactly the state the issue objected to. It also silences the two #1499 log lines (the begin-time `UNFILLABLE` warning and the observation warning on a display-global mode change), because `0` promises pre-#1499 behaviour and a line the runtime never used to print is part of that promise. **One switch for BOTH halves on purpose**: a floor without the denial lets an app walk straight back into the mode the floor removed it from, and a denial without the floor refuses a mode the session is already in — half of this feature is not a state anyone should be able to bisect into. #1510's LEGACY floor is deliberately NOT affected (a legacy app was sized for its floored mode at `xrGetSystem`, so leaving it in a wider one would make the compromise view scale and the mode disagree). See [View-Configuration Model](../reference/view-configuration-model.md) |
| `DXR_UNDER_SUBMIT` | `state_trackers/oxr/oxr_session_frame_end.c` (`DEBUG_GET_ONCE_NUM_OPTION`, one TU; the value→enum map is the pure `oxr_under_submit_from_setting()` in `oxr_view_config_rule.h`) | `1` | App | ADR-041 staging switch for the fixed-view-count submission contract. `0` = strict: every view configuration must submit exactly the count `xrLocateViews` returned. `1` (default) = strict **plus** the deprecated `PRIMARY_STEREO` 1-view arm (an `XR_DXR_display_info` instance submitting 1 while the active rendering mode is 1-view), which is accepted and logged **once per session** with a line naming the fix — released demos submit one view in 2D mode and need the compat window. `2` = kill switch: the pre-ADR-041 rules, `PRIMARY_MULTIVIEW_DXR` under-submit included. Out-of-range values **clamp to an end** rather than falling back to the default, so a typo cannot silently land on the default. The default flips to `0` in the first runtime release after `displayxr-common` and the five `displayxr-demo-*` demos ship the alias submission — the trigger is that shipment, not a date. Not a user-facing setting: it is a compat bisect knob with no performance dimension. Unreachable from a CTS run at any value (a conformance session never enables `XR_DXR_display_info`, which the compat arm requires). Distinct from `DXR_VIEW_CONFIG_LEGACY`, which restores the pre-#1486 *reporting*. See [ADR-041](../adr/ADR-041-fixed-view-count-with-per-frame-activity.md) and [View-Configuration Model](../reference/view-configuration-model.md) |
| `DXR_STEREO_CAMERA_DEV_ALLOW` | `ipc/server/ipc_server_stereo_camera.c` (`authorise_locked`, `DEBUG_GET_ONCE_BOOL_OPTION`) | off | Svc | ADR-043 **R1-only** stand-in for camera consent: stream start and calibration reads are REFUSED (`XR_ERROR_PERMISSION_INSUFFICIENT`) unless it is set in the SERVICE environment. Deleted when R3 lands the OS + DisplayXR consent path. Never a setting |
| `DXR_IPC_FD` | `auxiliary/util/u_sandbox.c:181`; `ipc/client/ipc_client_connection.c:211, 234` | unset | Both / App | #1056 adopts an embedder-supplied service socket. Not a setting |
| `DXR_IPC_HANDLE` | `ipc/client/ipc_client_connection.c:437, 456` | unset | App | Windows analogue of the above |
| `DXR_WL_ORIGIN_STAMP` | `compositor/vk_native/comp_vk_native_compositor.c` (`vk_debug_origin_stamp`, Linux + Wayland) | off | App | #1748 measurement hook: writes a barcode of each frame's present origin into its top-left 352x8 device px — it **overwrites content**. Read back by the extension's `DISPLAYXR_STAMP_AUDIT=1` (shell environment) in `scripts/linux/move_sync/`. Never for a real session |
Expand Down
Loading
Loading