diff --git a/docs/adr/ADR-046-depth-aware-cursor.md b/docs/adr/ADR-046-depth-aware-cursor.md index f4ae66b7a6..e9c900a0b6 100644 --- a/docs/adr/ADR-046-depth-aware-cursor.md +++ b/docs/adr/ADR-046-depth-aware-cursor.md @@ -121,7 +121,7 @@ The eye compares disparities, so the margin, clamp and slew rates are expressed | Parameter | Default | Why | |---|---|---| -| margin in front of content | 0.03 | about 2 mm crossed on screen at 65 mm IPD: reads as in front without looking detached. Spec v3 briefly lowered it to 0.005 after a head-tracked-panel test; Amendment 2 A.2 restores 0.03. Apps can override it (`XrCursorDepthOptionsDXR::margin`) | +| margin in front of content | 0.015 | about 1 mm crossed on screen at 63–65 mm IPD: reads as in front without looking detached. Spec v3 briefly lowered it to 0.005 after a head-tracked-panel test; Amendment 2 A.2 restored 0.03, then halved it to 0.015 (2026-10-11). Apps can override it (`XrCursorDepthOptionsDXR::margin`) | | clamp | [−0.6, +0.6] — **superseded by Amendment 2: [−0.6, 0], never behind the glass** | t ≥ 0.625: never more than about 3/8 of the way to the eye | | rise time constant | 30 ms | never lag behind content that comes forward, or the violation is visible | | sink time constant | 250 ms | no flicker when the footprint crosses an edge | @@ -489,9 +489,11 @@ implementation that the others call. `[−0.6, +0.6]` clamp with `[−0.6, 0]`. Rationale: a cursor behind the glass sits behind the page, the window chrome and the desktop, which recreates the depth conflict it exists to remove. -2. **Margin 0.03 eye baselines**, about 2 mm of crossed on-screen disparity at a 65 mm IPD. - This is the value the reference app (`cube_handle_metal_macos`) has shown since Phase 1. - Implementations expose it as a `margin` option and keep 0.03 as the default. +2. **Margin 0.015 eye baselines**, about 1 mm of crossed on-screen disparity at a 63–65 mm IPD. + Implementations expose it as a `margin` option and keep 0.015 as the default. + *Halved 2026-10-11 after the Windows Leia panel check: 0.03 floated visibly off the content.* + (As first written, A.2 was 0.03, about 2 mm, the value the reference app + `cube_handle_metal_macos` had shown since Phase 1.) 3. **On the plane, it is the OS cursor.** Nothing is drawn while the filtered disparity is above the **draw threshold**, −0.005 baselines (about 0.3 mm on screen) and never less than 0.5 px on the panel. While nothing is drawn, the OS cursor is the cursor, untouched. The drawn @@ -562,7 +564,7 @@ sprite and the depth, and the runtime owns the drawing. Apps get the same contra #### §A as built in the runtime (#1847, `XR_DXR_cursor_depth` spec v4) - `u_cursor_depth_target()` is A.1 verbatim; the upper clamp is 0 whatever the tuning says - (`max_disparity` defaults to 0 and a value > 0 is treated as 0). Margin default 0.03 (A.2), + (`max_disparity` defaults to 0 and a value > 0 is treated as 0). Margin default 0.015 (A.2; 0.03 until 2026-10-11), still overridable through `XrCursorDepthOptionsDXR::margin`. - A.3 is `u_cursor_depth_should_draw()` (threshold −0.005). The state tracker reports it as `XrCursorDepthPlacementDXR::isActive`, whose meaning was already "draw nothing, show the OS diff --git a/docs/specs/extensions/XR_DXR_cursor_depth.md b/docs/specs/extensions/XR_DXR_cursor_depth.md index 5562d50e68..46b7467610 100644 --- a/docs/specs/extensions/XR_DXR_cursor_depth.md +++ b/docs/specs/extensions/XR_DXR_cursor_depth.md @@ -123,7 +123,7 @@ costs nothing on its own. | Field | Meaning | |---|---| | `anchorMode` | Where the sprite goes laterally as the viewer's head moves. `XR_CURSOR_DEPTH_ANCHOR_MODE_HYBRID_DXR` (0, the default), `_SCREEN_DXR` (1), `_WORLD_DXR` (2). An unknown value behaves as HYBRID. | -| `margin` | How far in front of the content the cursor floats, as a disparity in eye-baseline units. ≤ 0 or non-finite selects the runtime default, 0.03 (v3 used 0.005). | +| `margin` | How far in front of the content the cursor floats, as a disparity in eye-baseline units. ≤ 0 or non-finite selects the runtime default, 0.015 (v3 used 0.005; v4–v6 used 0.03 until 2026-10-11). | **Anchor modes.** E is the cyclopean eye, S the canvas point under the pointer, f the unit display normal pointing away from the viewer, e = dot(S − E, f), d the filtered disparity and @@ -229,7 +229,7 @@ along the display normal measured from the cyclopean eye: - **never behind the glass** (v4, ADR-046 Amendment 2 A.1): target = d < 0 ? max(d − margin, −0.6) : 0. Content on or behind the display plane leaves the cursor on the plane; -- margin: 0.03 baseline in front of the content (overridable with `XrCursorDepthOptionsDXR::margin`); +- margin: 0.015 baseline in front of the content, about 1 mm of crossed on-screen disparity at a 63–65 mm IPD (overridable with `XrCursorDepthOptionsDXR::margin`); - lateral anchor: HYBRID unless `XrCursorDepthOptionsDXR` selects another mode (§2.4); - clamp: d ∈ [−0.6, 0]; - draw threshold (v4, Amendment 2 A.3): `isActive` only while the filtered d ≤ −0.005 (about 0.3 mm @@ -326,3 +326,4 @@ Reference: `test_apps/handle/cube_handle_metal_macos` (`DISPLAYXR_CURSOR_DEPTH_D | 4 | No new structs; **behaviour changes** (ADR-046 Amendment 2 A, one cursor policy everywhere). **Never behind the glass:** target = d < 0 ? max(d − margin, −0.6) : 0, so content on or behind the display plane leaves the cursor on the plane (v1–v3 let it sink onto content behind the glass, up to +0.6). **Margin back to 0.03** (the reference app's value since Phase 1). **Draw threshold:** `isActive` is now also `XR_FALSE` while the filtered disparity is above −0.005, which is the existing meaning of `isActive = XR_FALSE` (draw nothing, show the OS cursor); `disparity` / `targetDisparity` stay filled as diagnostics. HYBRID stays the default anchor. | | 5 | **The runtime draws** (ADR-046 Amendment 2 B). New structs only: `XrCursorDepthDrawInfoDXR` (1004999324, on the hint: the sprite swapchain or `XR_NULL_HANDLE` for the default look, hotspot, size in metres, visibility) and `XrCursorDepthDrawStateDXR` (1004999325, on `XrViewState`: `willDraw`, the app's cue to hide its OS cursor). Drawn after the app's layers and before the display processor, per view, with the projection layer's own views (same-space rule). Default look: the current OS cursor shape, else the classic arrow. Metal (in-process) first; other backends report `willDraw = XR_FALSE`. App-drawn placement (v1–v4) unchanged. | | 6 | **Lifted content** (ADR-046 Amendment 1, runtime#1907). New structs only: `XrCursorDepthLiftRectDXR` (1004999326, on `XrWeaveRectLiftDXR`: draw the cursor into this lifted rect's views) and `XrCursorDepthLiftStateDXR` (1004999327, on `XrWeaveOutputDXR`: `drawnLiftMask`). Placed from the lift conversion's depth map through the module's relief mapping (`xrt_dp_lift_depth::relief_*`), §3 policy, D3D11 service. §2.7. | +| 6 (2026-10-11) | No version bump, no struct change; **default margin halved, 0.03 → 0.015** (ADR-046 Amendment 2 A.2) after the Windows Leia panel check: 0.03 (≈2 mm crossed) floated visibly off the content, 0.015 is ≈1 mm. The margin is a default, not a contract (`XrCursorDepthOptionsDXR::margin` still overrides it), so apps need no change; one that wants the old look chains the options struct with `margin = 0.03`. | diff --git a/src/external/openxr_includes/openxr/XR_DXR_cursor_depth.h b/src/external/openxr_includes/openxr/XR_DXR_cursor_depth.h index a70b4087a0..c531b7f980 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_cursor_depth.h +++ b/src/external/openxr_includes/openxr/XR_DXR_cursor_depth.h @@ -53,14 +53,16 @@ * head-tracked panel: the default anchor is now HYBRID (the sprite follows the * pointer's line of sight while the pointer moves, and is world-fixed while it * is still, so it motion-parallaxes like the content it rests on) and the - * default margin was 0.005 (v4 restores 0.03). An app that wants the v1/v2 + * default margin was 0.005 (v4 restored 0.03, since halved to 0.015). An app that wants the v1/v2 * placement chains the options struct with XR_CURSOR_DEPTH_ANCHOR_MODE_SCREEN_DXR. * * Spec v4 (ADR-046 Amendment 2 A, one cursor policy everywhere) changes * behaviour, not structs: * - NEVER BEHIND THE GLASS: target = d < 0 ? max(d - margin, -0.6) : 0. * Content on or behind the display plane leaves the cursor on the plane. - * - default margin 0.03 again; + * - default margin 0.03 again (halved to 0.015 on 2026-10-11, a default + * change with no struct change: 0.03 floated visibly off the content on + * a real panel); * - isActive is XR_TRUE only while the FILTERED disparity is at least 0.005 * eye baselines in front of the plane. On the plane the OS cursor is the * cursor: draw nothing and show it. The filter keeps running while @@ -258,7 +260,7 @@ typedef struct XrCursorDepthOptionsDXR { /*! * How far in front of the content the cursor floats, as a disparity in * eye-baseline units (> 0). <= 0 or non-finite selects the runtime - * default (0.03; spec v3 used 0.005). + * default (0.015; spec v3 used 0.005, v4 0.03). */ float margin; } XrCursorDepthOptionsDXR; diff --git a/src/xrt/auxiliary/util/u_cursor_depth.c b/src/xrt/auxiliary/util/u_cursor_depth.c index b2354bad9c..729434a9ab 100644 --- a/src/xrt/auxiliary/util/u_cursor_depth.c +++ b/src/xrt/auxiliary/util/u_cursor_depth.c @@ -66,10 +66,11 @@ view_ray(const struct u_cursor_depth_view *view, float u, float v) void u_cursor_depth_tuning_defaults(struct u_cursor_depth_tuning *t) { - // 3% of the baseline: about 2 mm of crossed on-screen disparity at a 65 mm - // IPD - reads as in front without looking detached. Spec v3 briefly used - // 0.005; ADR-046 Amendment 2 A.2 restores 0.03, the value the reference - // app (cube_handle_metal_macos) has shown since Phase 1. + // 1.5% of the baseline: about 1 mm of crossed on-screen disparity at a + // 63-65 mm IPD - reads as in front without looking detached. Spec v3 + // briefly used 0.005; ADR-046 Amendment 2 A.2 set 0.03, halved to 0.015 + // on 2026-10-11 after the Windows Leia panel check (0.03 floated visibly + // off the content). t->margin = U_CURSOR_DEPTH_DEFAULT_MARGIN; // d = -0.6 is t = 0.625: the cursor may come ~3/8 of the way to the eye. t->min_disparity = -0.6f; diff --git a/src/xrt/auxiliary/util/u_cursor_depth.h b/src/xrt/auxiliary/util/u_cursor_depth.h index 32251d7070..2db8d5d00c 100644 --- a/src/xrt/auxiliary/util/u_cursor_depth.h +++ b/src/xrt/auxiliary/util/u_cursor_depth.h @@ -22,7 +22,7 @@ * Disparity, not distance, is what the eye compares, so margins, clamps * and slew rates are expressed there; * - ONE POLICY (ADR-046 Amendment 2 A): the cursor is never behind the - * glass. target = d < 0 ? max(d - margin, d_min) : 0, margin 0.03, and + * glass. target = d < 0 ? max(d - margin, d_min) : 0, margin 0.015, and * nothing is drawn (the OS cursor is the cursor) while the filtered * disparity is above the draw threshold, -0.005 * (u_cursor_depth_should_draw). The filter keeps stepping while nothing @@ -66,8 +66,14 @@ extern "C" { //! Default sprite height, as a fraction of the canvas height. #define U_CURSOR_DEPTH_DEFAULT_HEIGHT 0.03f -//! Default margin in front of content, eye-baseline units (Amendment 2 A.2). -#define U_CURSOR_DEPTH_DEFAULT_MARGIN 0.03f +/*! + * Default margin in front of content, eye-baseline units (Amendment 2 A.2): + * about 1 mm of crossed on-screen disparity at a 63-65 mm IPD. The single + * source for every runtime cursor path (in-process draw, zones, lifted video). + * Halved from 0.03 on 2026-10-11 after the Windows Leia panel check: 0.03 + * floated visibly off the content. + */ +#define U_CURSOR_DEPTH_DEFAULT_MARGIN 0.015f /*! * Default draw threshold, eye-baseline units (Amendment 2 A.3): a sprite is diff --git a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp index e594939e8e..5c0c74456e 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp @@ -2452,10 +2452,13 @@ cursor_draw(struct comp_d3d11_renderer *r, if (!r->cursor.announced) { r->cursor.announced = true; + // The views actually drawn (the request capped to the layout's + // tiles, as the loop below does), like the Metal/GL/VK notices. + const uint32_t drawn = q->count < layout->views ? q->count : layout->views; U_LOG_W( - "cursor_draw: D3D11 compositor drawing the cursor (%s, %u views, %s; ADR-046 Amendment 2 B, " + "cursor_draw: D3D11 compositor drawing the cursor (%s, %u view(s), %s; ADR-046 Amendment 2 B, " "one-time notice)", - q->xsc != nullptr ? "app sprite" : "default look", q->count, + q->xsc != nullptr ? "app sprite" : "default look", drawn, r->compose_active ? "linear compose" : "encoded fast path"); } diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp index 4648404b9e..93a4631bff 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp @@ -3838,10 +3838,13 @@ cursor_draw(struct comp_d3d12_renderer *r, if (!r->cursor.announced) { r->cursor.announced = true; + // The views actually drawn (the request capped to the layout's + // tiles, as the loop below does), like the Metal/GL/VK notices. + const uint32_t drawn = q->count < layout->views ? q->count : layout->views; U_LOG_W( - "cursor_draw: D3D12 compositor drawing the cursor (%s, %u views, %s; ADR-046 Amendment 2 B, " + "cursor_draw: D3D12 compositor drawing the cursor (%s, %u view(s), %s; ADR-046 Amendment 2 B, " "one-time notice)", - app_image ? "app sprite" : "default look", q->count, + app_image ? "app sprite" : "default look", drawn, r->compose_active ? "linear compose" : "encoded fast path"); } diff --git a/src/xrt/compositor/metal/comp_metal_compositor.m b/src/xrt/compositor/metal/comp_metal_compositor.m index fe7510b7a5..a78d73fe99 100644 --- a/src/xrt/compositor/metal/comp_metal_compositor.m +++ b/src/xrt/compositor/metal/comp_metal_compositor.m @@ -4196,13 +4196,6 @@ - (BOOL)wantsUpdateLayer } const float height = size_m / canvas_h_m; // sprite height, in units - if (!c->cursor_draw.announced) { - c->cursor_draw.announced = true; - U_LOG_W("cursor_draw: Metal compositor drawing the cursor (%s, %.1f mm tall, %u views; ADR-046 " - "Amendment 2 B, one-time notice)", - req->image != NULL ? "app sprite" : "default look", size_m * 1000.0f, req->view_count); - } - [encoder setRenderPipelineState:c->cursor_draw.pipe[premul ? 1 : 0][compose_linear ? 1 : 0]]; [encoder setDepthStencilState:c->depth_stencil_state_disabled]; [encoder setFragmentTexture:src atIndex:0]; @@ -4210,6 +4203,14 @@ - (BOOL)wantsUpdateLayer const uint32_t tiles = atlas_cols * atlas_rows; const uint32_t n = req->view_count < tiles ? req->view_count : tiles; + if (!c->cursor_draw.announced) { + c->cursor_draw.announced = true; + // The views actually drawn (= atlas tiles in use), not the locate's + // view count: a 2-view app on a 4-view-capable display draws 2. + U_LOG_W("cursor_draw: Metal compositor drawing the cursor (%s, %.1f mm tall, %u view(s); ADR-046 " + "Amendment 2 B, one-time notice)", + req->image != NULL ? "app sprite" : "default look", size_m * 1000.0f, n); + } const uint32_t att_w = (uint32_t)c->atlas_texture.width; const uint32_t att_h = (uint32_t)c->atlas_texture.height; for (uint32_t i = 0; i < n; i++) { diff --git a/tests/tests_aux_cursor_depth.cpp b/tests/tests_aux_cursor_depth.cpp index 9989c57568..b6dfc860d2 100644 --- a/tests/tests_aux_cursor_depth.cpp +++ b/tests/tests_aux_cursor_depth.cpp @@ -469,11 +469,19 @@ head(xrt_vec3 h, float u, float v) } // namespace -TEST_CASE("cursor_depth: margin default is 0.03 (Amendment 2 A.2)") +TEST_CASE("cursor_depth: margin default is 0.015 (Amendment 2 A.2)") { - // Spec v3 briefly used 0.005; Amendment 2 restores the reference app's 0.03. - CHECK(tuning().margin == Catch::Approx(0.03f)); - CHECK(U_CURSOR_DEPTH_DEFAULT_MARGIN == Catch::Approx(0.03f)); + // Spec v3 briefly used 0.005; Amendment 2 set 0.03, halved to 0.015 on + // 2026-10-11 after the Windows Leia panel check (0.03 floated visibly off + // the content). ~1 mm of crossed on-screen disparity at a 63-65 mm IPD. + CHECK(tuning().margin == Catch::Approx(0.015f)); + CHECK(U_CURSOR_DEPTH_DEFAULT_MARGIN == Catch::Approx(0.015f)); + + // The behaviour, not just the constant: content at d = -0.2 puts the + // default-tuned cursor at -0.215 (it was -0.23 with 0.03). + const auto t = tuning(); + CHECK(u_cursor_depth_target(&t, true, -0.2f) == Catch::Approx(-0.215f).margin(1e-6f)); + CHECK(0.015f * 64.0f == Catch::Approx(0.96f)); // ~1 mm at a 64 mm IPD } TEST_CASE("cursor_depth: HYBRID - pointer moving = SCREEN placement (on the cyclopean ray)")