Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/adr/ADR-046-depth-aware-cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/specs/extensions/XR_DXR_cursor_depth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`. |
8 changes: 5 additions & 3 deletions src/external/openxr_includes/openxr/XR_DXR_cursor_depth.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions src/xrt/auxiliary/util/u_cursor_depth.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 9 additions & 3 deletions src/xrt/auxiliary/util/u_cursor_depth.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
7 changes: 5 additions & 2 deletions src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
15 changes: 8 additions & 7 deletions src/xrt/compositor/metal/comp_metal_compositor.m
Original file line number Diff line number Diff line change
Expand Up @@ -4196,20 +4196,21 @@ - (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];
[encoder setFragmentSamplerState:c->sampler_linear atIndex:0];

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++) {
Expand Down
16 changes: 12 additions & 4 deletions tests/tests_aux_cursor_depth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
Loading