From 55b44ae72bb053507928cac1ce65eae91862b5f0 Mon Sep 17 00:00:00 2001 From: dfattal Date: Thu, 8 Oct 2026 00:16:34 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(lift):=20XR=5FDXR=5Flift=20v2=20API=20?= =?UTF-8?q?surface=20=E2=80=94=20XrLiftViewControlDXR=20+=20rect-relative?= =?UTF-8?q?=20DP=20viewpoint=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API surface for the lift viewpoint policy (ADR-048, #1859), committed first so the vendor plug-in can build against it: - XR_DXR_lift SPEC_VERSION 2: XrLiftViewControlDXR (281; ipdFactor, parallaxFactor, axisMode X/XY/XYZ, maxOffsetMeters, recenterMode OFF/EASE_TO_CURRENT/EASE_TO_NEUTRAL, hold + time constant), chained on XrLiftOptionsDXR or XrWeaveRectLiftDXR; XrLiftResultViewpointsDXR (282) echoes the viewpoints a result was synthesized for. - xrt_dp_lift_params: appended rect_width_m, rect_height_m, baseline_m, axis_mode, max_offset_m, viewpoint_frame (DISPLAY / RECT), announced by XRT_DP_LIFT_HAS_VIEWPOINT_POLICY (append-only per ADR-020, no ABI bump). - Allocation registry rows for 281/282; lift_convert doc names the frame. Part of #1858. Co-Authored-By: Claude Opus 5.5 --- src/external/openxr_includes/openxr/README.md | 2 +- .../openxr_includes/openxr/XR_DXR_lift.h | 112 +++++++++++++++++- .../include/xrt/xrt_display_processor_d3d11.h | 8 +- src/xrt/include/xrt/xrt_dp_lift.h | 54 +++++++++ 4 files changed, 168 insertions(+), 8 deletions(-) diff --git a/src/external/openxr_includes/openxr/README.md b/src/external/openxr_includes/openxr/README.md index d2decc59e2..fb62aa09d4 100644 --- a/src/external/openxr_includes/openxr/README.md +++ b/src/external/openxr_includes/openxr/README.md @@ -47,7 +47,7 @@ Rules: | 1004999250–259 | `XR_DXR_wayland_surface_binding` | 250 = `XR_TYPE_WAYLAND_SURFACE_BINDING_CREATE_INFO_DXR` (#757), 251 = `XR_TYPE_WAYLAND_SURFACE_GEOMETRY_DXR` (spec v2) — the app-declared surface size + output refresh, because a `wl_surface` has no intrinsic size and the compositor geometry service cannot bootstrap one. **250 was renumbered from 1004999210**, which collided with `XR_TYPE_DISPLAY_DESKTOP_POSITION_DXR` — the 210–219 decade was already `XR_DXR_display_info`'s. Safe to move: SPEC_VERSION 1, no shipped app chains it | | 1004999260–269 | `XR_DXR_depth_budget` | 260 = `XR_TYPE_REAR_DEPTH_BUDGET_DXR`, 261 = `XR_TYPE_CONTENT_BOUNDS_DXR`, 262 = `XR_TYPE_EVENT_DATA_REAR_DEPTH_BUDGET_STATE_CHANGED_DXR`, 263 = `XR_TYPE_CONTENT_MASK_DXR` (ADR-040). Was taken without a registry row — recorded retroactively in #1486 PR 2 | | 1004999270–279 | `XR_DXR_lift` | 270 = `XR_TYPE_LIFT_PROPERTIES_DXR`, 271 = `XR_TYPE_LIFT_STREAM_CREATE_INFO_DXR`, 272 = `XR_TYPE_LIFT_FRAME_SUBMIT_INFO_DXR`, 273 = `XR_TYPE_LIFT_OPTIONS_DXR`, 274 = `XR_TYPE_LIFT_RESULT_DXR`, 275 = `XR_TYPE_WEAVE_SUBMIT_LIFT_RECTS_DXR`, 276 = `XR_TYPE_WEAVE_RECT_LIFT_DXR`, 277 = `XR_TYPE_LIFT_BLOB_DXR` (spec v1, ADR-042). 278 = `XR_OBJECT_TYPE_LIFT_STREAM_DXR` — an `XrObjectType`, not an `XrStructureType`. 279 = `XR_LIFT_NOT_READY_DXR` — a **success**-class `XrResult` (positive) | -| 1004999280–289 | `XR_DXR_lift` (continued) | 280 = `XR_TYPE_LIFT_STREAM_STATS_DXR` (spec v1). A second decade because 270–279 is full | +| 1004999280–289 | `XR_DXR_lift` (continued) | 280 = `XR_TYPE_LIFT_STREAM_STATS_DXR` (spec v1). A second decade because 270–279 is full. Spec v2 (ADR-048): 281 = `XR_TYPE_LIFT_VIEW_CONTROL_DXR`, 282 = `XR_TYPE_LIFT_RESULT_VIEWPOINTS_DXR` | | 1004999290–299 | `XR_DXR_weave` (v14+ additions) | 290 = `XR_TYPE_WEAVE_SUBMIT_OVERLAY_UNCHANGED_DXR` (the v4 overlay atlas is unchanged since the previous submit, so a DP may reuse its lens prefilter of the 2D layer, spec v14, ADR-027 Amendment); 291 = `XR_TYPE_WEAVE_SUBMIT_OVERLAY_FILTER_DXR` (the caller's lens-filter strength for the 2D layer, 0..1, omitted = the DP's default, spec v15). The third weave decade (240–249 is full) | | 1004999300–309 | `XR_DXR_stereo_camera` | 300 = `XR_TYPE_STEREO_CAMERA_PROPERTIES_DXR`, 301 = `XR_TYPE_STEREO_CAMERA_CALIBRATION_DXR`, 302 = `XR_TYPE_STEREO_CAMERA_STREAM_CREATE_INFO_DXR`, 303 = `XR_TYPE_STEREO_CAMERA_FRAME_DXR`, 304 = `XR_TYPE_STEREO_CAMERA_STREAM_INFO_DXR`, 305 = `XR_TYPE_STEREO_CAMERA_STREAM_TRANSPORT_DXR`, 306 = `XR_TYPE_STEREO_CAMERA_STREAM_STATS_DXR`, 307 = `XR_TYPE_EVENT_DATA_STEREO_CAMERA_STATE_CHANGED_DXR`, 308 = `XR_TYPE_EVENT_DATA_STEREO_CAMERAS_CHANGED_DXR` (spec v1, ADR-043). 309 = `XR_OBJECT_TYPE_STEREO_CAMERA_STREAM_DXR` — an `XrObjectType`, not an `XrStructureType`. **Relocated from 290–299** on the rebase onto post-lift `main`: 290 had meanwhile been taken by `XR_TYPE_WEAVE_SUBMIT_OVERLAY_UNCHANGED_DXR` (weave spec v14). Safe to move: spec v1, draft PR, no shipped app chains it | | 1004999310–319 | `XR_DXR_stereo_camera` (continued) | 310 = `XR_STEREO_CAMERA_FRAME_NOT_READY_DXR` — a **success**-class `XrResult` (positive). A second decade because 300–309 is full. Spec v2 (R3, ADR-043 §7): 311 = `XR_TYPE_STEREO_CAMERA_CLIENT_INFO_DXR` (camera-only client declaration on `XrInstanceCreateInfo`); 312 = `XR_ERROR_STEREO_CAMERA_CONSENT_REFUSED_DXR`, 313 = `XR_ERROR_STEREO_CAMERA_DISABLED_DXR`, 314 = `XR_ERROR_STEREO_CAMERA_BUSY_DXR`, 316 = `XR_ERROR_STEREO_CAMERA_STREAM_ENDED_DXR` — **error**-class `XrResult`s (negated in the header); 315 = `XR_TYPE_EVENT_DATA_STEREO_CAMERA_STREAM_ENDED_DXR` | diff --git a/src/external/openxr_includes/openxr/XR_DXR_lift.h b/src/external/openxr_includes/openxr/XR_DXR_lift.h index c661443ab3..f96bf00c08 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_lift.h +++ b/src/external/openxr_includes/openxr/XR_DXR_lift.h @@ -53,7 +53,11 @@ * * Version history: 1 = initial (properties, streams, texture results, the * weave-rect lift chain, the Gaussian-splat blob path, per-stream priority - * scheduling and stream stats). + * scheduling and stream stats). 2 = viewpoint policy (ADR-048): TRACKED + * viewpoints are relative to the lifted rect's / submitting window's centre + * (was: the panel centre), XrLiftViewControlDXR (display-rig ipd / parallax + * factors, axis mode, offset clamp, recentering) and the per-result echo + * XrLiftResultViewpointsDXR. */ #ifndef XR_DXR_LIFT_H #define XR_DXR_LIFT_H 1 @@ -66,7 +70,7 @@ extern "C" { #endif #define XR_DXR_lift 1 -#define XR_DXR_lift_SPEC_VERSION 1 +#define XR_DXR_lift_SPEC_VERSION 2 #define XR_DXR_LIFT_EXTENSION_NAME "XR_DXR_lift" // Reserved 1004999270..279. Allocation registry: README.md in this directory. @@ -85,6 +89,9 @@ extern "C" { #define XR_LIFT_NOT_READY_DXR ((XrResult)1004999279) // Second decade 1004999280..289 (the first is full). #define XR_TYPE_LIFT_STREAM_STATS_DXR ((XrStructureType)1004999280) +// Spec v2 (ADR-048). +#define XR_TYPE_LIFT_VIEW_CONTROL_DXR ((XrStructureType)1004999281) +#define XR_TYPE_LIFT_RESULT_VIEWPOINTS_DXR ((XrStructureType)1004999282) //! Size of XrLiftPropertiesDXR::backend, NUL included. #define XR_LIFT_BACKEND_NAME_MAX_SIZE_DXR 32 @@ -176,6 +183,42 @@ typedef enum XrLiftPriorityDXR { XR_LIFT_PRIORITY_MAX_ENUM_DXR = 0x7FFFFFFF } XrLiftPriorityDXR; +/*! + * Which viewpoint components a TRACKED lift follows (spec v2, ADR-048). The + * others are pinned to the rect's centre line (x, y = 0) or to the reference + * viewing distance (z). X is the default: horizontal look-around only. + */ +typedef enum XrLiftAxisModeDXR { + XR_LIFT_AXIS_MODE_X_DXR = 1, + XR_LIFT_AXIS_MODE_XY_DXR = 2, + XR_LIFT_AXIS_MODE_XYZ_DXR = 3, + XR_LIFT_AXIS_MODE_MAX_ENUM_DXR = 0x7FFFFFFF +} XrLiftAxisModeDXR; + +/*! + * How a TRACKED lift's look-around returns to centre (spec v2, ADR-048). + * + * - OFF: the rendered offset is the viewer's offset from the rect centre, + * always (v1 behaviour, but rect-relative). + * - EASE_TO_CURRENT (default): the reference follows the viewer. Once the + * offset has stayed beyond a small threshold for recenterHoldSeconds, the + * reference slides toward the viewer's current position with time + * constant recenterTimeConstantSeconds — a viewer who settles off-axis + * sees the picture return to its straight-on composition, while a head + * movement still produces immediate look-around. + * - EASE_TO_NEUTRAL: after the same hold, the rendered offset decays toward + * 0 (the straight-on view of the rect centre) with the same time constant. + * + * Frame-rate independent; one filter per stream, reset when the stream is + * created and whenever tracking is lost. + */ +typedef enum XrLiftRecenterModeDXR { + XR_LIFT_RECENTER_MODE_OFF_DXR = 0, + XR_LIFT_RECENTER_MODE_EASE_TO_CURRENT_DXR = 1, + XR_LIFT_RECENTER_MODE_EASE_TO_NEUTRAL_DXR = 2, + XR_LIFT_RECENTER_MODE_MAX_ENUM_DXR = 0x7FFFFFFF +} XrLiftRecenterModeDXR; + typedef enum XrLiftBlobFormatDXR { //! Binary little-endian PLY in the reference 3DGS layout (x y z nx ny nz //! f_dc_0..2 [f_rest_*] opacity scale_0..2 rot_0..3). @@ -232,7 +275,9 @@ typedef struct XrLiftStreamCreateInfoDXR { * calibrated budget). @c viewCount is the number of views an NVIEW stream * produces (2 for SBS; ignored for DEPTH / GAUSSIANS), ≤ maxViews. With * EXPLICIT viewpoints, @c viewpoints holds @c viewCount display-space positions - * (metres); on the weave path viewpoints must be TRACKED. @c focalPx is the + * (metres; the runtime rebases them to the rect centre before the module sees + * them, spec v2); on the weave path viewpoints must be TRACKED. Chain + * XrLiftViewControlDXR on @c next to shape TRACKED viewpoints (spec v2). @c focalPx is the * submitted image's focal length in pixels of @c extent (for a photo, e.g. the * fx of an estimated intrinsics); <= 0 = unknown, the module assumes its * default field of view. Photo → Gaussians modules take it as input; DEPTH / @@ -250,6 +295,63 @@ typedef struct XrLiftOptionsDXR { float focalPx; //!< input focal length in input pixels; <= 0 = unknown (GAUSSIANS) } XrLiftOptionsDXR; +/*! + * Viewpoint policy for TRACKED lifts (spec v2, ADR-048). Chain on + * XrLiftOptionsDXR::next, or directly on XrWeaveRectLiftDXR::next (the + * direct chain wins if both are present). Ignored for EXPLICIT viewpoints and + * for DEPTH / GAUSSIANS streams. Sticky per stream, like XrLiftOptionsDXR: + * the last one sent applies until another is sent. + * + * Omitted entirely, the runtime applies the defaults: ipdFactor 1, + * parallaxFactor 1, axisMode X, maxOffsetMeters 0 (no clamp), recenterMode + * EASE_TO_CURRENT, hold 1 s, time constant 2 s. + * + * Frame: every viewpoint is relative to the CENTRE of the lifted region — the + * lifted weave rect, or for xrSubmitLiftFrameDXR the session's window (the + * panel centre when the session has none) — in display axes (+x right, +y + * up, +z toward the viewer), metres. + * + * Order of operations, per conversion: rebase the eyes to the rect centre; + * scale their separation about their midpoint by @c ipdFactor (display-rig + * step 1a); lerp their midpoint toward the nominal viewer (0, 0, nominal + * distance) by 1 - @c parallaxFactor (step 1b); mask the midpoint's axes not + * in @c axisMode; recenter (see XrLiftRecenterModeDXR); clamp the midpoint's + * x / y offset to @c maxOffsetMeters. + * + * @c recenterHoldSeconds < 0 and @c recenterTimeConstantSeconds <= 0 select + * the defaults (1 s, 2 s). + */ +typedef struct XrLiftViewControlDXR { + XrStructureType type; //!< XR_TYPE_LIFT_VIEW_CONTROL_DXR + const void* XR_MAY_ALIAS next; + float ipdFactor; //!< [0, 1]: eye-separation scale (0 = mono) + float parallaxFactor; //!< [0, 1]: 0 = always the nominal viewer, 1 = follow + XrLiftAxisModeDXR axisMode; + float maxOffsetMeters; //!< x / y offset clamp, metres; <= 0 = none + XrLiftRecenterModeDXR recenterMode; + float recenterHoldSeconds; //!< < 0 = default (1 s) + float recenterTimeConstantSeconds; //!< <= 0 = default (2 s) +} XrLiftViewControlDXR; + +/*! + * The viewpoints a texture result was actually synthesized for (spec v2, + * ADR-048), after the viewpoint policy — so an app can render matching 3D + * content for the same eyes. Chain on XrLiftResultDXR::next; filled on every + * successful acquire. Same frame as XrLiftViewControlDXR (rect-centre + * relative, display axes, metres); @c rectCenter is that origin in display + * space (panel centre = 0) and @c rectSize the region's physical size. + * @c viewpointCountOutput 0 = no viewpoints were known (the module used its + * defaults), or a DEPTH stream. + */ +typedef struct XrLiftResultViewpointsDXR { + XrStructureType type; //!< XR_TYPE_LIFT_RESULT_VIEWPOINTS_DXR + void* XR_MAY_ALIAS next; + uint32_t viewpointCountOutput; //!< 0..XR_LIFT_MAX_VIEWS_DXR + XrVector3f viewpoints[XR_LIFT_MAX_VIEWS_DXR]; + XrVector3f rectCenter; //!< display space, metres + XrExtent2Df rectSize; //!< metres +} XrLiftResultViewpointsDXR; + /*! * One frame into a stream. NON-BLOCKING: the runtime snapshots the input's * @c extent (top-left sub-rect of @c inputTexture) into its own mailbox before @@ -288,7 +390,7 @@ typedef struct XrLiftFrameSubmitInfoDXR { */ typedef struct XrLiftResultDXR { XrStructureType type; //!< XR_TYPE_LIFT_RESULT_DXR - void* XR_MAY_ALIAS next; + void* XR_MAY_ALIAS next; //!< chain XrLiftResultViewpointsDXR here (v2) uint64_t frameId; //!< the xrSubmitLiftFrameDXR frame this converts XrTime sourceTime; //!< that frame's sourceTime, verbatim void* outputTexture; //!< shared HANDLE on first acquire / realloc, else NULL @@ -343,7 +445,7 @@ typedef struct XrLiftBlobDXR { */ typedef struct XrWeaveRectLiftDXR { XrStructureType type; //!< XR_TYPE_WEAVE_RECT_LIFT_DXR - const void* XR_MAY_ALIAS next; //!< chain XrLiftOptionsDXR here + const void* XR_MAY_ALIAS next; //!< chain XrLiftOptionsDXR / XrLiftViewControlDXR here uint32_t rectIndex; //!< index into XrWeaveSubmitRectsDXR::rects XrLiftStreamDXR stream; } XrWeaveRectLiftDXR; diff --git a/src/xrt/include/xrt/xrt_display_processor_d3d11.h b/src/xrt/include/xrt/xrt_display_processor_d3d11.h index 4c14b8a4db..dfd9eb7af8 100644 --- a/src/xrt/include/xrt/xrt_display_processor_d3d11.h +++ b/src/xrt/include/xrt/xrt_display_processor_d3d11.h @@ -635,8 +635,12 @@ struct xrt_display_processor_d3d11 * @param input_resource ID3D11Resource* on the lift device, RGBA8, exactly * @p w x @p h. Valid for the duration of the call. * @param p per-frame parameters (never NULL). - * @param viewpoints_xyz @p viewpoint_floats / 3 display-space eye positions - * (metres) to synthesize for. The runtime ALWAYS + * @param viewpoints_xyz @p viewpoint_floats / 3 eye positions (metres) to + * synthesize for, in the frame @p p->viewpoint_frame + * names: relative to the lifted RECT's centre from a + * runtime with XRT_DP_LIFT_HAS_VIEWPOINT_POLICY (the + * viewpoint policy already applied, ADR-048), else + * relative to the panel centre. The runtime ALWAYS * passes them when it has them: the app's explicit * viewpoints, else the panel DP's predicted tracked * eyes (the pair; a module spreads N views around diff --git a/src/xrt/include/xrt/xrt_dp_lift.h b/src/xrt/include/xrt/xrt_dp_lift.h index f2f25c3ff7..48ee688caf 100644 --- a/src/xrt/include/xrt/xrt_dp_lift.h +++ b/src/xrt/include/xrt/xrt_dp_lift.h @@ -99,6 +99,30 @@ struct xrt_dp_lift_stream_info float input_scale; //!< (0,1]: convert at reduced resolution; 1 = native }; +/*! + * @name Viewpoint policy (ADR-048) — xrt_dp_lift_params appended fields + * + * Announced by XRT_DP_LIFT_HAS_VIEWPOINT_POLICY (append-only, ADR-020: no ABI + * bump). A plug-in built against it reads the fields only when + * xrt_dp_lift_params::struct_size covers them; a runtime that predates them + * passes a shorter struct_size and display-frame viewpoints (panel centre). + * @{ + */ +#define XRT_DP_LIFT_HAS_VIEWPOINT_POLICY 1 + +//! xrt_dp_lift_params::viewpoint_frame: viewpoints are relative to the PANEL +//! centre (the pre-ADR-048 contract; what a short struct_size implies). +#define XRT_DP_LIFT_VIEWPOINT_FRAME_DISPLAY 0u +//! xrt_dp_lift_params::viewpoint_frame: viewpoints are relative to the CENTRE +//! of the lifted rect (or the submitting window), display axes, metres. +#define XRT_DP_LIFT_VIEWPOINT_FRAME_RECT 1u + +//! xrt_dp_lift_params::axis_mode — values match XR_LIFT_AXIS_MODE_*_DXR. +#define XRT_DP_LIFT_AXIS_X 1u //!< horizontal look-around only (default) +#define XRT_DP_LIFT_AXIS_XY 2u //!< + vertical +#define XRT_DP_LIFT_AXIS_XYZ 3u //!< + distance +/*! @} */ + //! Upper bound on explicit viewpoints the runtime passes to lift_convert. #define XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS 8 @@ -131,6 +155,36 @@ struct xrt_dp_lift_params * modules ignore it. */ float focal_px; + + /* + * ── Viewpoint policy (ADR-048, XRT_DP_LIFT_HAS_VIEWPOINT_POLICY) ──────── + * Appended; read only when struct_size covers them. The RUNTIME owns the + * policy — rebasing to the rect, ipd / parallax factors, axis masking, + * clamping and recentering are already applied to the viewpoints + * lift_convert receives. The plug-in only translates them into its + * module's units: normalise by baseline_m (not a fixed eye distance), + * honour every component it is sent (axis_mode says which ones the runtime + * let through — the others are already 0 / at the reference distance), and + * map the offset range onto its own range using max_offset_m and the rect + * size. + */ + + //! Physical width of the lifted region (rect / window) in metres; 0 = unknown. + float rect_width_m; + //! Physical height of the lifted region in metres; 0 = unknown. + float rect_height_m; + //! Distance between the outermost viewpoints of the pair the runtime + //! resolved (eye separation after the ipd factor), metres; 0 = no + //! viewpoints / unknown (use the module's own default). + float baseline_m; + //! XRT_DP_LIFT_AXIS_*: which midpoint components follow the viewer. + uint32_t axis_mode; + //! The clamp the runtime applied to the x / y offset of the viewpoints' + //! midpoint from the rect centre, metres; 0 = unclamped. + float max_offset_m; + //! XRT_DP_LIFT_VIEWPOINT_FRAME_*: origin of the viewpoints passed to + //! lift_convert. Always RECT from a runtime that fills this field. + uint32_t viewpoint_frame; }; /*! From 5b0f37bc82f2458775f18df763e46a6c7524a972 Mon Sep 17 00:00:00 2001 From: dfattal Date: Thu, 8 Oct 2026 00:53:20 -0700 Subject: [PATCH 2/4] feat(lift): window-relative viewpoints + per-stream viewpoint policy on the lift thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R1 + R3 of the lift viewpoint policy (ADR-048, #1859): - R1: every lift viewpoint is rebased to the centre of the lifted region — the weave rect on the weave client's window, or for xrSubmitLiftFrameDXR the session's window (panel centre when there is none) — using the service's existing client window metrics. The DP gets viewpoint_frame RECT plus the region's size in metres. - R3: TRACKED viewpoints run through the stream's policy before lift_convert: ipd factor (rig step 1a), parallax factor toward the nominal viewer (1b), axis mask (X default), per-stream recenter filter (EASE_TO_CURRENT default: 1 s hold, 2 s time constant; EASE_TO_NEUTRAL; OFF), x/y clamp. baseline_m / axis_mode / max_offset_m filled for the DP. Filter is frame-rate independent and resets on stream create and on tracking loss. DXR_LIFT_RECENTER=off forces recentering off (A/B). - XrLiftViewControlDXR parsed from XrLiftOptionsDXR and XrWeaveRectLiftDXR, carried over IPC, sticky per stream; XrLiftResultViewpointsDXR echoes the viewpoints, rect centre and size on every acquire. - Pure-C math in u_lift_viewpoint.{h,c} with Catch2 tests (tests_aux_lift_viewpoint). Default behaviour changes for existing lifted content: rect-centred viewpoints and the ease-to-current recentering. Part of #1858. Co-Authored-By: Claude Opus 5.5 --- src/xrt/auxiliary/util/CMakeLists.txt | 2 + src/xrt/auxiliary/util/u_lift_viewpoint.c | 234 ++++++++++++++++++ src/xrt/auxiliary/util/u_lift_viewpoint.h | 130 ++++++++++ .../d3d11_service/comp_d3d11_service.cpp | 71 +++++- .../d3d11_service/comp_d3d11_service.h | 6 + .../compositor/d3d11_service/d3d11_lift.cpp | 220 ++++++++++++++-- src/xrt/compositor/d3d11_service/d3d11_lift.h | 20 +- src/xrt/include/xrt/xrt_lift.h | 37 +++ src/xrt/ipc/client/ipc_client_compositor.c | 3 +- src/xrt/ipc/client/ipc_client_lift.c | 11 + src/xrt/ipc/client/ipc_client_lift.h | 1 + src/xrt/ipc/server/ipc_server_handler.c | 19 +- src/xrt/ipc/shared/ipc_protocol.h | 10 +- src/xrt/state_trackers/oxr/oxr_lift.c | 60 ++++- src/xrt/state_trackers/oxr/oxr_weave.c | 17 ++ src/xrt/targets/cli/cli_cmd_lift.cpp | 5 +- tests/CMakeLists.txt | 1 + tests/tests_aux_lift_viewpoint.cpp | 228 +++++++++++++++++ 18 files changed, 1034 insertions(+), 41 deletions(-) create mode 100644 src/xrt/auxiliary/util/u_lift_viewpoint.c create mode 100644 src/xrt/auxiliary/util/u_lift_viewpoint.h create mode 100644 tests/tests_aux_lift_viewpoint.cpp diff --git a/src/xrt/auxiliary/util/CMakeLists.txt b/src/xrt/auxiliary/util/CMakeLists.txt index 441a40fa14..0fc069438b 100644 --- a/src/xrt/auxiliary/util/CMakeLists.txt +++ b/src/xrt/auxiliary/util/CMakeLists.txt @@ -28,6 +28,8 @@ add_library( u_cursor_depth.h u_lift_mailbox.c u_lift_mailbox.h + u_lift_viewpoint.c + u_lift_viewpoint.h u_snap_grid.c u_snap_grid.h u_bitwise.h diff --git a/src/xrt/auxiliary/util/u_lift_viewpoint.c b/src/xrt/auxiliary/util/u_lift_viewpoint.c new file mode 100644 index 0000000000..98cb262d28 --- /dev/null +++ b/src/xrt/auxiliary/util/u_lift_viewpoint.c @@ -0,0 +1,234 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief XR_DXR_lift viewpoint policy (ADR-048) - see u_lift_viewpoint.h. + * @ingroup aux_util + */ + +#include "util/u_lift_viewpoint.h" + +#include +#include + +static float +clamp01(float v, float dflt) +{ + if (!isfinite(v)) { + return dflt; + } + return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); +} + +void +u_lift_view_control_default(struct u_lift_view_control *vc) +{ + vc->ipd_factor = 1.0f; + vc->parallax_factor = 1.0f; + vc->axis_mode = U_LIFT_AXIS_X; + vc->max_offset_m = 0.0f; + vc->recenter_mode = U_LIFT_RECENTER_EASE_TO_CURRENT; + vc->hold_s = U_LIFT_RECENTER_HOLD_DEFAULT_S; + vc->tau_s = U_LIFT_RECENTER_TAU_DEFAULT_S; +} + +void +u_lift_view_control_sanitize(struct u_lift_view_control *vc) +{ + vc->ipd_factor = clamp01(vc->ipd_factor, 1.0f); + vc->parallax_factor = clamp01(vc->parallax_factor, 1.0f); + if (vc->axis_mode < U_LIFT_AXIS_X || vc->axis_mode > U_LIFT_AXIS_XYZ) { + vc->axis_mode = U_LIFT_AXIS_X; + } + if (!isfinite(vc->max_offset_m) || vc->max_offset_m < 0.0f) { + vc->max_offset_m = 0.0f; + } + if (vc->recenter_mode > U_LIFT_RECENTER_EASE_TO_NEUTRAL) { + vc->recenter_mode = U_LIFT_RECENTER_EASE_TO_CURRENT; + } + if (!isfinite(vc->hold_s) || vc->hold_s < 0.0f) { + vc->hold_s = U_LIFT_RECENTER_HOLD_DEFAULT_S; + } + if (!isfinite(vc->tau_s) || vc->tau_s <= 0.0f) { + vc->tau_s = U_LIFT_RECENTER_TAU_DEFAULT_S; + } +} + +void +u_lift_recenter_reset(struct u_lift_recenter *r) +{ + memset(r, 0, sizeof(*r)); +} + +void +u_lift_viewpoint_rebase(float *xyz, uint32_t count, const float centre[3]) +{ + for (uint32_t i = 0; i < count; i++) { + xyz[3 * i + 0] -= centre[0]; + xyz[3 * i + 1] -= centre[1]; + xyz[3 * i + 2] -= centre[2]; + } +} + +//! Length of @p d over the axes @p axis_mode lets through. +static float +live_len(const float d[3], uint32_t axis_mode) +{ + float s = d[0] * d[0]; + if (axis_mode >= U_LIFT_AXIS_XY) { + s += d[1] * d[1]; + } + if (axis_mode >= U_LIFT_AXIS_XYZ) { + s += d[2] * d[2]; + } + return sqrtf(s); +} + +/*! + * One step of the recenter filter on midpoint @p c (in / out). Neutral is the + * rect's straight-on viewer (0, 0, nz). Frame-rate independent: the ease + * factor per step is 1 - exp(-dt / tau). + */ +static void +recenter_step(const struct u_lift_view_control *vc, struct u_lift_recenter *r, float nz, uint64_t now_ns, float c[3]) +{ + const float neutral[3] = {0.0f, 0.0f, nz}; + if (!r->valid) { + // The reference starts at the straight-on viewer, so the first frames + // look around exactly as with recentering off. + r->valid = true; + r->last_ns = now_ns; + memcpy(r->ref, neutral, sizeof(r->ref)); + r->gain = 1.0f; + r->beyond_s = 0.0f; + r->easing = false; + } + float dt = now_ns > r->last_ns ? (float)((double)(now_ns - r->last_ns) * 1e-9) : 0.0f; + if (dt > U_LIFT_RECENTER_MAX_DT_S) { + dt = U_LIFT_RECENTER_MAX_DT_S; + } + r->last_ns = now_ns; + const float k = 1.0f - expf(-dt / vc->tau_s); + + if (vc->recenter_mode == U_LIFT_RECENTER_EASE_TO_CURRENT) { + const float d[3] = {c[0] - r->ref[0], c[1] - r->ref[1], c[2] - r->ref[2]}; + const float len = live_len(d, vc->axis_mode); + r->beyond_s = len > U_LIFT_RECENTER_THRESHOLD_M ? r->beyond_s + dt : 0.0f; + if (!r->easing && len > U_LIFT_RECENTER_THRESHOLD_M && r->beyond_s >= vc->hold_s) { + r->easing = true; + } + if (r->easing) { + for (int i = 0; i < 3; i++) { + r->ref[i] += d[i] * k; + } + const float d2[3] = {c[0] - r->ref[0], c[1] - r->ref[1], c[2] - r->ref[2]}; + if (live_len(d2, vc->axis_mode) < 0.25f * U_LIFT_RECENTER_THRESHOLD_M) { + r->easing = false; // settled; the next excursion holds again first + r->beyond_s = 0.0f; + } + } + // Rendered midpoint = the straight-on viewer + the offset from the reference. + for (int i = 0; i < 3; i++) { + c[i] = neutral[i] + (c[i] - r->ref[i]); + } + } else if (vc->recenter_mode == U_LIFT_RECENTER_EASE_TO_NEUTRAL) { + const float d[3] = {c[0] - neutral[0], c[1] - neutral[1], c[2] - neutral[2]}; + if (live_len(d, vc->axis_mode) > U_LIFT_RECENTER_THRESHOLD_M) { + r->beyond_s += dt; + if (r->beyond_s >= vc->hold_s) { + r->gain -= r->gain * k; // the rendered offset decays toward 0 + } + } else { + // Back on the rect's axis: look-around returns, same time constant. + r->beyond_s = 0.0f; + r->gain += (1.0f - r->gain) * k; + } + for (int i = 0; i < 3; i++) { + c[i] = neutral[i] + d[i] * r->gain; + } + } +} + +void +u_lift_viewpoint_apply(const struct u_lift_view_control *vc, + struct u_lift_recenter *r, + float nominal_z, + uint64_t now_ns, + const float *in_xyz, + uint32_t count, + float *out_xyz, + float *out_baseline_m, + float *out_mid) +{ + if (out_baseline_m != NULL) { + *out_baseline_m = 0.0f; + } + if (count == 0 || in_xyz == NULL || out_xyz == NULL) { + return; + } + const uint32_t n = count > U_LIFT_VIEWPOINT_MAX ? U_LIFT_VIEWPOINT_MAX : count; + const float nz = nominal_z > 0.0f ? nominal_z : U_LIFT_NOMINAL_Z_DEFAULT_M; + + float c[3] = {0.0f, 0.0f, 0.0f}; + for (uint32_t i = 0; i < n; i++) { + for (int a = 0; a < 3; a++) { + c[a] += in_xyz[3 * i + a]; + } + } + for (int a = 0; a < 3; a++) { + c[a] /= (float)n; + } + + // 1. ipd factor: offsets from the midpoint (kept aside: out may alias in). + float d[3 * U_LIFT_VIEWPOINT_MAX]; + for (uint32_t i = 0; i < n; i++) { + for (int a = 0; a < 3; a++) { + d[3 * i + a] = (in_xyz[3 * i + a] - c[a]) * vc->ipd_factor; + } + } + + // 2. parallax: lerp the midpoint toward the nominal viewer. + const float p = vc->parallax_factor; + c[0] *= p; + c[1] *= p; + c[2] = nz + p * (c[2] - nz); + + // 3. axis mask. + if (vc->axis_mode < U_LIFT_AXIS_XY) { + c[1] = 0.0f; + } + if (vc->axis_mode < U_LIFT_AXIS_XYZ) { + c[2] = nz; + } + + // 4. recenter. + if (r != NULL && vc->recenter_mode != U_LIFT_RECENTER_OFF) { + recenter_step(vc, r, nz, now_ns, c); + } + + // 5. clamp the lateral offset. + if (vc->max_offset_m > 0.0f) { + for (int a = 0; a < 2; a++) { + if (c[a] > vc->max_offset_m) { + c[a] = vc->max_offset_m; + } else if (c[a] < -vc->max_offset_m) { + c[a] = -vc->max_offset_m; + } + } + } + + for (uint32_t i = 0; i < n; i++) { + for (int a = 0; a < 3; a++) { + out_xyz[3 * i + a] = c[a] + d[3 * i + a]; + } + } + if (out_baseline_m != NULL && n >= 2) { + const float *f = &d[0]; + const float *l = &d[3 * (n - 1)]; + const float dx = l[0] - f[0], dy = l[1] - f[1], dz = l[2] - f[2]; + *out_baseline_m = sqrtf(dx * dx + dy * dy + dz * dz); + } + if (out_mid != NULL) { + memcpy(out_mid, c, sizeof(c)); + } +} diff --git a/src/xrt/auxiliary/util/u_lift_viewpoint.h b/src/xrt/auxiliary/util/u_lift_viewpoint.h new file mode 100644 index 0000000000..eba8e31f88 --- /dev/null +++ b/src/xrt/auxiliary/util/u_lift_viewpoint.h @@ -0,0 +1,130 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief XR_DXR_lift viewpoint policy (ADR-048): the pure math the service's + * lift thread applies to TRACKED eyes before a conversion. + * + * Input: the tracked eye pair (or N eyes), ALREADY rebased to the centre of the + * lifted rect (display axes, metres, +z toward the viewer). Output: the + * viewpoints the vendor module synthesizes for. In order: + * + * 1. ipd factor - scale each eye's offset from the pair's midpoint + * (display rig step 1a, docs/architecture/kooima-projection.md); + * 2. parallax - lerp the midpoint toward the nominal viewer + * (0, 0, nominal_z) by 1 - parallax_factor (step 1b); + * 3. axis mask - X: midpoint y = 0 and z = nominal_z; XY: z = nominal_z; + * 4. recenter - the per-stream ease filter (see u_lift_recenter); + * 5. clamp - midpoint x / y to +- max_offset_m (0 = unclamped). + * + * No locks, no clock, no GPU: the caller passes time in, so the filter is + * unit-testable (tests/tests_aux_lift_viewpoint.cpp). + * + * @ingroup aux_util + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//! Values match XR_LIFT_AXIS_MODE_*_DXR / XRT_DP_LIFT_AXIS_*. +#define U_LIFT_AXIS_X 1u +#define U_LIFT_AXIS_XY 2u +#define U_LIFT_AXIS_XYZ 3u + +//! Values match XR_LIFT_RECENTER_MODE_*_DXR. +#define U_LIFT_RECENTER_OFF 0u +#define U_LIFT_RECENTER_EASE_TO_CURRENT 1u +#define U_LIFT_RECENTER_EASE_TO_NEUTRAL 2u + +#define U_LIFT_RECENTER_HOLD_DEFAULT_S 1.0f +#define U_LIFT_RECENTER_TAU_DEFAULT_S 2.0f +//! Midpoint offset (metres, live axes) below which the viewer counts as "at +//! the reference": the hold timer does not run. +#define U_LIFT_RECENTER_THRESHOLD_M 0.005f +//! Upper bound on the filter's time step: a stalled stream (paused, module +//! busy for seconds) resumes smoothly instead of jumping. +#define U_LIFT_RECENTER_MAX_DT_S 0.25f +//! Nominal viewing distance used when the display reports none. +#define U_LIFT_NOMINAL_Z_DEFAULT_M 0.5f +//! Most eyes u_lift_viewpoint_apply processes (extra input eyes are ignored). +#define U_LIFT_VIEWPOINT_MAX 16 + +//! The policy knobs (XrLiftViewControlDXR, normalised). +struct u_lift_view_control +{ + float ipd_factor; //!< [0, 1] + float parallax_factor; //!< [0, 1] + uint32_t axis_mode; //!< U_LIFT_AXIS_* + float max_offset_m; //!< <= 0 = unclamped + uint32_t recenter_mode; //!< U_LIFT_RECENTER_* + float hold_s; //!< >= 0 + float tau_s; //!< > 0 +}; + +//! Per-stream recenter state. Zero-initialised = reset. +struct u_lift_recenter +{ + bool valid; //!< false = the next sample re-initialises + uint64_t last_ns; //!< time of the previous sample + float ref[3]; //!< EASE_TO_CURRENT: the reference viewer midpoint + float gain; //!< EASE_TO_NEUTRAL: rendered fraction of the offset, [0, 1] + float beyond_s; //!< how long the offset has stayed beyond the threshold + bool easing; //!< EASE_TO_CURRENT: hold elapsed, the reference is moving +}; + +//! The defaults (ADR-048): ipd 1, parallax 1, X, unclamped, EASE_TO_CURRENT, 1 s, 2 s. +void +u_lift_view_control_default(struct u_lift_view_control *vc); + +/*! + * Clamp / default every field of @p vc into its valid range: factors to + * [0, 1], unknown axis mode -> X, unknown recenter mode -> EASE_TO_CURRENT, + * hold < 0 -> default, tau <= 0 -> default, non-finite values -> defaults. + */ +void +u_lift_view_control_sanitize(struct u_lift_view_control *vc); + +//! Forget the filter state (stream create, tracking loss). +void +u_lift_recenter_reset(struct u_lift_recenter *r); + +/*! + * Rebase display-space points to a rect centre: p - centre, in place. + * Translation only: a lifted rect lies flat on the panel. + */ +void +u_lift_viewpoint_rebase(float *xyz, uint32_t count, const float centre[3]); + +/*! + * Run the policy on @p count eyes (@p in_xyz, 3 floats each, rect-relative). + * + * @param vc sanitized policy + * @param r per-stream filter state (updated); NULL = no recentering + * @param nominal_z nominal viewing distance, metres (<= 0 = default 0.5) + * @param now_ns monotonic time of this sample + * @param in_xyz input eyes + * @param count number of eyes (<= U_LIFT_VIEWPOINT_MAX used) + * @param out_xyz count * 3 floats (may alias @p in_xyz) + * @param out_baseline_m optional: distance between the first and last output eye + * @param out_mid optional: the output midpoint (3 floats) + */ +void +u_lift_viewpoint_apply(const struct u_lift_view_control *vc, + struct u_lift_recenter *r, + float nominal_z, + uint64_t now_ns, + const float *in_xyz, + uint32_t count, + float *out_xyz, + float *out_baseline_m, + float *out_mid); + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp index 1571d06afe..8eb68b2da7 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp @@ -24544,6 +24544,47 @@ comp_d3d11_service_compositor_export_transparent_output_fence(struct xrt_composi * */ +/*! + * ADR-048: the physical region a lift frame came from — rect @p rect (in the + * client window's pixels, @p win_w x @p win_h; NULL = the whole window) of + * client @p xc's window, as a centre in display space (metres, +y up) and a + * size. Without window metrics (no window, headless connection) the frame is + * the panel: centre 0, the display's size, valid = false. + */ +static void +lift_rect_frame(struct d3d11_service_system *sys, + struct xrt_compositor *xc, + const struct xrt_rect *rect, + uint32_t win_w, + uint32_t win_h, + struct xrt_lift_rect_frame *out) +{ + memset(out, 0, sizeof(*out)); + out->width_m = sys->base.info.display_width_m; + out->height_m = sys->base.info.display_height_m; + struct xrt_window_metrics wm = {}; + if (xc == nullptr || xc->destroy != compositor_destroy || + !comp_d3d11_service_get_client_app_window_metrics(&sys->base, xc, &wm) || !wm.valid || + wm.window_width_m <= 0.0f || wm.window_height_m <= 0.0f) { + return; + } + out->center[0] = wm.window_center_offset_x_m; + out->center[1] = wm.window_center_offset_y_m; + out->center[2] = wm.window_center_offset_z_m; + out->width_m = wm.window_width_m; + out->height_m = wm.window_height_m; + if (rect != nullptr && win_w > 0 && win_h > 0) { + // Rect fractions of the window (window pixels, y down) -> metres (y up). + const float fx = ((float)rect->offset.w + 0.5f * (float)rect->extent.w) / (float)win_w - 0.5f; + const float fy = ((float)rect->offset.h + 0.5f * (float)rect->extent.h) / (float)win_h - 0.5f; + out->center[0] += fx * wm.window_width_m; + out->center[1] -= fy * wm.window_height_m; + out->width_m = (float)rect->extent.w / (float)win_w * wm.window_width_m; + out->height_m = (float)rect->extent.h / (float)win_h * wm.window_height_m; + } + out->valid = true; +} + static const struct xrt_lift_weave_rect * lift_binding_for_rect(const struct xrt_lift_weave_rect *rects, uint32_t count, uint32_t rect_index) { @@ -24605,9 +24646,12 @@ lift_weave_rect_batch(struct d3d11_service_system *sys, return true; // nothing visible to lift; nothing to weave either } uint64_t frame_id = 0; + struct xrt_lift_rect_frame frame; + lift_rect_frame(sys, &c->base.base, &rect, win_w, win_h, &frame); (void)d3d11_lift_submit_srv_locked(lift, owner, b->stream_id, in_srv, in_w, in_h, (uint32_t)x0, (uint32_t)y0, (uint32_t)(x1 - x0), (uint32_t)(y1 - y0), (int64_t)os_monotonic_get_ns(), - b->has_params ? &b->params : nullptr, &frame_id); + b->has_params ? &b->params : nullptr, + b->has_view_control ? &b->view_control : nullptr, &frame, &frame_id); const float rx = (float)rect.offset.w, ry = (float)rect.offset.h; const float rw = (float)rect.extent.w, rh = (float)rect.extent.h; @@ -24709,9 +24753,12 @@ lift_weave_rects_nview(struct d3d11_service_system *sys, const uint32_t x0 = (uint32_t)fx0, y0 = (uint32_t)fy0; const uint32_t w = (uint32_t)(fx1 - fx0), h = (uint32_t)(fy1 - fy0); uint64_t frame_id = 0; - (void)d3d11_lift_submit_srv_locked(lift, owner, b->stream_id, crop_srv, packed_w, packed_h, x0, y0, w, h, - (int64_t)os_monotonic_get_ns(), b->has_params ? &b->params : nullptr, - &frame_id); + struct xrt_lift_rect_frame frame; + lift_rect_frame(sys, &c->base.base, &r, win_w, win_h, &frame); + (void)d3d11_lift_submit_srv_locked(lift, owner, b->stream_id, crop_srv, packed_w, packed_h, x0, y0, w, + h, (int64_t)os_monotonic_get_ns(), + b->has_params ? &b->params : nullptr, + b->has_view_control ? &b->view_control : nullptr, &frame, &frame_id); struct d3d11_lift_pin pin = {}; if (!d3d11_lift_pin_latest(lift, owner, b->stream_id, &pin) || pin.view_count == 0) { @@ -31896,8 +31943,9 @@ svc_lift(struct xrt_system_compositor *xsysc) if (sys->lift == nullptr) { void *lift_factory = sys->base.info.dp_factory_d3d11_lift; void *fallback = comp_dp_factory_for_window(&sys->base.info, COMP_DP_PRIMARY_MONITOR, COMP_DP_API_D3D11); - sys->lift = d3d11_lift_create(sys->device.get(), sys->context.get(), &sys->immediate_ctx_mutex, - lift_factory, fallback, svc_lift_eyes, (void *)xsysc); + sys->lift = + d3d11_lift_create(sys->device.get(), sys->context.get(), &sys->immediate_ctx_mutex, lift_factory, + fallback, svc_lift_eyes, (void *)xsysc, sys->base.info.nominal_viewer_z_m); U_LOG_W("[lift] lift module created (lift-only factory %s, fallback factory %s)", lift_factory != nullptr ? "present" : "absent", fallback != nullptr ? "present" : "absent"); } @@ -31960,6 +32008,7 @@ comp_d3d11_service_lift_release_owner(struct xrt_system_compositor *xsysc, uint6 extern "C" xrt_result_t comp_d3d11_service_lift_submit(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, uint64_t owner, uint64_t id, xrt_graphics_buffer_handle_t handle, @@ -31970,6 +32019,7 @@ comp_d3d11_service_lift_submit(struct xrt_system_compositor *xsysc, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_floats, + const struct xrt_lift_view_control *view_control, uint64_t *out_frame_id) { struct d3d11_lift *l = svc_lift(xsysc); @@ -31979,8 +32029,11 @@ comp_d3d11_service_lift_submit(struct xrt_system_compositor *xsysc, } return XRT_ERROR_FEATURE_NOT_SUPPORTED; } + // ADR-048: the session's window is the frame (the panel when it has none). + struct xrt_lift_rect_frame frame; + lift_rect_frame(d3d11_service_system_from_xrt(xsysc), xc, nullptr, 0, 0, &frame); return d3d11_lift_submit_handle(l, owner, id, (HANDLE)handle, is_dxgi, w, h, source_time, params, viewpoints, - viewpoint_floats, out_frame_id); + viewpoint_floats, view_control, &frame, out_frame_id); } extern "C" xrt_result_t @@ -32007,6 +32060,10 @@ comp_d3d11_service_lift_acquire(struct xrt_system_compositor *xsysc, out->format = r.format; out->view_count = r.view_count; out->output_realloc = r.output_realloc; + out->viewpoint_count = r.viewpoint_count; + memcpy(out->viewpoints, r.viewpoints, sizeof(out->viewpoints)); + memcpy(out->rect_center, r.rect_center, sizeof(out->rect_center)); + memcpy(out->rect_size, r.rect_size, sizeof(out->rect_size)); return xret; } diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.h b/src/xrt/compositor/d3d11_service/comp_d3d11_service.h index e76e65397d..6767a164d0 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.h +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.h @@ -1270,9 +1270,14 @@ comp_d3d11_service_lift_release_owner(struct xrt_system_compositor *xsysc, uint6 * the stream's latest-wins mailbox and return its frame id (0 = the module is * not up yet; the frame was not taken). XRT_ERROR_WEAVE_REFUSED = transient * (keyed-mutex miss), retry next frame. Takes ownership of an NT @p handle. + * @p xc is the submitting session's compositor (NULL on a headless + * connection): its window is the frame the viewpoints are rebased to + * (ADR-048); without one, the panel centre. @p view_control NULL = the + * stream's last policy. */ xrt_result_t comp_d3d11_service_lift_submit(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, uint64_t owner, uint64_t id, xrt_graphics_buffer_handle_t handle, @@ -1283,6 +1288,7 @@ comp_d3d11_service_lift_submit(struct xrt_system_compositor *xsysc, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_floats, + const struct xrt_lift_view_control *view_control, uint64_t *out_frame_id); //! xrAcquireLiftResultDXR. @p out_ready false = nothing newer (NOT READY). diff --git a/src/xrt/compositor/d3d11_service/d3d11_lift.cpp b/src/xrt/compositor/d3d11_service/d3d11_lift.cpp index 499de27617..798f458988 100644 --- a/src/xrt/compositor/d3d11_service/d3d11_lift.cpp +++ b/src/xrt/compositor/d3d11_service/d3d11_lift.cpp @@ -15,6 +15,7 @@ #include "xrt/xrt_display_processor_d3d11.h" #include "util/u_lift_mailbox.h" +#include "util/u_lift_viewpoint.h" #include "util/u_logging.h" #include "os/os_time.h" @@ -25,6 +26,7 @@ #include #include +#include #include #include #include @@ -240,6 +242,10 @@ struct lift_in_slot //! The part of the submitted rect this frame holds (letterbox crop): //! x0, y0, x1, y1 normalised to the rect. {0,0,1,1} = the whole rect. float active[4] = {0.0f, 0.0f, 1.0f, 1.0f}; + //! ADR-048: the stream's viewpoint policy and the region this frame came + //! from, as of this frame's submit. + xrt_lift_view_control vc = {}; + xrt_lift_rect_frame frame = {}; }; //! One output ring slot: created on the LIFT device, opened on the SERVICE device. @@ -255,6 +261,11 @@ struct lift_out_slot std::shared_ptr> blob; //!< GAUSSIANS uint32_t blob_format = 0; float active[4] = {0.0f, 0.0f, 1.0f, 1.0f}; //!< the input's lift_in_slot::active + //! ADR-048 echo: the viewpoints this result was synthesized for. + uint32_t vp_count = 0; + float vps[3 * XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS] = {}; + float rect_center[3] = {}; + float rect_size[2] = {}; }; //! One letterbox profile readback (service device, producer thread only). @@ -277,6 +288,9 @@ struct lift_stream xrt_dp_lift_params last_params = {}; float last_viewpoints[3 * XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS] = {}; uint32_t last_viewpoint_floats = 0; + //! ADR-048: the sticky viewpoint policy (XrLiftViewControlDXR), set at + //! stream create to the defaults; replaced by every submit that carries one. + xrt_lift_view_control vc = {}; uint32_t priority = U_LIFT_PRIORITY_NORMAL; //!< XrLiftPriorityDXR bool dead = false; //!< destroy requested; the lift thread reaps it @@ -288,6 +302,11 @@ struct lift_stream bool dp_failed = false; ID3D11Texture2D *exact_tex = nullptr; uint32_t exact_w = 0, exact_h = 0; + //! ADR-048 recenter filter (lift thread only). Zero = reset: stream create. + u_lift_recenter recenter = {}; + //! The last policy WARNed about (lift thread only): WARN on change, never per frame. + xrt_lift_view_control logged_vc = {}; + bool logged_vc_valid = false; // Caller-input import cache (service device, producer thread only). HANDLE imp_handle = nullptr; @@ -334,6 +353,11 @@ struct d3d11_lift xrt_dp_factory_d3d11_fn_t fallback_factory = nullptr; bool (*eyes_fn)(void *ud, struct xrt_eye_positions *out) = nullptr; void *eyes_ud = nullptr; + //! The display's nominal viewing distance (ADR-048 straight-on viewer). + float nominal_z = U_LIFT_NOMINAL_Z_DEFAULT_M; + //! DXR_LIFT_RECENTER=off, read once at create: force recentering OFF for + //! every stream (A/B against the ADR-048 default ease-back). + bool recenter_off = false; // Snapshot blit (service device). ID3D11VertexShader *snap_vs = nullptr; @@ -800,6 +824,119 @@ lift_reap(d3d11_lift *l, std::unique_lock &lk) } } +//! The viewpoints one conversion uses (ADR-048), and what the DP is told about them. +struct viewpoint_result +{ + uint32_t floats = 0; //!< 3 x viewpoints in the caller's array + float centre[3] = {}; //!< rect centre, display space + float rect_w = 0.0f; //!< metres; 0 = unknown + float rect_h = 0.0f; //!< metres; 0 = unknown + float baseline = 0.0f; //!< first-to-last viewpoint distance + float mid[3] = {}; //!< midpoint after the policy + uint32_t axis_mode = XRT_DP_LIFT_AXIS_X; + float max_offset = 0.0f; + uint32_t recenter_mode = U_LIFT_RECENTER_OFF; +}; + +/*! + * Resolve the viewpoints of one conversion (lift thread, no lock held). + * + * Every viewpoint is rebased to the lifted region's centre (@p frame; the + * panel centre when it has none). EXPLICIT viewpoints (the caller's, in + * @p vps on entry) get only that rebase. TRACKED ones are resolved HERE, per + * conversion, from the panel's predicted eyes (the lift DP has no tracker + * session of its own) and then run through the stream's viewpoint policy + * (u_lift_viewpoint.h): rig factors, axis mask, recenter filter, clamp. + * DEPTH / GAUSSIANS streams get none. + */ +static viewpoint_result +resolve_viewpoints(d3d11_lift *l, + lift_stream &st, + uint32_t view_count, + const xrt_lift_view_control &in_vc, + const xrt_lift_rect_frame &frame, + float *vps, + uint32_t vp_floats) +{ + viewpoint_result r; + if (frame.valid) { + memcpy(r.centre, frame.center, sizeof(r.centre)); + } + r.rect_w = frame.width_m > 0.0f ? frame.width_m : 0.0f; + r.rect_h = frame.height_m > 0.0f ? frame.height_m : 0.0f; + + if (vp_floats >= 3) { + // EXPLICIT: the app's own viewpoints, only rebased. + const uint32_t n = vp_floats / 3; + u_lift_viewpoint_rebase(vps, n, r.centre); + r.floats = 3 * n; + r.axis_mode = XRT_DP_LIFT_AXIS_XYZ; + for (uint32_t i = 0; i < n; i++) { + for (int a = 0; a < 3; a++) { + r.mid[a] += vps[3 * i + a] / (float)n; + } + } + if (n >= 2) { + const float *f = &vps[0]; + const float *e = &vps[3 * (n - 1)]; + const float dx = e[0] - f[0], dy = e[1] - f[1], dz = e[2] - f[2]; + r.baseline = sqrtf(dx * dx + dy * dy + dz * dz); + } + return r; + } + if (l->eyes_fn == nullptr || st.info.mode == XRT_DP_LIFT_MODE_GAUSSIANS || + st.info.mode == XRT_DP_LIFT_MODE_DEPTH) { + return r; + } + + u_lift_view_control vc = {in_vc.ipd_factor, in_vc.parallax_factor, in_vc.axis_mode, in_vc.max_offset_m, + in_vc.recenter_mode, in_vc.hold_s, in_vc.tau_s}; + u_lift_view_control_sanitize(&vc); + if (l->recenter_off) { + vc.recenter_mode = U_LIFT_RECENTER_OFF; + } + r.axis_mode = vc.axis_mode; + r.max_offset = vc.max_offset_m; + r.recenter_mode = vc.recenter_mode; + // WARN when a stream's effective policy changes (an app sets it once, or + // on a user toggle), never per frame. + const xrt_lift_view_control eff = {vc.ipd_factor, vc.parallax_factor, vc.axis_mode, vc.max_offset_m, + vc.recenter_mode, vc.hold_s, vc.tau_s}; + if (!st.logged_vc_valid || memcmp(&eff, &st.logged_vc, sizeof(eff)) != 0) { + st.logged_vc = eff; + st.logged_vc_valid = true; + U_LOG_W( + "[lift] stream %llu viewpoint policy: ipd=%.2f parallax=%.2f axis=%u max_offset=%.3f m " + "recenter=%u hold=%.2f s tau=%.2f s (rect-relative viewpoints, ADR-048)", + (unsigned long long)st.id, vc.ipd_factor, vc.parallax_factor, vc.axis_mode, vc.max_offset_m, + vc.recenter_mode, vc.hold_s, vc.tau_s); + } + + struct xrt_eye_positions eyes = {}; + if (!l->eyes_fn(l->eyes_ud, &eyes) || !eyes.valid || eyes.count < 2) { + u_lift_recenter_reset(&st.recenter); // no eyes: the filter starts over + return r; + } + if (!eyes.is_tracking) { + // Tracking lost: the vendor's fallback eyes are still used, but the + // recenter filter forgets its reference (ADR-048: reset on loss). + u_lift_recenter_reset(&st.recenter); + } + // The pair (a module spreads N views around it), or N eyes when the panel + // reports exactly the stream's view count. + uint32_t n = eyes.count == view_count ? eyes.count : 2; + n = n > XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS ? XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS : n; + for (uint32_t i = 0; i < n; i++) { + vps[3 * i + 0] = eyes.eyes[i].x; + vps[3 * i + 1] = eyes.eyes[i].y; + vps[3 * i + 2] = eyes.eyes[i].z; + } + u_lift_viewpoint_rebase(vps, n, r.centre); + u_lift_viewpoint_apply(&vc, &st.recenter, l->nominal_z, os_monotonic_get_ns(), vps, n, vps, &r.baseline, r.mid); + r.floats = 3 * n; + return r; +} + /*! * Convert the pending frame of @p st. Called with @p lk HELD; drops it around * every GPU / module call and returns with it held. @@ -814,33 +951,28 @@ lift_convert_one(d3d11_lift *l, lift_stream &st, std::unique_lock &l } st.converting = true; lift_in_slot &in = st.in[in_slot]; - const xrt_dp_lift_params params = in.params; + xrt_dp_lift_params params = in.params; float vps[3 * XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS]; memcpy(vps, in.viewpoints, sizeof(vps)); uint32_t vp_floats = in.viewpoint_floats; float active[4]; memcpy(active, in.active, sizeof(active)); + const xrt_lift_view_control in_vc = in.vc; + const xrt_lift_rect_frame frame = in.frame; const uint32_t w = meta.width, h = meta.height; lk.unlock(); - // TRACKED viewpoints are resolved HERE, per conversion, from the panel's - // predicted eyes, and always handed to the module explicitly: the lift DP - // has no tracker session of its own. The pair (a module spreads N views - // around it); nothing when no eyes are known yet. - if (vp_floats == 0 && l->eyes_fn != nullptr && st.info.mode != XRT_DP_LIFT_MODE_GAUSSIANS && - st.info.mode != XRT_DP_LIFT_MODE_DEPTH) { - struct xrt_eye_positions eyes = {}; - if (l->eyes_fn(l->eyes_ud, &eyes) && eyes.valid && eyes.count >= 2) { - uint32_t n = eyes.count == params.view_count ? eyes.count : 2; - n = n > XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS ? XRT_DP_LIFT_MAX_EXPLICIT_VIEWPOINTS : n; - for (uint32_t i = 0; i < n; i++) { - vps[3 * i + 0] = eyes.eyes[i].x; - vps[3 * i + 1] = eyes.eyes[i].y; - vps[3 * i + 2] = eyes.eyes[i].z; - } - vp_floats = 3 * n; - } - } + // ADR-048: rect-relative viewpoints + the stream's viewpoint policy, and + // what the DP is told about them (appended xrt_dp_lift_params fields). + const viewpoint_result vr = resolve_viewpoints(l, st, params.view_count, in_vc, frame, vps, vp_floats); + vp_floats = vr.floats; + params.struct_size = (uint32_t)sizeof(params); + params.rect_width_m = vr.rect_w; + params.rect_height_m = vr.rect_h; + params.baseline_m = vr.baseline; + params.axis_mode = vr.axis_mode; + params.max_offset_m = vr.max_offset; + params.viewpoint_frame = XRT_DP_LIFT_VIEWPOINT_FRAME_RECT; bool ok = true; // 1. The module's own stream, created lazily (module contract: lift thread only). @@ -945,6 +1077,11 @@ lift_convert_one(d3d11_lift *l, lift_stream &st, std::unique_lock &l l->lift_context->Flush(); o.view_count = views; memcpy(o.active, active, sizeof(o.active)); + o.vp_count = vp_floats / 3; + memcpy(o.vps, vps, sizeof(o.vps)); + memcpy(o.rect_center, vr.centre, sizeof(o.rect_center)); + o.rect_size[0] = vr.rect_w; + o.rect_size[1] = vr.rect_h; } } } else if (ok && is_blob) { @@ -974,6 +1111,14 @@ lift_convert_one(d3d11_lift *l, lift_stream &st, std::unique_lock &l (unsigned long long)st.id, (unsigned long long)st.mb.submitted, (unsigned long long)st.mb.converted, (unsigned long long)st.mb.dropped, (unsigned long long)st.mb.failed, st.mb.lat_last_ns / 1e6, st.mb.lat_ema_ns / 1e6, st.mb.lat_min_ns / 1e6, st.mb.lat_max_ns / 1e6); + if (vp_floats >= 3) { + U_LOG_I( + "[lift] stream %llu viewpoints: rect centre=(%.3f, %.3f) m size=%.3fx%.3f m mid=(%.3f, " + "%.3f, " + "%.3f) baseline=%.4f m axis=%u recenter=%u", + (unsigned long long)st.id, vr.centre[0], vr.centre[1], vr.rect_w, vr.rect_h, vr.mid[0], + vr.mid[1], vr.mid[2], vr.baseline, vr.axis_mode, vr.recenter_mode); + } } l->cv.notify_all(); } @@ -1123,7 +1268,8 @@ d3d11_lift_create(ID3D11Device *svc_device, void *lift_factory, void *fallback_factory, bool (*eyes_fn)(void *ud, struct xrt_eye_positions *out), - void *eyes_ud) + void *eyes_ud, + float nominal_z) { if (svc_device == nullptr || svc_context == nullptr || svc_ctx_mutex == nullptr) { return nullptr; @@ -1138,6 +1284,14 @@ d3d11_lift_create(ID3D11Device *svc_device, l->fallback_factory = (xrt_dp_factory_d3d11_fn_t)fallback_factory; l->eyes_fn = eyes_fn; l->eyes_ud = eyes_ud; + l->nominal_z = nominal_z > 0.0f ? nominal_z : U_LIFT_NOMINAL_Z_DEFAULT_M; + { + const char *rc = getenv("DXR_LIFT_RECENTER"); + l->recenter_off = rc != nullptr && (_stricmp(rc, "off") == 0 || strcmp(rc, "0") == 0); + if (l->recenter_off) { + U_LOG_W("[lift] DXR_LIFT_RECENTER=%s: viewpoint recentering forced OFF for every stream", rc); + } + } (void)svc_device->QueryInterface(__uuidof(ID3D11Device1), (void **)&l->svc_device1); (void)svc_context->QueryInterface(__uuidof(ID3D11DeviceContext4), (void **)&l->svc_context4); @@ -1345,6 +1499,12 @@ d3d11_lift_stream_create(struct d3d11_lift *l, st->last_params.strength = 1.0f; st->last_params.inpaint = 1; st->last_params.view_count = m == XRT_DP_LIFT_MODE_NVIEW ? 4 : 2; + { + u_lift_view_control d; + u_lift_view_control_default(&d); + st->vc = {d.ipd_factor, d.parallax_factor, d.axis_mode, d.max_offset_m, + d.recenter_mode, d.hold_s, d.tau_s}; + } *out_id = st->id; U_LOG_W("[lift] stream %llu created (mode=%u hint=%u scale=%.2f, owner=%llu)", (unsigned long long)st->id, m, info->content_hint, st->info.input_scale, (unsigned long long)owner); @@ -1648,6 +1808,8 @@ submit_locked(d3d11_lift *l, const xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_floats, + const xrt_lift_view_control *view_control, + const xrt_lift_rect_frame *frame, uint32_t max_input_edge, bool letterbox, uint64_t *out_frame_id) @@ -1715,8 +1877,13 @@ submit_locked(d3d11_lift *l, memcpy(st->last_viewpoints, viewpoints, st->last_viewpoint_floats * sizeof(float)); } } + if (view_control != nullptr) { + st->vc = *view_control; // sticky (sanitized on the lift thread) + } lift_in_slot &in = st->in[slot]; in.params = st->last_params; + in.vc = st->vc; + in.frame = frame != nullptr ? *frame : xrt_lift_rect_frame{}; if (capped && in.params.focal_px > 0.0f) { in.params.focal_px *= (float)dw / (float)cw; // focal is in INPUT pixels (a crop keeps the scale) } @@ -1768,6 +1935,8 @@ d3d11_lift_submit_srv_locked(struct d3d11_lift *l, uint32_t h, int64_t source_time, const struct xrt_dp_lift_params *params, + const struct xrt_lift_view_control *view_control, + const struct xrt_lift_rect_frame *frame, uint64_t *out_frame_id) { if (l == nullptr || !l->snap_ok || src == nullptr || out_frame_id == nullptr) { @@ -1775,7 +1944,7 @@ d3d11_lift_submit_srv_locked(struct d3d11_lift *l, } // Weave-rect snapshot: the service's size cap applies. return submit_locked(l, owner, id, src, src_tw, src_th, x, y, w, h, source_time, params, nullptr, 0, - l->max_input_edge, l->letterbox, out_frame_id); + view_control, frame, l->max_input_edge, l->letterbox, out_frame_id); } xrt_result_t @@ -1790,6 +1959,8 @@ d3d11_lift_submit_handle(struct d3d11_lift *l, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_floats, + const struct xrt_lift_view_control *view_control, + const struct xrt_lift_rect_frame *frame, uint64_t *out_frame_id) { if (out_frame_id != nullptr) { @@ -1884,7 +2055,8 @@ d3d11_lift_submit_handle(struct d3d11_lift *l, // An app's explicit frame: its size and content are the app's choice — // never capped, never letterbox-cropped. xret = submit_locked(l, owner, id, st->imp_srv, st->imp_w, st->imp_h, 0, 0, w, h, source_time, params, - viewpoints, viewpoint_floats, /*max_input_edge*/ 0, /*letterbox*/ false, out_frame_id); + viewpoints, viewpoint_floats, view_control, frame, /*max_input_edge*/ 0, + /*letterbox*/ false, out_frame_id); } if (acquired) { st->imp_km->ReleaseSync(0); @@ -2070,6 +2242,10 @@ d3d11_lift_acquire_result( out->format = o->format; out->view_count = o->view_count; out->output_realloc = realloc; + out->viewpoint_count = o->vp_count > XRT_LIFT_MAX_VIEWS ? XRT_LIFT_MAX_VIEWS : o->vp_count; + memcpy(out->viewpoints, o->vps, sizeof(out->viewpoints)); + memcpy(out->rect_center, o->rect_center, sizeof(out->rect_center)); + memcpy(out->rect_size, o->rect_size, sizeof(out->rect_size)); *out_ready = true; unpin(); return XRT_SUCCESS; diff --git a/src/xrt/compositor/d3d11_service/d3d11_lift.h b/src/xrt/compositor/d3d11_service/d3d11_lift.h index f0669050d6..5eb13497cd 100644 --- a/src/xrt/compositor/d3d11_service/d3d11_lift.h +++ b/src/xrt/compositor/d3d11_service/d3d11_lift.h @@ -67,6 +67,11 @@ struct d3d11_lift_result_info uint32_t format; //!< DXGI_FORMAT uint32_t view_count; bool output_realloc; //!< the export texture changed: the caller must re-import it + //! ADR-048 echo: viewpoints the result was synthesized for (rect-relative). + uint32_t viewpoint_count; + float viewpoints[3 * XRT_LIFT_MAX_VIEWS]; + float rect_center[3]; + float rect_size[2]; }; //! One acquired blob result (xrAcquireLiftBlobDXR). @@ -115,6 +120,9 @@ struct d3d11_lift_pin * to every TRACKED conversion. Called from the lift * thread with no lift lock held. May be NULL. * @param eyes_ud user data for @p eyes_fn. + * @param nominal_z the display's nominal viewing distance, metres + * (<= 0 = 0.5 m) — the viewpoint policy's straight-on + * viewer (ADR-048). */ struct d3d11_lift * d3d11_lift_create(ID3D11Device *svc_device, @@ -123,7 +131,8 @@ d3d11_lift_create(ID3D11Device *svc_device, void *lift_factory, void *fallback_factory, bool (*eyes_fn)(void *ud, struct xrt_eye_positions *out), - void *eyes_ud); + void *eyes_ud, + float nominal_z); //! Stop the lift thread, destroy every stream and the lift DP/device. void @@ -157,6 +166,9 @@ d3d11_lift_stream_mode(struct d3d11_lift *lift, uint64_t owner, uint64_t id); * Takes the service context mutex internally — call WITHOUT it held. * The handle is the service's to close (a duplicated NT handle) unless * @p is_dxgi. @p params NULL = the stream's last parameters. + * @p view_control NULL = the stream's last policy (initially the ADR-048 + * defaults). @p frame = the submitting window (centre + size) the viewpoints + * are rebased to; NULL / invalid = the panel centre. */ xrt_result_t d3d11_lift_submit_handle(struct d3d11_lift *lift, @@ -170,6 +182,8 @@ d3d11_lift_submit_handle(struct d3d11_lift *lift, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_floats, + const struct xrt_lift_view_control *view_control, + const struct xrt_lift_rect_frame *frame, uint64_t *out_frame_id); /*! @@ -179,6 +193,8 @@ d3d11_lift_submit_handle(struct d3d11_lift *lift, * @p src. The snapshot's long edge is capped at DXR_LIFT_MAX_INPUT_EDGE * (default 1920, 0 = off; u_lift_cap_dims) — the result is stretched back into * the rect, so consumers must sample it, never assume result size == rect size. + * @p view_control / @p frame as for d3d11_lift_submit_handle (@p frame = the + * lifted rect's physical centre + size). */ xrt_result_t d3d11_lift_submit_srv_locked(struct d3d11_lift *lift, @@ -193,6 +209,8 @@ d3d11_lift_submit_srv_locked(struct d3d11_lift *lift, uint32_t h, int64_t source_time, const struct xrt_dp_lift_params *params, + const struct xrt_lift_view_control *view_control, + const struct xrt_lift_rect_frame *frame, uint64_t *out_frame_id); //! Pin the stream's latest result for a service-device read (the weave). diff --git a/src/xrt/include/xrt/xrt_lift.h b/src/xrt/include/xrt/xrt_lift.h index ea0ad5af81..2a078936f6 100644 --- a/src/xrt/include/xrt/xrt_lift.h +++ b/src/xrt/include/xrt/xrt_lift.h @@ -27,6 +27,35 @@ extern "C" { //! Max explicit viewpoints per submit (mirrors XR_LIFT_MAX_VIEWS_DXR). #define XRT_LIFT_MAX_VIEWS 8 +/*! + * The viewpoint policy of a stream (XrLiftViewControlDXR, ADR-048). Values + * are the XR_DXR_lift enum values (axis 1..3, recenter 0..2); the service + * sanitizes them (u_lift_view_control_sanitize). + */ +struct xrt_lift_view_control +{ + float ipd_factor; + float parallax_factor; + uint32_t axis_mode; + float max_offset_m; + uint32_t recenter_mode; + float hold_s; //!< < 0 = default + float tau_s; //!< <= 0 = default +}; + +/*! + * The physical region a lift frame came from (ADR-048): its centre in display + * space (metres, panel centre = 0, +y up) and its size. TRACKED and EXPLICIT + * viewpoints are rebased to @c center before the module sees them. + */ +struct xrt_lift_rect_frame +{ + float center[3]; + float width_m; + float height_m; + bool valid; //!< false = the panel centre, size unknown +}; + //! One acquired texture result. struct xrt_lift_result { @@ -39,6 +68,12 @@ struct xrt_lift_result uint32_t format; //!< DXGI_FORMAT uint32_t view_count; bool output_realloc; //!< export texture changed: re-export it to the caller + //! ADR-048 echo: the viewpoints this result was synthesized for, relative + //! to @c rect_center (display axes, metres). 0 = none known. + uint32_t viewpoint_count; + float viewpoints[3 * XRT_LIFT_MAX_VIEWS]; + float rect_center[3]; //!< display space, metres + float rect_size[2]; //!< metres }; //! One acquired blob result. @@ -57,6 +92,8 @@ struct xrt_lift_weave_rect uint32_t rect_index; bool has_params; struct xrt_dp_lift_params params; + bool has_view_control; //!< XrLiftViewControlDXR chained (ADR-048) + struct xrt_lift_view_control view_control; }; //! One stream's counters + effective rate (XrLiftStreamStatsDXR). diff --git a/src/xrt/ipc/client/ipc_client_compositor.c b/src/xrt/ipc/client/ipc_client_compositor.c index 9d77981c01..5148320aa9 100644 --- a/src/xrt/ipc/client/ipc_client_compositor.c +++ b/src/xrt/ipc/client/ipc_client_compositor.c @@ -3230,10 +3230,11 @@ comp_ipc_client_compositor_lift_submit(struct xrt_compositor *xc, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_count, + const struct xrt_lift_view_control *view_control, uint64_t *out_frame_id) { return ipc_client_lift_submit(lift_conn(xc), stream_id, handle, is_dxgi, width, height, source_time, params, - viewpoints, viewpoint_count, out_frame_id); + viewpoints, viewpoint_count, view_control, out_frame_id); } xrt_result_t diff --git a/src/xrt/ipc/client/ipc_client_lift.c b/src/xrt/ipc/client/ipc_client_lift.c index 801904d4fa..48ac8e2629 100644 --- a/src/xrt/ipc/client/ipc_client_lift.c +++ b/src/xrt/ipc/client/ipc_client_lift.c @@ -94,6 +94,7 @@ ipc_client_lift_submit(struct ipc_connection *ipc_c, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_count, + const struct xrt_lift_view_control *view_control, uint64_t *out_frame_id) { if (ipc_c == NULL || out_frame_id == NULL) { @@ -109,6 +110,10 @@ ipc_client_lift_submit(struct ipc_connection *ipc_c, if (params != NULL) { args.has_params = 1; params_to_ipc(params, viewpoints, viewpoint_count, &args.params); + if (view_control != NULL) { + args.params.has_view_control = 1; + args.params.view_control = *view_control; + } } xrt_graphics_buffer_handle_t handles[1] = {handle}; #if defined(XRT_GRAPHICS_BUFFER_HANDLE_IS_WIN32_HANDLE) @@ -150,6 +155,10 @@ ipc_client_lift_acquire(struct ipc_connection *ipc_c, out_result->format = r.format; out_result->view_count = r.view_count; out_result->output_realloc = r.output_realloc != 0; + out_result->viewpoint_count = r.viewpoint_count > XRT_LIFT_MAX_VIEWS ? XRT_LIFT_MAX_VIEWS : r.viewpoint_count; + memcpy(out_result->viewpoints, r.viewpoints, sizeof(out_result->viewpoints)); + memcpy(out_result->rect_center, r.rect_center, sizeof(out_result->rect_center)); + memcpy(out_result->rect_size, r.rect_size, sizeof(out_result->rect_size)); return XRT_SUCCESS; } @@ -264,6 +273,8 @@ ipc_client_lift_weave_rects(struct ipc_connection *ipc_c, uint32_t count, const args.rects[i].inpaint = rects[i].params.inpaint; args.rects[i].view_count = rects[i].params.view_count; args.rects[i].focal_px = rects[i].params.focal_px; + args.rects[i].has_view_control = rects[i].has_view_control ? 1u : 0u; + args.rects[i].view_control = rects[i].view_control; } return ipc_call_lift_weave_rects(ipc_c, &args); } diff --git a/src/xrt/ipc/client/ipc_client_lift.h b/src/xrt/ipc/client/ipc_client_lift.h index 8fe026d781..e2aafceb3c 100644 --- a/src/xrt/ipc/client/ipc_client_lift.h +++ b/src/xrt/ipc/client/ipc_client_lift.h @@ -53,6 +53,7 @@ ipc_client_lift_submit(struct ipc_connection *ipc_c, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_count, + const struct xrt_lift_view_control *view_control, uint64_t *out_frame_id); //! @p out_ready false = nothing newer than the last acquire (NOT READY). diff --git a/src/xrt/ipc/server/ipc_server_handler.c b/src/xrt/ipc/server/ipc_server_handler.c index f18494d856..e8e176946f 100644 --- a/src/xrt/ipc/server/ipc_server_handler.c +++ b/src/xrt/ipc/server/ipc_server_handler.c @@ -8445,11 +8445,14 @@ ipc_handle_lift_submit_frame(volatile struct ipc_client_state *ics, lift_params_from_ipc(&args->params, ¶ms); uint32_t vp_count = args->params.viewpoint_count > IPC_LIFT_MAX_VIEWS ? IPC_LIFT_MAX_VIEWS : args->params.viewpoint_count; - // The handle is the service's from here (closed or cached inside). - return comp_d3d11_service_lift_submit(xsysc, lift_owner(ics), args->stream_id, in_handle, in_is_dxgi, - args->width, args->height, args->source_time, - args->has_params ? ¶ms : NULL, args->params.viewpoints, - args->has_params ? 3 * vp_count : 0, out_frame_id); + const bool has_vc = args->has_params && args->params.has_view_control != 0; + // The handle is the service's from here (closed or cached inside). The + // session's compositor (NULL on a headless connection) names the window the + // viewpoints are rebased to (ADR-048). + return comp_d3d11_service_lift_submit( + xsysc, ics->xc, lift_owner(ics), args->stream_id, in_handle, in_is_dxgi, args->width, args->height, + args->source_time, args->has_params ? ¶ms : NULL, args->params.viewpoints, + args->has_params ? 3 * vp_count : 0, has_vc ? &args->params.view_control : NULL, out_frame_id); #else (void)args; weave_submit_release_handles(handles, handle_count); @@ -8486,6 +8489,10 @@ ipc_handle_lift_acquire_result(volatile struct ipc_client_state *ics, out_lift_result->format = r.format; out_lift_result->view_count = r.view_count; out_lift_result->output_realloc = r.output_realloc ? 1u : 0u; + out_lift_result->viewpoint_count = r.viewpoint_count; + memcpy(out_lift_result->viewpoints, r.viewpoints, sizeof(out_lift_result->viewpoints)); + memcpy(out_lift_result->rect_center, r.rect_center, sizeof(out_lift_result->rect_center)); + memcpy(out_lift_result->rect_size, r.rect_size, sizeof(out_lift_result->rect_size)); return xret; #else (void)stream_id; @@ -8654,6 +8661,8 @@ ipc_handle_lift_weave_rects(volatile struct ipc_client_state *ics, const struct rects[i].params.inpaint = w->inpaint; rects[i].params.view_count = w->view_count > IPC_LIFT_MAX_VIEWS ? IPC_LIFT_MAX_VIEWS : w->view_count; rects[i].params.focal_px = w->focal_px; + rects[i].has_view_control = w->has_view_control != 0; + rects[i].view_control = w->view_control; } if (!comp_d3d11_service_lift_set_weave_rects(ics->xc, lift_owner(ics), args->count, rects)) { // A rect naming a stream this connection does not own (or a non-SBS/NVIEW diff --git a/src/xrt/ipc/shared/ipc_protocol.h b/src/xrt/ipc/shared/ipc_protocol.h index 68e69483fa..502143ac01 100644 --- a/src/xrt/ipc/shared/ipc_protocol.h +++ b/src/xrt/ipc/shared/ipc_protocol.h @@ -1065,6 +1065,8 @@ struct ipc_lift_params uint32_t viewpoint_count; //!< 0 = tracked eyes; else explicit, <= IPC_LIFT_MAX_VIEWS float focal_px; //!< input focal length, input pixels; <= 0 = unknown float viewpoints[3 * IPC_LIFT_MAX_VIEWS]; //!< xyz, display space, metres + uint32_t has_view_control; //!< XrLiftViewControlDXR chained (ADR-048) + struct xrt_lift_view_control view_control; }; /*! @@ -1103,7 +1105,10 @@ struct ipc_lift_result uint32_t format; //!< DXGI_FORMAT uint32_t view_count; uint32_t output_realloc; //!< 1 = the export texture changed since the last acquire - uint32_t reserved; + uint32_t viewpoint_count; //!< ADR-048 echo; 0 = none + float viewpoints[3 * IPC_LIFT_MAX_VIEWS]; + float rect_center[3]; + float rect_size[2]; }; /*! @@ -1123,7 +1128,8 @@ struct ipc_lift_weave_rect uint32_t inpaint; uint32_t view_count; float focal_px; - uint32_t reserved; + uint32_t has_view_control; //!< XrLiftViewControlDXR chained (ADR-048) + struct xrt_lift_view_control view_control; }; /*! diff --git a/src/xrt/state_trackers/oxr/oxr_lift.c b/src/xrt/state_trackers/oxr/oxr_lift.c index be32b00f4f..727e3a98e7 100644 --- a/src/xrt/state_trackers/oxr/oxr_lift.c +++ b/src/xrt/state_trackers/oxr/oxr_lift.c @@ -60,6 +60,7 @@ comp_ipc_client_compositor_lift_submit(struct xrt_compositor *xc, const struct xrt_dp_lift_params *params, const float *viewpoints, uint32_t viewpoint_count, + const struct xrt_lift_view_control *view_control, uint64_t *out_frame_id); xrt_result_t comp_ipc_client_compositor_lift_acquire(struct xrt_compositor *xc, @@ -184,6 +185,34 @@ lift_validate_options(struct oxr_logger *log, const XrLiftOptionsDXR *o, bool we return XR_SUCCESS; } +/*! + * XrLiftViewControlDXR (spec v2, ADR-048) -> the runtime's view control. + * Validates the enums; the service clamps the floats. Shared with + * oxr_weave.c (the weave-rect chain). + */ +XrResult +oxr_lift_view_control_from_xr(struct oxr_logger *log, const XrLiftViewControlDXR *v, struct xrt_lift_view_control *out) +{ + if (v->axisMode < XR_LIFT_AXIS_MODE_X_DXR || v->axisMode > XR_LIFT_AXIS_MODE_XYZ_DXR) { + return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, "XrLiftViewControlDXR::axisMode (%d) invalid", + (int)v->axisMode); + } + if (v->recenterMode < XR_LIFT_RECENTER_MODE_OFF_DXR || + v->recenterMode > XR_LIFT_RECENTER_MODE_EASE_TO_NEUTRAL_DXR) { + return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, "XrLiftViewControlDXR::recenterMode (%d) invalid", + (int)v->recenterMode); + } + memset(out, 0, sizeof(*out)); + out->ipd_factor = v->ipdFactor; + out->parallax_factor = v->parallaxFactor; + out->axis_mode = (uint32_t)v->axisMode; + out->max_offset_m = v->maxOffsetMeters; + out->recenter_mode = (uint32_t)v->recenterMode; + out->hold_s = v->recenterHoldSeconds; + out->tau_s = v->recenterTimeConstantSeconds; + return XR_SUCCESS; +} + static XrResult oxr_lift_stream_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb) { @@ -336,6 +365,8 @@ oxr_xrSubmitLiftFrameDXR(XrLiftStreamDXR stream, const XrLiftFrameSubmitInfoDXR const struct xrt_dp_lift_params *pp = NULL; float vps[3 * XR_LIFT_MAX_VIEWS_DXR]; uint32_t vp_count = 0; + struct xrt_lift_view_control vc; + const struct xrt_lift_view_control *vcp = NULL; const XrLiftOptionsDXR *opt = OXR_GET_INPUT_FROM_CHAIN(submitInfo, XR_TYPE_LIFT_OPTIONS_DXR, XrLiftOptionsDXR); if (opt != NULL) { XrResult vr = lift_validate_options(&log, opt, /*weave_path*/ false); @@ -344,6 +375,15 @@ oxr_xrSubmitLiftFrameDXR(XrLiftStreamDXR stream, const XrLiftFrameSubmitInfoDXR } lift_params_from_xr(opt, st->mode, ¶ms); pp = ¶ms; + const XrLiftViewControlDXR *v = + OXR_GET_INPUT_FROM_CHAIN(opt, XR_TYPE_LIFT_VIEW_CONTROL_DXR, XrLiftViewControlDXR); + if (v != NULL) { + vr = oxr_lift_view_control_from_xr(&log, v, &vc); + if (vr != XR_SUCCESS) { + return vr; + } + vcp = &vc; + } if (opt->viewpointSource == XR_LIFT_VIEWPOINT_SOURCE_EXPLICIT_DXR) { vp_count = opt->viewCount; for (uint32_t i = 0; i < vp_count; i++) { @@ -357,7 +397,7 @@ oxr_xrSubmitLiftFrameDXR(XrLiftStreamDXR stream, const XrLiftFrameSubmitInfoDXR xrt_result_t xret = comp_ipc_client_compositor_lift_submit( &sess->xcn->base, st->id, (xrt_graphics_buffer_handle_t)(intptr_t)submitInfo->inputTexture, submitInfo->inputIsDxgi == XR_TRUE, (uint32_t)submitInfo->extent.width, (uint32_t)submitInfo->extent.height, - (int64_t)submitInfo->sourceTime, pp, vp_count > 0 ? vps : NULL, vp_count, frameId); + (int64_t)submitInfo->sourceTime, pp, vp_count > 0 ? vps : NULL, vp_count, vcp, frameId); return lift_xret(&log, sess, xret, "xrSubmitLiftFrameDXR"); } @@ -401,6 +441,24 @@ oxr_xrAcquireLiftResultDXR(XrLiftStreamDXR stream, XrLiftResultDXR *result) result->viewCount = r.view_count; result->latency = (XrDuration)r.latency_ns; + // Spec v2 (ADR-048): echo the viewpoints this result was synthesized for. + XrLiftResultViewpointsDXR *echo = + OXR_GET_OUTPUT_FROM_CHAIN(result, XR_TYPE_LIFT_RESULT_VIEWPOINTS_DXR, XrLiftResultViewpointsDXR); + if (echo != NULL) { + uint32_t n = r.viewpoint_count > XR_LIFT_MAX_VIEWS_DXR ? XR_LIFT_MAX_VIEWS_DXR : r.viewpoint_count; + echo->viewpointCountOutput = n; + for (uint32_t i = 0; i < XR_LIFT_MAX_VIEWS_DXR; i++) { + echo->viewpoints[i].x = i < n ? r.viewpoints[3 * i + 0] : 0.0f; + echo->viewpoints[i].y = i < n ? r.viewpoints[3 * i + 1] : 0.0f; + echo->viewpoints[i].z = i < n ? r.viewpoints[3 * i + 2] : 0.0f; + } + echo->rectCenter.x = r.rect_center[0]; + echo->rectCenter.y = r.rect_center[1]; + echo->rectCenter.z = r.rect_center[2]; + echo->rectSize.width = r.rect_size[0]; + echo->rectSize.height = r.rect_size[1]; + } + // Handles: first acquire and every reallocation (the weave output pattern). if (r.output_realloc || !st->exported) { bool have_tex = false, have_fence = false; diff --git a/src/xrt/state_trackers/oxr/oxr_weave.c b/src/xrt/state_trackers/oxr/oxr_weave.c index 8a78f9fa0a..b7fe0710cc 100644 --- a/src/xrt/state_trackers/oxr/oxr_weave.c +++ b/src/xrt/state_trackers/oxr/oxr_weave.c @@ -108,6 +108,9 @@ xrt_result_t comp_ipc_client_compositor_lift_weave_rects(struct xrt_compositor *xc, uint32_t count, const struct xrt_lift_weave_rect *rects); +//! XrLiftViewControlDXR -> xrt (oxr_lift.c). +XrResult +oxr_lift_view_control_from_xr(struct oxr_logger *log, const XrLiftViewControlDXR *v, struct xrt_lift_view_control *out); #endif // Forward decls of the IPC-bridge wrappers (defined in ipc_client_compositor.c). @@ -806,6 +809,20 @@ oxr_xrWeaveSubmitDXR(XrSession session, const XrWeaveSubmitInfoDXR *submitInfo, lr[i].params.view_count = o->viewCount; lr[i].params.focal_px = o->focalPx > 0.0f ? o->focalPx : 0.0f; } + // Spec v2 (ADR-048): the viewpoint policy, chained directly on the + // rect (wins) or on its XrLiftOptionsDXR. + const XrLiftViewControlDXR *v = + OXR_GET_INPUT_FROM_CHAIN(e, XR_TYPE_LIFT_VIEW_CONTROL_DXR, XrLiftViewControlDXR); + if (v == NULL && o != NULL) { + v = OXR_GET_INPUT_FROM_CHAIN(o, XR_TYPE_LIFT_VIEW_CONTROL_DXR, XrLiftViewControlDXR); + } + if (v != NULL) { + XrResult vr = oxr_lift_view_control_from_xr(&log, v, &lr[i].view_control); + if (vr != XR_SUCCESS) { + return vr; + } + lr[i].has_view_control = true; + } } xrt_result_t lx = comp_ipc_client_compositor_lift_weave_rects(&sess->xcn->base, lifts->liftCount, lr); if (lx == XRT_ERROR_IPC_FAILURE) { diff --git a/src/xrt/targets/cli/cli_cmd_lift.cpp b/src/xrt/targets/cli/cli_cmd_lift.cpp index a21081ae14..9ba24a5825 100644 --- a/src/xrt/targets/cli/cli_cmd_lift.cpp +++ b/src/xrt/targets/cli/cli_cmd_lift.cpp @@ -645,8 +645,9 @@ cmd_probe(int argc, const char **argv) } uint64_t t_submit = os_monotonic_get_ns(); uint64_t frame_id = 0; - xret = ipc_client_lift_submit(&ipc_c, sid, (xrt_graphics_buffer_handle_t)g.in_handle, false, - (uint32_t)iw, (uint32_t)ih, (int64_t)i, ¶ms, nullptr, 0, &frame_id); + xret = + ipc_client_lift_submit(&ipc_c, sid, (xrt_graphics_buffer_handle_t)g.in_handle, false, (uint32_t)iw, + (uint32_t)ih, (int64_t)i, ¶ms, nullptr, 0, nullptr, &frame_id); if (xret != XRT_SUCCESS) { printf("%6d submit refused (xrt_result=%d) — retrying next frame\n", i, (int)xret); continue; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 343ca4e912..0e836d98f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,7 @@ set(tests tests_dp_vk_snap_window_rect tests_weave_snap_grid tests_lift_mailbox + tests_aux_lift_viewpoint tests_space_overseer_rig tests_rig_composer tests_aux_x11_scale diff --git a/tests/tests_aux_lift_viewpoint.cpp b/tests/tests_aux_lift_viewpoint.cpp new file mode 100644 index 0000000000..8caad3903e --- /dev/null +++ b/tests/tests_aux_lift_viewpoint.cpp @@ -0,0 +1,228 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief XR_DXR_lift viewpoint policy (ADR-048): rebase, rig factors, axis + * mask, clamp and the per-stream recenter filter. + * + * What must hold: + * - the defaults keep v1's X-only look-around for the first second; + * - ipd / parallax follow the display-rig steps 1a / 1b; + * - masked axes are pinned (y = 0, z = nominal) and the clamp bounds x / y; + * - EASE_TO_CURRENT: nothing moves before the hold; after it the rendered + * offset decays with the time constant, independent of the frame rate; + * - EASE_TO_NEUTRAL decays the offset and restores look-around on return; + * - a reset (stream create / tracking loss) forgets the filter. + */ + +#include "catch_amalgamated.hpp" + +#include "util/u_lift_viewpoint.h" + +#include + +namespace { + +constexpr uint64_t kMs = 1000ull * 1000ull; +constexpr float kNz = 0.6f; + +//! A symmetric eye pair around midpoint (x, y, z), 64 mm apart. +void +pair(float x, float y, float z, float out[6]) +{ + out[0] = x - 0.032f; + out[1] = y; + out[2] = z; + out[3] = x + 0.032f; + out[4] = y; + out[5] = z; +} + +//! Run the policy at a fixed midpoint for @p seconds at @p hz; returns the last rendered midpoint x. +float +run(const u_lift_view_control &vc, u_lift_recenter &r, float x, float seconds, float hz, uint64_t &t) +{ + float in[6], out[6], mid[3] = {}; + pair(x, 0.0f, kNz, in); + const int steps = (int)std::lround(seconds * hz); + const uint64_t dt = (uint64_t)(1e9 / hz); + for (int i = 0; i < steps; i++) { + t += dt; + u_lift_viewpoint_apply(&vc, &r, kNz, t, in, 2, out, nullptr, mid); + } + return mid[0]; +} + +} // namespace + +TEST_CASE("lift viewpoint: rebase subtracts the rect centre", "[lift_viewpoint]") +{ + float p[6] = {0.10f, 0.02f, 0.6f, 0.164f, 0.02f, 0.6f}; + const float centre[3] = {0.10f, 0.02f, 0.0f}; + u_lift_viewpoint_rebase(p, 2, centre); + CHECK(p[0] == Catch::Approx(0.0f).margin(1e-6)); + CHECK(p[3] == Catch::Approx(0.064f)); + CHECK(p[1] == Catch::Approx(0.0f).margin(1e-6)); + CHECK(p[2] == Catch::Approx(0.6f)); +} + +TEST_CASE("lift viewpoint: sanitize fills defaults", "[lift_viewpoint]") +{ + u_lift_view_control vc = {}; + vc.ipd_factor = 2.0f; + vc.parallax_factor = -1.0f; + vc.axis_mode = 0; + vc.max_offset_m = -3.0f; + vc.recenter_mode = 9; + vc.hold_s = -1.0f; + vc.tau_s = 0.0f; + u_lift_view_control_sanitize(&vc); + CHECK(vc.ipd_factor == 1.0f); + CHECK(vc.parallax_factor == 0.0f); + CHECK(vc.axis_mode == U_LIFT_AXIS_X); + CHECK(vc.max_offset_m == 0.0f); + CHECK(vc.recenter_mode == U_LIFT_RECENTER_EASE_TO_CURRENT); + CHECK(vc.hold_s == U_LIFT_RECENTER_HOLD_DEFAULT_S); + CHECK(vc.tau_s == U_LIFT_RECENTER_TAU_DEFAULT_S); +} + +TEST_CASE("lift viewpoint: ipd and parallax factors (rig steps 1a / 1b)", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + vc.recenter_mode = U_LIFT_RECENTER_OFF; + vc.axis_mode = U_LIFT_AXIS_XYZ; + vc.ipd_factor = 0.5f; + vc.parallax_factor = 0.5f; + float in[6], out[6], base = 0.0f, mid[3]; + pair(0.10f, 0.04f, 0.8f, in); + u_lift_viewpoint_apply(&vc, nullptr, kNz, 0, in, 2, out, &base, mid); + CHECK(base == Catch::Approx(0.032f)); + CHECK(mid[0] == Catch::Approx(0.05f)); + CHECK(mid[1] == Catch::Approx(0.02f)); + CHECK(mid[2] == Catch::Approx(kNz + 0.5f * (0.8f - kNz))); + CHECK(out[0] == Catch::Approx(0.05f - 0.016f)); + CHECK(out[3] == Catch::Approx(0.05f + 0.016f)); + + vc.ipd_factor = 0.0f; // mono + u_lift_viewpoint_apply(&vc, nullptr, kNz, 0, in, 2, out, &base, mid); + CHECK(base == Catch::Approx(0.0f).margin(1e-6)); + CHECK(out[0] == Catch::Approx(out[3])); +} + +TEST_CASE("lift viewpoint: axis mask and clamp", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + vc.recenter_mode = U_LIFT_RECENTER_OFF; + float in[6], out[6], mid[3]; + pair(0.30f, 0.05f, 0.9f, in); + + vc.axis_mode = U_LIFT_AXIS_X; + u_lift_viewpoint_apply(&vc, nullptr, kNz, 0, in, 2, out, nullptr, mid); + CHECK(mid[0] == Catch::Approx(0.30f)); + CHECK(mid[1] == 0.0f); + CHECK(mid[2] == Catch::Approx(kNz)); + + vc.axis_mode = U_LIFT_AXIS_XY; + u_lift_viewpoint_apply(&vc, nullptr, kNz, 0, in, 2, out, nullptr, mid); + CHECK(mid[1] == Catch::Approx(0.05f)); + CHECK(mid[2] == Catch::Approx(kNz)); + + vc.axis_mode = U_LIFT_AXIS_XYZ; + u_lift_viewpoint_apply(&vc, nullptr, kNz, 0, in, 2, out, nullptr, mid); + CHECK(mid[2] == Catch::Approx(0.9f)); + + vc.max_offset_m = 0.1f; + u_lift_viewpoint_apply(&vc, nullptr, kNz, 0, in, 2, out, nullptr, mid); + CHECK(mid[0] == Catch::Approx(0.1f)); + CHECK(mid[1] == Catch::Approx(0.05f)); + // The eye separation survives the clamp. + CHECK(out[3] - out[0] == Catch::Approx(0.064f)); +} + +TEST_CASE("lift viewpoint: EASE_TO_CURRENT holds, then decays with tau", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); // hold 1 s, tau 2 s + u_lift_recenter r = {}; + uint64_t t = 1000 * kMs; + + // Before the hold: the full offset (v1 look-around). + float x = run(vc, r, 0.10f, 0.9f, 60.0f, t); + CHECK(x == Catch::Approx(0.10f)); + + // Past the hold by one time constant: ~e^-1 of the offset left. + run(vc, r, 0.10f, 0.1f, 60.0f, t); + x = run(vc, r, 0.10f, 2.0f, 60.0f, t); + CHECK(x == Catch::Approx(0.10f * std::exp(-1.0f)).margin(0.004f)); + + // Long after: settled straight-on. + x = run(vc, r, 0.10f, 20.0f, 60.0f, t); + CHECK(std::fabs(x) < 0.002f); + + // A new head movement looks around immediately, relative to the new reference. + x = run(vc, r, 0.15f, 1.0f / 60.0f, 60.0f, t); + CHECK(x == Catch::Approx(0.05f).margin(0.003f)); +} + +TEST_CASE("lift viewpoint: the filter is frame-rate independent", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + u_lift_recenter a = {}, b = {}; + uint64_t ta = 0, tb = 0; + float xa = run(vc, a, 0.10f, 3.0f, 30.0f, ta); + float xb = run(vc, b, 0.10f, 3.0f, 120.0f, tb); + CHECK(xa == Catch::Approx(xb).margin(0.003f)); +} + +TEST_CASE("lift viewpoint: a small offset never starts the hold", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + u_lift_recenter r = {}; + uint64_t t = 0; + float x = run(vc, r, 0.003f, 10.0f, 60.0f, t); + CHECK(x == Catch::Approx(0.003f)); +} + +TEST_CASE("lift viewpoint: EASE_TO_NEUTRAL decays and recovers", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + vc.recenter_mode = U_LIFT_RECENTER_EASE_TO_NEUTRAL; + u_lift_recenter r = {}; + uint64_t t = 0; + float x = run(vc, r, 0.10f, 0.5f, 60.0f, t); + CHECK(x == Catch::Approx(0.10f)); + x = run(vc, r, 0.10f, 20.0f, 60.0f, t); + CHECK(std::fabs(x) < 0.002f); + // Back on axis for a while: look-around is restored. + run(vc, r, 0.0f, 20.0f, 60.0f, t); + x = run(vc, r, 0.10f, 1.0f / 60.0f, 60.0f, t); + CHECK(x == Catch::Approx(0.10f).margin(0.003f)); +} + +TEST_CASE("lift viewpoint: reset forgets the reference", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + u_lift_recenter r = {}; + uint64_t t = 0; + run(vc, r, 0.10f, 20.0f, 60.0f, t); + u_lift_recenter_reset(&r); + float x = run(vc, r, 0.10f, 1.0f / 60.0f, 60.0f, t); + CHECK(x == Catch::Approx(0.10f)); +} + +TEST_CASE("lift viewpoint: OFF is the plain rect-relative offset", "[lift_viewpoint]") +{ + u_lift_view_control vc; + u_lift_view_control_default(&vc); + vc.recenter_mode = U_LIFT_RECENTER_OFF; + u_lift_recenter r = {}; + uint64_t t = 0; + float x = run(vc, r, 0.10f, 20.0f, 60.0f, t); + CHECK(x == Catch::Approx(0.10f)); +} From e9fe1ca52a5654077ad2933f7a059cd658e02e45 Mon Sep 17 00:00:00 2001 From: dfattal Date: Thu, 8 Oct 2026 00:53:25 -0700 Subject: [PATCH 3/4] docs(lift): ADR-048 lift viewpoint policy + XR_DXR_lift v2 spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R4 of #1859: XR_DXR_lift.md §4.1 (viewpoint frame, XrLiftViewControlDXR, recentering, echo, what the DP is told), contract row, version history and header table (v2); ADR-048; plug-in interface reference for the appended xrt_dp_lift_params fields; extensions index note; ADR index regenerated. Part of #1858. Co-Authored-By: Claude Opus 5.5 --- docs/README.md | 1 + docs/adr/ADR-048-lift-viewpoint-policy.md | 129 ++++++++++++++++++++++ docs/adr/README.md | 1 + docs/reference/xrt_plugin_iface.md | 22 +++- docs/specs/extensions/XR_DXR_lift.md | 70 +++++++++++- docs/specs/extensions/index.json | 2 +- 6 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 docs/adr/ADR-048-lift-viewpoint-policy.md diff --git a/docs/README.md b/docs/README.md index f28f8e4f3e..c591be08e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 --- diff --git a/docs/adr/ADR-048-lift-viewpoint-policy.md b/docs/adr/ADR-048-lift-viewpoint-policy.md new file mode 100644 index 0000000000..4f312735cd --- /dev/null +++ b/docs/adr/ADR-048-lift-viewpoint-policy.md @@ -0,0 +1,129 @@ +# 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_TO_CURRENT (default).** The reference starts at the straight-on viewer. When the + viewer's offset from it has stayed beyond a few millimetres for `recenterHoldSeconds` + (default 1 s), the reference slides toward the viewer with time constant + `recenterTimeConstantSeconds` (default 2 s). Someone who settles off-axis sees the picture + return to its intended composition; a new head movement still produces immediate look-around. +- **EASE_TO_NEUTRAL.** After the same hold the rendered offset decays to 0; look-around returns + when the viewer comes back to the rect's axis. +- **OFF.** The plain rect-relative offset. + +### 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_TO_CURRENT, 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. diff --git a/docs/adr/README.md b/docs/adr/README.md index 36a32f0e7b..89c4499652 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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 diff --git a/docs/reference/xrt_plugin_iface.md b/docs/reference/xrt_plugin_iface.md index 9d0807c498..b6b013ffb0 100644 --- a/docs/reference/xrt_plugin_iface.md +++ b/docs/reference/xrt_plugin_iface.md @@ -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); @@ -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 diff --git a/docs/specs/extensions/XR_DXR_lift.md b/docs/specs/extensions/XR_DXR_lift.md index 59be587437..a6f1a82d34 100644 --- a/docs/specs/extensions/XR_DXR_lift.md +++ b/docs/specs/extensions/XR_DXR_lift.md @@ -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 @@ -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_TO_CURRENT | how the look-around returns to centre (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_TO_CURRENT` (default) — the reference viewer position starts at the straight-on viewer. + When the viewer's offset from it has stayed beyond the threshold for the hold time, the + reference slides toward the viewer with the time constant. A viewer who settles off-axis sees + the picture return to its straight-on composition; a new head movement still looks around + immediately, relative to where they settled. +- `EASE_TO_NEUTRAL` — after the same hold the rendered offset decays toward 0; it comes back + (same time constant) once the viewer returns to the rect's axis. +- `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_TO_CURRENT 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) @@ -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` | @@ -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_TO_CURRENT 1 s / 2 s), `XrLiftResultViewpointsDXR` echo. | ## Probing on a Windows box — gotchas (first N0 run, 2026-09-25) diff --git a/docs/specs/extensions/index.json b/docs/specs/extensions/index.json index 8203f98643..b2a6390abd 100644 --- a/docs/specs/extensions/index.json +++ b/docs/specs/extensions/index.json @@ -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", From edf61ecdf81d85ceb3526bc5c8b800bd1a854a5a Mon Sep 17 00:00:00 2001 From: dfattal Date: Thu, 8 Oct 2026 02:31:19 -0700 Subject: [PATCH 4/4] refactor(lift): collapse recenter modes to OFF / EASE_BACK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design change (David, #1859): the camera eases back to the scene camera origin — moving the head gives temporary look-around, then after the hold the camera returns to centre with no look-around. That is the former EASE_TO_CURRENT behaviour, now XR_LIFT_RECENTER_MODE_EASE_BACK_DXR (value 1, still the default). EASE_TO_NEUTRAL (value 2) is removed outright: the extension is unreleased and spec v2 is introduced by this same PR. Internal enums, the u_lift_viewpoint filter (no gain path), unit tests, log strings (recenter=ease-back/off), spec §4.1 and ADR-048 updated. Defaults (EASE_BACK, 1 s hold, 2 s) and DXR_LIFT_RECENTER=off unchanged. Part of #1858. Co-Authored-By: Claude Opus 5.5 --- docs/adr/ADR-048-lift-viewpoint-policy.md | 20 +++--- docs/specs/extensions/XR_DXR_lift.md | 20 +++--- .../openxr_includes/openxr/XR_DXR_lift.h | 21 +++---- src/xrt/auxiliary/util/u_lift_viewpoint.c | 63 +++++++------------ src/xrt/auxiliary/util/u_lift_viewpoint.h | 16 ++--- .../compositor/d3d11_service/d3d11_lift.cpp | 15 +++-- src/xrt/include/xrt/xrt_lift.h | 2 +- src/xrt/state_trackers/oxr/oxr_lift.c | 3 +- tests/tests_aux_lift_viewpoint.cpp | 28 ++++----- 9 files changed, 91 insertions(+), 97 deletions(-) diff --git a/docs/adr/ADR-048-lift-viewpoint-policy.md b/docs/adr/ADR-048-lift-viewpoint-policy.md index 4f312735cd..ed468dbef6 100644 --- a/docs/adr/ADR-048-lift-viewpoint-policy.md +++ b/docs/adr/ADR-048-lift-viewpoint-policy.md @@ -83,15 +83,19 @@ policy is identical for every vendor and testable without hardware. One filter per stream, frame-rate independent, reset on stream create and on tracking loss: -- **EASE_TO_CURRENT (default).** The reference starts at the straight-on viewer. When the - viewer's offset from it has stayed beyond a few millimetres for `recenterHoldSeconds` - (default 1 s), the reference slides toward the viewer with time constant - `recenterTimeConstantSeconds` (default 2 s). Someone who settles off-axis sees the picture - return to its intended composition; a new head movement still produces immediate look-around. -- **EASE_TO_NEUTRAL.** After the same hold the rendered offset decays to 0; look-around returns - when the viewer comes back to the rect's axis. +- **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 @@ -101,7 +105,7 @@ and size — so an app compositing its own 3D over a lifted frame can render for ### D5. Defaults change behaviour, with an A/B switch -Defaults are ipd 1, parallax 1, axis X, no clamp, EASE_TO_CURRENT, 1 s hold, 2 s time constant. +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 diff --git a/docs/specs/extensions/XR_DXR_lift.md b/docs/specs/extensions/XR_DXR_lift.md index a6f1a82d34..16fa61d7d6 100644 --- a/docs/specs/extensions/XR_DXR_lift.md +++ b/docs/specs/extensions/XR_DXR_lift.md @@ -157,7 +157,7 @@ stream**: the last one sent applies until another is sent. Omitted, the defaults | `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_TO_CURRENT | how the look-around returns to centre (below) | +| `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) | @@ -166,17 +166,17 @@ Order, per conversion: rebase → ipd factor → parallax factor → axis mask **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_TO_CURRENT` (default) — the reference viewer position starts at the straight-on viewer. - When the viewer's offset from it has stayed beyond the threshold for the hold time, the - reference slides toward the viewer with the time constant. A viewer who settles off-axis sees - the picture return to its straight-on composition; a new head movement still looks around - immediately, relative to where they settled. -- `EASE_TO_NEUTRAL` — after the same hold the rendered offset decays toward 0; it comes back - (same time constant) once the viewer returns to the rect's axis. +- `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_TO_CURRENT default. `DXR_LIFT_RECENTER=off` (service environment, read once when +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 @@ -439,7 +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_TO_CURRENT 1 s / 2 s), `XrLiftResultViewpointsDXR` echo. | +| 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) diff --git a/src/external/openxr_includes/openxr/XR_DXR_lift.h b/src/external/openxr_includes/openxr/XR_DXR_lift.h index f96bf00c08..4dcd4b8430 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_lift.h +++ b/src/external/openxr_includes/openxr/XR_DXR_lift.h @@ -200,22 +200,21 @@ typedef enum XrLiftAxisModeDXR { * * - OFF: the rendered offset is the viewer's offset from the rect centre, * always (v1 behaviour, but rect-relative). - * - EASE_TO_CURRENT (default): the reference follows the viewer. Once the - * offset has stayed beyond a small threshold for recenterHoldSeconds, the - * reference slides toward the viewer's current position with time - * constant recenterTimeConstantSeconds — a viewer who settles off-axis - * sees the picture return to its straight-on composition, while a head - * movement still produces immediate look-around. - * - EASE_TO_NEUTRAL: after the same hold, the rendered offset decays toward - * 0 (the straight-on view of the rect centre) with the same time constant. + * - EASE_BACK (default): the camera eases back to the scene camera origin. + * Moving the head gives look-around temporarily; once the viewer's offset + * has stayed beyond a small threshold for recenterHoldSeconds, the camera + * returns to centre (no look-around) with time constant + * recenterTimeConstantSeconds. Any new head motion gives immediate, + * temporary look-around again. (The reference viewer position follows + * the head, so the rendered offset decays to 0 wherever the viewer + * settles.) * * Frame-rate independent; one filter per stream, reset when the stream is * created and whenever tracking is lost. */ typedef enum XrLiftRecenterModeDXR { XR_LIFT_RECENTER_MODE_OFF_DXR = 0, - XR_LIFT_RECENTER_MODE_EASE_TO_CURRENT_DXR = 1, - XR_LIFT_RECENTER_MODE_EASE_TO_NEUTRAL_DXR = 2, + XR_LIFT_RECENTER_MODE_EASE_BACK_DXR = 1, XR_LIFT_RECENTER_MODE_MAX_ENUM_DXR = 0x7FFFFFFF } XrLiftRecenterModeDXR; @@ -304,7 +303,7 @@ typedef struct XrLiftOptionsDXR { * * Omitted entirely, the runtime applies the defaults: ipdFactor 1, * parallaxFactor 1, axisMode X, maxOffsetMeters 0 (no clamp), recenterMode - * EASE_TO_CURRENT, hold 1 s, time constant 2 s. + * EASE_BACK, hold 1 s, time constant 2 s. * * Frame: every viewpoint is relative to the CENTRE of the lifted region — the * lifted weave rect, or for xrSubmitLiftFrameDXR the session's window (the diff --git a/src/xrt/auxiliary/util/u_lift_viewpoint.c b/src/xrt/auxiliary/util/u_lift_viewpoint.c index 98cb262d28..a4b3c8962e 100644 --- a/src/xrt/auxiliary/util/u_lift_viewpoint.c +++ b/src/xrt/auxiliary/util/u_lift_viewpoint.c @@ -27,7 +27,7 @@ u_lift_view_control_default(struct u_lift_view_control *vc) vc->parallax_factor = 1.0f; vc->axis_mode = U_LIFT_AXIS_X; vc->max_offset_m = 0.0f; - vc->recenter_mode = U_LIFT_RECENTER_EASE_TO_CURRENT; + vc->recenter_mode = U_LIFT_RECENTER_EASE_BACK; vc->hold_s = U_LIFT_RECENTER_HOLD_DEFAULT_S; vc->tau_s = U_LIFT_RECENTER_TAU_DEFAULT_S; } @@ -43,8 +43,8 @@ u_lift_view_control_sanitize(struct u_lift_view_control *vc) if (!isfinite(vc->max_offset_m) || vc->max_offset_m < 0.0f) { vc->max_offset_m = 0.0f; } - if (vc->recenter_mode > U_LIFT_RECENTER_EASE_TO_NEUTRAL) { - vc->recenter_mode = U_LIFT_RECENTER_EASE_TO_CURRENT; + if (vc->recenter_mode > U_LIFT_RECENTER_EASE_BACK) { + vc->recenter_mode = U_LIFT_RECENTER_EASE_BACK; } if (!isfinite(vc->hold_s) || vc->hold_s < 0.0f) { vc->hold_s = U_LIFT_RECENTER_HOLD_DEFAULT_S; @@ -85,9 +85,12 @@ live_len(const float d[3], uint32_t axis_mode) } /*! - * One step of the recenter filter on midpoint @p c (in / out). Neutral is the - * rect's straight-on viewer (0, 0, nz). Frame-rate independent: the ease - * factor per step is 1 - exp(-dt / tau). + * One step of the ease-back filter on midpoint @p c (in / out). The rendered + * midpoint is the straight-on viewer (0, 0, nz) plus the head's offset from a + * REFERENCE that follows the head: after the hold the reference slides to the + * head, so the camera returns to the scene camera origin; a new movement is an + * offset from the reference again (temporary look-around). Frame-rate + * independent: the ease factor per step is 1 - exp(-dt / tau). */ static void recenter_step(const struct u_lift_view_control *vc, struct u_lift_recenter *r, float nz, uint64_t now_ns, float c[3]) @@ -99,7 +102,6 @@ recenter_step(const struct u_lift_view_control *vc, struct u_lift_recenter *r, f r->valid = true; r->last_ns = now_ns; memcpy(r->ref, neutral, sizeof(r->ref)); - r->gain = 1.0f; r->beyond_s = 0.0f; r->easing = false; } @@ -110,43 +112,26 @@ recenter_step(const struct u_lift_view_control *vc, struct u_lift_recenter *r, f r->last_ns = now_ns; const float k = 1.0f - expf(-dt / vc->tau_s); - if (vc->recenter_mode == U_LIFT_RECENTER_EASE_TO_CURRENT) { - const float d[3] = {c[0] - r->ref[0], c[1] - r->ref[1], c[2] - r->ref[2]}; - const float len = live_len(d, vc->axis_mode); - r->beyond_s = len > U_LIFT_RECENTER_THRESHOLD_M ? r->beyond_s + dt : 0.0f; - if (!r->easing && len > U_LIFT_RECENTER_THRESHOLD_M && r->beyond_s >= vc->hold_s) { - r->easing = true; - } - if (r->easing) { - for (int i = 0; i < 3; i++) { - r->ref[i] += d[i] * k; - } - const float d2[3] = {c[0] - r->ref[0], c[1] - r->ref[1], c[2] - r->ref[2]}; - if (live_len(d2, vc->axis_mode) < 0.25f * U_LIFT_RECENTER_THRESHOLD_M) { - r->easing = false; // settled; the next excursion holds again first - r->beyond_s = 0.0f; - } - } - // Rendered midpoint = the straight-on viewer + the offset from the reference. + const float d[3] = {c[0] - r->ref[0], c[1] - r->ref[1], c[2] - r->ref[2]}; + const float len = live_len(d, vc->axis_mode); + r->beyond_s = len > U_LIFT_RECENTER_THRESHOLD_M ? r->beyond_s + dt : 0.0f; + if (!r->easing && len > U_LIFT_RECENTER_THRESHOLD_M && r->beyond_s >= vc->hold_s) { + r->easing = true; + } + if (r->easing) { for (int i = 0; i < 3; i++) { - c[i] = neutral[i] + (c[i] - r->ref[i]); + r->ref[i] += d[i] * k; } - } else if (vc->recenter_mode == U_LIFT_RECENTER_EASE_TO_NEUTRAL) { - const float d[3] = {c[0] - neutral[0], c[1] - neutral[1], c[2] - neutral[2]}; - if (live_len(d, vc->axis_mode) > U_LIFT_RECENTER_THRESHOLD_M) { - r->beyond_s += dt; - if (r->beyond_s >= vc->hold_s) { - r->gain -= r->gain * k; // the rendered offset decays toward 0 - } - } else { - // Back on the rect's axis: look-around returns, same time constant. + const float d2[3] = {c[0] - r->ref[0], c[1] - r->ref[1], c[2] - r->ref[2]}; + if (live_len(d2, vc->axis_mode) < 0.25f * U_LIFT_RECENTER_THRESHOLD_M) { + r->easing = false; // settled; the next excursion holds again first r->beyond_s = 0.0f; - r->gain += (1.0f - r->gain) * k; - } - for (int i = 0; i < 3; i++) { - c[i] = neutral[i] + d[i] * r->gain; } } + // Rendered midpoint = the straight-on viewer + the offset from the reference. + for (int i = 0; i < 3; i++) { + c[i] = neutral[i] + (c[i] - r->ref[i]); + } } void diff --git a/src/xrt/auxiliary/util/u_lift_viewpoint.h b/src/xrt/auxiliary/util/u_lift_viewpoint.h index eba8e31f88..26fe5a3f02 100644 --- a/src/xrt/auxiliary/util/u_lift_viewpoint.h +++ b/src/xrt/auxiliary/util/u_lift_viewpoint.h @@ -14,7 +14,9 @@ * 2. parallax - lerp the midpoint toward the nominal viewer * (0, 0, nominal_z) by 1 - parallax_factor (step 1b); * 3. axis mask - X: midpoint y = 0 and z = nominal_z; XY: z = nominal_z; - * 4. recenter - the per-stream ease filter (see u_lift_recenter); + * 4. recenter - the per-stream ease-back filter (see u_lift_recenter): + * after a hold the camera returns to the scene camera + * origin; new head motion gives temporary look-around; * 5. clamp - midpoint x / y to +- max_offset_m (0 = unclamped). * * No locks, no clock, no GPU: the caller passes time in, so the filter is @@ -39,8 +41,7 @@ extern "C" { //! Values match XR_LIFT_RECENTER_MODE_*_DXR. #define U_LIFT_RECENTER_OFF 0u -#define U_LIFT_RECENTER_EASE_TO_CURRENT 1u -#define U_LIFT_RECENTER_EASE_TO_NEUTRAL 2u +#define U_LIFT_RECENTER_EASE_BACK 1u #define U_LIFT_RECENTER_HOLD_DEFAULT_S 1.0f #define U_LIFT_RECENTER_TAU_DEFAULT_S 2.0f @@ -72,19 +73,18 @@ struct u_lift_recenter { bool valid; //!< false = the next sample re-initialises uint64_t last_ns; //!< time of the previous sample - float ref[3]; //!< EASE_TO_CURRENT: the reference viewer midpoint - float gain; //!< EASE_TO_NEUTRAL: rendered fraction of the offset, [0, 1] + float ref[3]; //!< the reference viewer midpoint (follows the head) float beyond_s; //!< how long the offset has stayed beyond the threshold - bool easing; //!< EASE_TO_CURRENT: hold elapsed, the reference is moving + bool easing; //!< hold elapsed: the reference is moving toward the head }; -//! The defaults (ADR-048): ipd 1, parallax 1, X, unclamped, EASE_TO_CURRENT, 1 s, 2 s. +//! The defaults (ADR-048): ipd 1, parallax 1, X, unclamped, EASE_BACK, 1 s, 2 s. void u_lift_view_control_default(struct u_lift_view_control *vc); /*! * Clamp / default every field of @p vc into its valid range: factors to - * [0, 1], unknown axis mode -> X, unknown recenter mode -> EASE_TO_CURRENT, + * [0, 1], unknown axis mode -> X, unknown recenter mode -> EASE_BACK, * hold < 0 -> default, tau <= 0 -> default, non-finite values -> defaults. */ void diff --git a/src/xrt/compositor/d3d11_service/d3d11_lift.cpp b/src/xrt/compositor/d3d11_service/d3d11_lift.cpp index 798f458988..356d161260 100644 --- a/src/xrt/compositor/d3d11_service/d3d11_lift.cpp +++ b/src/xrt/compositor/d3d11_service/d3d11_lift.cpp @@ -824,6 +824,13 @@ lift_reap(d3d11_lift *l, std::unique_lock &lk) } } +//! Log name of a recenter mode. +static const char * +recenter_str(uint32_t mode) +{ + return mode == U_LIFT_RECENTER_EASE_BACK ? "ease-back" : "off"; +} + //! The viewpoints one conversion uses (ADR-048), and what the DP is told about them. struct viewpoint_result { @@ -907,9 +914,9 @@ resolve_viewpoints(d3d11_lift *l, st.logged_vc_valid = true; U_LOG_W( "[lift] stream %llu viewpoint policy: ipd=%.2f parallax=%.2f axis=%u max_offset=%.3f m " - "recenter=%u hold=%.2f s tau=%.2f s (rect-relative viewpoints, ADR-048)", + "recenter=%s hold=%.2f s tau=%.2f s (rect-relative viewpoints, ADR-048)", (unsigned long long)st.id, vc.ipd_factor, vc.parallax_factor, vc.axis_mode, vc.max_offset_m, - vc.recenter_mode, vc.hold_s, vc.tau_s); + recenter_str(vc.recenter_mode), vc.hold_s, vc.tau_s); } struct xrt_eye_positions eyes = {}; @@ -1115,9 +1122,9 @@ lift_convert_one(d3d11_lift *l, lift_stream &st, std::unique_lock &l U_LOG_I( "[lift] stream %llu viewpoints: rect centre=(%.3f, %.3f) m size=%.3fx%.3f m mid=(%.3f, " "%.3f, " - "%.3f) baseline=%.4f m axis=%u recenter=%u", + "%.3f) baseline=%.4f m axis=%u recenter=%s", (unsigned long long)st.id, vr.centre[0], vr.centre[1], vr.rect_w, vr.rect_h, vr.mid[0], - vr.mid[1], vr.mid[2], vr.baseline, vr.axis_mode, vr.recenter_mode); + vr.mid[1], vr.mid[2], vr.baseline, vr.axis_mode, recenter_str(vr.recenter_mode)); } } l->cv.notify_all(); diff --git a/src/xrt/include/xrt/xrt_lift.h b/src/xrt/include/xrt/xrt_lift.h index 2a078936f6..7a8e221ca1 100644 --- a/src/xrt/include/xrt/xrt_lift.h +++ b/src/xrt/include/xrt/xrt_lift.h @@ -29,7 +29,7 @@ extern "C" { /*! * The viewpoint policy of a stream (XrLiftViewControlDXR, ADR-048). Values - * are the XR_DXR_lift enum values (axis 1..3, recenter 0..2); the service + * are the XR_DXR_lift enum values (axis 1..3, recenter 0 off / 1 ease-back); the service * sanitizes them (u_lift_view_control_sanitize). */ struct xrt_lift_view_control diff --git a/src/xrt/state_trackers/oxr/oxr_lift.c b/src/xrt/state_trackers/oxr/oxr_lift.c index 727e3a98e7..7a9239f3f4 100644 --- a/src/xrt/state_trackers/oxr/oxr_lift.c +++ b/src/xrt/state_trackers/oxr/oxr_lift.c @@ -197,8 +197,7 @@ oxr_lift_view_control_from_xr(struct oxr_logger *log, const XrLiftViewControlDXR return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, "XrLiftViewControlDXR::axisMode (%d) invalid", (int)v->axisMode); } - if (v->recenterMode < XR_LIFT_RECENTER_MODE_OFF_DXR || - v->recenterMode > XR_LIFT_RECENTER_MODE_EASE_TO_NEUTRAL_DXR) { + if (v->recenterMode < XR_LIFT_RECENTER_MODE_OFF_DXR || v->recenterMode > XR_LIFT_RECENTER_MODE_EASE_BACK_DXR) { return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, "XrLiftViewControlDXR::recenterMode (%d) invalid", (int)v->recenterMode); } diff --git a/tests/tests_aux_lift_viewpoint.cpp b/tests/tests_aux_lift_viewpoint.cpp index 8caad3903e..0ca9d0de43 100644 --- a/tests/tests_aux_lift_viewpoint.cpp +++ b/tests/tests_aux_lift_viewpoint.cpp @@ -9,9 +9,9 @@ * - the defaults keep v1's X-only look-around for the first second; * - ipd / parallax follow the display-rig steps 1a / 1b; * - masked axes are pinned (y = 0, z = nominal) and the clamp bounds x / y; - * - EASE_TO_CURRENT: nothing moves before the hold; after it the rendered - * offset decays with the time constant, independent of the frame rate; - * - EASE_TO_NEUTRAL decays the offset and restores look-around on return; + * - EASE_BACK: nothing moves before the hold; after it the camera returns + * to the scene camera origin with the time constant, independent of the + * frame rate, and new head motion gives temporary look-around; * - a reset (stream create / tracking loss) forgets the filter. */ @@ -81,7 +81,7 @@ TEST_CASE("lift viewpoint: sanitize fills defaults", "[lift_viewpoint]") CHECK(vc.parallax_factor == 0.0f); CHECK(vc.axis_mode == U_LIFT_AXIS_X); CHECK(vc.max_offset_m == 0.0f); - CHECK(vc.recenter_mode == U_LIFT_RECENTER_EASE_TO_CURRENT); + CHECK(vc.recenter_mode == U_LIFT_RECENTER_EASE_BACK); CHECK(vc.hold_s == U_LIFT_RECENTER_HOLD_DEFAULT_S); CHECK(vc.tau_s == U_LIFT_RECENTER_TAU_DEFAULT_S); } @@ -141,7 +141,7 @@ TEST_CASE("lift viewpoint: axis mask and clamp", "[lift_viewpoint]") CHECK(out[3] - out[0] == Catch::Approx(0.064f)); } -TEST_CASE("lift viewpoint: EASE_TO_CURRENT holds, then decays with tau", "[lift_viewpoint]") +TEST_CASE("lift viewpoint: EASE_BACK holds, then returns to the origin with tau", "[lift_viewpoint]") { u_lift_view_control vc; u_lift_view_control_default(&vc); // hold 1 s, tau 2 s @@ -187,21 +187,21 @@ TEST_CASE("lift viewpoint: a small offset never starts the hold", "[lift_viewpoi CHECK(x == Catch::Approx(0.003f)); } -TEST_CASE("lift viewpoint: EASE_TO_NEUTRAL decays and recovers", "[lift_viewpoint]") +TEST_CASE("lift viewpoint: EASE_BACK after a return to centre gives look-around again", "[lift_viewpoint]") { u_lift_view_control vc; u_lift_view_control_default(&vc); - vc.recenter_mode = U_LIFT_RECENTER_EASE_TO_NEUTRAL; u_lift_recenter r = {}; uint64_t t = 0; - float x = run(vc, r, 0.10f, 0.5f, 60.0f, t); - CHECK(x == Catch::Approx(0.10f)); - x = run(vc, r, 0.10f, 20.0f, 60.0f, t); + // Settle off-axis: the camera returns to the origin. + float x = run(vc, r, 0.10f, 20.0f, 60.0f, t); + CHECK(std::fabs(x) < 0.002f); + // Walk back to the rect axis: that motion is temporary look-around the other way... + x = run(vc, r, 0.0f, 1.0f / 60.0f, 60.0f, t); + CHECK(x == Catch::Approx(-0.10f).margin(0.003f)); + // ...which eases back to the origin too. + x = run(vc, r, 0.0f, 20.0f, 60.0f, t); CHECK(std::fabs(x) < 0.002f); - // Back on axis for a while: look-around is restored. - run(vc, r, 0.0f, 20.0f, 60.0f, t); - x = run(vc, r, 0.10f, 1.0f / 60.0f, 60.0f, t); - CHECK(x == Catch::Approx(0.10f).margin(0.003f)); } TEST_CASE("lift viewpoint: reset forgets the reference", "[lift_viewpoint]")