Repository navigation
feat(cursor): the runtime draws the depth cursor — D3D11 + D3D12 (ADR-046 Amendment 2 §B) - #1944
Conversation
31bc165 to
629bc44
Compare
|
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.
Reference implementations: |
…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>
629bc44 to
44d6898
Compare
…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>
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.
No Notes:
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 🤖 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>
|
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 🤖 Generated with Claude Code |
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 v5XrCursorDepthDrawInfoDXR/XrCursorDepthDrawStateDXRonly — 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 reportwillDraw = truewhere 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 viau_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 scalezone_3d.rectby. It sizes the sprite withu_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_drawableon 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: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.
d3d_shared/comp_cursor_shaders.h, compiled withD3DCompile(the way every in-process D3D shader here is) on the first cursor frame only. ADXR_LAYEREDvariant samples array layer 0 of a layered sprite, as the spec requires.[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:
_SRGBcompose target through the source's format-honest (decoding) view._SRGB(scene-linear) app sprite forces the frame to compose._SRGBview, so the white arrow comes out white on either path.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 withDXR_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_winandcube_handle_d3d12_winfollowcube_handle_metal_macos:DISPLAYXR_CURSOR_DEPTH_DRAW=runtime(default) orapp;DISPLAYXR_CURSOR_DEPTH_SPRITE=customgives a 64×64 cyan ring sprite swapchain, filled withUpdateSubresourceon D3D11 and an upload buffer +CopyTextureRegionon D3D12;DISPLAYXR_CURSOR_DEPTH_UV=u,vscripts the pointer;DISPLAYXR_CURSOR_DEPTH=0opts out.The pointer UV comes from
GetCursorPos+ScreenToClientover the client rect. The OS cursor is hidden over the client area exactly whilewillDraw:WM_SETCURSOR→SetCursor(NULL), plus an immediateSetCursorwhen the flag flips. D3D12 posts the flip from its render thread to the window thread. Difference from Metal:appmode 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_winadopt the cursor through the sharedtest_apps/common/cursor_depth_zones.h. That header gains an additive Win32 path (initWin32) ondxr::CursorDepth's Win32 adapter (displayxr-common v2.29.0, already thetest_appspin). It provides the pointer UV, theWM_SETCURSORhide, and chains the hint on the topmost 3D zone's locate only.DISPLAYXR_CURSOR_DEPTH=0,_DRAW=runtime|appand_UV=u,v(WINDOW-normalised) work as in the macOS zones apps.Build:
comp_cursor_lookgainscomp_cursor_look_win.con WIN32 (links user32 + gdi32);comp_d3d11/comp_d3d12linkcomp_cursor_look. Two additive fields onstruct 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_LOOKcensus row is updated.How the OS-cursor mirror works (Windows,
comp_cursor_look_win.c)GetCursorInfo()→HCURSOR. The call is global, not per-thread or per-process, so the compositor thread can make it.GetIconInfogives the hotspot and native size: the colour bitmap, or the mask height ÷ 2 for monochrome cursors.GetSystemMetricsForDpi(SM_CYCURSOR, GetDpiForWindow(hwnd))(resolved dynamically). If the image's own size is larger (accessibility pointer sizes), that size is used instead.DrawIconExinto a top-down 32-bit DIB twice, over black and over white.u_cursor_draw_unblendrecovers 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.height_pt= drawn px ÷ (DPI/96), and the compositor multiplies back by its window's DPI/96.HCURSOR+ DPI. Each frame costs oneGetCursorInfo, and the image is re-rasterised only when the shape or DPI changes. An unreadable handle is remembered and not retried.SetCursor(NULL)as soon as a locate reportswillDraw, which on D3D11 happens before that frame'sxrEndFrame. SoGetCursorInfousually 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 usesIDC_ARROW. An app that changes its cursor per region insideWM_SETCURSORand wants that shape should pass its own sprite.Evidence
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.scripts/build-mingw-check.sh aux_util): PASSED, and a manual-fsyntax-onlyofcomp_cursor_look_win.c(the GDI path, also clean under-Wall -Wextra),comp_cursor_look.c,comp_d3d11_renderer.cppandcomp_d3d12_renderer.cppis clean. The two compositor.cppfiles stop at WIL, the known MinGW limit, so MSVC CI is canonical for them.check_displayxr_app.py: no issues on either cube app.Runtime,TestAppsandCTS d3d11, and thecomp_cursor_lookrefactor head. See the checks for the current head.check_displayxr_app.pyis clean on both, and a MinGW-fsyntax-only -Wallof a translation unit using the shared header's Win32 path is clean. The appmain.cppfiles themselves are only checked by MSVC CI.Windows verification recipe
Build:
scripts\build_windows.bat buildthenscripts\build_windows.bat test-apps. Register the ABI-matched DP if this box needs it:scripts\register_dev_plugin.batfor sim_display, or the dev Leia plug-in. Use a non-elevated terminal soXR_RUNTIME_JSONis honoured. No service is needed; these are in-process_handleapps.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):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._package, env set first)set DISPLAYXR_CURSOR_DEPTH_UV=0.5,0.5thenrun_cube_handle_d3d11_win.bat, then triggerXR_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 nothis compositor cannot draw the cursor yetWARN.set DISPLAYXR_CURSOR_DEPTH_UV=0.1,0.1(off the cube)willDraw=FALSE, nocursor_draw:lines.set DXR_CURSOR_DEFAULT_LOOK=arrowset DISPLAYXR_CURSOR_DEPTH_SPRITE=custom(app sprite, …).DISPLAYXR_CURSOR_DEPTH_UV), eyeball the panelset DISPLAYXR_CURSOR_DEPTH_DRAW=appcursor_depth: uv=…).run_cube_handle_d3d12_win.bat(…cube_handle_d3d12_wintrigger)D3D12 compositor drawing the cursor.cube_zones_d3d11_win(non-elevated,XR_RUNTIME_JSONas for the cube apps) withDISPLAYXR_CURSOR_DEPTH_UV=0.25,0.55cursor_depth (zones): zone #0 … willDraw=TRUE.0.75,0.45, then0.5,0.1(Local2D strip),0.523,0.5(gap),0.05,0.925(zone A, off the cube)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.cube_zones_d3d12_win, UV0.25,0.60/0.75,0.60/0.5,0.15/0.25,0.93willDraw=TRUE, cursor clipped to its zone); nothing above the zones;willDraw=FALSEoff the cube.set DXR_SWAPCHAIN_ENCODING=unorm+SPRITE=custom(the sprite becomes UNORM)Color (#1610) … compose targetWARN).Not tested
GetDpiForWindow= 96.Not done (follow-ups)
svc_draw_cursor_quads) still reportswillDraw = XR_FALSEfor the v5 draw request.dxr::CursorDepth(feat(cursor): the runtime draws the depth cursor — Vulkan + GL (ADR-046 Amendment 2 §B) #1946); the zones apps use it.DISPLAYXR_CURSOR_DEPTH_DRAW=appin the D3D cube apps.🤖 Generated with Claude Code