Skip to content

feat: dxr::CursorDepth — app-side helper for the runtime-drawn depth cursor (ADR-046 Amendment 2) - #75

Merged
dfattal merged 1 commit into
mainfrom
feat/cursor-depth-helper
Oct 11, 2026
Merged

dfattal merged 1 commit into
mainfrom
feat/cursor-depth-helper

Conversation

@dfattal

@dfattal dfattal commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

What

common/cursor_depth.h: dxr::CursorDepth, the app-side helper for the depth-aware cursor (XR_DXR_cursor_depth v5, ADR-046 Amendment 2 §B: the runtime draws, the app chooses the look). The runtime-side contract is merged on displayxr-runtime main (#1941). With this helper, a native OpenXR app adopts the runtime-drawn cursor in about 10 lines. The app writes only "ray → first hit".

Adoption

#include "cursor_depth.h"

dxr::CursorDepth cursor;                          // inert until enable()
cursor.enable(cursorDepthSpecVersion);            // extensionVersion if you enabled XR_DXR_cursor_depth, else 0
cursor.setWindowCocoa((__bridge void *)view);     // or setWindowWin32(hwnd), or
                                                  // setPlatform(dxr::cursorPlatformX11(dpy, win))
cursor.setRayCast([&](const dxr::CursorRay &r) -> std::optional<float> {
    return scene.raycast(r.origin, r.dir);        // t of the first hit, or std::nullopt
});

// every frame:
cursor.chain(locateInfo, viewState);
xrLocateViews(session, &locateInfo, &viewState, n, &n, views.data());
cursor.afterLocate(views.data(), activeViewCount); // OS cursor hidden exactly while willDraw
  • Zero cost when not enabled. enable(0) (or never calling it) leaves both struct chains untouched and never touches the OS cursor. When enabled, a frame allocates nothing.
  • Depth source. Three options:
    • setRayCast: footprint rays from the two outer views of the previous locate; the hit nearest the viewer is kept.
    • setHitTest: full control, with the rays already built.
    • useDepthLayer(true) (spec v2): the runtime reads the submitted depth.
  • Footprint is the same rule the runtime and the web SDK use (cursorArrowFootprint): the tip, plus a ring around the arrow body. A custom sprite's footprint follows its hotspot and aspect. dxr::cursorFootprintRays() is public for apps that do their own chaining.
  • Chaining is gated per spec version:
    • on XrViewLocateInfo: hint → [drawInfo v5] → [options v3] → [source v2] → the app's previous chain;
    • on XrViewState: drawState → [placement] → previous.
  • Never no cursor. In Runtime mode the OS cursor is hidden only while willDraw. On a pre-v5 runtime it is never hidden. DrawMode::App keeps v1–v4 app-drawn placement working (placement(), appShouldDraw()).
  • No graphics-API code. The core is header-only and platform-neutral, so it is also part of displayxr::rules (Android).
Window Adapter Hide
Win32 HWND cursor_depth_win32.cpp WM_SETCURSOR/HTCLIENT → SetCursor(NULL) via a chained wndproc (installable from the render thread), client area only
Cocoa NSView* cursor_depth_macos.mm balanced NSCursor hide/unhide, on the main thread
Xlib (Display*, Window) header-only cursor_depth_x11.h (the app links libX11; this lib does not) invisible pixmap cursor on the window
Wayland / Android / displayxr::linux_window / anything else manual: setPointerUV() / clearPointerUV() + onDepthCursorShownChanged(cb) yours, e.g. win.set_cursor(shown ? DxrCursor::Hidden : DxrCursor::Default)

Other changes:

  • The standalone extension-header pin moves to displayxr-extensions@af2de88 (XR_DXR_cursor_depth SPEC_VERSION 5).
  • README section added.
  • The Linux CI legs build and run the new test, plus an X11 adapter smoke test under Xvfb.

Evidence

  • tests/cursor_depth_test.cpp (new displayxr_cursor_depth_test) passes on macOS, and all 6 ctest targets pass locally. It covers:
    • canvas UV, flipped and unflipped;
    • the footprint against the web SDK's numbers;
    • footprint rays against a Kooima pair, also under a camera-rig transform (rotation + translation + ×3 scale). Every ray crosses the canvas at its footprint point;
    • nearest-hit reduction;
    • chain order for v1 / v5 / every optional struct / pointer off the canvas;
    • nothing chained when disabled;
    • show/hide transitions, including the pre-v5 "never hide" rule and the destructor restore.
  • Mutation check: I applied 9 targeted mutants to the core (outer-view index, arrow offset, chain link, nearest-vs-farthest, the stale-willDraw guard, the disabled guard, the fov→ray v-axis, and two spec-version gates). The test killed all 9.
  • cursor_depth_win32.cpp compiles clean with MinGW-w64 -Wall -Wextra. The header and .mm are clean under clang -Wall -Wextra.
  • End-to-end: the runtime's cube_handle_metal_macos is ported onto this branch: test_apps(cube_handle_metal_macos): adopt the depth cursor through dxr::CursorDepth (needs displayxr-common tag) displayxr-runtime#1943 (draft). I built it against this branch via FETCHCONTENT_SOURCE_DIR_DISPLAYXR_COMMON, ran it headless (SIM_DISPLAY_OUTPUT=sbs DISPLAYXR_CURSOR_DEPTH_UV=…) and read the atlas captures:
    • At UV 0.5,0.5 the runtime-drawn arrow sits over the cube with −21 to −24 px disparity and target disparity −0.066. A baseline binary built from main's hand-written code measured the same on the same box (−21 / −24 px, −0.066).
    • At UV 0.1,0.1 nothing is drawn.
    • The DISPLAYXR_CURSOR_DEPTH_DRAW=app crosshair and the DISPLAYXR_CURSOR_DEPTH_SPRITE=custom ring both render.
    • With DISPLAYXR_CURSOR_DEPTH=0 nothing is drawn.

Not tested

  • The Win32 adapter has only been compiled (MinGW), never run. That covers the wndproc chaining, cursor hiding over the client area, the refresh message, and restore on destruction. No Windows box was used, and the runtime's D3D backends report willDraw = FALSE today anyway.
  • X11 adapter: it has not been compiled or run locally (no Xlib on this mac). The only evidence is the new Linux CI target (compile + Xvfb smoke).
  • Wayland / Android: the manual path has unit-test coverage only. No app on those platforms uses it yet.
  • macOS adapter, interactive: the headless runs use the scripted setPointerUV(). I did not exercise real mouse movement with NSCursor hide/unhide on a panel.
  • No tag. The runtime cube-app PR needs a displayxr-common tag that contains this change before it can merge.

🤖 Generated with Claude Code

…cursor (ADR-046 Amendment 2)

XR_DXR_cursor_depth v5 lets the runtime draw the depth cursor; the app only
owns the depth under the pointer. common/cursor_depth.h does the rest of the
per-frame bookkeeping every app would otherwise re-type:

- pointer -> canvas UV (window adapters: Win32 HWND, Cocoa NSView*, Xlib
  Display*/Window; a manual setPointerUV() + onDepthCursorShownChanged()
  path for Wayland / Android / displayxr::linux_window / anything else);
- the cursor FOOTPRINT (tip + ring around the arrow body, the same rule as
  the runtime and the web SDK's cursorArrowFootprint) as rays from the two
  outer views of the PREVIOUS locate; setRayCast() reduces "ray -> first
  hit" to the hint's nearest point, setHitTest() gives full control,
  useDepthLayer() hands it to the runtime (spec v2);
- chaining hint [-> drawInfo] [-> options] [-> source] on XrViewLocateInfo
  and drawState [-> placement] on XrViewState, gated per spec version;
- hiding the OS cursor exactly while willDraw (or, in DrawMode::App, while
  the placement is active), never on a runtime that cannot draw.

Zero cost when not enabled: enable(0) leaves both chains untouched and the
OS cursor alone; enabled, a frame allocates nothing. No graphics-API code.
The core is header-only and platform-neutral, so it is part of
displayxr::rules (Android legs can use it).

Tests (tests/cursor_depth_test.cpp): canvas UV, footprint vs the web rule,
footprint rays against a Kooima pair under a camera-rig transform, nearest
hit, chain order per spec version, show/hide transitions; all 9 mutants of
the core are killed. The X11 adapter gets a compile + Xvfb smoke target on
the Linux CI legs.

The standalone extension-header pin moves to displayxr-extensions@af2de88
(XR_DXR_cursor_depth SPEC_VERSION 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/cursor-depth-helper branch from b627517 to 4ff0525 Compare October 11, 2026 09:06
@dfattal
dfattal merged commit 1d70764 into main Oct 11, 2026
6 checks passed
@dfattal
dfattal deleted the feat/cursor-depth-helper branch October 11, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant