Skip to content

feat(cursor): the runtime draws the depth cursor — D3D11 + D3D12 (ADR-046 Amendment 2 §B) - #1944

Merged
dfattal merged 6 commits into
mainfrom
feat/cursor-runtime-draw-d3d
Oct 11, 2026
Merged

dfattal merged 6 commits into
mainfrom
feat/cursor-runtime-draw-d3d

Conversation

@dfattal

@dfattal dfattal commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Rebased on #1908 (spec v6), #1946 (Vulkan + GL, shared comp_cursor_look), #1950 (cursor in display-zones frames) and #1951 (willDraw drawability gate). Draft until a Windows box runs the verification recipe below.

Extends ADR-046 Amendment 2 §B ("the runtime draws the depth cursor") from Metal (#1941) to the in-process D3D11 and D3D12 compositors, and adopts it in cube_handle_d3d11_win / cube_handle_d3d12_win. No API change: it implements the v5 XrCursorDepthDrawInfoDXR/XrCursorDepthDrawStateDXR only — no new type values, no spec bump (the current spec stays v6, from #1908).

What changed

  • State tracker (additive only). oxr_session_cursor_draw_supported() / oxr_session_cursor_draw_hand_request() gain a D3D11 and a D3D12 arm (comp_d3d11/12_compositor_set_cursor_draw_request). These backends now report willDraw = true where they used to log the one-time "cannot draw" WARN. Nothing else in the state tracker is touched.

  • Compositors (comp_d3d11_compositor.cpp, comp_d3d12_compositor.cpp). The next commit consumes the one-frame request with one field test. A cursor frame never zero-copies. The request becomes per-tile corners via u_cursor_draw_corners(): sprite height in metres ÷ canvas height in metres, where canvas metres = (zone canvas or target) px × the panel's pixel pitch from the window metrics. The corners go to the renderer before the projection pass. Multi-screen routed frames draw nothing (one-time WARN), the same as the Metal gap.

  • Display zones (feat(cursor): runtime-drawn depth cursor in display-zones frames (#1949) #1950). The resolve step copies the request and calls u_cursor_draw_into_zone(&req, tgt_width, tgt_height, clip), using the window dims both renderers scale zone_3d.rect by. It sizes the sprite with u_cursor_draw_canvas_px_h() and passes the clip to the renderer, which scissors to tile box ∩ clip. It skips 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.

  • willDraw gate (fix(cursor): willDraw only when the runtime really draws; stale HYBRID anchor put the cursor behind the eye #1951). This is state-tracker-side (u_cursor_draw_placement_drawable on located + submitted views). D3D goes through the same dispatch, so it is gated the same way.

  • Renderers (comp_d3d11_renderer.cpp, comp_d3d12_renderer.cpp). The cursor is drawn last in the window-space pass:

    • after the projection, quad and window-space layers;
    • before the compose publish and, on D3D12, before the atlas's final barrier — so before the DP.

    Each tile gets one straight-alpha triangle strip, depth off (no DSV), no culling, scissored to the tile. A frame that has a cursor but no window-space layer still runs the pass.

    • HLSL: one shared pair in d3d_shared/comp_cursor_shaders.h, compiled with D3DCompile (the way every in-process D3D shader here is) on the first cursor frame only. A DXR_LAYERED variant samples array layer 0 of a layered sprite, as the spec requires.
    • D3D11: its own lazily built blend, rasterizer and CLAMP sampler states.
    • D3D12: reuses the quad root signature (t0 table + 32 root constants; the cursor uses 16). It has four PSOs, [layered][_SRGB target], because a D3D12 PSO bakes its RTV format. The app sprite is dipped RENDER_TARGET → PSR → RENDER_TARGET, as window-space layers are.
  • Colour (ADR-044). The cursor gets the frame's colour treatment:

    • On a composing frame it goes into the private _SRGB compose target through the source's format-honest (decoding) view.
    • On a fast-path frame it goes straight into the encoded atlas through the non-decoding view.
    • A non-_SRGB (scene-linear) app sprite forces the frame to compose.
    • The default look is uploaded once per shape into a TYPELESS texture that has both a UNORM and an _SRGB view, so the white arrow comes out white on either path.
    • As on Metal, the cursor is not counted as a contributing layer, so on fast-path frames its anti-aliased edges blend in encoded space.
  • Default look: the shared compositor/util/comp_cursor_look (feat(cursor): the runtime draws the depth cursor — Vulkan + GL (ADR-046 Amendment 2 §B) #1946's entry point, also used by Metal, vk_native and GL). This PR adds its Windows half, comp_cursor_look_win.c (see below). Off macOS/Windows, or with DXR_CURSOR_DEFAULT_LOOK=arrow, it is the shared procedural arrow. The look is premultiplied, so D3D11/D3D12 blend it premultiplied (D3D11: a second blend state; D3D12: a third PSO variant) and the app sprite straight.

  • New pure helper in u_cursor_depth: u_cursor_draw_unblend() (premultiplied output), with tests.

  • Reference apps. cube_handle_d3d11_win and cube_handle_d3d12_win follow cube_handle_metal_macos:

    • an analytic cube-footprint hit test feeds the hint;
    • DISPLAYXR_CURSOR_DEPTH_DRAW=runtime (default) or app;
    • DISPLAYXR_CURSOR_DEPTH_SPRITE=custom gives a 64×64 cyan ring sprite swapchain, filled with UpdateSubresource on D3D11 and an upload buffer + CopyTextureRegion on D3D12;
    • DISPLAYXR_CURSOR_DEPTH_UV=u,v scripts the pointer; DISPLAYXR_CURSOR_DEPTH=0 opts out.

    The pointer UV comes from GetCursorPos + ScreenToClient over the client rect. The OS cursor is hidden over the client area exactly while willDraw: WM_SETCURSOR → SetCursor(NULL), plus an immediate SetCursor when the flag flips. D3D12 posts the flip from its render thread to the window thread. Difference from Metal: app mode keeps the OS cursor and logs the placement, with no in-app crosshair on these renderers.

  • Zones reference apps. cube_zones_d3d11_win / cube_zones_d3d12_win adopt the cursor through the shared test_apps/common/cursor_depth_zones.h. That header gains an additive Win32 path (initWin32) on dxr::CursorDepth's Win32 adapter (displayxr-common v2.29.0, already the test_apps pin). It provides the pointer UV, the WM_SETCURSOR hide, and chains the hint on the topmost 3D zone's locate only. DISPLAYXR_CURSOR_DEPTH=0, _DRAW=runtime|app and _UV=u,v (WINDOW-normalised) work as in the macOS zones apps.

  • Build: comp_cursor_look gains comp_cursor_look_win.c on WIN32 (links user32 + gdi32); comp_d3d11 / comp_d3d12 link comp_cursor_look. Two additive fields on struct comp_cursor_look (os_scale, os_failed).

  • Docs: ADR-046 gets a new "§B as built on D3D11 and D3D12" section plus status-line and Not-yet edits; the spec's v5 backends line is updated; the DXR_CURSOR_DEFAULT_LOOK census row is updated.

How the OS-cursor mirror works (Windows, comp_cursor_look_win.c)

  1. GetCursorInfo() → HCURSOR. The call is global, not per-thread or per-process, so the compositor thread can make it.
  2. GetIconInfo gives the hotspot and native size: the colour bitmap, or the mask height ÷ 2 for monochrome cursors.
  3. The cursor is drawn at the size the OS draws it on this panel, GetSystemMetricsForDpi(SM_CYCURSOR, GetDpiForWindow(hwnd)) (resolved dynamically). If the image's own size is larger (accessibility pointer sizes), that size is used instead.
  4. It is drawn with DrawIconEx into a top-down 32-bit DIB twice, over black and over white. u_cursor_draw_unblend recovers premultiplied RGBA: alpha = 1 − mean(white − black), and the render over black is the premultiplied colour. This reads every cursor type the same way (32-bit alpha, colour + AND mask, monochrome AND/XOR) and makes XOR "invert" pixels opaque white. The undefined GDI byte 3 is never read.
  5. Physical size = drawn pixel height × the panel's pixel pitch. The look reports height_pt = drawn px ÷ (DPI/96), and the compositor multiplies back by its window's DPI/96.
  6. The result is cached by HCURSOR + DPI. Each frame costs one GetCursorInfo, and the image is re-rasterised only when the shape or DPI changes. An unreadable handle is remembered and not retried.
  7. The hidden-cursor case. The app hides its cursor with SetCursor(NULL) as soon as a locate reports willDraw, which on D3D11 happens before that frame's xrEndFrame. So GetCursorInfo usually returns NULL on exactly the frames that draw. In that case the mirror uses the class cursor of the window under the pointer (WindowFromPoint → GCLP_HCURSOR). If there is none, it keeps the last shape, and failing that uses IDC_ARROW. An app that changes its cursor per region inside WM_SETCURSOR and wants that shape should pass its own sprite.

Evidence

  • Unit tests (macOS, build-tests, on the current head). tests_aux_cursor_depth: 40/40 cases, 24617 assertions, including feat(cursor): runtime-drawn depth cursor in display-zones frames (#1949) #1950/fix(cursor): willDraw only when the runtime really draws; stale HYBRID anchor put the cursor behind the eye #1951's. Mine: unblend recovers premultiplied colour + alpha over a sweep of coverages (and un-premultiplies back to the drawn colour); unblend handles monochrome AND/XOR, with inverted pixels opaque white. Mutants: alpha from GDI byte 3 → 60 assertions fail; colour taken from the white render → 53 fail. Restored, all pass.
  • MinGW-w64 (scripts/build-mingw-check.sh aux_util): PASSED, and a manual -fsyntax-only of comp_cursor_look_win.c (the GDI path, also clean under -Wall -Wextra), comp_cursor_look.c, comp_d3d11_renderer.cpp and comp_d3d12_renderer.cpp is clean. The two compositor .cpp files stop at WIL, the known MinGW limit, so MSVC CI is canonical for them.
  • check_displayxr_app.py: no issues on either cube app.
  • Windows CI: green on each previous head: pre-feat(cursor): the runtime draws the depth cursor — Vulkan + GL (ADR-046 Amendment 2 §B) #1946, including Runtime, TestApps and CTS d3d11, and the comp_cursor_look refactor head. See the checks for the current head.
  • Zones apps: check_displayxr_app.py is clean on both, and a MinGW -fsyntax-only -Wall of a translation unit using the shared header's Win32 path is clean. The app main.cpp files themselves are only checked by MSVC CI.

Windows verification recipe

Build: scripts\build_windows.bat build then scripts\build_windows.bat test-apps. Register the ABI-matched DP if this box needs it: scripts\register_dev_plugin.bat for sim_display, or the dev Leia plug-in. Use a non-elevated terminal so XR_RUNTIME_JSON is honoured. No service is needed; these are in-process _handle apps.

Capture. The in-process D3D11 and D3D12 compositors use the shared file trigger (u_capture_intent), which writes the post-compose, pre-weave atlas (the cursor is in it):

del %TEMP%\displayxr_atlas*.png 2>nul
type nul > %TEMP%\displayxr_atlas_trigger.cube_handle_d3d11_win
:: ~1 s later: %TEMP%\displayxr_atlas.cube_handle_d3d11_win.png   (d3d12: ...cube_handle_d3d12_win)

Use the post-compose trigger, not .projection: the projection-only capture is taken before the cursor is drawn. Alpha is forced opaque in the PNG, which is fine here. Per-run logs are in %LOCALAPPDATA%\DisplayXR\DisplayXR_cube_handle_d3d11_win.<pid>_<ts>.log.

# Run (from _package, env set first) Expected
1 set DISPLAYXR_CURSOR_DEPTH_UV=0.5,0.5 then run_cube_handle_d3d11_win.bat, then trigger The atlas shows the OS arrow in both tiles at the pointer row, over the cube. Tile-local x of the arrow tip differs between tiles: right-tile x − left-tile x < 0 (crossed = in front). Metal at a fixed sim eye gave about −24 px. Log, once each: XR_DXR_cursor_depth: runtime-drawn cursor requested (default look), cursor_draw: default look mirrors the OS cursor (WxH px…) (32×32 at 100 % scaling, 48×48 at 150 %), cursor_draw: D3D11 compositor drawing the cursor (default look, N views, …). App log: cursor_depth: runtime willDraw=TRUE. There must be no this compositor cannot draw the cursor yet WARN.
2 set DISPLAYXR_CURSOR_DEPTH_UV=0.1,0.1 (off the cube) Nothing drawn: no cursor in the atlas, willDraw=FALSE, no cursor_draw: lines.
3 as 1 + set DXR_CURSOR_DEFAULT_LOOK=arrow The classic white arrow with a dark outline, tip on the pointer, the same disparity sign. It must be white, not grey: that checks colour.
4 as 1 + set DISPLAYXR_CURSOR_DEPTH_SPRITE=custom A cyan ring with a centre dot, centred on the pointer (hotspot 0.5,0.5), about 1 cm tall on the panel, the same disparity sign. Log: (app sprite, …).
5 Live mouse (unset DISPLAYXR_CURSOR_DEPTH_UV), eyeball the panel Over the cube the OS cursor disappears and the depth cursor pops out in 3D. Off the cube, over the floor or empty space, the flat OS cursor is back. On the title bar or borders the OS cursor is always shown. Hidden only while drawn.
6 set DISPLAYXR_CURSOR_DEPTH_DRAW=app The OS cursor is never hidden, nothing is drawn by the runtime, and placement is logged (cursor_depth: uv=…).
7 Repeat 1–6 with run_cube_handle_d3d12_win.bat (…cube_handle_d3d12_win trigger) Same results. Log line says D3D12 compositor drawing the cursor.
9 cube_zones_d3d11_win (non-elevated, XR_RUNTIME_JSON as for the cube apps) with DISPLAYXR_CURSOR_DEPTH_UV=0.25,0.55 The cube is in zone A (window (0,180) 640×540). The cursor is drawn in zone A's tiles only, clipped to the zone. App log: cursor_depth (zones): zone #0 … willDraw=TRUE.
10 as 9, with UV 0.75,0.45, then 0.5,0.1 (Local2D strip), 0.523,0.5 (gap), 0.05,0.925 (zone A, off the cube) First: zone #1 … willDraw=TRUE, cursor in zone B only, clipped. Strip and gap: nothing drawn, OS cursor kept. Off the cube: willDraw=FALSE, nothing drawn. If a scripted UV misses the cube, nudge v by ±0.03.
11 cube_zones_d3d12_win, UV 0.25,0.60 / 0.75,0.60 / 0.5,0.15 / 0.25,0.93 In order: zone A, then zone B (each willDraw=TRUE, cursor clipped to its zone); nothing above the zones; willDraw=FALSE off the cube.
12 Zones apps, live mouse The OS cursor disappears only over a cube inside a 3D zone; the title bar, borders, strip, gap and empty zone area keep it. A cursor at a zone edge never bleeds into the neighbouring zone or the 2D strip.
8 Optional: 1 with set DXR_SWAPCHAIN_ENCODING=unorm + SPRITE=custom (the sprite becomes UNORM) The ring is the same cyan, and the frame composes (Color (#1610) … compose target WARN).

Not tested

  • All runtime behaviour on Windows. I wrote this on macOS and have run none of the D3D11/D3D12 drawing, the GDI cursor mirror, the DPI sizing or the test apps. Correctness rests on mirroring the Metal pass and the existing D3D layer passes, the MinGW syntax check (renderers only) and MSVC CI compiling it. Every row of the recipe above is unverified.
  • No real panel and no eye tracking, on any backend.
  • The display-zones path (no D3D zones frame has been run), transparent sessions, the D3D12 epic: in-process tier parity — panel-rate weave everywhere (VK / D3D12 to match d3d11-hybrid smoothness) #1264 reroute (external atlas) and the Epic: weave/repaint next to scanout — output-device split for hybrid GPUs #918 output-device split. The cursor is drawn into the atlas before the bridge copy, but this has not been run.
  • Animated cursors: only frame 0 is drawn. Per-monitor DPI changes mid-session should re-rasterise, but this is untested. A DPI-unaware host app gets GetDpiForWindow = 96.
  • The hidden-cursor class-cursor fallback against an app whose class cursor is NULL (it falls back to the last shape or the arrow).

Not done (follow-ups)

🤖 Generated with Claude Code

@dfattal

dfattal commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up from #1950 (#1949: depth cursor in display-zones frames). Once #1950 lands, here is what the D3D11/D3D12 draw needs to add. #1950 does not touch D3D.

  • u_cursor_draw_request gains zone_valid, zone_id and zone_rect (xrt_rect, client-window px, as submitted). The state tracker now sends a zones frame a zone request: its views are ZONE-normalised, projected through the hinted zone's layer.
  • At the D3D draw site:
    1. Copy the request, then call u_cursor_draw_into_zone(&req, target_w, target_h, clip). target_w and target_h are the same window dims your zone pass uses to scale zone_3d.rect into the tile. If it returns false, draw nothing. On success it rewrites the views into tile-normalised coordinates and returns the zone's clip (x, y, w, h, tile-normalised, top-left).
    2. Size the sprite with canvas_px_h = u_cursor_draw_canvas_px_h(&req, canvas_px_h), which gives the zone's height for a zone request.
    3. Set the scissor to the tile box ∩ clip.
    4. Replace any zones_frame -> skip guard with the mismatch guard: skip only when zones_frame != req.zone_valid.
  • A request without a zone is untouched: clip is the whole tile, so the non-zones path is byte-identical.

Reference implementations: metal_cursor_draw, vk_cursor_resolve + cursor_record_draws, and gl_cursor_draw in #1950.

dfattal and others added 4 commits October 11, 2026 06:26
…R-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>
…/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>
…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>
…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
dfattal force-pushed the feat/cursor-runtime-draw-d3d branch from 629bc44 to 44d6898 Compare October 11, 2026 13:29
…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>
@dfattal

dfattal commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

Windows verification (win box: ConceptD 7 SR laptop + DS1, LeiaSR q4, released Leia plug-in 2.9.3, in-process from this branch's dev build at 14b558b via XR_RUNTIME_JSON)

Result: works. David eyeballed the live mouse rows on the panel: the OS cursor disappears over the cube and the arrow pops out in 3D; the flat OS cursor is back over the floor, background, title bar and borders; no flicker at the cube edge.

Row D3D11 D3D12
1 default look over cube OS-style arrow in both tiles; right-tile tip x − left-tile x = −18 px (crossed); D3D11 compositor drawing the cursor, willDraw=TRUE arrow in both tiles, −17 px crossed; D3D12 compositor drawing the cursor
2 off cube nothing drawn, willDraw=FALSE same
3 DXR_CURSOR_DEFAULT_LOOK=arrow classic arrow, white (>235/channel), same sign same
4 SPRITE=custom cyan ring + dot centred on the pointer, both tiles, crossed same (−17 px)
5 live mouse ✅ David (shares the pass; scripted rows identical)
6 DRAW=app runtime draws nothing, placement logged same
9/10 zones d3d11 zone #0 / zone #1 willDraw=TRUE, arrow clipped to its zone; strip (0.5,0.1): nothing; off-cube (0.05,0.925): willDraw=FALSE —
11 zones d3d12 — zone #0 / #1 willDraw=TRUE clipped; above zones nothing; off-cube willDraw=FALSE

No cannot draw the cursor yet WARN anywhere.

Notes:

  • Note: the scripted UV 0.5,0.5 misses the cube on this box (it sits at about v = 0.38 in a 1280×720 window). With no tracked face the eyes coincide → 2D → willDraw=FALSE, so the verification needs a viewer at the panel.
  • The one-time line reads 4 views on the D3D11 and D3D12 cube paths while the app submits 2 (zones D3D12 says 2 views). Worth a look.
  • The expected cursor_draw: default look mirrors the OS cursor (WxH px…) line never appears in either log, though the drawn default look does match the OS arrow.
  • Row 8 (DXR_SWAPCHAIN_ENCODING=unorm) not run.

David's feedback: the cursor floats too far in front of the content. He asks to halve the margin (0.03 → 0.015 baseline, ≈1 mm on the glass). Raised with mac/cursor on tmp-3d-cursor, since the value lives in the shared u_cursor_depth policy.

🤖 Generated with Claude Code

…e 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>
@dfattal

dfattal commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed a4ef0d0 on top (win/runtime): fixes the two log quirks from the Windows run. The cursor request is now capped to the arming locate's ACTIVE view count, so the cube apps' 4 submitted views no longer feed drawability and the notice reads 2 views. The first OS-cursor mirror now logs a one-time WARN (default look mirrors the OS cursor (64x64 px at 240 DPI …)); before, it was INFO, which never reaches the log file. Re-verified on the panel; David's live check passes.

🤖 Generated with Claude Code

@dfattal
dfattal marked this pull request as ready for review October 11, 2026 22:56
@dfattal
dfattal requested a review from a team as a code owner October 11, 2026 22:56
@dfattal
dfattal merged commit a6df80c into main Oct 11, 2026
42 checks passed
@dfattal
dfattal deleted the feat/cursor-runtime-draw-d3d branch October 11, 2026 23:12
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