Skip to content

feat(cursor): runtime-drawn depth cursor in display-zones frames (#1949) - #1950

Merged
dfattal merged 2 commits into
mainfrom
feat/cursor-zones
Oct 11, 2026
Merged

dfattal merged 2 commits into
mainfrom
feat/cursor-zones

Conversation

@dfattal

@dfattal dfattal commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #1949.

The runtime-drawn depth cursor (ADR-046 Amendment 2 §B, #1941 Metal / #1946 Vulkan+GL) skipped display-zones frames and reported willDraw = false. With this PR it draws one cursor per frame, in the zone whose zone-scoped locate carried the hint (the zone under the pointer).

Design

  • State tracker. The arming locate records the XrDisplayZoneDXR::zoneId it carried. That locate's canvas is the zone rect, so cursorUV, the placement and its unit are zone-relative already. In a zones frame, xrEndFrame maps the placement through the views of the projection layer chained with that zone: its submitted poses and fovs, with the same-space rule unchanged. The result is a zone-normalised map. u_cursor_draw_request gains zone_valid, zone_id and zone_rect, where zone_rect is the rect as submitted (authoritative per ADR-027). The fallback is unchanged in three cases: no zone with that id, a zone-scoped hint in a frame without zones, and a zones frame whose hint was not zone-scoped. In each, nothing is drawn, the next locates report willDraw = false, and a one-time WARN is logged.
  • Two pure aux helpers used at every draw site:
    • u_cursor_draw_into_zone() re-expresses the zone-normalised views in the tile exactly as each compositor places the zone's own layer (zone rect × tile / window). It also returns the zone's sub-rect as a clip.
    • u_cursor_draw_canvas_px_h() sizes the sprite against the zone's height.
  • Metal, vk_native, GL now accept zone requests and scissor the draw to tile ∩ zone, so the cursor never bleeds into a 2D zone or another 3D zone. A request without a zone is untouched: its clip is the whole tile, as before.
  • There is no new XrStructureType and no spec bump, because willDraw already reports the change to the app frame by frame. The spec text and the ADR-046 as-built notes are updated.
  • §0 still holds: there is zero cost without a request. Every new line runs only on a frame whose request is already set.
  • D3D11/D3D12 are untouched (draft feat(cursor): the runtime draws the depth cursor — D3D11 + D3D12 (ADR-046 Amendment 2 §B) #1944). That PR only has to call the same two helpers at its draw site; see the comment there.

Evidence (this Mac, sim-display, scripted pointer DISPLAYXR_CURSOR_DEPTH_UV, atlas capture)

  • Unit tests (tests_aux_cursor_depth): two new cases.
    • An off-centre zone with 4 eyes (N-view) and an off-axis viewer. The remapped zone map equals the map computed with views framed to the whole window (the oracle) for every view, at two disparities. Also checked: the hotspot lands at the zone UV inside the zone, and the canvas height is the zone's.
    • Clip and refusal cases: a zone hanging off the window edge, an empty zone, a zone outside the window, and no target.
    • All pass (24468 assertions). Mutation check: 8/8 mutants killed (axis scales, origin offsets, clip clamp, outside-accept, zone ignored, canvas height).
  • cube_zones_metal_macos (Metal, 2 tiles of 1512×823):
    • Pointer at window UV (0.25, 0.60), i.e. zone A at UV (0.500, 0.467): willDraw=TRUE, disparity −0.049. The arrow is on zone A's cube in both tiles at x ≈ 385 and ≈ 370, y ≈ 493 (expected 378, 494). It pops out (crossed disparity). Zone B is clean.
    • Pointer at (0.745, 0.47), in zone B: the arrow is drawn only on zone B's cube, at (1128, 385) and (1123, 387), expected (1126, 387). Zone A is clean.
    • Pointer at (0.5, 0.1), over the Local2D strip: no hint, nothing drawn anywhere.
  • cube_zones_vk_macos (vk_native, 2 tiles of 1280×720):
    • Zone A: the arrow is at (328, 432) and (312, 432), expected 320, 432. Zone B is clean.
    • Zone B: the arrow is on zone B's cube only. Zone A is clean.
  • Regression without zones: cube_handle_metal_macos, cube_handle_vk_macos and cube_handle_gl_macos (DISPLAYXR_CURSOR_DEPTH=1) all still draw the cursor in front of the cube in both tiles.
  • The test apps share test_apps/common/cursor_depth_zones.h, a thin wrapper over dxr::CursorDepth (displayxr-common v2.29.0, the same pin as the cube_handle apps). It picks the topmost 3D zone under the pointer, feeds the pointer in that zone's UV, and chains the hint only on that zone's locate.

Not tested

  • GL zones path: there is no _handle GL zones app on macOS, and the cube_zones_texture_* apps were not adopted. The GL change is covered only by the shared aux math, the matching scissor arithmetic and the non-zones GL regression run.
  • Real Leia hardware, Windows and Linux. Linux vk_native shares the renderer code, but nothing was run there.
  • Overlapping zones (O key) with the pointer in the overlap. The topmost zone is picked by layer order, and the clip is that zone's rect, so the cursor is drawn over the lower zone's content in the overlap. That is correct for alpha-over composition, but it was not captured.
  • Live mouse (only the scripted pointer was used), the custom-sprite path in zones, and the depth-layer source (XrCursorDepthSourceDXR) in zones. Zone layers have no depth variant, so that source still reports no content there.

🤖 Generated with Claude Code

dfattal and others added 2 commits October 11, 2026 05:21
The runtime-drawn cursor (ADR-046 Amendment 2 B) refused zones frames and
reported willDraw = false. Now one cursor per frame is drawn in the zone
whose zone-scoped locate carried the hint (the zone under the pointer).

- State tracker: the arming locate records its XrDisplayZoneDXR zoneId; in
  a zones frame xrEndFrame maps the placement through the views of the
  projection layer chained with that zone (zone-normalised map) and hands
  the compositor the zone rect as submitted. Missing zone / mismatched
  hint: not drawn, willDraw = false, one-time WARN (same fallback).
- u_cursor_draw_into_zone(): re-express the zone map in the tile the way
  each compositor places the zone's own layer, and return the zone's clip;
  u_cursor_draw_canvas_px_h(): size against the zone's height.
- Metal, vk_native, GL: draw zone requests, scissored to tile ∩ zone.
- Tests: N-view (4 eyes) off-axis zone vs full-window oracle, clip and
  refusals; all 8 mutants killed.
- Spec + ADR-046 as-built notes. No new struct, no spec bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared test_apps/common/cursor_depth_zones.h wraps dxr::CursorDepth for a
zones app: pick the topmost 3D zone under the pointer, feed the pointer in
that zone's UV, chain the hint only on that zone's locate. Pins
displayxr-common v2.29.0 (as the cube_handle apps). DISPLAYXR_CURSOR_DEPTH=0
opts out; DISPLAYXR_CURSOR_DEPTH_UV=u,v scripts a window-normalised pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal marked this pull request as ready for review October 11, 2026 12:35
@dfattal
dfattal requested a review from a team as a code owner October 11, 2026 12:35
@dfattal
dfattal merged commit 53e7b7e into main Oct 11, 2026
42 checks passed
@dfattal
dfattal deleted the feat/cursor-zones branch October 11, 2026 12:35
dfattal added a commit that referenced this pull request Oct 11, 2026
…es (#1949)

Adopt #1950 at the D3D resolve step, matching Metal / vk_native / GL: a
zone request is re-expressed in the tile with u_cursor_draw_into_zone()
(the window dims the renderers scale zone_3d.rect by), sized against the
zone with u_cursor_draw_canvas_px_h(), and scissored to tile box ∩ the
returned clip (new comp_d3d11/12_cursor_quads::clip). Skip only on a zones
frame / zone request mismatch (one-time WARN). A non-zone request clips to
the whole tile, so that path is unchanged.

#1951's willDraw gate is state-tracker-side (u_cursor_draw_placement_drawable
on located + submitted views); D3D goes through the same dispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dfattal added a commit that referenced this pull request Oct 11, 2026
…-046 Amendment 2 §B) (#1944)

* feat(cursor): the runtime draws the depth cursor on D3D11 + D3D12 (ADR-046 Amendment 2 B)

XR_DXR_cursor_depth v5 on the in-process D3D11 and D3D12 compositors,
mirroring the Metal pass (#1941). The state tracker dispatch is additive:
oxr_session_cursor_draw_supported / _hand_request now also route to
comp_d3d11/12_compositor_set_cursor_draw_request, so these backends report
willDraw = true instead of the one-time "cannot draw" WARN.

Compositors: the one-frame request is consumed by the next commit (one
field test otherwise); a cursor frame never zero-copies; the request is
resolved into per-tile corners (u_cursor_draw_corners, sprite metres over
canvas metres from the window metrics' pixel pitch) and handed to the
renderer before the projection pass. Routed multi-screen frames draw
nothing (one-time WARN).

Renderers: drawn LAST in the window-space pass - after the projection,
quad and window-space layers, before the compose publish and (D3D12) the
atlas's final barrier, so before the DP. One straight-alpha strip per
tile, depth off, no culling, scissored. Shared HLSL in
d3d_shared/comp_cursor_shaders.h, D3DCompile'd on the first cursor frame
only (DXR_LAYERED variant samples array layer 0). D3D12 reuses the quad
root signature, four PSOs [layered][_SRGB target]. Colour per ADR-044: the
frame's compose target + format-honest source view, or the encoded atlas
+ non-decoding view; a non-_SRGB sprite makes the frame compose.

Default look (u_cursor_look, aux, API-agnostic): Windows mirrors the OS
cursor - GetCursorInfo -> HCURSOR, DrawIconEx into a 32-bit DIB over black
and over white, u_cursor_draw_unblend recovers straight RGBA (handles
alpha, colour+mask and monochrome AND/XOR cursors; XOR pixels -> opaque
white), at GetSystemMetricsForDpi(SM_CYCURSOR, window DPI), cached by
HCURSOR + DPI. While the app has hidden its cursor (SetCursor(NULL)) the
class cursor of the window under the pointer is used. Elsewhere or with
DXR_CURSOR_DEFAULT_LOOK=arrow: the classic arrow, un-premultiplied.

Pure helpers u_cursor_draw_unpremultiply / u_cursor_draw_unblend and the
arrow path of u_cursor_look are unit-tested (mutants killed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test_apps: depth cursor (XR_DXR_cursor_depth v5) in cube_handle_d3d11/d3d12_win

Reference adoption mirroring cube_handle_metal_macos: analytic cube
footprint hit test -> XrCursorDepthHintDXR; DISPLAYXR_CURSOR_DEPTH_DRAW=
runtime (default) | app; DISPLAYXR_CURSOR_DEPTH_SPRITE=custom (64x64 cyan
ring sprite swapchain, filled via UpdateSubresource / CopyTextureRegion);
DISPLAYXR_CURSOR_DEPTH_UV=u,v scripted pointer; DISPLAYXR_CURSOR_DEPTH=0
opt-out. The OS cursor is hidden over the client area exactly while
willDraw (WM_SETCURSOR -> SetCursor(NULL); D3D12 posts the flip from its
render thread to the window thread). App mode keeps the OS cursor and logs
the placement (no in-app crosshair on these renderers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(cursor): D3D11/D3D12 default look -> the shared comp_cursor_look

Follow #1946, which moved the default look into
compositor/util/comp_cursor_look (Metal, vk_native, GL): one entry point for
every backend instead of a second module in aux.

- The Windows GetCursorInfo/DrawIconEx mirror becomes the Windows platform
  half, comp_cursor_look_win.c (the stub is now !__APPLE__ && !_WIN32;
  links user32 + gdi32). Backing scale = DPI / 96; height_pt = drawn px /
  scale. Hidden-cursor fallback (class cursor under the pointer, else last,
  else IDC_ARROW) and the unreadable-handle memo kept (two additive fields).
- aux u_cursor_look.{c,h} removed; u_cursor_draw_unblend now outputs
  PREMULTIPLIED (the look's contract), u_cursor_draw_unpremultiply dropped.
- D3D11/D3D12 renderers blend the default look premultiplied (D3D11: a
  second blend state; D3D12: a third PSO variant), the app sprite straight.
- comp_d3d11 / comp_d3d12 link comp_cursor_look. ADR section updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(cursor): D3D11/D3D12 draw the depth cursor in display-zones frames (#1949)

Adopt #1950 at the D3D resolve step, matching Metal / vk_native / GL: a
zone request is re-expressed in the tile with u_cursor_draw_into_zone()
(the window dims the renderers scale zone_3d.rect by), sized against the
zone with u_cursor_draw_canvas_px_h(), and scissored to tile box ∩ the
returned clip (new comp_d3d11/12_cursor_quads::clip). Skip only on a zones
frame / zone request mismatch (one-time WARN). A non-zone request clips to
the whole tile, so that path is unchanged.

#1951's willDraw gate is state-tracker-side (u_cursor_draw_placement_drawable
on located + submitted views); D3D goes through the same dispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test_apps: depth cursor on cube_zones_d3d11/d3d12_win (display zones, #1949)

Same adoption as cube_zones_{metal,vk}_macos: the shared
test_apps/common/cursor_depth_zones.h (dxr::CursorDepth, displayxr-common
v2.29.0) picks the topmost 3D zone under the pointer and chains the hint
on that zone's locate only. The header gains an additive Win32 path
(initWin32: dxr::CursorDepth's Win32 window adapter for pointer UV and the
WM_SETCURSOR hide; app mode attaches no adapter). The apps enable
XR_DXR_cursor_depth unless DISPLAYXR_CURSOR_DEPTH=0;
DISPLAYXR_CURSOR_DEPTH_DRAW=runtime|app, DISPLAYXR_CURSOR_DEPTH_UV=u,v
(window-normalised).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cursor): cap the runtime-drawn cursor to the ACTIVE views; log the first OS-cursor mirror (#1944 Windows verification)

Two quirks from the Windows panel run of #1944:

- The cursor request took proj->viewCount views. An app submitting for the worst-case mode
  under PRIMARY_MULTIVIEW_DXR (the D3D cube apps submit 4 while the active mode has 2) put
  inactive views into the request: the one-time notice read "4 views", and those views' poses
  fed u_cursor_draw_placement_drawable. The arming locate now records its active view count
  (sess->cursor_draw.view_count) and xrEndFrame caps the request to it. The renderers already
  clamped drawing to the layout's tiles, so pixels are unchanged.
- "default look mirrors the OS cursor" was U_LOG_I, which never reaches the log file, so the
  mirror looked like it never ran. The first mirror is now a one-time WARN; later shape
  changes stay INFO.

Verified on the win SR laptop: the notice reads "2 views", the mirror line logs
"64x64 px at 240 DPI", and David's live mouse check still passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

cursor_depth: runtime draw in display-zones frames (ADR-046 Amendment 2 §B)

1 participant