diff --git a/docs/adr/ADR-046-depth-aware-cursor.md b/docs/adr/ADR-046-depth-aware-cursor.md index ed32c29711..f4ae66b7a6 100644 --- a/docs/adr/ADR-046-depth-aware-cursor.md +++ b/docs/adr/ADR-046-depth-aware-cursor.md @@ -1,6 +1,6 @@ # ADR-046: Depth-aware cursor — opt-in only; the app knows the depth, the runtime places the cursor -**Status:** Proposed (2026-10-06) · Phase 1 implemented · Phase 3a implemented (Metal) · spec v3 anchor modes (2026-10-07) · Amendment 1 (lifted content, 2026-10-10) accepted · Amendment 2 (one policy, runtime draws, app chooses the look, 2026-10-11) accepted, §B built on Metal, Vulkan and GL (spec v5) · spec: +**Status:** Proposed (2026-10-06) · Phase 1 implemented · Phase 3a implemented (Metal) · spec v3 anchor modes (2026-10-07) · Amendment 1 (lifted content, 2026-10-10) accepted · Amendment 2 (one policy, runtime draws, app chooses the look, 2026-10-11) accepted, §B built on Metal, Vulkan, GL, D3D11 and D3D12 (spec v5) · spec: [XR_DXR_cursor_depth.md](../specs/extensions/XR_DXR_cursor_depth.md) · sibling of [ADR-040](ADR-040-rear-depth-budget.md) (the same cue conflict, at the cursor instead of the desktop) @@ -605,7 +605,7 @@ sprite and the depth, and the runtime owns the drawing. Apps get the same contra metres per point (pixel pitch × backing scale). Off the main thread the last snapshot is kept, else the classic arrow (12 × 19, white fill, dark outline, rasterised procedurally, 4 × 4 supersampled). `DXR_CURSOR_DEFAULT_LOOK=arrow` forces the arrow. -- **Not yet:** D3D11, D3D12 and the service (`svc_draw_cursor_quads`) report +- **Not yet:** the service (`svc_draw_cursor_quads`) reports `willDraw = XR_FALSE`; display zones (since drawn, runtime#1949, below); multi-screen routed frames draw per tile without the segment routing on Metal (Vulkan and GL skip them); the 0.5 px floor of A.3 is not applied (−0.005 is above it on current panels). @@ -690,11 +690,63 @@ sprite and the depth, and the runtime owns the drawing. Apps get the same contra neighbouring 3D zone. `u_cursor_draw_canvas_px_h()` sizes the sprite against the zone's height (the canvas its unit was built for). A non-zone request is untouched (clip = whole tile). Zero cost unchanged: all of it runs only on a requested frame. -- **D3D11 / D3D12** (draft runtime#1944) adopt the same two helpers at their draw site. +- **D3D11 / D3D12** (runtime#1944) use the same two helpers in their resolve step (`u_cursor_draw_into_zone` with the window dims the renderer scales `zone_3d.rect` by; the scissor is tile box ∩ the returned clip), skipping only on a zones-frame / zone-request mismatch. - **Reference app.** `cube_zones_{metal,vk}_macos` adopt `dxr::CursorDepth` via `test_apps/common/cursor_depth_zones.h`: pick the topmost 3D zone under the pointer, hand the helper the pointer in that zone's UV, chain the hint only on that zone's locate. +#### §B as built on D3D11 and D3D12 (in-process; `XR_DXR_cursor_depth` spec v5) + +- **Same contract as Metal.** The state tracker hands `comp_d3d11/12_compositor_set_cursor_draw_request` + the same one-frame `u_cursor_draw_request`; the commit consumes it with one field test. A cursor + frame never zero-copies. The compositor resolves the request into per-tile corners + (`u_cursor_draw_corners`) — sprite height in metres ÷ the canvas height in metres (canvas px × + the panel's pixel pitch from the window metrics) — and hands them to its renderer before the + projection pass. The renderer draws them **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 display processor. One alpha-blended triangle strip per tile + (straight for the app's sprite, premultiplied for the default look), depth off (no DSV), no + culling, scissored to the tile (∩ the zone in a display-zones frame — see the zones section + above). Multi-screen routed frames draw nothing (one-time WARN). `willDraw` is gated in the + state tracker (`u_cursor_draw_placement_drawable`, runtime#1951) for every backend alike. +- **Shaders.** One shared HLSL pair, `d3d_shared/comp_cursor_shaders.h` (corners in b0 — a + D3D11 constant buffer, 16 D3D12 root constants on the existing quad root signature), compiled + with `D3DCompile` on the first cursor frame only, like every other in-process shader here. A + `DXR_LAYERED` variant samples array layer 0 of a layered sprite. D3D12 builds six pipelines + (`[straight | straight layered | premultiplied][_SRGB target]`), since a D3D12 PSO bakes its + blend and RTV format. +- **Colour (ADR-044).** The cursor takes the frame's treatment: on a composing frame it is drawn + into the private `_SRGB` compose target through the source's format-honest view (decoding), on + a fast-path frame straight into the encoded atlas through the non-decoding view. A + non-`_SRGB` app sprite (scene-linear, owes the encode) makes the frame compose; the cursor is + not counted as a contributing layer, so a fast-path frame blends the sprite's anti-aliased + edges in encoded space, as on Metal. The default look is uploaded once per shape into a + TYPELESS texture with a UNORM and an `_SRGB` view, so the white arrow is white either way. +- **Default look (A.6) — the shared `comp_cursor_look`** (the same entry point Metal, Vulkan and + GL use), whose Windows half is `comp_cursor_look_win.c`: `GetCursorInfo()` → `HCURSOR`, drawn + with `DrawIconEx` into a 32-bit DIB **twice, over black and over white**, and recovered by + `u_cursor_draw_unblend` (alpha = 1 − (white − black); the render over black is the + premultiplied colour). That reads every cursor kind the same way — 32-bit alpha, colour + AND + mask, monochrome AND/XOR — and turns XOR (invert) pixels into opaque white. Drawn at the size + the OS draws it on this panel: `GetSystemMetricsForDpi(SM_CYCURSOR, dpi)` with the window's + `GetDpiForWindow` (passed as the look's backing scale, DPI / 96), or the image's own size when + larger (accessibility pointer sizes); its `height_pt` is that size in 96-DPI points, so the + physical size is `height_pt` × DPI / 96 × the panel's pixel pitch. Cached by `HCURSOR` + scale, + so it re-rasterises only on a shape change. `GetCursorInfo` is global, so the compositor thread + may call it. While the app has hidden its cursor with `SetCursor(NULL)` — which it does as soon + as a locate reports `willDraw`, i.e. on most drawn frames — the shape is the **class cursor of + the window under the pointer**, else the last shape, else `IDC_ARROW`. With + `DXR_CURSOR_DEFAULT_LOOK=arrow`, or if the cursor cannot be read: the shared classic arrow. +- **App sprite.** Straight alpha, last released image, hotspot as given; `sizeMeters` ≤ 0 = 19 px + at the window's DPI × pixel pitch (the classic arrow's size, as Metal's 19 pt). D3D12 dips the + image RENDER_TARGET → PIXEL_SHADER_RESOURCE → RENDER_TARGET around the draw, as for + window-space layers. +- **Reference apps.** `cube_handle_d3d11_win` / `cube_handle_d3d12_win` adopt v5 the way + `cube_handle_metal_macos` does (analytic cube hit test → hint; `DISPLAYXR_CURSOR_DEPTH_DRAW`, + `_SPRITE=custom`, `_UV`, `DISPLAYXR_CURSOR_DEPTH=0`), hiding the OS cursor over the client + area exactly while `willDraw` (`WM_SETCURSOR` → `SetCursor(NULL)`). Their `app` mode keeps the + OS cursor and logs the placement; it draws no in-app crosshair. + ### Rollout 1. **Groundwork:** diff --git a/docs/roadmap/control-panel-performance-settings.md b/docs/roadmap/control-panel-performance-settings.md index b0e6f88b8a..b44c2b804f 100644 --- a/docs/roadmap/control-panel-performance-settings.md +++ b/docs/roadmap/control-panel-performance-settings.md @@ -350,7 +350,7 @@ library linked into both · **CLI** = `displayxr-cli.exe` (reporting only, contr | `DXR_WEAVE_REPAINT_TRACE` | `auxiliary/util/u_repaint_gate.h` (`u_repaint_trace_*`, wired into the vk_native + d3d11 repaint loops) | `getenv`, cached | off | App | 4 | #1257 verification instrumentation: one WARN row per ~5 s per loop with real tick cadence, replay/pace durations, and per-gate bail counts. Says where missing repaints went; the witness only says how many landed | | `DXR_APP_FRAME_DIVISOR` | `auxiliary/util/u_app_partition.h` (`u_app_partition_throttle` in each in-process wait_frame; engagement read by the repaint gate + loop tick) | `getenv`, cached | off (1) | App | 2 | #1257 slot partition: xrWaitFrame releases the app every Dth vblank (D=2..8) and the repaint loop fills the other slots at panel rate with a KNOWN N=D schedule — steady output cadence, no fire/commit collision by construction. **Supported tier: the #918 split (d3d11 bridge / hybrid) only** — verified steady 60 with eyeball sign-off; in-process vk_native/d3d12/gl tiers REFUSE cleanly (fill-loop tick starvation, 17-19 ms vs 1.8-2.3 ms on the bridge; follow-up). The natural Control Panel "smooth motion, lower app rate" control and the hook for face-tracking-driven rate later. Not wired into Metal or the IPC/service path | | `DXR_APP_FRAME_DIVISOR_ANY_TIER` | `auxiliary/util/u_app_partition.h` (`u_app_partition_throttle` tier gate) | `getenv`, cached | off | App | 4 | Bring-up override: lets the partition throttle engage on an UNSUPPORTED tier (which collapses the panel there today). For the in-process-tier follow-up work only | -| `DXR_CURSOR_DEFAULT_LOOK` | `compositor/util/comp_cursor_look.c` (`force_arrow`, shared by the Metal, vk_native and GL compositors) | `getenv`, `static` cached | unset (mirror the OS cursor on macOS; the arrow elsewhere) | App | 4 | `arrow` forces the classic-arrow fallback for the runtime-drawn depth cursor's default look (XR_DXR_cursor_depth v5, ADR-046 Amendment 2 B) instead of mirroring `[NSCursor currentCursor]`. Verification only | +| `DXR_CURSOR_DEFAULT_LOOK` | `compositor/util/comp_cursor_look.c` (`force_arrow`, shared by the Metal, vk_native, GL, D3D11 and D3D12 compositors) | `getenv`, `static` cached | unset (mirror the OS cursor on macOS and Windows; the arrow elsewhere) | App | 4 | `arrow` forces the classic-arrow fallback for the runtime-drawn depth cursor's default look (XR_DXR_cursor_depth v5, ADR-046 Amendment 2 B) instead of mirroring the OS cursor (`[NSCursor currentCursor]` on macOS, `GetCursorInfo` on Windows). Verification only | | `DXR_DP_FORWARD_HORIZON` | `compositor/util/comp_weave_latency_win.h` (`predict_weave_to_scanout_ns`, feeds the DP `set_predicted_scanout` slot from the split + d3d11 + d3d12 weave paths) | `getenv`, cached | **on** (kill switch) | App | 1 | #206: per-weave FORWARD horizon for the vendor eye predictor, computed from the vsync-locked vblank grid (DXGI frame statistics — measured period, not nominal). `0` disables → the DP falls back to its retrospective smoothed heuristic, the pre-#206 behavior | | `DXR_DP_FORWARD_HORIZON_LOOP` | `compositor/util/comp_weave_latency_win.h` (`predict_weave_to_scanout_ns` probes it; `po_observe`, fed from `after_present`'s resolve pass, learns the value) | `getenv`, cached | **on** (kill switch) | App | 1 | #1435: learns the pipeline's whole-period offset (composition frame on DComp chains, queued frame at governor depth ≥ 2) from realised flips — mode of the last 32 resolved weaves, ≥ 24 agreeing — and adds it to the forward horizon. `0` pins the offset at 0, the pre-#1435 feed | | `DXR_FILL_FENCE_PARK` | `compositor/vk_native/comp_vk_native_compositor.c` (fill path of `vk_dp_weave_and_present`) | `getenv`, cached | **on** (kill switch) | App | 1 | #1264 S1: a fill's GPU fence wait releases the compositor lock (re-validated on retake; superseded fills are dropped). `0` restores the lock-held synchronous wait | diff --git a/docs/specs/extensions/XR_DXR_cursor_depth.md b/docs/specs/extensions/XR_DXR_cursor_depth.md index d750516c05..5562d50e68 100644 --- a/docs/specs/extensions/XR_DXR_cursor_depth.md +++ b/docs/specs/extensions/XR_DXR_cursor_depth.md @@ -192,12 +192,14 @@ the runtime cannot draw (another backend, a space mismatch, a zones frame withou `XR_FALSE`, so the fallback is always the flat OS cursor, never no cursor. The runtime never touches the OS cursor itself. -**Backends (v5).** The in-process Metal, Vulkan (vk_native) and OpenGL compositors draw. D3D11, -D3D12 and the IPC/service path report `willDraw = XR_FALSE` with a one-time WARN (ADR-046 +**Backends (v5).** The in-process Metal, Vulkan (vk_native), OpenGL, D3D11 and D3D12 compositors +draw. The IPC/service path reports `willDraw = XR_FALSE` with a one-time WARN (ADR-046 Amendment 2 rollout step 3). On Vulkan the cursor is drawn after the projection, quad and equirect2 layers but **under** window-space layers, which that compositor stamps in a later pass (ADR-046 Amendment 2, "§B as built on Vulkan and GL"). The default look mirrors the OS cursor on -macOS only; elsewhere it is the classic arrow. +macOS and Windows (on Windows `GetCursorInfo`, or - while the app has hidden it - the class +cursor of the window under the pointer, drawn at the size the OS draws it on the panel); +elsewhere it is the classic arrow. ### 2.7 `XrCursorDepthLiftRectDXR` / `XrCursorDepthLiftStateDXR` (spec v6) — lifted content diff --git a/src/xrt/auxiliary/util/u_cursor_depth.c b/src/xrt/auxiliary/util/u_cursor_depth.c index 6032ab563e..b2354bad9c 100644 --- a/src/xrt/auxiliary/util/u_cursor_depth.c +++ b/src/xrt/auxiliary/util/u_cursor_depth.c @@ -750,3 +750,35 @@ u_cursor_draw_arrow_rgba(uint8_t *rgba, uint32_t w, uint32_t h) } } } + +void +u_cursor_draw_unblend(const uint8_t *on_black, const uint8_t *on_white, uint32_t pixel_count, uint8_t *out) +{ + if (on_black == NULL || on_white == NULL || out == NULL) { + return; + } + for (uint32_t i = 0; i < pixel_count; i++) { + const uint8_t *b = on_black + (size_t)i * 4; + const uint8_t *w = on_white + (size_t)i * 4; + uint8_t *o = out + (size_t)i * 4; + // How much of the background shows through, summed over 3 channels. + // Negative where the cursor inverts (w < b): that is fully opaque. + int through = 0; + for (int c = 0; c < 3; c++) { + through += (int)w[c] - (int)b[c]; + } + int a = 255 - (through + 1) / 3; + if (a > 255) { + a = 255; + } + if (a <= 0) { + o[0] = o[1] = o[2] = o[3] = 0; + continue; + } + // Over black, the render IS colour x coverage: premultiplied already. + for (int c = 0; c < 3; c++) { + o[c] = b[c] > a ? (uint8_t)a : b[c]; + } + o[3] = (uint8_t)a; + } +} diff --git a/src/xrt/auxiliary/util/u_cursor_depth.h b/src/xrt/auxiliary/util/u_cursor_depth.h index 156997cf81..32251d7070 100644 --- a/src/xrt/auxiliary/util/u_cursor_depth.h +++ b/src/xrt/auxiliary/util/u_cursor_depth.h @@ -622,6 +622,23 @@ u_cursor_draw_canvas_px_h(const struct u_cursor_draw_request *req, uint32_t canv void u_cursor_draw_arrow_rgba(uint8_t *rgba, uint32_t w, uint32_t h); +/*! + * Recover a cursor image from two renders of it: one over opaque BLACK, one + * over opaque WHITE. This is how the Windows default look reads ANY HCURSOR - + * 32-bit alpha, colour + AND mask, or monochrome AND/XOR - without branching + * on its type: whatever the OS draws, the two backgrounds reveal it. + * + * Per pixel, with b and w the two renders (bytes 0..2 colour, byte 3 ignored): + * alpha = 1 - mean(w - b), and the render over black IS the premultiplied + * colour (clamped to alpha). A pixel the cursor INVERTS (white over black, + * black over white - the I-beam's XOR pixels) comes out opaque WHITE: a + * compositor cannot invert what is behind it before the weave, and white is + * the classic arrow's fill. Output: PREMULTIPLIED alpha (as the classic arrow + * and comp_cursor_look), channels in the input's order. + */ +void +u_cursor_draw_unblend(const uint8_t *on_black, const uint8_t *on_white, uint32_t pixel_count, uint8_t *out); + #ifdef __cplusplus } #endif diff --git a/src/xrt/compositor/CMakeLists.txt b/src/xrt/compositor/CMakeLists.txt index 5407030165..8f097591ca 100644 --- a/src/xrt/compositor/CMakeLists.txt +++ b/src/xrt/compositor/CMakeLists.txt @@ -268,6 +268,11 @@ target_include_directories(comp_cursor_look PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if(APPLE) target_sources(comp_cursor_look PRIVATE util/comp_cursor_look_macos.m) target_link_libraries(comp_cursor_look PRIVATE "-framework AppKit") +elseif(WIN32) + # The Windows OS-cursor mirror (GetCursorInfo / DrawIconEx into a DIB), + # shared by the D3D11 and D3D12 compositors. + target_sources(comp_cursor_look PRIVATE util/comp_cursor_look_win.c) + target_link_libraries(comp_cursor_look PRIVATE user32 gdi32) endif() ### diff --git a/src/xrt/compositor/d3d11/CMakeLists.txt b/src/xrt/compositor/d3d11/CMakeLists.txt index f3175de931..7af6984687 100644 --- a/src/xrt/compositor/d3d11/CMakeLists.txt +++ b/src/xrt/compositor/d3d11/CMakeLists.txt @@ -62,6 +62,8 @@ if(WIN32 AND XRT_HAVE_D3D11) aux_d3d comp_xbridge comp_d3d11_segs + # XR_DXR_cursor_depth v5: the runtime-drawn cursor's default look + comp_cursor_look xrt-external-stb d3d11 d3d12 diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp index 2a570085fb..dabe9147f9 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp @@ -74,6 +74,8 @@ #include "util/u_fill_thread_win.h" #include "util/u_image_capture.h" #include "util/u_rear_budget.h" +#include "util/u_cursor_depth.h" +#include "util/comp_cursor_look.h" // XR_DXR_depth_budget: the API-agnostic runner (validate -> analyse -> policy // -> publish -> dump) shared with the other native compositors. #include "util/comp_rear_budget.h" @@ -725,6 +727,25 @@ struct comp_d3d11_compositor //! Per-frame capture intent populated at top of layer_commit. See //! u_capture_intent.h. struct u_capture_intent capture_intent; + + /*! + * XR_DXR_cursor_depth v5, the runtime draws the cursor (ADR-046 + * Amendment 2 B). + * + * ZERO COST UNLESS REQUESTED: all zero for a session that never chained + * XrCursorDepthDrawInfoDXR. layer_commit tests `req.requested` and does + * nothing else; the renderer's shaders and the default look are built on + * the first real request only. + */ + struct + { + //! One-frame request from the state tracker, consumed by the next commit. + struct u_cursor_draw_request req; + //! The default look (the OS cursor mirror, else the arrow). + struct comp_cursor_look look; + bool warned_routed; + bool warned_zones; + } cursor_draw; }; /* @@ -3478,6 +3499,134 @@ d3d11_repaint_thread(struct comp_d3d11_compositor *c) } } +/* + * + * XR_DXR_cursor_depth v5 - the runtime draws the cursor (ADR-046 Amendment 2 B) + * + */ + +/*! + * Resolve this frame's cursor request into per-tile quads for the renderer: + * pick the sprite (the app's swapchain, or the default look - the OS cursor + * mirror, else the arrow), its size in metres, aspect and hotspot, and map it + * through each view's placement (u_cursor_draw_corners). False = draw nothing + * this frame. + */ +static bool +d3d11_cursor_resolve(struct comp_d3d11_compositor *c, + const struct u_cursor_draw_request *req_in, + const struct xrt_window_metrics *metrics, + const struct u_canvas_rect *eff_canvas, + uint32_t tgt_width, + uint32_t tgt_height, + struct comp_d3d11_cursor_quads *out) +{ + // A routed (multi-screen) frame's tiles are per-segment mosaics; the + // per-view map is for whole tiles. + if (c->eff_layout.route_count > 0) { + if (!c->cursor_draw.warned_routed) { + c->cursor_draw.warned_routed = true; + U_LOG_W("cursor_draw [d3d11]: multi-screen routed frame - cursor not drawn (one-time warning)"); + } + return false; + } + // #1949 display zones: a zones frame carries a ZONE request (views + // zone-normalised, through the hinted zone's layer) and nothing else + // does. Skip only on a mismatch - the state tracker never sends one; + // belt and braces. + if (c->zones_frame != req_in->zone_valid) { + if (!c->cursor_draw.warned_zones) { + c->cursor_draw.warned_zones = true; + U_LOG_W( + "cursor_draw [d3d11]: zones frame / zone request mismatch - cursor not drawn (one-time " + "warning)"); + } + return false; + } + // Re-express a zone request in the tile exactly the way the renderer + // places the zone's own layer (zone rect scaled by tile / target), and + // clip the draw to the zone. A request without a zone is untouched and + // clips to the whole tile. + struct u_cursor_draw_request zreq = *req_in; + if (!u_cursor_draw_into_zone(&zreq, tgt_width, tgt_height, out->clip)) { + return false; + } + const struct u_cursor_draw_request *req = &zreq; + // Panel pixel pitch (metres per pixel): every DisplayXR process is per- + // monitor DPI aware, so window and panel pixels are the same pixels. + if (metrics == nullptr || metrics->display_pixel_height == 0 || !(metrics->display_height_m > 0.0f)) { + return false; + } + const float m_per_px = metrics->display_height_m / (float)metrics->display_pixel_height; + // The canvas the request's unit was built for: the zone's height for a + // zone request, else the zone-mask canvas or the target (window). + const uint32_t canvas_px_h = + u_cursor_draw_canvas_px_h(req, (eff_canvas->valid && eff_canvas->h > 0) ? eff_canvas->h : tgt_height); + if (canvas_px_h == 0) { + return false; + } + const float canvas_h_m = (float)canvas_px_h * m_per_px; + + // The panel's DPI sizes the OS cursor (and the arrow fallback). + HWND dpi_hwnd = c->hwnd != nullptr ? c->hwnd : c->app_hwnd; + UINT dpi = dpi_hwnd != nullptr ? GetDpiForWindow(dpi_hwnd) : 0; + if (dpi == 0) { + dpi = 96; + } + + float aspect, hot_u, hot_v, size_m; + if (req->image != NULL) { + uint32_t sw = 0, sh = 0; + comp_d3d11_swapchain_get_dimensions(req->image, &sw, &sh); + if (sw == 0 || sh == 0) { + return false; + } + aspect = (float)sw / (float)sh; + hot_u = req->hot_u; + hot_v = req->hot_v; + // <= 0: the OS cursor's size (the classic arrow's 19 px at this DPI). + size_m = + req->size_m > 0.0f ? req->size_m : U_CURSOR_DRAW_DEFAULT_POINTS * ((float)dpi / 96.0f) * m_per_px; + out->xsc = req->image; + out->image_index = req->image_index; + } else { + // Shared with every backend (comp_cursor_look): the OS cursor + // mirror on Windows, at this window's DPI, else the arrow. + if (!comp_cursor_look_update(&c->cursor_draw.look, (float)dpi / 96.0f)) { + return false; + } + const struct comp_cursor_look *look = &c->cursor_draw.look; + aspect = look->aspect; + hot_u = look->hot_u; + hot_v = look->hot_v; + // OS points x (DPI / 96) = panel pixels, x the pixel pitch. + size_m = req->size_m > 0.0f ? req->size_m : look->height_pt * ((float)dpi / 96.0f) * m_per_px; + out->look = look; + } + const float height = size_m / canvas_h_m; // sprite height, in units (canvas heights at t) + if (!(height > 0.0f) || !std::isfinite(height)) { + return false; + } + out->count = req->view_count < XRT_MAX_VIEWS ? req->view_count : XRT_MAX_VIEWS; + for (uint32_t i = 0; i < out->count; i++) { + u_cursor_draw_corners(&req->view[i], height, aspect, hot_u, hot_v, out->corners[i]); + } + return true; +} + +extern "C" void +comp_d3d11_compositor_set_cursor_draw_request(struct xrt_compositor *xc, const struct u_cursor_draw_request *req) +{ + if (xc == nullptr || req == nullptr) { + return; + } + struct comp_d3d11_compositor *c = d3d11_comp(xc); + // Same thread as the commit that consumes it (xrEndFrame), but the lock + // keeps it honest against a future caller. + std::lock_guard lock(c->mutex); + c->cursor_draw.req = *req; +} + static xrt_result_t d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sync_handle) { @@ -3515,6 +3664,17 @@ d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl // lands) or end of frame (POST_COMPOSE). u_capture_intent_poll(&c->capture_intent, &c->mcp_capture); + // XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B). ZERO COST UNLESS + // REQUESTED: one field test, false for every session that never chained + // XrCursorDepthDrawInfoDXR. The request covers this commit only. + struct u_cursor_draw_request cursor_req; + bool cursor_pending = false; + if (c->cursor_draw.req.requested) { + cursor_req = c->cursor_draw.req; + c->cursor_draw.req.requested = false; + cursor_pending = u_cursor_draw_should_draw(&cursor_req); + } + // Phase 1 diagnostic — env-gated per-client commit interval. Mirrors // the same `[CLIENT_FRAME_NS]` line emitted by the SERVICE compositor's // `compositor_layer_commit` so per-app frame rates are directly @@ -3844,6 +4004,13 @@ d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl zc_srv = nullptr; } + // XR_DXR_cursor_depth v5: a frame carrying a runtime-drawn cursor composes + // into the atlas - the cursor is drawn there, never into the app's image. + if (cursor_pending) { + zero_copy = false; + zc_srv = nullptr; + } + // No CPU-side wait for the app's GPU work here, by design: the app renders // on this same immediate context, and D3D11 executes it in submission order, // so every read of an app swapchain image below is already ordered after @@ -3914,11 +4081,24 @@ d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl struct xrt_window_metrics canvas = {}; const bool have_canvas = comp_d3d11_compositor_get_window_metrics(xc, &canvas); + // XR_DXR_cursor_depth v5: resolve the cursor BEFORE the projection + // pass - its sprite's encoding takes part in the frame's compose + // decision. Drawn by the window-space pass, last. + if (cursor_pending) { + struct comp_d3d11_cursor_quads quads = {}; + const bool draw = d3d11_cursor_resolve(c, &cursor_req, have_canvas ? &canvas : nullptr, + &eff_canvas, tgt_width, tgt_height, &quads); + comp_d3d11_renderer_set_cursor(c->renderer, draw ? &quads : nullptr); + } + xret = comp_d3d11_renderer_draw_projection_pass(c->renderer, &c->layer_accum, &render_eyes, tgt_width, tgt_height, have_canvas ? &canvas : nullptr, &c->eff_layout); if (xret != XRT_SUCCESS) { U_LOG_E("Failed to render projection pass"); + if (cursor_pending) { + comp_d3d11_renderer_set_cursor(c->renderer, nullptr); + } return xret; } @@ -3929,6 +4109,10 @@ d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl xret = comp_d3d11_renderer_draw_window_space_pass( c->renderer, &c->layer_accum, tgt_width, tgt_height, &c->eff_layout); + // The cursor covered this frame only. + if (cursor_pending) { + comp_d3d11_renderer_set_cursor(c->renderer, nullptr); + } if (xret != XRT_SUCCESS) { U_LOG_E("Failed to render window-space pass"); return xret; @@ -4322,6 +4506,8 @@ d3d11_compositor_destroy(struct xrt_compositor *xc) u_capture_dims_set_provider(NULL, c); mcp_capture_uninstall(); mcp_capture_fini(&c->mcp_capture); + // XR_DXR_cursor_depth v5: the default look's image (NULL unless ever drawn). + comp_cursor_look_fini(&c->cursor_draw.look); #ifdef XRT_FEATURE_DEBUG_GUI // Stop debug GUI first (before destroying resources it may reference) diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.h b/src/xrt/compositor/d3d11/comp_d3d11_compositor.h index 15f43bde68..15732d2908 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.h @@ -19,6 +19,7 @@ // Forward declarations struct comp_d3d11_compositor; struct xrt_system_devices; +struct u_cursor_draw_request; #ifdef __cplusplus extern "C" { @@ -279,6 +280,18 @@ comp_d3d11_compositor_set_view_routing(struct xrt_compositor *xc, const struct x void comp_d3d11_compositor_set_rear_budget_requested(struct xrt_compositor *xc, bool requested); +/*! + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): draw the cursor sprite into + * every view tile at the NEXT layer_commit only, after the app's layers and + * before the display processor. Called by the state tracker only on a frame + * whose locate chained XrCursorDepthDrawInfoDXR and reported willDraw; a + * compositor never handed a request does no cursor work at all. + * + * @ingroup comp_d3d11 + */ +void +comp_d3d11_compositor_set_cursor_draw_request(struct xrt_compositor *xc, const struct u_cursor_draw_request *req); + /*! * XR_DXR_depth_budget: the latest budget the render thread computed. * diff --git a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp index c42eeb7ab4..e594939e8e 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp @@ -13,6 +13,9 @@ #include "comp_d3d11_swapchain.h" // The embedded HLSL and the constant-buffer structs that must match it. #include "d3d11_layer_shaders.h" +// XR_DXR_cursor_depth v5: the runtime-drawn cursor quad (shared with D3D12). +#include "d3d_shared/comp_cursor_shaders.h" +#include "util/comp_cursor_look.h" #include "util/comp_layer_accum.h" // #1580: the ONE per-view camera every layer type is projected through. @@ -186,6 +189,34 @@ struct comp_d3d11_renderer //! @ref u_color_legacy_unorm_encoded, cached at create. bool legacy_color; + + /*! + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B) — the runtime-drawn + * cursor. ZERO COST UNLESS REQUESTED: `frame.count` is 0 on every frame + * of a session that never asked, and every resource below stays NULL + * until the first frame that draws one. + */ + struct + { + //! This frame's cursor (set by the compositor, cleared after the pass). + struct comp_d3d11_cursor_quads frame; + ID3D11VertexShader *vs; + ID3D11PixelShader *ps; + ID3D11PixelShader *ps_array; //!< layered sprite: samples array layer 0 + ID3D11Buffer *cb; + ID3D11BlendState *blend; //!< straight alpha (the app's sprite) + ID3D11BlendState *blend_premul; //!< premultiplied (the default look) + ID3D11RasterizerState *raster; //!< no culling, scissor on + ID3D11SamplerState *sampler; //!< linear, CLAMP (no edge bleed) + bool failed; //!< creation failed once: never retried per frame + //! The default look's texture: TYPELESS storage with a non-decoding + //! (UNORM) and a decoding (_SRGB) view, re-uploaded when its serial moves. + ID3D11Texture2D *look_tex; + ID3D11ShaderResourceView *look_srv_raw; + ID3D11ShaderResourceView *look_srv_srgb; + uint64_t look_generation; + bool announced; + } cursor; }; // The compositor's borrowed handles, handed over explicitly — see @@ -828,6 +859,19 @@ renderer_frame_takes_fast_path(struct comp_d3d11_renderer *r, struct comp_layer_ } } + // XR_DXR_cursor_depth v5: the cursor sprite is a source this frame too. A + // scene-linear (non-`_SRGB`) app sprite owes the encode, which only the + // compose target applies. The default look is encoded bytes by + // construction (comp_cursor_look), so it never forces a compose. The cursor + // is deliberately NOT counted as a contributing layer: on a fast-path + // frame its edges blend in encoded space, as on Metal - a few + // anti-aliased pixels, not worth a full-atlas copy every frame the + // pointer is over content. + if (r->cursor.frame.count > 0 && r->cursor.frame.xsc != nullptr && + !comp_d3d11_swapchain_is_srgb(r->cursor.frame.xsc)) { + all_sources_srgb = false; + } + return u_color_compose_fast_path(r->legacy_color, contributing, base_is_projection, all_sources_srgb); } @@ -1538,6 +1582,18 @@ comp_d3d11_renderer_destroy(struct comp_d3d11_renderer **renderer_ptr) x = nullptr; \ } + // XR_DXR_cursor_depth v5 (all NULL unless a cursor was ever drawn). + SAFE_RELEASE(r->cursor.look_srv_srgb); + SAFE_RELEASE(r->cursor.look_srv_raw); + SAFE_RELEASE(r->cursor.look_tex); + SAFE_RELEASE(r->cursor.sampler); + SAFE_RELEASE(r->cursor.raster); + SAFE_RELEASE(r->cursor.blend_premul); + SAFE_RELEASE(r->cursor.blend); + SAFE_RELEASE(r->cursor.cb); + SAFE_RELEASE(r->cursor.ps_array); + SAFE_RELEASE(r->cursor.ps); + SAFE_RELEASE(r->cursor.vs); SAFE_RELEASE(r->depth_stencil_state); SAFE_RELEASE(r->rasterizer_state); SAFE_RELEASE(r->blend_opaque); @@ -2149,6 +2205,342 @@ comp_d3d11_renderer_draw_projection_pass(struct comp_d3d11_renderer *renderer, return XRT_SUCCESS; } +/* + * + * XR_DXR_cursor_depth v5 — the runtime draws the cursor (ADR-046 Amendment 2 B) + * + * The D3D11 twin of the Metal pass: after the app's layers and before the + * display processor, one straight-alpha textured quad per view tile, depth + * off, scissored to the tile, at the corners the compositor resolved from the + * state tracker's per-view map. Everything is built on the first frame that + * draws one; a session that never asks pays one integer test per frame. + * + */ + +static void +cursor_release_pipeline(struct comp_d3d11_renderer *r) +{ + if (r->cursor.sampler != nullptr) { + r->cursor.sampler->Release(); + r->cursor.sampler = nullptr; + } + if (r->cursor.raster != nullptr) { + r->cursor.raster->Release(); + r->cursor.raster = nullptr; + } + if (r->cursor.blend_premul != nullptr) { + r->cursor.blend_premul->Release(); + r->cursor.blend_premul = nullptr; + } + if (r->cursor.blend != nullptr) { + r->cursor.blend->Release(); + r->cursor.blend = nullptr; + } + if (r->cursor.cb != nullptr) { + r->cursor.cb->Release(); + r->cursor.cb = nullptr; + } + if (r->cursor.ps_array != nullptr) { + r->cursor.ps_array->Release(); + r->cursor.ps_array = nullptr; + } + if (r->cursor.ps != nullptr) { + r->cursor.ps->Release(); + r->cursor.ps = nullptr; + } + if (r->cursor.vs != nullptr) { + r->cursor.vs->Release(); + r->cursor.vs = nullptr; + } +} + +//! Shaders + fixed-function state, built on the first cursor frame only. +static bool +cursor_ensure_pipeline(struct comp_d3d11_renderer *r) +{ + if (r->cursor.vs != nullptr) { + return true; + } + if (r->cursor.failed) { + return false; + } + r->cursor.failed = true; // cleared on success; a failure is never retried per frame + + auto internals = get_internals(r->c); + ID3D11Device *device = internals.device; + ID3DBlob *blob = nullptr; + bool ok = compile_shader(device, cursor_quad_vs_source, "main", "vs_5_0", &blob) == XRT_SUCCESS; + if (ok) { + ok = SUCCEEDED(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, + &r->cursor.vs)); + blob->Release(); + blob = nullptr; + } + if (ok) { + ok = compile_shader(device, cursor_quad_ps_source, "main", "ps_5_0", &blob) == XRT_SUCCESS; + } + if (ok) { + ok = SUCCEEDED( + device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &r->cursor.ps)); + blob->Release(); + blob = nullptr; + } + if (ok) { + // Same source, Texture2DArray: a layered sprite swapchain's SRV. + static const D3D_SHADER_MACRO layered[] = {{"DXR_LAYERED", "1"}, {nullptr, nullptr}}; + ok = compile_shader(device, cursor_quad_ps_source, "main", "ps_5_0", &blob, layered) == XRT_SUCCESS; + } + if (ok) { + ok = SUCCEEDED(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, + &r->cursor.ps_array)); + blob->Release(); + blob = nullptr; + } + if (ok) { + D3D11_BUFFER_DESC bd = {}; + bd.ByteWidth = sizeof(CursorQuadConstants); + bd.Usage = D3D11_USAGE_DEFAULT; + bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + ok = SUCCEEDED(device->CreateBuffer(&bd, nullptr, &r->cursor.cb)); + } + if (ok) { + // Straight alpha for the app's sprite (the spec), premultiplied + // for the default look (comp_cursor_look); alpha accumulates + // "over" either way, so a transparent session's atlas keeps a + // correct coverage. + D3D11_BLEND_DESC bd = {}; + bd.RenderTarget[0].BlendEnable = TRUE; + bd.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + bd.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + bd.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + bd.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + bd.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + bd.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + ok = SUCCEEDED(device->CreateBlendState(&bd, &r->cursor.blend)); + if (ok) { + bd.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; + ok = SUCCEEDED(device->CreateBlendState(&bd, &r->cursor.blend_premul)); + } + } + if (ok) { + // No culling: the sprite's winding follows the view's axes. The + // scissor is mandatory - a cursor at a tile edge must not spill into + // the neighbour view (the DP would weave it into the wrong eye). + D3D11_RASTERIZER_DESC rd = {}; + rd.FillMode = D3D11_FILL_SOLID; + rd.CullMode = D3D11_CULL_NONE; + rd.DepthClipEnable = TRUE; + rd.ScissorEnable = TRUE; + ok = SUCCEEDED(device->CreateRasterizerState(&rd, &r->cursor.raster)); + } + if (ok) { + // CLAMP: with WRAP, bilinear filtering at the sprite's edge would + // pull in the opposite edge (the arrow's tip would fade). + D3D11_SAMPLER_DESC sd = {}; + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.ComparisonFunc = D3D11_COMPARISON_NEVER; + sd.MaxLOD = D3D11_FLOAT32_MAX; + ok = SUCCEEDED(device->CreateSamplerState(&sd, &r->cursor.sampler)); + } + if (!ok) { + U_LOG_E("cursor_draw [d3d11]: pipeline creation failed - the runtime-drawn cursor is not drawn"); + cursor_release_pipeline(r); + return false; + } + r->cursor.failed = false; + return true; +} + +//! Upload the default look when its image changed (cursor shape or DPI). +static bool +cursor_upload_look(struct comp_d3d11_renderer *r, const struct comp_cursor_look *look) +{ + if (look == nullptr || look->rgba == nullptr || look->w == 0 || look->h == 0) { + return false; + } + if (r->cursor.look_tex != nullptr && r->cursor.look_generation == look->generation) { + return true; + } + if (r->cursor.look_srv_srgb != nullptr) { + r->cursor.look_srv_srgb->Release(); + r->cursor.look_srv_srgb = nullptr; + } + if (r->cursor.look_srv_raw != nullptr) { + r->cursor.look_srv_raw->Release(); + r->cursor.look_srv_raw = nullptr; + } + if (r->cursor.look_tex != nullptr) { + r->cursor.look_tex->Release(); + r->cursor.look_tex = nullptr; + } + + auto internals = get_internals(r->c); + // TYPELESS: the same encoded bytes are read raw on the fast path (into the + // encoded atlas) and decoded on the compose path (into the `_SRGB` RTV). + D3D11_TEXTURE2D_DESC td = {}; + td.Width = look->w; + td.Height = look->h; + td.MipLevels = 1; + td.ArraySize = 1; + td.Format = DXGI_FORMAT_R8G8B8A8_TYPELESS; + td.SampleDesc.Count = 1; + td.Usage = D3D11_USAGE_IMMUTABLE; + td.BindFlags = D3D11_BIND_SHADER_RESOURCE; + D3D11_SUBRESOURCE_DATA init = {}; + init.pSysMem = look->rgba; + init.SysMemPitch = look->w * 4; + HRESULT hr = internals.device->CreateTexture2D(&td, &init, &r->cursor.look_tex); + if (FAILED(hr)) { + r->cursor.look_tex = nullptr; + return false; + } + D3D11_SHADER_RESOURCE_VIEW_DESC sd = {}; + sd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MipLevels = 1; + sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + hr = internals.device->CreateShaderResourceView(r->cursor.look_tex, &sd, &r->cursor.look_srv_raw); + if (SUCCEEDED(hr)) { + sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + hr = internals.device->CreateShaderResourceView(r->cursor.look_tex, &sd, &r->cursor.look_srv_srgb); + } + if (FAILED(hr)) { + return false; // released on the next upload or at destroy + } + r->cursor.look_generation = look->generation; + return true; +} + +/*! + * Draw this frame's cursor into every tile of whatever target the frame paints + * (the atlas, or the private compose target before its publish). + */ +static void +cursor_draw(struct comp_d3d11_renderer *r, + const struct comp_d3d11_eff_layout *layout, + uint32_t target_width, + uint32_t target_height) +{ + const struct comp_d3d11_cursor_quads *q = &r->cursor.frame; + if (!cursor_ensure_pipeline(r)) { + return; + } + + ID3D11ShaderResourceView *srv = nullptr; + ID3D11PixelShader *ps = r->cursor.ps; + if (q->xsc != nullptr) { + // A layered swapchain's SRV is a whole-array Texture2DArray: the + // spec samples array layer 0. + if (comp_d3d11_swapchain_get_array_size(q->xsc) > 1) { + ps = r->cursor.ps_array; + } + // Format-honest on the compose path, non-decoding on the fast path: + // the same rule as every other layer source (ADR-044). + srv = layer_source_srv(r, q->xsc, q->image_index); + } else { + if (!cursor_upload_look(r, q->look)) { + return; + } + srv = r->compose_active ? r->cursor.look_srv_srgb : r->cursor.look_srv_raw; + } + if (srv == nullptr) { + return; + } + + if (!r->cursor.announced) { + r->cursor.announced = true; + U_LOG_W( + "cursor_draw: D3D11 compositor drawing the cursor (%s, %u views, %s; ADR-046 Amendment 2 B, " + "one-time notice)", + q->xsc != nullptr ? "app sprite" : "default look", q->count, + r->compose_active ? "linear compose" : "encoded fast path"); + } + + auto internals = get_internals(r->c); + ID3D11DeviceContext *ctx = internals.context; + ID3D11RenderTargetView *rtv = (r->compose_active && r->compose_rtv != nullptr) ? r->compose_rtv : r->atlas_rtv; + ctx->OMSetRenderTargets(1, &rtv, nullptr); // no DSV: depth test OFF, never occluded + ctx->OMSetBlendState(q->xsc != nullptr ? r->cursor.blend : r->cursor.blend_premul, nullptr, 0xFFFFFFFF); + ctx->RSSetState(r->cursor.raster); + ctx->IASetInputLayout(nullptr); + ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + ctx->VSSetShader(r->cursor.vs, nullptr, 0); + ctx->VSSetConstantBuffers(0, 1, &r->cursor.cb); + ctx->PSSetShader(ps, nullptr, 0); + ctx->PSSetShaderResources(0, 1, &srv); + ctx->PSSetSamplers(0, 1, &r->cursor.sampler); + + static const float uv[4][2] = {{0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}}; + const uint32_t n = q->count < layout->views ? q->count : layout->views; + for (uint32_t i = 0; i < n && i < XRT_MAX_VIEWS; i++) { + float tx = 0.0f, ty = 0.0f, tw = 0.0f, th = 0.0f; + get_view_tile_box(r, i, layout, target_width, target_height, &tx, &ty, &tw, &th); + if (tw <= 0.0f || th <= 0.0f) { + continue; + } + D3D11_VIEWPORT vp = {}; + vp.TopLeftX = tx; + vp.TopLeftY = ty; + vp.Width = tw; + vp.Height = th; + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + ctx->RSSetViewports(1, &vp); + // Tile box ∩ the request's clip (a display zone's sub-rect, else + // the whole tile). + D3D11_RECT sr = {}; + sr.left = (LONG)floorf(tx + q->clip[0] * tw); + sr.top = (LONG)floorf(ty + q->clip[1] * th); + sr.right = (LONG)ceilf(tx + (q->clip[0] + q->clip[2]) * tw); + sr.bottom = (LONG)ceilf(ty + (q->clip[1] + q->clip[3]) * th); + sr.left = sr.left < (LONG)tx ? (LONG)tx : sr.left; + sr.top = sr.top < (LONG)ty ? (LONG)ty : sr.top; + sr.right = sr.right > (LONG)(tx + tw) ? (LONG)(tx + tw) : sr.right; + sr.bottom = sr.bottom > (LONG)(ty + th) ? (LONG)(ty + th) : sr.bottom; + if (sr.right <= sr.left || sr.bottom <= sr.top) { + continue; + } + ctx->RSSetScissorRects(1, &sr); + + CursorQuadConstants cc = {}; + for (int k = 0; k < 4; k++) { + cc.corners[k][0] = q->corners[i][k][0]; + cc.corners[k][1] = q->corners[i][k][1]; + cc.corners[k][2] = uv[k][0]; + cc.corners[k][3] = uv[k][1]; + } + ctx->UpdateSubresource(r->cursor.cb, 0, nullptr, &cc, 0, 0); + ctx->Draw(4, 0); + } + + // Hand the pipeline back the way the layer passes expect it. + ID3D11ShaderResourceView *null_srv = nullptr; + ctx->PSSetShaderResources(0, 1, &null_srv); + ctx->RSSetState(r->rasterizer_state); + ctx->OMSetBlendState(r->blend_opaque, nullptr, 0xFFFFFFFF); +} + +extern "C" void +comp_d3d11_renderer_set_cursor(struct comp_d3d11_renderer *renderer, const struct comp_d3d11_cursor_quads *quads) +{ + if (renderer == nullptr) { + return; + } + if (quads == nullptr || quads->count == 0) { + renderer->cursor.frame.count = 0; + renderer->cursor.frame.xsc = nullptr; + renderer->cursor.frame.look = nullptr; + return; + } + renderer->cursor.frame = *quads; + if (renderer->cursor.frame.count > XRT_MAX_VIEWS) { + renderer->cursor.frame.count = XRT_MAX_VIEWS; + } +} + extern "C" xrt_result_t comp_d3d11_renderer_draw_window_space_pass(struct comp_d3d11_renderer *renderer, struct comp_layer_accum *layers, @@ -2166,7 +2558,10 @@ comp_d3d11_renderer_draw_window_space_pass(struct comp_d3d11_renderer *renderer, break; } } - if (!any_window_space) { + // XR_DXR_cursor_depth v5: the runtime-drawn cursor goes LAST, so a frame + // carrying one runs this pass even without a window-space layer. + const bool any_cursor = renderer->cursor.frame.count > 0; + if (!any_window_space && !any_cursor) { return XRT_SUCCESS; } @@ -2189,6 +2584,12 @@ comp_d3d11_renderer_draw_window_space_pass(struct comp_d3d11_renderer *renderer, } } + // Over every app layer, never occluded, before the publish (so a + // composing frame's cursor is encoded once, with everything else). + if (any_cursor) { + cursor_draw(renderer, layout, target_width, target_height); + } + renderer_publish_compose_to_atlas(renderer); return XRT_SUCCESS; } diff --git a/src/xrt/compositor/d3d11/comp_d3d11_renderer.h b/src/xrt/compositor/d3d11/comp_d3d11_renderer.h index 11ddb1effb..0b35ba019a 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.h @@ -13,6 +13,7 @@ // #1580: the canvas metres branch (b) of the per-view camera resolver needs. #include "xrt/xrt_display_metrics.h" #include "xrt/xrt_results.h" +#include "xrt/xrt_limits.h" #include #include @@ -21,6 +22,8 @@ struct comp_d3d11_renderer; struct comp_d3d11_compositor; struct comp_layer_accum; +struct xrt_swapchain; +struct comp_cursor_look; #ifdef __cplusplus extern "C" { @@ -205,6 +208,52 @@ comp_d3d11_renderer_draw_window_space_pass(struct comp_d3d11_renderer *renderer, uint32_t target_height, const struct comp_d3d11_eff_layout *layout); +/*! + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): one frame's runtime-drawn + * cursor, resolved by the compositor - where the sprite lands in each view + * tile and what it samples. + * + * Drawn LAST by @ref comp_d3d11_renderer_draw_window_space_pass - over the + * projection, quad and window-space layers, never occluded, before the + * display processor - one straight-alpha quad per tile, depth off, scissored + * to the tile. The colour treatment is the frame's (ADR-044): through the + * private `_SRGB` compose target when the frame composes, with the source's + * format-honest view; else straight into the atlas through non-decoding + * views. A sprite that is not `_SRGB` (scene-linear, owes the encode) makes + * the frame compose. + */ +struct comp_d3d11_cursor_quads +{ + //! Tiles to draw (view i -> tile i); 0 = no cursor this frame. + uint32_t count; + //! Per tile, the quad's corners in tile-normalised coordinates (origin + //! top-left, y down) in u_cursor_draw_corners() order. + float corners[XRT_MAX_VIEWS][4][2]; + //! The draw's clip in every tile, tile-normalised (x, y, w, h; origin + //! top-left): the whole tile {0, 0, 1, 1}, or a display zone's sub-rect + //! (u_cursor_draw_into_zone, #1949). The scissor is tile box ∩ clip. + float clip[4]; + //! The app's sprite swapchain (sampled at @ref image_index), or NULL for + //! @ref look. + struct xrt_swapchain *xsc; + uint32_t image_index; + //! The default look (compositor-owned, outlives the frame). + //! Its pixels are PREMULTIPLIED (comp_cursor_look); an app sprite is + //! STRAIGHT (the spec). The renderer blends each accordingly. + const struct comp_cursor_look *look; +}; + +/*! + * Hand the renderer this frame's cursor (NULL or count 0 = none). Call before + * @ref comp_d3d11_renderer_draw_projection_pass (the sprite's colour encoding + * takes part in the frame's compose decision) and clear it after the + * window-space pass. A renderer never handed a cursor builds nothing for it. + * + * @ingroup comp_d3d11 + */ +void +comp_d3d11_renderer_set_cursor(struct comp_d3d11_renderer *renderer, const struct comp_d3d11_cursor_quads *quads); + /*! * Get the atlas texture SRV for weaving. * diff --git a/src/xrt/compositor/d3d12/CMakeLists.txt b/src/xrt/compositor/d3d12/CMakeLists.txt index 81ffad6c9c..a1f99300ee 100644 --- a/src/xrt/compositor/d3d12/CMakeLists.txt +++ b/src/xrt/compositor/d3d12/CMakeLists.txt @@ -42,6 +42,8 @@ if(WIN32 AND XRT_HAVE_D3D12) # link it without dragging the other one in. aux_d3d comp_xbridge + # XR_DXR_cursor_depth v5: the runtime-drawn cursor's default look + comp_cursor_look xrt-external-stb d3d12 dxgi diff --git a/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp b/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp index 7252be247c..3a8496692a 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp @@ -68,6 +68,8 @@ struct comp_vk_split; // the reroute fields exist either way; the code does not #include "util/u_capture_dims.h" #include "util/u_image_capture.h" #include "util/u_hud.h" +#include "util/u_cursor_depth.h" +#include "util/comp_cursor_look.h" #include // STB_IMAGE_WRITE_STATIC scopes all stbi_write_* to this TU. @@ -798,6 +800,22 @@ struct comp_d3d12_compositor //! Per-frame capture intent. See u_capture_intent.h. struct u_capture_intent capture_intent; + + /*! + * XR_DXR_cursor_depth v5, the runtime draws the cursor (ADR-046 + * Amendment 2 B). ZERO COST UNLESS REQUESTED: all zero for a session that + * never chained XrCursorDepthDrawInfoDXR; layer_commit tests + * `req.requested` and does nothing else. + */ + struct + { + //! One-frame request from the state tracker, consumed by the next commit. + struct u_cursor_draw_request req; + //! The default look (the OS cursor mirror, else the arrow). + struct comp_cursor_look look; + bool warned_routed; + bool warned_zones; + } cursor_draw; }; /* @@ -4667,6 +4685,134 @@ d3d12_repaint_thread(struct comp_d3d12_compositor *c) } } +/* + * + * XR_DXR_cursor_depth v5 - the runtime draws the cursor (ADR-046 Amendment 2 B) + * + */ + +/*! + * Resolve this frame's cursor request into per-tile quads for the renderer: + * pick the sprite (the app's swapchain, or the default look - the OS cursor + * mirror, else the arrow), its size in metres, aspect and hotspot, and map it + * through each view's placement (u_cursor_draw_corners). False = draw nothing + * this frame. + */ +static bool +d3d12_cursor_resolve(struct comp_d3d12_compositor *c, + const struct u_cursor_draw_request *req_in, + const struct xrt_window_metrics *metrics, + const struct u_canvas_rect *eff_canvas, + uint32_t tgt_width, + uint32_t tgt_height, + struct comp_d3d12_cursor_quads *out) +{ + // A routed (multi-screen) frame's tiles are per-segment mosaics; the + // per-view map is for whole tiles. + if (c->eff_layout.route_count > 0) { + if (!c->cursor_draw.warned_routed) { + c->cursor_draw.warned_routed = true; + U_LOG_W("cursor_draw [d3d12]: multi-screen routed frame - cursor not drawn (one-time warning)"); + } + return false; + } + // #1949 display zones: a zones frame carries a ZONE request (views + // zone-normalised, through the hinted zone's layer) and nothing else + // does. Skip only on a mismatch - the state tracker never sends one; + // belt and braces. + if (c->zones_frame != req_in->zone_valid) { + if (!c->cursor_draw.warned_zones) { + c->cursor_draw.warned_zones = true; + U_LOG_W( + "cursor_draw [d3d12]: zones frame / zone request mismatch - cursor not drawn (one-time " + "warning)"); + } + return false; + } + // Re-express a zone request in the tile exactly the way the renderer + // places the zone's own layer (zone rect scaled by tile / target), and + // clip the draw to the zone. A request without a zone is untouched and + // clips to the whole tile. + struct u_cursor_draw_request zreq = *req_in; + if (!u_cursor_draw_into_zone(&zreq, tgt_width, tgt_height, out->clip)) { + return false; + } + const struct u_cursor_draw_request *req = &zreq; + // Panel pixel pitch (metres per pixel): every DisplayXR process is per- + // monitor DPI aware, so window and panel pixels are the same pixels. + if (metrics == nullptr || metrics->display_pixel_height == 0 || !(metrics->display_height_m > 0.0f)) { + return false; + } + const float m_per_px = metrics->display_height_m / (float)metrics->display_pixel_height; + // The canvas the request's unit was built for: the zone's height for a + // zone request, else the zone-mask canvas or the target (window). + const uint32_t canvas_px_h = + u_cursor_draw_canvas_px_h(req, (eff_canvas->valid && eff_canvas->h > 0) ? eff_canvas->h : tgt_height); + if (canvas_px_h == 0) { + return false; + } + const float canvas_h_m = (float)canvas_px_h * m_per_px; + + // The panel's DPI sizes the OS cursor (and the arrow fallback). + HWND dpi_hwnd = c->hwnd != nullptr ? c->hwnd : c->app_hwnd; + UINT dpi = dpi_hwnd != nullptr ? GetDpiForWindow(dpi_hwnd) : 0; + if (dpi == 0) { + dpi = 96; + } + + float aspect, hot_u, hot_v, size_m; + if (req->image != NULL) { + uint32_t sw = 0, sh = 0; + comp_d3d12_swapchain_get_dimensions(req->image, &sw, &sh); + if (sw == 0 || sh == 0) { + return false; + } + aspect = (float)sw / (float)sh; + hot_u = req->hot_u; + hot_v = req->hot_v; + // <= 0: the OS cursor's size (the classic arrow's 19 px at this DPI). + size_m = + req->size_m > 0.0f ? req->size_m : U_CURSOR_DRAW_DEFAULT_POINTS * ((float)dpi / 96.0f) * m_per_px; + out->xsc = req->image; + out->image_index = req->image_index; + } else { + // Shared with every backend (comp_cursor_look): the OS cursor + // mirror on Windows, at this window's DPI, else the arrow. + if (!comp_cursor_look_update(&c->cursor_draw.look, (float)dpi / 96.0f)) { + return false; + } + const struct comp_cursor_look *look = &c->cursor_draw.look; + aspect = look->aspect; + hot_u = look->hot_u; + hot_v = look->hot_v; + // OS points x (DPI / 96) = panel pixels, x the pixel pitch. + size_m = req->size_m > 0.0f ? req->size_m : look->height_pt * ((float)dpi / 96.0f) * m_per_px; + out->look = look; + } + const float height = size_m / canvas_h_m; // sprite height, in units (canvas heights at t) + if (!(height > 0.0f) || !std::isfinite(height)) { + return false; + } + out->count = req->view_count < XRT_MAX_VIEWS ? req->view_count : XRT_MAX_VIEWS; + for (uint32_t i = 0; i < out->count; i++) { + u_cursor_draw_corners(&req->view[i], height, aspect, hot_u, hot_v, out->corners[i]); + } + return true; +} + +extern "C" void +comp_d3d12_compositor_set_cursor_draw_request(struct xrt_compositor *xc, const struct u_cursor_draw_request *req) +{ + if (xc == nullptr || req == nullptr) { + return; + } + struct comp_d3d12_compositor *c = d3d12_comp(xc); + // Same thread as the commit that consumes it (xrEndFrame), but the lock + // keeps it honest against a future caller. + std::lock_guard lock(c->mutex); + c->cursor_draw.req = *req; +} + static xrt_result_t d3d12_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sync_handle) { @@ -4685,6 +4831,17 @@ d3d12_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl // lands) or end of frame (POST_COMPOSE). u_capture_intent_poll(&c->capture_intent, &c->mcp_capture); + // XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B). ZERO COST UNLESS + // REQUESTED: one field test, false for every session that never chained + // XrCursorDepthDrawInfoDXR. The request covers this commit only. + struct u_cursor_draw_request cursor_req; + bool cursor_pending = false; + if (c->cursor_draw.req.requested) { + cursor_req = c->cursor_draw.req; + c->cursor_draw.req.requested = false; + cursor_pending = u_cursor_draw_should_draw(&cursor_req); + } + // Get predicted eye positions struct xrt_eye_positions eye_pos = {}; if (c->display_processor != nullptr) { @@ -5073,6 +5230,13 @@ d3d12_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl zc_resource = nullptr; } + // XR_DXR_cursor_depth v5: a frame carrying a runtime-drawn cursor composes + // into the atlas - the cursor is drawn there, never into the app's image. + if (cursor_pending) { + zero_copy = false; + zc_resource = nullptr; + } + // Reset command allocator and command list c->cmd_allocator->Reset(); c->cmd_list->Reset(c->cmd_allocator, nullptr); @@ -5169,11 +5333,24 @@ d3d12_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl struct xrt_window_metrics canvas = {}; const bool have_canvas = comp_d3d12_compositor_get_window_metrics(xc, &canvas); + // XR_DXR_cursor_depth v5: resolve the cursor BEFORE the projection + // pass - its sprite's encoding takes part in the frame's compose + // decision. Drawn by the window-space pass, last. + if (cursor_pending) { + struct comp_d3d12_cursor_quads quads = {}; + const bool draw = d3d12_cursor_resolve(c, &cursor_req, have_canvas ? &canvas : nullptr, + &eff_canvas, tgt_width, tgt_height, &quads); + comp_d3d12_renderer_set_cursor(c->renderer, draw ? &quads : nullptr); + } + xret = comp_d3d12_renderer_draw_projection_pass(c->renderer, c->cmd_list, &c->layer_accum, &left_eye, &right_eye, &render_eyes, tgt_width, tgt_height, have_canvas ? &canvas : nullptr, &c->eff_layout); if (xret != XRT_SUCCESS) { U_LOG_E("Failed to render projection pass"); + if (cursor_pending) { + comp_d3d12_renderer_set_cursor(c->renderer, nullptr); + } return xret; } @@ -5213,6 +5390,10 @@ d3d12_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl xret = comp_d3d12_renderer_draw_window_space_pass( c->renderer, c->cmd_list, &c->layer_accum, tgt_width, tgt_height, &c->eff_layout); + // The cursor covered this frame only. + if (cursor_pending) { + comp_d3d12_renderer_set_cursor(c->renderer, nullptr); + } if (xret != XRT_SUCCESS) { U_LOG_E("Failed to render window-space pass"); return xret; @@ -5992,6 +6173,8 @@ d3d12_compositor_destroy(struct xrt_compositor *xc) u_capture_dims_set_provider(NULL, c); mcp_capture_uninstall(); mcp_capture_fini(&c->mcp_capture); + // XR_DXR_cursor_depth v5: the default look's image (NULL unless ever drawn). + comp_cursor_look_fini(&c->cursor_draw.look); #ifdef COMP_D3D12_HAVE_D3D11_FILL_ARM // #1264 reroute teardown: the arm first (its consumer holds NT opens of diff --git a/src/xrt/compositor/d3d12/comp_d3d12_compositor.h b/src/xrt/compositor/d3d12/comp_d3d12_compositor.h index 7339e5d9b9..c788870abd 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_compositor.h +++ b/src/xrt/compositor/d3d12/comp_d3d12_compositor.h @@ -19,6 +19,7 @@ // Forward declarations struct comp_d3d12_compositor; struct xrt_system_devices; +struct u_cursor_draw_request; #ifdef __cplusplus extern "C" { @@ -120,6 +121,18 @@ comp_d3d12_compositor_get_window_metrics(struct xrt_compositor *xc, void comp_d3d12_compositor_set_rear_budget_requested(struct xrt_compositor *xc, bool requested); +/*! + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): draw the cursor sprite into + * every view tile at the NEXT layer_commit only, after the app's layers and + * before the display processor. Called by the state tracker only on a frame + * whose locate chained XrCursorDepthDrawInfoDXR and reported willDraw; a + * compositor never handed a request does no cursor work at all. + * + * @ingroup comp_d3d12 + */ +void +comp_d3d12_compositor_set_cursor_draw_request(struct xrt_compositor *xc, const struct u_cursor_draw_request *req); + /*! * XR_DXR_depth_budget: the latest budget the render thread computed. * diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp index f04bd06670..4648404b9e 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp @@ -13,6 +13,9 @@ // #1602: the equirect2 HLSL, shared verbatim with both D3D11 paths. The ray / // sphere math is ~100 lines and must not be forked — see the header's preamble. #include "d3d_shared/comp_equirect2_shaders.h" +// XR_DXR_cursor_depth v5: the runtime-drawn cursor quad (shared with D3D11). +#include "d3d_shared/comp_cursor_shaders.h" +#include "util/comp_cursor_look.h" #include "util/comp_layer_accum.h" // #1580: the ONE per-view camera every layer type is projected through, plus @@ -588,6 +591,35 @@ struct comp_d3d12_renderer //! @ref u_color_legacy_unorm_encoded, cached at create. bool legacy_color; + + /*! + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B) — the runtime-drawn + * cursor. ZERO COST UNLESS REQUESTED: `frame.count` is 0 on every frame + * of a session that never asked, and every resource below stays NULL + * until the first frame that draws one. Shares the quad root signature + * (SRV table + 32 root constants, static CLAMP sampler); its own pipeline + * per RTV format, like every other atlas pipeline here (#1610). + */ + struct + { + //! This frame's cursor (set by the compositor, cleared after the pass). + struct comp_d3d12_cursor_quads frame; + //! [variant][target is the `_SRGB` compose target], no depth. + //! Variant: CURSOR_PSO_STRAIGHT / _STRAIGHT_LAYERED (the app's sprite, + //! straight alpha, Texture2D / Texture2DArray) or _PREMUL (the default + //! look, premultiplied - comp_cursor_look). + ID3D12PipelineState *pso[3][2]; + bool failed; //!< creation failed once: never retried per frame + //! The default look's texture: TYPELESS storage, read through a UNORM + //! (fast path) or `_SRGB` (compose path) SRV. Rests in + //! PIXEL_SHADER_RESOURCE once uploaded. + ID3D12Resource *look_tex; + //! Its upload buffer, kept until the next upload: the GPU has + //! retired it by then (every commit resets the allocator first). + ID3D12Resource *look_upload; + uint64_t look_generation; + bool announced; + } cursor; }; /*! @@ -1177,6 +1209,17 @@ renderer_frame_takes_fast_path(struct comp_d3d12_renderer *r, struct comp_layer_ } } + // XR_DXR_cursor_depth v5: the cursor sprite is a source this frame too. A + // scene-linear (non-`_SRGB`) app sprite owes the encode, which only the + // compose target applies; the default look is encoded bytes. Not counted + // as a contributing layer - see the D3D11 twin for why. + if (r->cursor.frame.count > 0 && r->cursor.frame.xsc != nullptr) { + void *res = comp_d3d12_swapchain_get_resource(r->cursor.frame.xsc, 0); + if (res != nullptr && !comp_d3d12_swapchain_resource_is_srgb(res)) { + all_sources_srgb = false; + } + } + return u_color_compose_fast_path(r->legacy_color, contributing, base_is_projection, all_sources_srgb); } @@ -2797,6 +2840,24 @@ comp_d3d12_renderer_destroy(struct comp_d3d12_renderer **renderer_ptr) comp_d3d12_renderer *r = *renderer_ptr; + // XR_DXR_cursor_depth v5 (all NULL unless a cursor was ever drawn). + for (uint32_t l = 0; l < 3; l++) { + for (uint32_t rt = 0; rt < 2; rt++) { + if (r->cursor.pso[l][rt] != nullptr) { + r->cursor.pso[l][rt]->Release(); + r->cursor.pso[l][rt] = nullptr; + } + } + } + if (r->cursor.look_upload != nullptr) { + r->cursor.look_upload->Release(); + r->cursor.look_upload = nullptr; + } + if (r->cursor.look_tex != nullptr) { + r->cursor.look_tex->Release(); + r->cursor.look_tex = nullptr; + } + if (r->flatten_srv_heap != nullptr) { r->flatten_srv_heap->Release(); } @@ -3493,6 +3554,408 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_renderer *renderer, return XRT_SUCCESS; } +/* + * + * XR_DXR_cursor_depth v5 — the runtime draws the cursor (ADR-046 Amendment 2 B) + * + * The D3D12 twin of the D3D11 / Metal pass: after the app's layers and before + * the display processor, one straight-alpha textured quad per view tile, depth + * off, scissored to the tile. Recorded onto the frame's command list inside + * the window-space pass, so it lands before the atlas's final barrier (and + * before the compose publish, so a composing frame encodes it once). + * Everything is built on the first frame that draws one. + * + */ + +static void +cursor_release_pipelines(struct comp_d3d12_renderer *r) +{ + for (uint32_t l = 0; l < 3; l++) { + for (uint32_t rt = 0; rt < 2; rt++) { + if (r->cursor.pso[l][rt] != nullptr) { + r->cursor.pso[l][rt]->Release(); + r->cursor.pso[l][rt] = nullptr; + } + } + } +} + +//! The cursor pipeline variants (first index of `cursor.pso`). +enum +{ + CURSOR_PSO_STRAIGHT = 0, + CURSOR_PSO_STRAIGHT_LAYERED = 1, + CURSOR_PSO_PREMUL = 2, +}; + +//! The six pipelines, built on the first cursor frame only. +static bool +cursor_ensure_pipeline(struct comp_d3d12_renderer *r) +{ + if (r->cursor.pso[CURSOR_PSO_PREMUL][1] != nullptr) { + return true; // built last: all six exist + } + if (r->cursor.failed) { + return false; + } + r->cursor.failed = true; // cleared on success; a failure is never retried per frame + + ID3DBlob *vs = nullptr; + ID3DBlob *ps[2] = {nullptr, nullptr}; + // ps[1]: same source, Texture2DArray - a layered sprite (array layer 0). + static const D3D_SHADER_MACRO layered[] = {{"DXR_LAYERED", "1"}, {nullptr, nullptr}}; + HRESULT hr = compile_shader(cursor_quad_vs_source, "main", "vs_5_0", &vs); + if (SUCCEEDED(hr)) { + hr = compile_shader(cursor_quad_ps_source, "main", "ps_5_0", &ps[0]); + } + if (SUCCEEDED(hr)) { + hr = compile_shader(cursor_quad_ps_source, "main", "ps_5_0", &ps[1], layered); + } + if (SUCCEEDED(hr)) { + // The quad root signature: t0 table (param 0), 32 root constants at + // b0 (param 1; the cursor reads the first 16), CLAMP linear s0. + D3D12_GRAPHICS_PIPELINE_STATE_DESC d = {}; + d.pRootSignature = r->quad_root_signature; + d.VS.pShaderBytecode = vs->GetBufferPointer(); + d.VS.BytecodeLength = vs->GetBufferSize(); + // Colour: SrcAlpha (straight sprite) or One (premultiplied default + // look), set per variant below; alpha accumulates "over". + d.BlendState.RenderTarget[0].BlendEnable = TRUE; + d.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_SRC_ALPHA; + d.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_INV_SRC_ALPHA; + d.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; + d.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE; + d.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA; + d.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; + d.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + // No culling: the sprite's winding follows the view's axes. + d.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + d.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + d.RasterizerState.DepthClipEnable = TRUE; + // Depth OFF: no DSV format, DepthEnable false - never occluded. + d.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + d.NumRenderTargets = 1; + d.SampleDesc.Count = 1; + d.SampleMask = UINT_MAX; + ID3D12Device *device = get_internals(r->c)->device; + for (uint32_t l = 0; l < 3 && SUCCEEDED(hr); l++) { + ID3DBlob *pl = l == CURSOR_PSO_STRAIGHT_LAYERED ? ps[1] : ps[0]; + d.PS.pShaderBytecode = pl->GetBufferPointer(); + d.PS.BytecodeLength = pl->GetBufferSize(); + d.BlendState.RenderTarget[0].SrcBlend = + l == CURSOR_PSO_PREMUL ? D3D12_BLEND_ONE : D3D12_BLEND_SRC_ALPHA; + for (uint32_t rt = 0; rt < 2 && SUCCEEDED(hr); rt++) { + d.RTVFormats[0] = rt == 0 ? DXGI_FORMAT_R8G8B8A8_UNORM : kComposeRtvFormat; + hr = device->CreateGraphicsPipelineState( + &d, __uuidof(ID3D12PipelineState), + reinterpret_cast(&r->cursor.pso[l][rt])); + } + } + } + for (uint32_t l = 0; l < 2; l++) { + if (ps[l] != nullptr) { + ps[l]->Release(); + } + } + if (vs != nullptr) { + vs->Release(); + } + if (FAILED(hr)) { + U_LOG_E( + "cursor_draw [d3d12]: pipeline creation failed (0x%08x) - the runtime-drawn cursor is not drawn", + (unsigned)hr); + cursor_release_pipelines(r); + return false; + } + r->cursor.failed = false; + return true; +} + +/*! + * Upload the default look when its image changed (cursor shape or DPI): + * records the copy onto @p cmd_list and leaves the texture in + * PIXEL_SHADER_RESOURCE. + */ +static bool +cursor_upload_look(struct comp_d3d12_renderer *r, + ID3D12GraphicsCommandList *cmd_list, + const struct comp_cursor_look *look) +{ + if (look == nullptr || look->rgba == nullptr || look->w == 0 || look->h == 0) { + return false; + } + if (r->cursor.look_tex != nullptr && r->cursor.look_generation == look->generation) { + return true; + } + // The previous texture and upload buffer were last used by an earlier + // commit, which the GPU has retired (its allocator was reset since). + if (r->cursor.look_upload != nullptr) { + r->cursor.look_upload->Release(); + r->cursor.look_upload = nullptr; + } + if (r->cursor.look_tex != nullptr) { + r->cursor.look_tex->Release(); + r->cursor.look_tex = nullptr; + } + r->cursor.look_generation = 0; + + ID3D12Device *device = get_internals(r->c)->device; + + // TYPELESS: the same encoded bytes are read raw on the fast path and + // decoded on the compose path. + D3D12_RESOURCE_DESC td = {}; + td.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + td.Width = look->w; + td.Height = look->h; + td.DepthOrArraySize = 1; + td.MipLevels = 1; + td.Format = DXGI_FORMAT_R8G8B8A8_TYPELESS; + td.SampleDesc.Count = 1; + td.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + td.Flags = D3D12_RESOURCE_FLAG_NONE; + D3D12_HEAP_PROPERTIES heap = {}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + HRESULT hr = + device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &td, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + __uuidof(ID3D12Resource), reinterpret_cast(&r->cursor.look_tex)); + if (FAILED(hr)) { + r->cursor.look_tex = nullptr; + return false; + } + r->cursor.look_tex->SetName(L"DXR.cursor_default_look"); + + D3D12_PLACED_SUBRESOURCE_FOOTPRINT fp = {}; + UINT rows = 0; + UINT64 row_bytes = 0; + UINT64 total = 0; + device->GetCopyableFootprints(&td, 0, 1, 0, &fp, &rows, &row_bytes, &total); + + D3D12_HEAP_PROPERTIES up_heap = {}; + up_heap.Type = D3D12_HEAP_TYPE_UPLOAD; + D3D12_RESOURCE_DESC bd = {}; + bd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + bd.Width = total; + bd.Height = 1; + bd.DepthOrArraySize = 1; + bd.MipLevels = 1; + bd.Format = DXGI_FORMAT_UNKNOWN; + bd.SampleDesc.Count = 1; + bd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + hr = device->CreateCommittedResource(&up_heap, D3D12_HEAP_FLAG_NONE, &bd, D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, __uuidof(ID3D12Resource), + reinterpret_cast(&r->cursor.look_upload)); + if (FAILED(hr)) { + r->cursor.look_upload = nullptr; + r->cursor.look_tex->Release(); + r->cursor.look_tex = nullptr; + return false; + } + uint8_t *mapped = nullptr; + D3D12_RANGE no_read = {0, 0}; + hr = r->cursor.look_upload->Map(0, &no_read, reinterpret_cast(&mapped)); + if (FAILED(hr) || mapped == nullptr) { + r->cursor.look_upload->Release(); + r->cursor.look_upload = nullptr; + r->cursor.look_tex->Release(); + r->cursor.look_tex = nullptr; + return false; + } + for (uint32_t y = 0; y < look->h; y++) { + memcpy(mapped + fp.Offset + (size_t)y * fp.Footprint.RowPitch, look->rgba + (size_t)y * look->w * 4, + (size_t)look->w * 4); + } + r->cursor.look_upload->Unmap(0, nullptr); + + D3D12_TEXTURE_COPY_LOCATION dst = {}; + dst.pResource = r->cursor.look_tex; + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst.SubresourceIndex = 0; + D3D12_TEXTURE_COPY_LOCATION src = {}; + src.pResource = r->cursor.look_upload; + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint = fp; + cmd_list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); + + D3D12_RESOURCE_BARRIER b = {}; + b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + b.Transition.pResource = r->cursor.look_tex; + b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + b.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + b.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + cmd_list->ResourceBarrier(1, &b); + + r->cursor.look_generation = look->generation; + return true; +} + +/*! + * Record this frame's cursor into every tile of whatever target the frame + * paints (the atlas, or the private compose target before its publish). + * @return true if anything was drawn. + */ +static bool +cursor_draw(struct comp_d3d12_renderer *r, + ID3D12GraphicsCommandList *cmd_list, + const struct comp_d3d12_eff_layout *layout, + uint32_t vp_w, + uint32_t vp_h) +{ + const struct comp_d3d12_cursor_quads *q = &r->cursor.frame; + if (!cursor_ensure_pipeline(r)) { + return false; + } + + ID3D12Resource *src = nullptr; + DXGI_FORMAT src_format = DXGI_FORMAT_UNKNOWN; + bool app_image = false; + bool layered = false; + if (q->xsc != nullptr) { + src = static_cast(comp_d3d12_swapchain_get_resource(q->xsc, q->image_index)); + if (src == nullptr) { + return false; + } + // A layered sprite: the spec samples array layer 0. + layered = src->GetDesc().DepthOrArraySize > 1; + // Format-honest on the compose path, non-decoding on the fast path: + // the same rule as every other layer source (ADR-044). + src_format = layer_source_format(r, src); + app_image = true; + } else { + if (!cursor_upload_look(r, cmd_list, q->look)) { + return false; + } + src = r->cursor.look_tex; + src_format = r->compose_active ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : DXGI_FORMAT_R8G8B8A8_UNORM; + } + + const uint32_t srv_slot = r->next_srv_slot; + if (srv_slot >= r->srv_heap_size) { + U_LOG_E("D3D12 renderer: SRV heap overflow in the cursor pass (slot %u >= %u)", srv_slot, + r->srv_heap_size); + return false; + } + r->next_srv_slot++; + + if (!r->cursor.announced) { + r->cursor.announced = true; + U_LOG_W( + "cursor_draw: D3D12 compositor drawing the cursor (%s, %u views, %s; ADR-046 Amendment 2 B, " + "one-time notice)", + app_image ? "app sprite" : "default look", q->count, + r->compose_active ? "linear compose" : "encoded fast path"); + } + + // The app's image rests in RENDER_TARGET (OpenXR D3D12); sample it in + // PIXEL_SHADER_RESOURCE and hand it back, as the window-space layers do. + D3D12_RESOURCE_BARRIER src_barrier = {}; + src_barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + src_barrier.Transition.pResource = src; + src_barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + src_barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + src_barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + if (app_image) { + cmd_list->ResourceBarrier(1, &src_barrier); + } + + ID3D12Device *device = get_internals(r->c)->device; + D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {}; + srv_desc.Format = src_format; + if (layered) { + srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; + srv_desc.Texture2DArray.MipLevels = 1; + srv_desc.Texture2DArray.FirstArraySlice = 0; + srv_desc.Texture2DArray.ArraySize = 1; + } else { + srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srv_desc.Texture2D.MipLevels = 1; + } + srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + D3D12_CPU_DESCRIPTOR_HANDLE srv_cpu = r->srv_heap->GetCPUDescriptorHandleForHeapStart(); + srv_cpu.ptr += (SIZE_T)r->srv_descriptor_size * srv_slot; + device->CreateShaderResourceView(src, &srv_desc, srv_cpu); + + cmd_list->SetGraphicsRootSignature(r->quad_root_signature); + const uint32_t variant = + !app_image ? CURSOR_PSO_PREMUL : (layered ? CURSOR_PSO_STRAIGHT_LAYERED : CURSOR_PSO_STRAIGHT); + cmd_list->SetPipelineState(r->cursor.pso[variant][r->compose_active ? 1 : 0]); + ID3D12DescriptorHeap *heaps[] = {r->srv_heap}; + cmd_list->SetDescriptorHeaps(1, heaps); + D3D12_GPU_DESCRIPTOR_HANDLE srv_gpu = r->srv_heap->GetGPUDescriptorHandleForHeapStart(); + srv_gpu.ptr += (UINT64)r->srv_descriptor_size * srv_slot; + cmd_list->SetGraphicsRootDescriptorTable(0, srv_gpu); + D3D12_CPU_DESCRIPTOR_HANDLE rtv = renderer_target_rtv(r); + cmd_list->OMSetRenderTargets(1, &rtv, FALSE, nullptr); // no DSV: depth OFF + cmd_list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + static const float uv[4][2] = {{0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}}; + const uint32_t n = q->count < layout->views ? q->count : layout->views; + for (uint32_t i = 0; i < n && i < XRT_MAX_VIEWS; i++) { + // The same tile box the window-space layers use (#542 grid). + const uint32_t tile_x = (i % layout->cols) * layout->tile_w; + const uint32_t tile_y = (i / layout->cols) * layout->tile_h; + D3D12_VIEWPORT vp = {}; + vp.TopLeftX = (float)tile_x; + vp.TopLeftY = (float)tile_y; + vp.Width = (float)vp_w; + vp.Height = (float)vp_h; + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + cmd_list->RSSetViewports(1, &vp); + // Mandatory: a cursor at a tile edge must not spill into the + // neighbour view (the DP would weave it into the wrong eye). + // Tile box ∩ the request's clip (a display zone's sub-rect, else + // the whole tile). + D3D12_RECT sr = {}; + sr.left = (LONG)floorf((float)tile_x + q->clip[0] * (float)vp_w); + sr.top = (LONG)floorf((float)tile_y + q->clip[1] * (float)vp_h); + sr.right = (LONG)ceilf((float)tile_x + (q->clip[0] + q->clip[2]) * (float)vp_w); + sr.bottom = (LONG)ceilf((float)tile_y + (q->clip[1] + q->clip[3]) * (float)vp_h); + sr.left = sr.left < (LONG)tile_x ? (LONG)tile_x : sr.left; + sr.top = sr.top < (LONG)tile_y ? (LONG)tile_y : sr.top; + sr.right = sr.right > (LONG)(tile_x + vp_w) ? (LONG)(tile_x + vp_w) : sr.right; + sr.bottom = sr.bottom > (LONG)(tile_y + vp_h) ? (LONG)(tile_y + vp_h) : sr.bottom; + if (sr.right <= sr.left || sr.bottom <= sr.top) { + continue; + } + cmd_list->RSSetScissorRects(1, &sr); + + CursorQuadConstants cc = {}; + for (int k = 0; k < 4; k++) { + cc.corners[k][0] = q->corners[i][k][0]; + cc.corners[k][1] = q->corners[i][k][1]; + cc.corners[k][2] = uv[k][0]; + cc.corners[k][3] = uv[k][1]; + } + cmd_list->SetGraphicsRoot32BitConstants(1, 16, &cc, 0); + cmd_list->DrawInstanced(4, 1, 0, 0); + } + + if (app_image) { + src_barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + src_barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + cmd_list->ResourceBarrier(1, &src_barrier); + } + return true; +} + +extern "C" void +comp_d3d12_renderer_set_cursor(struct comp_d3d12_renderer *renderer, const struct comp_d3d12_cursor_quads *quads) +{ + if (renderer == nullptr) { + return; + } + if (quads == nullptr || quads->count == 0) { + renderer->cursor.frame.count = 0; + renderer->cursor.frame.xsc = nullptr; + renderer->cursor.frame.look = nullptr; + return; + } + renderer->cursor.frame = *quads; + if (renderer->cursor.frame.count > XRT_MAX_VIEWS) { + renderer->cursor.frame.count = XRT_MAX_VIEWS; + } +} + extern "C" xrt_result_t comp_d3d12_renderer_draw_window_space_pass(struct comp_d3d12_renderer *renderer, void *cmd_list_ptr, @@ -3534,12 +3997,19 @@ comp_d3d12_renderer_draw_window_space_pass(struct comp_d3d12_renderer *renderer, } } + // XR_DXR_cursor_depth v5: the runtime-drawn cursor, LAST - over every app + // layer, never occluded, before the publish and the final barrier. + bool drew_cursor = false; + if (renderer->cursor.frame.count > 0) { + drew_cursor = cursor_draw(renderer, cmd_list, layout, ws_vp_w, ws_vp_h); + } + // #1610: republish only when this pass actually drew, so a composing // frame without a window-space layer pays for exactly one atlas copy, not // two. (Each render_window_space_layer states its own target, so the // capture round trip the caller may have run in between costs nothing to // recover from.) - if (any_window_space) { + if (any_window_space || drew_cursor) { renderer_publish_compose_to_atlas(renderer, cmd_list); } diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.h b/src/xrt/compositor/d3d12/comp_d3d12_renderer.h index 9a3d6a41a2..5eb2a72498 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.h +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.h @@ -13,6 +13,7 @@ // #1580: the eye set and the canvas metres the per-view camera resolver takes. #include "xrt/xrt_display_metrics.h" #include "xrt/xrt_results.h" +#include "xrt/xrt_limits.h" #include #include @@ -21,6 +22,8 @@ struct comp_d3d12_renderer; struct comp_d3d12_compositor; struct comp_layer_accum; +struct xrt_swapchain; +struct comp_cursor_look; #ifdef __cplusplus extern "C" { @@ -185,6 +188,46 @@ comp_d3d12_renderer_draw_window_space_pass(struct comp_d3d12_renderer *renderer, uint32_t target_height, const struct comp_d3d12_eff_layout *layout); +/*! + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): one frame's runtime-drawn + * cursor, resolved by the compositor. The D3D12 twin of + * comp_d3d11_cursor_quads: drawn LAST by + * @ref comp_d3d12_renderer_draw_window_space_pass (over every app layer, + * before its final barrier and the display processor), one straight-alpha + * quad per tile, depth off, scissored to the tile, in the frame's colour + * treatment (ADR-044). A non-`_SRGB` sprite makes the frame compose. + */ +struct comp_d3d12_cursor_quads +{ + //! Tiles to draw (view i -> tile i); 0 = no cursor this frame. + uint32_t count; + //! Per tile, the quad's corners in tile-normalised coordinates (origin + //! top-left, y down) in u_cursor_draw_corners() order. + float corners[XRT_MAX_VIEWS][4][2]; + //! The draw's clip in every tile, tile-normalised (x, y, w, h; origin + //! top-left): the whole tile {0, 0, 1, 1}, or a display zone's sub-rect + //! (u_cursor_draw_into_zone, #1949). The scissor is tile box ∩ clip. + float clip[4]; + //! The app's sprite swapchain (sampled at @ref image_index; its images + //! rest in RENDER_TARGET), or NULL for @ref look. + struct xrt_swapchain *xsc; + uint32_t image_index; + //! The default look (compositor-owned, outlives the frame). + //! Its pixels are PREMULTIPLIED (comp_cursor_look); an app sprite is + //! STRAIGHT (the spec). The renderer blends each accordingly. + const struct comp_cursor_look *look; +}; + +/*! + * Hand the renderer this frame's cursor (NULL or count 0 = none). Call before + * @ref comp_d3d12_renderer_draw_projection_pass and clear it after the + * window-space pass. A renderer never handed a cursor builds nothing for it. + * + * @ingroup comp_d3d12 + */ +void +comp_d3d12_renderer_set_cursor(struct comp_d3d12_renderer *renderer, const struct comp_d3d12_cursor_quads *quads); + /*! * Multi-screen M6: linearly scale one rect of a texture into one rect of a * render target — the flat-2D fill of a segment no DP can weave (and of the diff --git a/src/xrt/compositor/d3d_shared/comp_cursor_shaders.h b/src/xrt/compositor/d3d_shared/comp_cursor_shaders.h new file mode 100644 index 0000000000..906b812742 --- /dev/null +++ b/src/xrt/compositor/d3d_shared/comp_cursor_shaders.h @@ -0,0 +1,82 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Embedded HLSL for the runtime-drawn depth cursor (D3D11 + D3D12). + * @author David Fattal + * @ingroup comp_util + * + * XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): after the app's layers are + * in the atlas and before the display processor, one textured quad per view + * tile. The corners arrive in TILE-NORMALISED coordinates (origin top-left, + * y down, 0..1 spans the tile - u_cursor_draw_corners()), so the viewport is + * the tile and the vertex shader maps them straight to clip space. + * + * INTERNAL to the native D3D compositor libraries. API-agnostic SM 5.0 + * (t0 / s0 / b0): D3D11 binds b0 as a constant buffer, D3D12 as 16 root + * constants - the same reason comp_masked_composite_shaders.h is shared. + * Compiled on the FIRST cursor request only: a session that never asks for a + * runtime-drawn cursor never compiles these (ADR-046 section 0). + * + * No colour arithmetic, on purpose (ADR-044): the backend picks the source + * view (decoding or not) and the target view (`_SRGB` or not) exactly as it + * does for every other layer, and the hardware does the transfer function. + */ + +#pragma once + +/*! + * The cursor quad's constants: corners[k].xy = tile-normalised position, + * corners[k].zw = texture uv, in triangle-strip order (top-left, bottom-left, + * top-right, bottom-right - u_cursor_draw_corners()'s order). 16 floats. + */ +struct CursorQuadConstants +{ + float corners[4][4]; +}; + +static const char *cursor_quad_vs_source = R"( +cbuffer CursorCB : register(b0) +{ + float4 corners[4]; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 uv : TEXCOORD0; +}; + +VS_OUTPUT main(uint vertex_id : SV_VertexID) +{ + float4 c = corners[vertex_id % 4]; + VS_OUTPUT o; + o.position = float4(c.x * 2.0 - 1.0, 1.0 - c.y * 2.0, 0.0, 1.0); + o.uv = c.zw; + return o; +} +)"; + +// The texel as stored: the blend state carries the alpha convention - +// SrcAlpha / InvSrcAlpha for the app's STRAIGHT sprite (the spec), One / +// InvSrcAlpha for the PREMULTIPLIED default look (comp_cursor_look); alpha +// is One / InvSrcAlpha either way. +// Compiled with `DXR_LAYERED` defined for a layered (arraySize > 1) sprite +// swapchain, whose SRV is a Texture2DArray: the spec samples array layer 0. +static const char *cursor_quad_ps_source = R"( +#ifdef DXR_LAYERED +Texture2DArray sprite_tex : register(t0); +#else +Texture2D sprite_tex : register(t0); +#endif +SamplerState sprite_samp : register(s0); + +float4 main(float4 pos : SV_Position, float2 uv : TEXCOORD0) : SV_Target +{ +#ifdef DXR_LAYERED + return sprite_tex.Sample(sprite_samp, float3(uv, 0.0)); +#else + return sprite_tex.Sample(sprite_samp, uv); +#endif +} +)"; diff --git a/src/xrt/compositor/util/comp_cursor_look.c b/src/xrt/compositor/util/comp_cursor_look.c index 287718bf82..6b035ba4d8 100644 --- a/src/xrt/compositor/util/comp_cursor_look.c +++ b/src/xrt/compositor/util/comp_cursor_look.c @@ -80,16 +80,17 @@ comp_cursor_look_fini(struct comp_cursor_look *look) { free(look->rgba); look->rgba = NULL; + look->os_failed = NULL; if (look->os_cursor != NULL) { comp_cursor_look_platform_release(look->os_cursor); look->os_cursor = NULL; } } -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(_WIN32) /* * No OS-cursor mirror on this platform yet (Linux: XFixesGetCursorImage is - * the follow-up; Windows: GetCursorInfo + GetIconInfo). The arrow it is. + * the follow-up). The arrow it is. */ bool comp_cursor_look_platform_mirror(struct comp_cursor_look *look, float backing_scale) diff --git a/src/xrt/compositor/util/comp_cursor_look.h b/src/xrt/compositor/util/comp_cursor_look.h index d473b7e61b..a599fef83f 100644 --- a/src/xrt/compositor/util/comp_cursor_look.h +++ b/src/xrt/compositor/util/comp_cursor_look.h @@ -18,9 +18,11 @@ * points. A compositor only uploads the pixels whenever @ref * comp_cursor_look::generation changes and samples them. * - * OS-cursor mirroring is macOS-only today ([NSCursor currentCursor], read - * only when the commit runs on the main thread - AppKit is main-thread-only). - * Elsewhere, and whenever the shape cannot be read, the look is the arrow. + * OS-cursor mirroring: macOS ([NSCursor currentCursor], read only when the + * commit runs on the main thread - AppKit is main-thread-only; + * comp_cursor_look_macos.m) and Windows (GetCursorInfo + DrawIconEx over + * black and white, at the panel's DPI; comp_cursor_look_win.c). Elsewhere, + * and whenever the shape cannot be read, the look is the arrow. * `DXR_CURSOR_DEFAULT_LOOK=arrow` forces the arrow everywhere. * * ZERO COST UNLESS REQUESTED (ADR-046 section 0): nothing here runs, and @@ -56,6 +58,10 @@ struct comp_cursor_look uint64_t generation; //! The mirrored OS cursor (platform object, retained), NULL for the arrow. void *os_cursor; + //! Windows: the backing scale (DPI / 96) the mirrored image was made at, + //! and a cursor that could not be read (not retried every frame). + float os_scale; + void *os_failed; }; /*! @@ -70,8 +76,8 @@ bool comp_cursor_look_update(struct comp_cursor_look *look, float backing_scale); /*! - * Backing pixels per OS point of the main screen (macOS), 1 elsewhere. Read on - * the main thread and cached; off the main thread before the first read, 2 on + * Backing pixels per OS point of the main screen (macOS), the system DPI / 96 + * (Windows), 1 elsewhere. macOS: read on the main thread and cached; off the main thread before the first read, 2 on * macOS. The panel's metres per OS point is its pixel pitch times this. */ float @@ -84,7 +90,8 @@ comp_cursor_look_fini(struct comp_cursor_look *look); /* * - * Internal - the platform half (comp_cursor_look_macos.m on Apple). + * Internal - the platform half (comp_cursor_look_macos.m on Apple, + * comp_cursor_look_win.c on Windows). * */ diff --git a/src/xrt/compositor/util/comp_cursor_look_win.c b/src/xrt/compositor/util/comp_cursor_look_win.c new file mode 100644 index 0000000000..8e03de727e --- /dev/null +++ b/src/xrt/compositor/util/comp_cursor_look_win.c @@ -0,0 +1,258 @@ +// Copyright 2026, The DisplayXR Project and its contributors +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief The runtime-drawn cursor's default look, Windows half: mirror the + * current Win32 cursor (XR_DXR_cursor_depth v5, ADR-046 Amendment 2 + * A.6 / B). + * @author David Fattal + * @ingroup comp_util + * + * GetCursorInfo() -> HCURSOR, drawn with DrawIconEx() into a top-down 32-bit + * DIB twice - over black and over white - and recovered by + * u_cursor_draw_unblend(). That reads every cursor kind the same way (32-bit + * alpha, colour + AND mask, monochrome AND/XOR) without branching on its type; + * the XOR ("invert the screen") pixels come out opaque white. + * + * GetCursorInfo() is global - not per thread or per process - so the + * compositor thread may call it. The image is drawn at the size the OS draws + * the cursor on this panel (SM_CYCURSOR at the panel's DPI, or the image's own + * size when larger) and cached by HCURSOR + scale: one GetCursorInfo() per + * drawn frame, a re-raster only when the shape or the DPI changes. + * + * Hidden cursor: an app hides its cursor with SetCursor(NULL) as soon as a + * locate reports willDraw, often before that frame's xrEndFrame, so on most + * drawn frames GetCursorInfo() has no cursor to report. The shape is then the + * CLASS cursor of the window under the pointer, else the last mirrored shape, + * else the system arrow (IDC_ARROW). + */ + +#include "comp_cursor_look.h" + +#include "util/u_cursor_depth.h" +#include "util/u_logging.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include +#include +#include + +//! Largest OS cursor image we mirror, per side (accessibility sizes reach 256). +#define CURSOR_LOOK_MAX_PX 256 + +typedef int(WINAPI *pfn_get_system_metrics_for_dpi)(int, UINT); +typedef UINT(WINAPI *pfn_get_dpi_for_system)(void); + +static FARPROC +user32_proc(const char *name) +{ + HMODULE user32 = GetModuleHandleA("user32.dll"); + return user32 != NULL ? GetProcAddress(user32, name) : NULL; +} + +//! The OS cursor's nominal height at @p dpi (SM_CYCURSOR), or 0 if unknown. +static int +cursor_size_for_dpi(UINT dpi) +{ + static pfn_get_system_metrics_for_dpi fn = NULL; + static bool looked = false; + if (!looked) { + looked = true; + fn = (pfn_get_system_metrics_for_dpi)(void *)user32_proc("GetSystemMetricsForDpi"); + } + return fn != NULL ? fn(SM_CYCURSOR, dpi) : 0; // pre-1607: the image's own size +} + +//! Rasterise @p hc over black and white; on success the look holds it. +static bool +snapshot(struct comp_cursor_look *look, HCURSOR hc, float scale) +{ + ICONINFO ii; + memset(&ii, 0, sizeof(ii)); + if (!GetIconInfo(hc, &ii)) { + return false; + } + BITMAP bm; + memset(&bm, 0, sizeof(bm)); + int native_w = 0; + int native_h = 0; + if (ii.hbmColor != NULL && GetObject(ii.hbmColor, sizeof(bm), &bm) != 0) { + native_w = bm.bmWidth; + native_h = bm.bmHeight; + } else if (ii.hbmMask != NULL && GetObject(ii.hbmMask, sizeof(bm), &bm) != 0) { + // Monochrome: the AND mask stacked on the XOR mask. + native_w = bm.bmWidth; + native_h = bm.bmHeight / 2; + } + const int hot_x = (int)ii.xHotspot; + const int hot_y = (int)ii.yHotspot; + if (ii.hbmColor != NULL) { + DeleteObject(ii.hbmColor); + } + if (ii.hbmMask != NULL) { + DeleteObject(ii.hbmMask); + } + if (native_w <= 0 || native_h <= 0) { + return false; + } + + // The size the OS draws it at on this panel: the per-DPI nominal size, or + // the image's own when larger (an accessibility pointer size loads a + // bigger image than SM_CYCURSOR reports). + const UINT dpi = (UINT)lrintf(96.0f * scale); + int th = cursor_size_for_dpi(dpi); + if (th < native_h) { + th = native_h; + } + const int tw = (int)lrintf((float)native_w * (float)th / (float)native_h); + if (tw <= 0 || th <= 0 || tw > CURSOR_LOOK_MAX_PX || th > CURSOR_LOOK_MAX_PX) { + return false; + } + + const size_t n = (size_t)tw * (size_t)th; + uint8_t *black = malloc(n * 4); + uint8_t *white = malloc(n * 4); + uint8_t *rgba = malloc(n * 4); + HDC screen = GetDC(NULL); + HDC mem = screen != NULL ? CreateCompatibleDC(screen) : NULL; + HBITMAP dib = NULL; + void *bits = NULL; + bool ok = black != NULL && white != NULL && rgba != NULL && mem != NULL; + if (ok) { + BITMAPINFO bmi; + memset(&bmi, 0, sizeof(bmi)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = tw; + bmi.bmiHeader.biHeight = -th; // top-down: row 0 is the top row + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + dib = CreateDIBSection(mem, &bmi, DIB_RGB_COLORS, &bits, NULL, 0); + ok = dib != NULL && bits != NULL; + } + if (ok) { + HGDIOBJ old = SelectObject(mem, dib); + memset(bits, 0x00, n * 4); + ok = DrawIconEx(mem, 0, 0, hc, tw, th, 0, NULL, DI_NORMAL) != 0; + GdiFlush(); + memcpy(black, bits, n * 4); + memset(bits, 0xFF, n * 4); + ok = ok && DrawIconEx(mem, 0, 0, hc, tw, th, 0, NULL, DI_NORMAL) != 0; + GdiFlush(); + memcpy(white, bits, n * 4); + SelectObject(mem, old); + } + if (ok) { + // DIB pixels are B, G, R, x: recover (premultiplied), then swap to + // R, G, B, A - the look's contract. + u_cursor_draw_unblend(black, white, (uint32_t)n, rgba); + for (size_t i = 0; i < n; i++) { + const uint8_t b = rgba[i * 4 + 0]; + rgba[i * 4 + 0] = rgba[i * 4 + 2]; + rgba[i * 4 + 2] = b; + } + } + if (dib != NULL) { + DeleteObject(dib); + } + if (mem != NULL) { + DeleteDC(mem); + } + if (screen != NULL) { + ReleaseDC(NULL, screen); + } + free(black); + free(white); + if (!ok) { + free(rgba); + return false; + } + + const float s = (float)th / (float)native_h; + free(look->rgba); + look->rgba = rgba; + look->w = (uint32_t)tw; + look->h = (uint32_t)th; + look->aspect = (float)tw / (float)th; + look->hot_u = ((float)hot_x * s) / (float)tw; + look->hot_v = ((float)hot_y * s) / (float)th; + look->height_pt = (float)th / scale; // OS points = 96-DPI pixels + look->os_cursor = (void *)hc; // shared USER object: nothing to retain + look->os_scale = scale; + look->generation++; + // Not per frame: only when the cursor shape (or the DPI) changes. The + // FIRST mirror is a one-time WARN (INFO is dropped from the log file, so + // an INFO-only line never showed that the mirror ran); later shape + // changes stay INFO. + static bool announced = false; // process-wide: one notice, whichever compositor mirrors first + if (!announced) { + announced = true; + U_LOG_W("cursor_draw: default look mirrors the OS cursor (%dx%d px at %u DPI, image %dx%d, hotspot " + "%d,%d; one-time notice, later shape changes log at INFO)", + tw, th, dpi, native_w, native_h, hot_x, hot_y); + } else { + U_LOG_I("cursor_draw: default look = the current OS cursor (%dx%d px at %u DPI, image %dx%d, hotspot %d,%d)", + tw, th, dpi, native_w, native_h, hot_x, hot_y); + } + return true; +} + +bool +comp_cursor_look_platform_mirror(struct comp_cursor_look *look, float backing_scale) +{ + const float scale = backing_scale > 0.0f ? backing_scale : comp_cursor_look_platform_backing_scale(); + + CURSORINFO ci; + memset(&ci, 0, sizeof(ci)); + ci.cbSize = sizeof(ci); + HCURSOR hc = GetCursorInfo(&ci) ? ci.hCursor : NULL; + if (hc == NULL) { + // Hidden (see the file comment): the class cursor of the window + // under the pointer, else the last mirrored shape, else IDC_ARROW. + POINT pt; + HWND under = GetCursorPos(&pt) ? WindowFromPoint(pt) : NULL; + hc = under != NULL ? (HCURSOR)GetClassLongPtr(under, GCLP_HCURSOR) : NULL; + if (hc == NULL) { + hc = look->os_cursor != NULL ? (HCURSOR)look->os_cursor : LoadCursor(NULL, IDC_ARROW); + } + } + if (hc == NULL) { + return false; + } + if ((void *)hc == look->os_cursor && look->os_scale == scale && look->rgba != NULL) { + return true; // unchanged: the common case + } + if ((void *)hc == look->os_failed) { + return false; // an unreadable shape is not retried every frame + } + if (snapshot(look, hc, scale)) { + return true; + } + look->os_failed = (void *)hc; + U_LOG_W("cursor_draw: could not read the OS cursor %p - keeping the last shape or the arrow", (void *)hc); + return false; +} + +void +comp_cursor_look_platform_release(void *os_cursor) +{ + // An HCURSOR from GetCursorInfo / LoadCursor(NULL, ...) / a class is a + // shared USER object: it is never destroyed by its reader. + (void)os_cursor; +} + +float +comp_cursor_look_platform_backing_scale(void) +{ + static float cached = 0.0f; + if (cached <= 0.0f) { + pfn_get_dpi_for_system fn = (pfn_get_dpi_for_system)(void *)user32_proc("GetDpiForSystem"); + const UINT dpi = fn != NULL ? fn() : 96; + cached = (float)(dpi != 0 ? dpi : 96) / 96.0f; + } + return cached; +} diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index 8dc335ae96..7411c5b4a9 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -3182,6 +3182,11 @@ struct oxr_session struct xrt_quat orientation; //! One sprite unit: the canvas height scaled by the placement's t. float unit; + //! That locate's ACTIVE view count. A projection layer may carry more + //! views (an app submitting for the worst-case mode under + //! PRIMARY_MULTIVIEW_DXR); only the active ones are tiles, so the + //! request is capped to this. + uint32_t view_count; //! The app's sprite, or NULL for the default look. struct oxr_swapchain *image; float hot_u, hot_v, size_m; diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 3f00f10dd6..6350957235 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -2319,9 +2319,9 @@ oxr_session_cursor_depth_hand_request(struct oxr_session *sess, const struct u_c /* * XR_DXR_cursor_depth v5 - the runtime draws the cursor (ADR-046 Amendment 2 - * B). The in-process Metal, vk_native and GL compositors draw; the others and - * the service report "cannot draw", so locates report willDraw = false and - * the app keeps its flat OS cursor - never no cursor. + * B). The in-process Metal, vk_native, GL, D3D11 and D3D12 compositors + * draw; the service reports "cannot draw", so locates report willDraw = + * false and the app keeps its flat OS cursor - never no cursor. */ bool oxr_session_cursor_draw_supported(struct oxr_session *sess) @@ -2344,8 +2344,18 @@ oxr_session_cursor_draw_supported(struct oxr_session *sess) return true; } #endif - // TODO(ADR-046 Amendment 2 B, rollout step 3): D3D11, D3D12 and the - // IPC/service path (svc_draw_cursor_quads). +#ifdef XRT_HAVE_D3D11_NATIVE_COMPOSITOR + if (sess->is_d3d11_native_compositor) { + return true; + } +#endif +#ifdef XRT_HAVE_D3D12_NATIVE_COMPOSITOR + if (sess->is_d3d12_native_compositor) { + return true; + } +#endif + // TODO(ADR-046 Amendment 2 B, rollout step 3): the IPC/service path + // (svc_draw_cursor_quads). return false; } @@ -2372,6 +2382,18 @@ oxr_session_cursor_draw_hand_request(struct oxr_session *sess, const struct u_cu comp_gl_compositor_set_cursor_draw_request(&sess->xcn->base, req); return true; } +#endif +#ifdef XRT_HAVE_D3D11_NATIVE_COMPOSITOR + if (sess->is_d3d11_native_compositor) { + comp_d3d11_compositor_set_cursor_draw_request(&sess->xcn->base, req); + return true; + } +#endif +#ifdef XRT_HAVE_D3D12_NATIVE_COMPOSITOR + if (sess->is_d3d12_native_compositor) { + comp_d3d12_compositor_set_cursor_draw_request(&sess->xcn->base, req); + return true; + } #endif (void)req; return false; @@ -2571,6 +2593,7 @@ oxr_session_arm_cursor_draw(struct oxr_session *sess, sess->cursor_draw.position = *position; sess->cursor_draw.orientation = g->orientation; sess->cursor_draw.unit = unit; + sess->cursor_draw.view_count = n; sess->cursor_draw.image = image; sess->cursor_draw.hot_u = draw_info->hotSpot.x; sess->cursor_draw.hot_v = draw_info->hotSpot.y; diff --git a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c index 574d7f55f3..6bcf5b951e 100644 --- a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c +++ b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c @@ -2076,7 +2076,13 @@ cursor_draw_submit(struct oxr_session *sess, const XrFrameEndInfo *frameEndInfo, struct u_cursor_draw_request req = {0}; req.requested = true; + // Only the views the arming locate reported active: a layer submitted for + // the worst-case mode carries views that are not tiles in this one, and + // their poses must not decide drawability. req.view_count = proj->viewCount < XRT_MAX_VIEWS ? proj->viewCount : XRT_MAX_VIEWS; + if (sess->cursor_draw.view_count >= 2 && req.view_count > sess->cursor_draw.view_count) { + req.view_count = sess->cursor_draw.view_count; + } struct u_cursor_depth_view submitted[XRT_MAX_VIEWS]; for (uint32_t i = 0; i < req.view_count; i++) { // XrPosef/XrFovf are layout-identical to xrt_pose/xrt_fov. diff --git a/test_apps/common/cursor_depth_zones.h b/test_apps/common/cursor_depth_zones.h index a63d55cca6..9fccdc5eb1 100644 --- a/test_apps/common/cursor_depth_zones.h +++ b/test_apps/common/cursor_depth_zones.h @@ -19,9 +19,13 @@ * afterLocate(zi, views, n) * end() pointer over no 3D zone: OS cursor back * - * The pointer is the window's (macOS: the NSView's), or a scripted - * WINDOW-normalised UV from DISPLAYXR_CURSOR_DEPTH_UV=u,v for headless - * verification. Opt out with DISPLAYXR_CURSOR_DEPTH=0. + * The pointer is the window's (macOS: the NSView's, init(); Windows: the + * HWND's client area, initWin32()), or a scripted WINDOW-normalised UV from + * DISPLAYXR_CURSOR_DEPTH_UV=u,v for headless verification. Opt out with + * DISPLAYXR_CURSOR_DEPTH=0. On Windows dxr::CursorDepth's Win32 adapter + * (setWindowWin32) hides the OS cursor over the client area exactly while the + * runtime reports willDraw - it subclasses the window procedure and answers + * WM_SETCURSOR, from any thread - so the app needs no cursor code of its own. */ #pragma once @@ -34,11 +38,21 @@ #include #include #include +#include #include #ifdef __OBJC__ #import #endif +#if defined(_WIN32) && !defined(__OBJC__) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace dxr_zones_cursor { @@ -84,6 +98,39 @@ class ZoneCursor m_cursor.enable(spec); if (spec == 0) return; m_cursor.setWindowCocoa(nsView); + setup(); + } + +#if defined(_WIN32) && !defined(__OBJC__) + /*! + * Windows: @p hwnd is the client window the zone rects are authored + * against (reads the pointer, hides the OS cursor via WM_SETCURSOR). + * DISPLAYXR_CURSOR_DEPTH_DRAW=app: hint + placement only, the OS cursor + * is kept (the D3D zone apps draw no in-app crosshair). + */ + void initWin32(uint32_t spec, void *hwnd, float cubeSize) + { + m_cubeSize = cubeSize; + m_view = hwnd; + m_cursor.enable(spec); + if (spec == 0) return; + const char *who = getenv("DISPLAYXR_CURSOR_DEPTH_DRAW"); + if (who != nullptr && strcmp(who, "app") == 0) { + // Hint + placement only. No window adapter: these apps draw no + // crosshair, so the OS cursor must never be hidden. + m_cursor.setDrawMode(dxr::CursorDepth::DrawMode::App); + } else { + m_cursor.setWindowWin32(hwnd); + } + setup(); + } +#endif + + bool enabled() const { return m_cursor.enabled(); } + +private: + void setup() + { m_cursor.setCursorHeight(0.04f); m_cursor.setWantPlacement(true); m_cursor.setRayCast([this](const dxr::CursorRay &r) -> std::optional { @@ -99,8 +146,7 @@ class ZoneCursor } } - bool enabled() const { return m_cursor.enabled(); } - +public: /*! * Pick the topmost 3D zone (the last in layer order) under the pointer and * hand dxr::CursorDepth the pointer in THAT zone's UV. @@ -166,6 +212,14 @@ class ZoneCursor NSPoint p = [view convertPoint:[view.window mouseLocationOutsideOfEventStream] fromView:nil]; const NSRect b = view.bounds; return dxr::cursorCanvasUV(p.x, p.y, b.size.width, b.size.height, !view.isFlipped, uv); +#elif defined(_WIN32) + HWND hwnd = static_cast(m_view); + POINT pt; + RECT rc; + if (hwnd == nullptr || !GetCursorPos(&pt) || !ScreenToClient(hwnd, &pt) || !GetClientRect(hwnd, &rc)) { + return false; + } + return dxr::cursorCanvasUV(pt.x, pt.y, rc.right - rc.left, rc.bottom - rc.top, false, uv); #else (void)uv; return false; diff --git a/test_apps/handle/cube_handle_d3d11_win/main.cpp b/test_apps/handle/cube_handle_d3d11_win/main.cpp index aaf85efe10..f2d1119ba7 100644 --- a/test_apps/handle/cube_handle_d3d11_win/main.cpp +++ b/test_apps/handle/cube_handle_d3d11_win/main.cpp @@ -27,8 +27,10 @@ #include "xr_session.h" #include +#include #include #include +#include #include #include #include @@ -411,6 +413,11 @@ static uint32_t g_hwOverrideModeIndex = 0; static UINT g_windowWidth = 1280; static UINT g_windowHeight = 720; static bool g_inSizeMove = false; // True while user is dragging/resizing the window +// XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): true exactly while the +// runtime reports willDraw - the runtime draws the cursor, so the OS cursor is +// hidden over the client area (WM_SETCURSOR below). Render loop and message +// pump share this thread, so no locking. +static bool g_hideOsCursor = false; static const uint32_t HUD_PIXEL_WIDTH = 380; static const uint32_t HUD_PIXEL_HEIGHT = 470; static const float CAMERA_HALF_TAN_VFOV = 0.32491969623f; // tan(18°) → 36° vFOV @@ -592,6 +599,17 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { ReleaseCapture(); return 0; + case WM_SETCURSOR: + // XR_DXR_cursor_depth v5: while the runtime draws the depth cursor, + // the OS cursor is hidden over the client area; everywhere else (title + // bar, borders) and whenever willDraw is false, the default handling + // shows the class cursor. + if (g_hideOsCursor && LOWORD(lParam) == HTCLIENT) { + SetCursor(nullptr); + return TRUE; + } + break; + case WM_SIZE: if (wParam != SIZE_MINIMIZED) { g_windowWidth = LOWORD(lParam); @@ -917,6 +935,204 @@ static bool CreateAndFillL2DPanel(XrSessionManager& xr, ID3D11Device* device, ID return true; } +// ============================================================================ +// XR_DXR_cursor_depth (ADR-046) — reference adoption, spec v5 (Amendment 2 B). +// +// Mirrors cube_handle_metal_macos. The app owns the depth: each frame it +// hit-tests the cube under the cursor FOOTPRINT (rays from the outer views of +// the previous locate through a ring of points around the hotspot) and hands +// the nearest hit to xrLocateViews in an XrCursorDepthHintDXR. +// +// DISPLAYXR_CURSOR_DEPTH=0 opt out (extension not enabled) +// DISPLAYXR_CURSOR_DEPTH_DRAW=runtime (default, needs spec v5) chain +// XrCursorDepthDrawInfoDXR: the RUNTIME +// draws the cursor into every view +// before weaving; the OS cursor is +// hidden exactly while willDraw. +// DISPLAYXR_CURSOR_DEPTH_DRAW=app hint + placement only. This D3D11 +// reference does NOT draw an in-app +// crosshair (the Metal app does): it +// keeps the OS cursor and logs the +// placement — the runtime-drawn path is +// what this app verifies. +// DISPLAYXR_CURSOR_DEPTH_SPRITE=custom hand the runtime our own sprite (a +// 64x64 cyan ring + centre dot, 1 cm) +// instead of its default look (the OS +// cursor's shape and size). +// DISPLAYXR_CURSOR_DEPTH_UV=u,v scripted cursor (canvas-normalised, +// origin top-left) for headless checks. +// ============================================================================ + +static const float kCursorCubeSize = 0.06f; // RenderScene's default cube edge +static const float kCursorCubeHeight = 0.03f; // RenderScene(..., cubeHeight = 0.03f) +static const float kCursorHeight = 0.04f; // hint footprint, fraction of canvas height + +static bool g_cursorRuntimeDraw = false; +static bool g_cursorCustomSprite = false; +static XrSwapchain g_cursorSprite = XR_NULL_HANDLE; + +static void CursorQuatRotate(const XrQuaternionf& q, float vx, float vy, float vz, XrVector3f& out) +{ + // v' = v + 2w (q x v) + 2 q x (q x v) + const float tx = 2.0f * (q.y * vz - q.z * vy); + const float ty = 2.0f * (q.z * vx - q.x * vz); + const float tz = 2.0f * (q.x * vy - q.y * vx); + out.x = vx + q.w * tx + (q.y * tz - q.z * ty); + out.y = vy + q.w * ty + (q.z * tx - q.x * tz); + out.z = vz + q.w * tz + (q.x * ty - q.y * tx); +} + +// Slab test against the cube as RenderScene places it: centred at +// (0, kCursorCubeHeight, 0), edge kCursorCubeSize, rotated `rot` about +Y +// (XMMatrixRotationY, row vectors: x' = c x + s z, z' = -s x + c z). +static bool CursorRayHitCube(const XrVector3f& o, const XrVector3f& d, float rot, float& tOut) +{ + const float c = cosf(rot), s = sinf(rot), h = kCursorCubeSize * 0.5f; + // local = R^T (world - centre) + const float ox = o.x, oy = o.y - kCursorCubeHeight, oz = o.z; + const float lo[3] = {c * ox - s * oz, oy, s * ox + c * oz}; + const float ld[3] = {c * d.x - s * d.z, d.y, s * d.x + c * d.z}; + float t0 = 0.0f, t1 = 1e30f; + for (int i = 0; i < 3; i++) { + if (fabsf(ld[i]) < 1e-9f) { + if (lo[i] < -h || lo[i] > h) return false; + continue; + } + float ta = (-h - lo[i]) / ld[i], tb = (h - lo[i]) / ld[i]; + if (ta > tb) std::swap(ta, tb); + if (ta > t0) t0 = ta; + if (tb < t1) t1 = tb; + if (t0 > t1) return false; + } + tOut = t0; + return true; +} + +// Ray from a located view through canvas point (u, v): Kooima frusta all +// frame the canvas, so (u, v) maps linearly onto the fov tangents. +static XrVector3f CursorViewRayDir(const XrView& view, float u, float v) +{ + const float tl = tanf(view.fov.angleLeft), tr = tanf(view.fov.angleRight); + const float tu = tanf(view.fov.angleUp), td = tanf(view.fov.angleDown); + XrVector3f d; + CursorQuatRotate(view.pose.orientation, tl + u * (tr - tl), tu - v * (tu - td), -1.0f, d); + return d; +} + +// Nearest cube point (to the viewer) under the cursor footprint, using the +// outer views of the previous locate. Footprint = the sprite plus 50%. +static bool CursorFootprintHit(const std::vector& views, uint32_t active, float u, float v, + float canvasAspect, float rot, XrVector3f& nearest) +{ + if (active < 2 || views.size() < active) return false; + const XrView* outer[2] = {&views[0], &views[active - 1]}; + XrVector3f fwd; + CursorQuatRotate(views[0].pose.orientation, 0.0f, 0.0f, -1.0f, fwd); + const float r = 0.75f * kCursorHeight; // footprint radius, in canvas heights + bool found = false; + float best = 1e30f; + for (int k = -1; k < 8; k++) { + float su = u, sv = v; + if (k >= 0) { + const float a = (float)k * 0.78539816f; + su += r * cosf(a) / canvasAspect; + sv += r * sinf(a); + } + for (const XrView* view : outer) { + const XrVector3f d = CursorViewRayDir(*view, su, sv); + float t; + if (!CursorRayHitCube(view->pose.position, d, rot, t)) continue; + const XrVector3f p = {view->pose.position.x + d.x * t, view->pose.position.y + d.y * t, + view->pose.position.z + d.z * t}; + const float depth = p.x * fwd.x + p.y * fwd.y + p.z * fwd.z; // smaller = nearer the viewer + if (depth < best) { + best = depth; + nearest = p; + found = true; + } + } + } + return found; +} + +// Apply the OS-cursor visibility right away when the pointer is over the +// client area (WM_SETCURSOR keeps it that way on later mouse moves). +static void ApplyOsCursorVisibility(HWND hwnd, bool hide) +{ + if (g_hideOsCursor == hide) return; + g_hideOsCursor = hide; + POINT p; + RECT rc; + if (GetCursorPos(&p) && ScreenToClient(hwnd, &p) && GetClientRect(hwnd, &rc) && PtInRect(&rc, p)) { + SetCursor(hide ? nullptr : LoadCursor(nullptr, IDC_ARROW)); + } +} + +// DISPLAYXR_CURSOR_DEPTH_SPRITE=custom: a 64x64 cyan ring with a centre dot +// (hotspot = the centre), dark-edged so it reads over the white cube. Filled +// once (acquire / fill / release); the runtime samples the last released image +// every frame, as STRAIGHT alpha. _SRGB when offered: the bytes are +// display-referred. +static bool CreateCursorSprite(XrSessionManager& xr, ID3D11DeviceContext* context) +{ + const uint32_t S = 64; + XrSwapchainCreateInfo sci = {XR_TYPE_SWAPCHAIN_CREATE_INFO}; + sci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT; + sci.format = PickPanelFormat(xr.session, (int64_t)DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + (int64_t)DXGI_FORMAT_R8G8B8A8_UNORM); + sci.sampleCount = 1; + sci.width = S; + sci.height = S; + sci.faceCount = 1; + sci.arraySize = 1; + sci.mipCount = 1; + if (XR_FAILED(xrCreateSwapchain(xr.session, &sci, &g_cursorSprite))) { + g_cursorSprite = XR_NULL_HANDLE; + return false; + } + uint32_t n = 0; + xrEnumerateSwapchainImages(g_cursorSprite, 0, &n, nullptr); + std::vector imgs(n, {XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR}); + if (n == 0 || XR_FAILED(xrEnumerateSwapchainImages(g_cursorSprite, n, &n, + (XrSwapchainImageBaseHeader*)imgs.data()))) { + return false; + } + XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; + uint32_t idx = 0; + if (XR_FAILED(xrAcquireSwapchainImage(g_cursorSprite, &ai, &idx))) { + return false; + } + XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + wi.timeout = XR_INFINITE_DURATION; + xrWaitSwapchainImage(g_cursorSprite, &wi); + + std::vector buf((size_t)S * S * 4); + auto cover = [](float d, float r0, float r1) { // 1 inside [r0, r1], 1-px soft edges + const float a = (std::min)(d - r0 + 0.5f, r1 - d + 0.5f); + return a <= 0.0f ? 0.0f : (a >= 1.0f ? 1.0f : a); + }; + for (uint32_t y = 0; y < S; y++) { + for (uint32_t x = 0; x < S; x++) { + const float d = hypotf((float)x + 0.5f - S * 0.5f, (float)y + 0.5f - S * 0.5f); + const float ring = (std::max)(cover(d, 21.0f, 27.0f), cover(d, 0.0f, 5.0f)); // cyan + const float edge = (std::max)(cover(d, 19.5f, 28.5f), cover(d, 0.0f, 6.5f)); // dark rim + uint8_t* px = &buf[((size_t)y * S + x) * 4]; + // RGBA, straight alpha: cyan where the ring is, near-black rim. + px[0] = 0; + px[1] = (uint8_t)(220.0f * ring); + px[2] = (uint8_t)(255.0f * ring); + px[3] = (uint8_t)(255.0f * (std::max)(ring, edge)); + } + } + context->UpdateSubresource(imgs[idx].texture, 0, nullptr, buf.data(), S * 4, 0); + + XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + xrReleaseSwapchainImage(g_cursorSprite, &ri); + LOG_INFO("Cursor sprite swapchain created: %ux%u (cyan ring, hotspot = centre, format %lld)", S, S, + (long long)sci.format); + return true; +} + // Render a single frame — called from the main loop and from WM_PAINT during // drag/resize so that rendering never stalls. static void RenderOneFrame(RenderState& rs) { @@ -1196,8 +1412,109 @@ static void RenderOneFrame(RenderState& rs) { } } + // XR_DXR_cursor_depth (ADR-046): hint from the previous + // locate's views (the hit test needs rays, the rays need + // views - one frame late, which the runtime's filter + // absorbs). Chained on THIS locate (the rig one whose + // views are submitted), in front of the rig struct. + static std::vector cursorPrevViews; + static uint32_t cursorPrevActive = 0; + XrCursorDepthHintDXR cursorHint = {XR_TYPE_CURSOR_DEPTH_HINT_DXR}; + XrCursorDepthPlacementDXR cursorPlacement = {XR_TYPE_CURSOR_DEPTH_PLACEMENT_DXR}; + // Spec v5: "runtime, draw it" - with our sprite (hotspot = + // its centre, 1 cm tall) or the runtime's default look + // (XR_NULL_HANDLE: the OS cursor's shape and size). + XrCursorDepthDrawInfoDXR cursorDrawInfo = {XR_TYPE_CURSOR_DEPTH_DRAW_INFO_DXR}; + cursorDrawInfo.image = g_cursorCustomSprite ? g_cursorSprite : XR_NULL_HANDLE; + cursorDrawInfo.hotSpot = {0.5f, 0.5f}; + cursorDrawInfo.sizeMeters = g_cursorCustomSprite ? 0.01f : 0.0f; + cursorDrawInfo.visible = XR_TRUE; + XrCursorDepthDrawStateDXR cursorDrawState = {XR_TYPE_CURSOR_DEPTH_DRAW_STATE_DXR}; + bool cursorInCanvas = false; + if (g_hasCursorDepthExt && g_windowWidth > 0 && g_windowHeight > 0) { + float u = -1.0f, v = -1.0f; + POINT cp; + RECT crc; + if (GetCursorPos(&cp) && ScreenToClient(rs.hwnd, &cp) && GetClientRect(rs.hwnd, &crc) && + crc.right > crc.left && crc.bottom > crc.top) { + u = (float)cp.x / (float)(crc.right - crc.left); + v = (float)cp.y / (float)(crc.bottom - crc.top); + } + // Scripted cursor for headless verification: DISPLAYXR_CURSOR_DEPTH_UV=u,v + static float fixedU = -1.0f, fixedV = -1.0f; + static bool fixedParsed = false; + if (!fixedParsed) { + fixedParsed = true; + const char* fx = getenv("DISPLAYXR_CURSOR_DEPTH_UV"); + if (fx == nullptr || sscanf(fx, "%f,%f", &fixedU, &fixedV) != 2) fixedU = fixedV = -1.0f; + } + if (fixedU >= 0.0f) { + u = fixedU; + v = fixedV; + } + cursorInCanvas = u >= 0.0f && u <= 1.0f && v >= 0.0f && v <= 1.0f; + if (cursorInCanvas) { + cursorHint.cursorUV = {u, v}; + cursorHint.cursorHeight = kCursorHeight; + XrVector3f hit = {0.0f, 0.0f, 0.0f}; + cursorHint.hasContent = + CursorFootprintHit(cursorPrevViews, cursorPrevActive, u, v, + (float)g_windowWidth / (float)g_windowHeight, + renderer.cubeRotation, hit) + ? XR_TRUE + : XR_FALSE; + cursorHint.nearestPoint = hit; + // Chain: locateInfo -> hint [-> draw info] -> (rig struct). + const void* hintTail = locateInfo.next; + if (g_cursorRuntimeDraw) { + cursorDrawInfo.next = hintTail; + hintTail = &cursorDrawInfo; + } + cursorHint.next = hintTail; + locateInfo.next = &cursorHint; + } + cursorPlacement.next = viewState.next; + viewState.next = &cursorPlacement; + if (g_cursorRuntimeDraw) { + cursorDrawState.next = viewState.next; + viewState.next = &cursorDrawState; + } + } + xrLocateViews(xr.session, &locateInfo, &viewState, 8, &viewCount, rawViews); + if (g_hasCursorDepthExt) { + cursorPrevViews.assign(rawViews, rawViews + (viewCount < 8 ? viewCount : 8)); + cursorPrevActive = monoMode ? 1 : (modeViewCount < viewCount ? modeViewCount : viewCount); + + // The app owns its window's OS cursor: hidden exactly + // while the runtime draws the depth cursor (willDraw - + // whenever it cannot draw, we keep the OS cursor). App + // mode keeps the OS cursor (no in-app crosshair here). + ApplyOsCursorVisibility(rs.hwnd, g_cursorRuntimeDraw && cursorDrawState.willDraw == XR_TRUE); + static int willDrawLogged = 0; + if (g_cursorRuntimeDraw && willDrawLogged < 3 && cursorInCanvas) { + static int lastWillDraw = -1; + if ((int)cursorDrawState.willDraw != lastWillDraw) { + lastWillDraw = (int)cursorDrawState.willDraw; + willDrawLogged++; + LOG_INFO("cursor_depth: runtime willDraw=%s (uv=%.3f,%.3f disparity=%.3f)", + cursorDrawState.willDraw ? "TRUE" : "FALSE", cursorHint.cursorUV.x, + cursorHint.cursorUV.y, cursorPlacement.disparity); + } + } + static int cursorLogged = 0; + if (cursorPlacement.isActive && cursorHint.hasContent && cursorLogged < 3) { + cursorLogged++; + LOG_INFO("cursor_depth: uv=(%.3f,%.3f) target=%.3f disparity=%.3f h=%.4f " + "pos=(%.3f,%.3f,%.3f)", + cursorHint.cursorUV.x, cursorHint.cursorUV.y, cursorPlacement.targetDisparity, + cursorPlacement.disparity, cursorPlacement.height, + cursorPlacement.pose.position.x, cursorPlacement.pose.position.y, + cursorPlacement.pose.position.z); + } + } + // Multi-screen M3: with the window spanning displays the // runtime hands out one view set per display; render ALL // of them (the sum is activeViewCount), each set's local @@ -2062,6 +2379,24 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine return 1; } + // XR_DXR_cursor_depth spec v5: who draws the cursor. Default: the runtime. + if (g_hasCursorDepthExt) { + const char* drawWho = getenv("DISPLAYXR_CURSOR_DEPTH_DRAW"); + const bool wantApp = drawWho != nullptr && strcmp(drawWho, "app") == 0; + if (!wantApp && g_cursorDepthSpecVersion >= 5) { + g_cursorRuntimeDraw = true; + const char* sprite = getenv("DISPLAYXR_CURSOR_DEPTH_SPRITE"); + g_cursorCustomSprite = sprite != nullptr && strcmp(sprite, "custom") == 0; + } else if (!wantApp) { + LOG_WARN("XR_DXR_cursor_depth v%u has no runtime-drawn cursor (needs v5) - hint only, OS cursor kept", + g_cursorDepthSpecVersion); + } + LOG_INFO("XR_DXR_cursor_depth draw: %s", + !g_cursorRuntimeDraw ? "APP (hint + placement only; OS cursor kept, no in-app crosshair)" + : g_cursorCustomSprite ? "RUNTIME, custom sprite (cyan ring)" + : "RUNTIME, default look"); + } + // INV-1.3: open on the 3D panel (#715) — one-shot move to the panel's // desktop position reported by xrGetSystemProperties (virtual-screen // coords, top-down; (0,0) = primary/unknown is safe), BEFORE @@ -2158,6 +2493,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine ShutdownLogging(); return 1; } + if (g_cursorRuntimeDraw && g_cursorCustomSprite && !CreateCursorSprite(xr, renderer.context.Get())) { + LOG_WARN("Cursor sprite swapchain creation failed - using the runtime's default look"); + g_cursorCustomSprite = false; + } // Enumerate D3D11 swapchain images std::vector swapchainImages; @@ -2303,6 +2642,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine g_xr = nullptr; CleanupHands(); + if (g_cursorSprite != XR_NULL_HANDLE) { + xrDestroySwapchain(g_cursorSprite); + g_cursorSprite = XR_NULL_HANDLE; + } CleanupOpenXR(xr); if (hudOk) CleanupHudRenderer(hudRenderer); CleanupD3D11(renderer); diff --git a/test_apps/handle/cube_handle_d3d11_win/xr_session.cpp b/test_apps/handle/cube_handle_d3d11_win/xr_session.cpp index be2c673de1..607844f69d 100644 --- a/test_apps/handle/cube_handle_d3d11_win/xr_session.cpp +++ b/test_apps/handle/cube_handle_d3d11_win/xr_session.cpp @@ -8,10 +8,13 @@ #include "xr_session.h" #include "logging.h" #include "dxr_view_config.h" +#include #include // XR_DXR_view_rig (#396 W7): app-local availability flag (see xr_session.h). bool g_hasViewRigExt = false; +bool g_hasCursorDepthExt = false; // XR_DXR_cursor_depth (ADR-046) +uint32_t g_cursorDepthSpecVersion = 0; bool g_hasHandTrackingExt = false; bool g_hasViewsChangeExt = false; // #1488 @@ -112,6 +115,10 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_DXR_LOCAL_3D_ZONE_EXTENSION_NAME) == 0) { g_zone.available = true; } + if (strcmp(ext.extensionName, XR_DXR_CURSOR_DEPTH_EXTENSION_NAME) == 0) { + g_hasCursorDepthExt = true; + g_cursorDepthSpecVersion = ext.extensionVersion; + } if (strcmp(ext.extensionName, XR_EXT_HAND_TRACKING_EXTENSION_NAME) == 0) { g_hasHandTrackingExt = true; } @@ -168,6 +175,15 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (g_zone.available) { enabledExtensions.push_back(XR_DXR_LOCAL_3D_ZONE_EXTENSION_NAME); } + if (g_hasCursorDepthExt) { + const char* opt = getenv("DISPLAYXR_CURSOR_DEPTH"); + if (opt != nullptr && strcmp(opt, "0") == 0) { + g_hasCursorDepthExt = false; // opted out: the runtime does no cursor work + } else { + enabledExtensions.push_back(XR_DXR_CURSOR_DEPTH_EXTENSION_NAME); + } + } + LOG_INFO("XR_DXR_cursor_depth: %s (v%u)", g_hasCursorDepthExt ? "ENABLED" : "off", g_cursorDepthSpecVersion); if (g_hasHandTrackingExt) { enabledExtensions.push_back(XR_EXT_HAND_TRACKING_EXTENSION_NAME); } diff --git a/test_apps/handle/cube_handle_d3d11_win/xr_session.h b/test_apps/handle/cube_handle_d3d11_win/xr_session.h index 47dc7d3a80..f738919a82 100644 --- a/test_apps/handle/cube_handle_d3d11_win/xr_session.h +++ b/test_apps/handle/cube_handle_d3d11_win/xr_session.h @@ -15,6 +15,7 @@ #include "xr_session_common.h" #include #include +#include // INV-1.3 (#715): 3D panel top-left in virtual-desktop pixels (top-down, // origin = primary top-left); (0,0) = primary/unknown. Filled by @@ -27,6 +28,12 @@ extern int32_t g_displayScreenTop; // vehicle; promote to xr_session_common when more consumers adopt it. extern bool g_hasViewRigExt; +// XR_DXR_cursor_depth (ADR-046) available + enabled on the instance, and the +// spec version the runtime advertises (v5 = the runtime can draw the cursor). +// Opt out with DISPLAYXR_CURSOR_DEPTH=0. App-local, like the view-rig flag. +extern bool g_hasCursorDepthExt; +extern uint32_t g_cursorDepthSpecVersion; + // XR_EXT_hand_tracking available + enabled on the instance (#825 Tier 2). // App-local for the same reason as the view-rig flag — this app is the // hand-tracking verify vehicle (`--hands` joint markers). diff --git a/test_apps/handle/cube_handle_d3d12_win/main.cpp b/test_apps/handle/cube_handle_d3d12_win/main.cpp index 1ff2656d95..eb233da264 100644 --- a/test_apps/handle/cube_handle_d3d12_win/main.cpp +++ b/test_apps/handle/cube_handle_d3d12_win/main.cpp @@ -22,9 +22,11 @@ #include "text_overlay.h" #include "xr_session.h" +#include #include #include #include +#include #include #include #include @@ -69,6 +71,13 @@ static bool g_hwOverride3D = false; // render-thread only static uint32_t g_hwOverrideModeIndex = 0; // render-thread only static UINT g_windowWidth = 1280; static UINT g_windowHeight = 720; +// XR_DXR_cursor_depth v5 (ADR-046 Amendment 2 B): true exactly while the +// runtime reports willDraw - the runtime draws the cursor, so the OS cursor is +// hidden over the client area. Written by the render thread, read by the +// window thread (WM_SETCURSOR); a flip is pushed to the window thread with +// kCursorVisMsg so it takes effect without waiting for a mouse move. +static std::atomic g_hideOsCursor{false}; +static constexpr UINT kCursorVisMsg = WM_APP + 0x2C5; // Fullscreen state static bool g_fullscreen = false; @@ -217,6 +226,29 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { ReleaseCapture(); // Outside mutex — WM_CAPTURECHANGED can safely re-enter return 0; + case WM_SETCURSOR: + // XR_DXR_cursor_depth v5: while the runtime draws the depth cursor, + // the OS cursor is hidden over the client area; everywhere else (title + // bar, borders) and whenever willDraw is false, the default handling + // shows the class cursor. + if (g_hideOsCursor.load() && LOWORD(lParam) == HTCLIENT) { + SetCursor(nullptr); + return TRUE; + } + break; + + case kCursorVisMsg: { + // Render thread: willDraw flipped. Apply now if the pointer is over + // the client area (SetCursor is per-thread input state, so it must + // run on this, the window's, thread). + POINT p; + RECT rc; + if (GetCursorPos(&p) && ScreenToClient(hwnd, &p) && GetClientRect(hwnd, &rc) && PtInRect(&rc, p)) { + SetCursor(g_hideOsCursor.load() ? nullptr : LoadCursor(nullptr, IDC_ARROW)); + } + return 0; + } + case WM_SIZE: if (wParam != SIZE_MINIMIZED) { std::lock_guard lock(g_inputMutex); @@ -543,6 +575,283 @@ static bool CreateAndFillL2DPanel(XrSessionManager& xr, ID3D12Device* device, ID return true; } +// ============================================================================ +// XR_DXR_cursor_depth (ADR-046) — reference adoption, spec v5 (Amendment 2 B). +// +// Mirrors cube_handle_metal_macos. The app owns the depth: each frame it +// hit-tests the cube under the cursor FOOTPRINT (rays from the outer views of +// the previous locate through a ring of points around the hotspot) and hands +// the nearest hit to xrLocateViews in an XrCursorDepthHintDXR. +// +// DISPLAYXR_CURSOR_DEPTH=0 opt out (extension not enabled) +// DISPLAYXR_CURSOR_DEPTH_DRAW=runtime (default, needs spec v5) chain +// XrCursorDepthDrawInfoDXR: the RUNTIME +// draws the cursor into every view +// before weaving; the OS cursor is +// hidden exactly while willDraw. +// DISPLAYXR_CURSOR_DEPTH_DRAW=app hint + placement only. This D3D12 +// reference does NOT draw an in-app +// crosshair (the Metal app does): it +// keeps the OS cursor and logs the +// placement — the runtime-drawn path is +// what this app verifies. +// DISPLAYXR_CURSOR_DEPTH_SPRITE=custom hand the runtime our own sprite (a +// 64x64 cyan ring + centre dot, 1 cm) +// instead of its default look (the OS +// cursor's shape and size). +// DISPLAYXR_CURSOR_DEPTH_UV=u,v scripted cursor (canvas-normalised, +// origin top-left) for headless checks. +// ============================================================================ + +static const float kCursorCubeSize = 0.06f; // RenderScene's default cube edge +static const float kCursorCubeHeight = 0.03f; // RenderScene(..., cubeHeight = 0.03f) +static const float kCursorHeight = 0.04f; // hint footprint, fraction of canvas height + +static bool g_cursorRuntimeDraw = false; +static bool g_cursorCustomSprite = false; +static XrSwapchain g_cursorSprite = XR_NULL_HANDLE; + +static void CursorQuatRotate(const XrQuaternionf& q, float vx, float vy, float vz, XrVector3f& out) +{ + // v' = v + 2w (q x v) + 2 q x (q x v) + const float tx = 2.0f * (q.y * vz - q.z * vy); + const float ty = 2.0f * (q.z * vx - q.x * vz); + const float tz = 2.0f * (q.x * vy - q.y * vx); + out.x = vx + q.w * tx + (q.y * tz - q.z * ty); + out.y = vy + q.w * ty + (q.z * tx - q.x * tz); + out.z = vz + q.w * tz + (q.x * ty - q.y * tx); +} + +// Slab test against the cube as RenderScene places it: centred at +// (0, kCursorCubeHeight, 0), edge kCursorCubeSize, rotated `rot` about +Y +// (XMMatrixRotationY, row vectors: x' = c x + s z, z' = -s x + c z). +static bool CursorRayHitCube(const XrVector3f& o, const XrVector3f& d, float rot, float& tOut) +{ + const float c = cosf(rot), s = sinf(rot), h = kCursorCubeSize * 0.5f; + // local = R^T (world - centre) + const float ox = o.x, oy = o.y - kCursorCubeHeight, oz = o.z; + const float lo[3] = {c * ox - s * oz, oy, s * ox + c * oz}; + const float ld[3] = {c * d.x - s * d.z, d.y, s * d.x + c * d.z}; + float t0 = 0.0f, t1 = 1e30f; + for (int i = 0; i < 3; i++) { + if (fabsf(ld[i]) < 1e-9f) { + if (lo[i] < -h || lo[i] > h) return false; + continue; + } + float ta = (-h - lo[i]) / ld[i], tb = (h - lo[i]) / ld[i]; + if (ta > tb) std::swap(ta, tb); + if (ta > t0) t0 = ta; + if (tb < t1) t1 = tb; + if (t0 > t1) return false; + } + tOut = t0; + return true; +} + +// Ray from a located view through canvas point (u, v): Kooima frusta all +// frame the canvas, so (u, v) maps linearly onto the fov tangents. +static XrVector3f CursorViewRayDir(const XrView& view, float u, float v) +{ + const float tl = tanf(view.fov.angleLeft), tr = tanf(view.fov.angleRight); + const float tu = tanf(view.fov.angleUp), td = tanf(view.fov.angleDown); + XrVector3f d; + CursorQuatRotate(view.pose.orientation, tl + u * (tr - tl), tu - v * (tu - td), -1.0f, d); + return d; +} + +// Nearest cube point (to the viewer) under the cursor footprint, using the +// outer views of the previous locate. Footprint = the sprite plus 50%. +static bool CursorFootprintHit(const std::vector& views, uint32_t active, float u, float v, + float canvasAspect, float rot, XrVector3f& nearest) +{ + if (active < 2 || views.size() < active) return false; + const XrView* outer[2] = {&views[0], &views[active - 1]}; + XrVector3f fwd; + CursorQuatRotate(views[0].pose.orientation, 0.0f, 0.0f, -1.0f, fwd); + const float r = 0.75f * kCursorHeight; // footprint radius, in canvas heights + bool found = false; + float best = 1e30f; + for (int k = -1; k < 8; k++) { + float su = u, sv = v; + if (k >= 0) { + const float a = (float)k * 0.78539816f; + su += r * cosf(a) / canvasAspect; + sv += r * sinf(a); + } + for (const XrView* view : outer) { + const XrVector3f d = CursorViewRayDir(*view, su, sv); + float t; + if (!CursorRayHitCube(view->pose.position, d, rot, t)) continue; + const XrVector3f p = {view->pose.position.x + d.x * t, view->pose.position.y + d.y * t, + view->pose.position.z + d.z * t}; + const float depth = p.x * fwd.x + p.y * fwd.y + p.z * fwd.z; // smaller = nearer the viewer + if (depth < best) { + best = depth; + nearest = p; + found = true; + } + } + } + return found; +} + +// The render thread owns willDraw; the window thread owns SetCursor. Flip the +// shared flag and nudge the window thread (WM_SETCURSOR keeps it that way on +// later mouse moves). +static void ApplyOsCursorVisibility(HWND hwnd, bool hide) +{ + if (g_hideOsCursor.load() == hide) return; + g_hideOsCursor.store(hide); + PostMessage(hwnd, kCursorVisMsg, 0, 0); +} + +// DISPLAYXR_CURSOR_DEPTH_SPRITE=custom: a 64x64 cyan ring with a centre dot +// (hotspot = the centre), dark-edged so it reads over the white cube. Filled +// once (acquire / fill / release); the runtime samples the last released image +// every frame, as STRAIGHT alpha. _SRGB when offered: the bytes are +// display-referred. +static bool CreateCursorSprite(XrSessionManager& xr, ID3D12Device* device, ID3D12CommandQueue* queue) +{ + if (device == nullptr || queue == nullptr) return false; + const uint32_t S = 64; + XrSwapchainCreateInfo sci = {XR_TYPE_SWAPCHAIN_CREATE_INFO}; + sci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT; + sci.format = PickPanelFormat(xr.session, (int64_t)DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + (int64_t)DXGI_FORMAT_R8G8B8A8_UNORM); + sci.sampleCount = 1; + sci.width = S; + sci.height = S; + sci.faceCount = 1; + sci.arraySize = 1; + sci.mipCount = 1; + if (XR_FAILED(xrCreateSwapchain(xr.session, &sci, &g_cursorSprite))) { + g_cursorSprite = XR_NULL_HANDLE; + return false; + } + uint32_t n = 0; + xrEnumerateSwapchainImages(g_cursorSprite, 0, &n, nullptr); + std::vector imgs(n, {XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR}); + if (n == 0 || XR_FAILED(xrEnumerateSwapchainImages(g_cursorSprite, n, &n, + (XrSwapchainImageBaseHeader*)imgs.data()))) { + return false; + } + XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; + uint32_t idx = 0; + if (XR_FAILED(xrAcquireSwapchainImage(g_cursorSprite, &ai, &idx))) { + return false; + } + XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + wi.timeout = XR_INFINITE_DURATION; + xrWaitSwapchainImage(g_cursorSprite, &wi); + + std::vector buf((size_t)S * S * 4); + auto cover = [](float d, float r0, float r1) { // 1 inside [r0, r1], 1-px soft edges + const float a = (std::min)(d - r0 + 0.5f, r1 - d + 0.5f); + return a <= 0.0f ? 0.0f : (a >= 1.0f ? 1.0f : a); + }; + for (uint32_t y = 0; y < S; y++) { + for (uint32_t x = 0; x < S; x++) { + const float d = hypotf((float)x + 0.5f - S * 0.5f, (float)y + 0.5f - S * 0.5f); + const float ring = (std::max)(cover(d, 21.0f, 27.0f), cover(d, 0.0f, 5.0f)); // cyan + const float edge = (std::max)(cover(d, 19.5f, 28.5f), cover(d, 0.0f, 6.5f)); // dark rim + uint8_t* px = &buf[((size_t)y * S + x) * 4]; + // RGBA, straight alpha: cyan where the ring is, near-black rim. + px[0] = 0; + px[1] = (uint8_t)(220.0f * ring); + px[2] = (uint8_t)(255.0f * ring); + px[3] = (uint8_t)(255.0f * (std::max)(ring, edge)); + } + } + // Upload buffer (256-byte-aligned rows per the D3D12 placed-footprint + // rule), then one-shot DIRECT list on the app queue: + // RENDER_TARGET -> COPY_DEST, copy, -> RENDER_TARGET (the state OpenXR + // D3D12 requires at release), fence-waited before the release. + const size_t srcStride = (size_t)S * 4; + const uint32_t alignedRowPitch = (uint32_t)((srcStride + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1) & + ~(size_t)(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1)); + D3D12_HEAP_PROPERTIES uploadHeap = {}; + uploadHeap.Type = D3D12_HEAP_TYPE_UPLOAD; + D3D12_RESOURCE_DESC ubDesc = {}; + ubDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + ubDesc.Width = (uint64_t)alignedRowPitch * S; + ubDesc.Height = 1; + ubDesc.DepthOrArraySize = 1; + ubDesc.MipLevels = 1; + ubDesc.Format = DXGI_FORMAT_UNKNOWN; + ubDesc.SampleDesc.Count = 1; + ubDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + ComPtr upload; + if (FAILED(device->CreateCommittedResource(&uploadHeap, D3D12_HEAP_FLAG_NONE, &ubDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&upload)))) { + LOG_ERROR("Cursor sprite: upload buffer create failed"); + return false; + } + uint8_t* mapped = nullptr; + D3D12_RANGE noRead = {0, 0}; + if (FAILED(upload->Map(0, &noRead, (void**)&mapped))) { + LOG_ERROR("Cursor sprite: upload buffer map failed"); + return false; + } + for (uint32_t y = 0; y < S; y++) { + memcpy(mapped + (size_t)y * alignedRowPitch, buf.data() + (size_t)y * srcStride, srcStride); + } + upload->Unmap(0, nullptr); + + ComPtr alloc; + ComPtr cl; + ComPtr fence; + if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&alloc))) || + FAILED(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, alloc.Get(), nullptr, IID_PPV_ARGS(&cl))) || + FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)))) { + LOG_ERROR("Cursor sprite: command objects create failed"); + return false; + } + ID3D12Resource* tex = imgs[idx].texture; + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = tex; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + cl->ResourceBarrier(1, &barrier); + + D3D12_TEXTURE_COPY_LOCATION srcLoc = {}; + srcLoc.pResource = upload.Get(); + srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLoc.PlacedFootprint.Offset = 0; + srcLoc.PlacedFootprint.Footprint.Format = (DXGI_FORMAT)sci.format; + srcLoc.PlacedFootprint.Footprint.Width = S; + srcLoc.PlacedFootprint.Footprint.Height = S; + srcLoc.PlacedFootprint.Footprint.Depth = 1; + srcLoc.PlacedFootprint.Footprint.RowPitch = alignedRowPitch; + D3D12_TEXTURE_COPY_LOCATION dstLoc = {}; + dstLoc.pResource = tex; + dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLoc.SubresourceIndex = 0; + cl->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, nullptr); + + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + cl->ResourceBarrier(1, &barrier); + cl->Close(); + + ID3D12CommandList* lists[] = {cl.Get()}; + queue->ExecuteCommandLists(1, lists); + HANDLE ev = CreateEventW(nullptr, FALSE, FALSE, nullptr); + queue->Signal(fence.Get(), 1); + if (fence->GetCompletedValue() < 1) { + fence->SetEventOnCompletion(1, ev); + WaitForSingleObject(ev, INFINITE); + } + CloseHandle(ev); + + XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + xrReleaseSwapchainImage(g_cursorSprite, &ri); + LOG_INFO("Cursor sprite swapchain created: %ux%u (cyan ring, hotspot = centre, format %lld)", S, S, + (long long)sci.format); + return true; +} + static void RenderThreadFunc( HWND hwnd, XrSessionManager* xr, @@ -747,8 +1056,109 @@ static void RenderThreadFunc( displayRig.perspectiveFactor = inputSnapshot.viewParams.perspectiveFactor; locateInfo.next = &displayRig; } + // XR_DXR_cursor_depth (ADR-046): hint from the previous + // locate's views (the hit test needs rays, the rays need + // views - one frame late, which the runtime's filter + // absorbs). Chained on THIS locate (the rig one whose + // views are submitted), in front of the rig struct. + static std::vector cursorPrevViews; + static uint32_t cursorPrevActive = 0; + XrCursorDepthHintDXR cursorHint = {XR_TYPE_CURSOR_DEPTH_HINT_DXR}; + XrCursorDepthPlacementDXR cursorPlacement = {XR_TYPE_CURSOR_DEPTH_PLACEMENT_DXR}; + // Spec v5: "runtime, draw it" - with our sprite (hotspot = + // its centre, 1 cm tall) or the runtime's default look + // (XR_NULL_HANDLE: the OS cursor's shape and size). + XrCursorDepthDrawInfoDXR cursorDrawInfo = {XR_TYPE_CURSOR_DEPTH_DRAW_INFO_DXR}; + cursorDrawInfo.image = g_cursorCustomSprite ? g_cursorSprite : XR_NULL_HANDLE; + cursorDrawInfo.hotSpot = {0.5f, 0.5f}; + cursorDrawInfo.sizeMeters = g_cursorCustomSprite ? 0.01f : 0.0f; + cursorDrawInfo.visible = XR_TRUE; + XrCursorDepthDrawStateDXR cursorDrawState = {XR_TYPE_CURSOR_DEPTH_DRAW_STATE_DXR}; + bool cursorInCanvas = false; + if (g_hasCursorDepthExt && windowW > 0 && windowH > 0) { + float u = -1.0f, v = -1.0f; + POINT cp; + RECT crc; + if (GetCursorPos(&cp) && ScreenToClient(hwnd, &cp) && GetClientRect(hwnd, &crc) && + crc.right > crc.left && crc.bottom > crc.top) { + u = (float)cp.x / (float)(crc.right - crc.left); + v = (float)cp.y / (float)(crc.bottom - crc.top); + } + // Scripted cursor for headless verification: DISPLAYXR_CURSOR_DEPTH_UV=u,v + static float fixedU = -1.0f, fixedV = -1.0f; + static bool fixedParsed = false; + if (!fixedParsed) { + fixedParsed = true; + const char* fx = getenv("DISPLAYXR_CURSOR_DEPTH_UV"); + if (fx == nullptr || sscanf(fx, "%f,%f", &fixedU, &fixedV) != 2) fixedU = fixedV = -1.0f; + } + if (fixedU >= 0.0f) { + u = fixedU; + v = fixedV; + } + cursorInCanvas = u >= 0.0f && u <= 1.0f && v >= 0.0f && v <= 1.0f; + if (cursorInCanvas) { + cursorHint.cursorUV = {u, v}; + cursorHint.cursorHeight = kCursorHeight; + XrVector3f hit = {0.0f, 0.0f, 0.0f}; + cursorHint.hasContent = + CursorFootprintHit(cursorPrevViews, cursorPrevActive, u, v, + (float)windowW / (float)windowH, + renderer->cubeRotation, hit) + ? XR_TRUE + : XR_FALSE; + cursorHint.nearestPoint = hit; + // Chain: locateInfo -> hint [-> draw info] -> (rig struct). + const void* hintTail = locateInfo.next; + if (g_cursorRuntimeDraw) { + cursorDrawInfo.next = hintTail; + hintTail = &cursorDrawInfo; + } + cursorHint.next = hintTail; + locateInfo.next = &cursorHint; + } + cursorPlacement.next = viewState.next; + viewState.next = &cursorPlacement; + if (g_cursorRuntimeDraw) { + cursorDrawState.next = viewState.next; + viewState.next = &cursorDrawState; + } + } + xrLocateViews(xr->session, &locateInfo, &viewState, 8, &viewCount, rawViews); + if (g_hasCursorDepthExt) { + cursorPrevViews.assign(rawViews, rawViews + (viewCount < 8 ? viewCount : 8)); + cursorPrevActive = monoMode ? 1 : (modeViewCount < viewCount ? modeViewCount : viewCount); + + // The app owns its window's OS cursor: hidden exactly + // while the runtime draws the depth cursor (willDraw - + // whenever it cannot draw, we keep the OS cursor). App + // mode keeps the OS cursor (no in-app crosshair here). + ApplyOsCursorVisibility(hwnd, g_cursorRuntimeDraw && cursorDrawState.willDraw == XR_TRUE); + static int willDrawLogged = 0; + if (g_cursorRuntimeDraw && willDrawLogged < 3 && cursorInCanvas) { + static int lastWillDraw = -1; + if ((int)cursorDrawState.willDraw != lastWillDraw) { + lastWillDraw = (int)cursorDrawState.willDraw; + willDrawLogged++; + LOG_INFO("cursor_depth: runtime willDraw=%s (uv=%.3f,%.3f disparity=%.3f)", + cursorDrawState.willDraw ? "TRUE" : "FALSE", cursorHint.cursorUV.x, + cursorHint.cursorUV.y, cursorPlacement.disparity); + } + } + static int cursorLogged = 0; + if (cursorPlacement.isActive && cursorHint.hasContent && cursorLogged < 3) { + cursorLogged++; + LOG_INFO("cursor_depth: uv=(%.3f,%.3f) target=%.3f disparity=%.3f h=%.4f " + "pos=(%.3f,%.3f,%.3f)", + cursorHint.cursorUV.x, cursorHint.cursorUV.y, cursorPlacement.targetDisparity, + cursorPlacement.disparity, cursorPlacement.height, + cursorPlacement.pose.position.x, cursorPlacement.pose.position.y, + cursorPlacement.pose.position.z); + } + } + // Multi-screen M3: with the window spanning displays the // runtime hands out one view set per display; render ALL // of them (the sum is activeViewCount), each set's local @@ -1374,6 +1784,24 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine return 1; } + // XR_DXR_cursor_depth spec v5: who draws the cursor. Default: the runtime. + if (g_hasCursorDepthExt) { + const char* drawWho = getenv("DISPLAYXR_CURSOR_DEPTH_DRAW"); + const bool wantApp = drawWho != nullptr && strcmp(drawWho, "app") == 0; + if (!wantApp && g_cursorDepthSpecVersion >= 5) { + g_cursorRuntimeDraw = true; + const char* sprite = getenv("DISPLAYXR_CURSOR_DEPTH_SPRITE"); + g_cursorCustomSprite = sprite != nullptr && strcmp(sprite, "custom") == 0; + } else if (!wantApp) { + LOG_WARN("XR_DXR_cursor_depth v%u has no runtime-drawn cursor (needs v5) - hint only, OS cursor kept", + g_cursorDepthSpecVersion); + } + LOG_INFO("XR_DXR_cursor_depth draw: %s", + !g_cursorRuntimeDraw ? "APP (hint + placement only; OS cursor kept, no in-app crosshair)" + : g_cursorCustomSprite ? "RUNTIME, custom sprite (cyan ring)" + : "RUNTIME, default look"); + } + // INV-1.3: open on the 3D panel (#715) — one-shot move to the panel's // desktop position reported by xrGetSystemProperties (virtual-screen // coords, top-down; (0,0) = primary/unknown is safe), BEFORE @@ -1424,6 +1852,12 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine ShutdownLogging(); return 1; } + // Before the render thread starts: the fill runs on the app queue. + if (g_cursorRuntimeDraw && g_cursorCustomSprite && + !CreateCursorSprite(xr, renderer.device.Get(), renderer.commandQueue.Get())) { + LOG_WARN("Cursor sprite swapchain creation failed - using the runtime's default look"); + g_cursorCustomSprite = false; + } // Enumerate D3D12 swapchain images (single SBS swapchain) std::vector swapchainImages; @@ -1599,6 +2033,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine if (hudOk) CleanupHudRenderer(hudRenderer); g_xr = nullptr; + if (g_cursorSprite != XR_NULL_HANDLE) { + xrDestroySwapchain(g_cursorSprite); + g_cursorSprite = XR_NULL_HANDLE; + } CleanupOpenXR(xr); CleanupD3D12(renderer); diff --git a/test_apps/handle/cube_handle_d3d12_win/xr_session.cpp b/test_apps/handle/cube_handle_d3d12_win/xr_session.cpp index cc948b9856..0056a787a6 100644 --- a/test_apps/handle/cube_handle_d3d12_win/xr_session.cpp +++ b/test_apps/handle/cube_handle_d3d12_win/xr_session.cpp @@ -8,9 +8,12 @@ #include "xr_session.h" #include "logging.h" #include "dxr_view_config.h" +#include #include bool g_hasViewRigExt = false; +bool g_hasCursorDepthExt = false; // XR_DXR_cursor_depth (ADR-046) +uint32_t g_cursorDepthSpecVersion = 0; // #439 Phase 3 — XR_DXR_local_3d_zone harness (see xr_session.h). ZoneMaskHarness g_zone; @@ -103,6 +106,10 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_DXR_LOCAL_3D_ZONE_EXTENSION_NAME) == 0) { g_zone.available = true; } + if (strcmp(ext.extensionName, XR_DXR_CURSOR_DEPTH_EXTENSION_NAME) == 0) { + g_hasCursorDepthExt = true; + g_cursorDepthSpecVersion = ext.extensionVersion; + } } LOG_INFO("XR_KHR_D3D12_enable: %s", hasD3D12 ? "AVAILABLE" : "NOT FOUND"); @@ -137,6 +144,15 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (g_zone.available) { enabledExtensions.push_back(XR_DXR_LOCAL_3D_ZONE_EXTENSION_NAME); } + if (g_hasCursorDepthExt) { + const char* opt = getenv("DISPLAYXR_CURSOR_DEPTH"); + if (opt != nullptr && strcmp(opt, "0") == 0) { + g_hasCursorDepthExt = false; // opted out: the runtime does no cursor work + } else { + enabledExtensions.push_back(XR_DXR_CURSOR_DEPTH_EXTENSION_NAME); + } + } + LOG_INFO("XR_DXR_cursor_depth: %s (v%u)", g_hasCursorDepthExt ? "ENABLED" : "off", g_cursorDepthSpecVersion); LOG_INFO("Enabling %zu extensions", enabledExtensions.size()); for (const auto& ext : enabledExtensions) { diff --git a/test_apps/handle/cube_handle_d3d12_win/xr_session.h b/test_apps/handle/cube_handle_d3d12_win/xr_session.h index 70b5b0ecb3..99c7a5f67e 100644 --- a/test_apps/handle/cube_handle_d3d12_win/xr_session.h +++ b/test_apps/handle/cube_handle_d3d12_win/xr_session.h @@ -12,6 +12,7 @@ #define XR_USE_GRAPHICS_API_D3D12 #include "xr_session_common.h" #include +#include #include // INV-1.3 (#715): 3D panel top-left in virtual-desktop pixels (top-down, @@ -25,6 +26,12 @@ extern int32_t g_displayScreenTop; // more consumers adopt it. extern bool g_hasViewRigExt; +// XR_DXR_cursor_depth (ADR-046) available + enabled on the instance, and the +// spec version the runtime advertises (v5 = the runtime can draw the cursor). +// Opt out with DISPLAYXR_CURSOR_DEPTH=0. App-local, like the view-rig flag. +extern bool g_hasCursorDepthExt; +extern uint32_t g_cursorDepthSpecVersion; + // #439 Phase 3 — XR_DXR_local_3d_zone harness (header v3 carries the Local2D // composition-layer + view-size-changed types). App-local for the same reason // as the view-rig flag: the shared XrSessionManager doesn't carry this diff --git a/test_apps/handle/cube_zones_d3d11_win/main.cpp b/test_apps/handle/cube_zones_d3d11_win/main.cpp index 01e589ed27..b4e3e3c773 100644 --- a/test_apps/handle/cube_zones_d3d11_win/main.cpp +++ b/test_apps/handle/cube_zones_d3d11_win/main.cpp @@ -52,6 +52,7 @@ #include "projection_depth.h" #include "text_overlay.h" #include "xr_session.h" +#include "cursor_depth_zones.h" // XR_DXR_cursor_depth on display zones (runtime#1949) #include #include @@ -118,6 +119,15 @@ struct DisplayZone { }; static DisplayZone g_zonesArr[kNumZones]; +// XR_DXR_cursor_depth (ADR-046, runtime#1949): the runtime draws the depth +// cursor in the 3D zone under the pointer. The hint rides that zone's +// zone-scoped locate (cursor_depth_zones.h, dxr::CursorDepth's Win32 adapter +// hides the OS cursor over the client area exactly while willDraw). +// DISPLAYXR_CURSOR_DEPTH=0 opts out; DISPLAYXR_CURSOR_DEPTH_DRAW=app keeps the +// OS cursor (hint + placement only); DISPLAYXR_CURSOR_DEPTH_UV=u,v scripts a +// WINDOW-normalised pointer. +static dxr_zones_cursor::ZoneCursor g_zoneCursor; + static const XrRect2Di kZoneARect = {{0, 180}, {640, 540}}; static const XrRect2Di kZoneBRect = {{700, 180}, {520, 360}}; static const XrRect2Di kZoneBOverlapRect = {{400, 300}, {520, 360}}; @@ -1009,6 +1019,17 @@ static void RenderZonesFrame(RenderState& rs, const XrFrameState& frameState) { XrSessionManager& xr = *rs.xr; D3D11Renderer& renderer = *rs.renderer; + // XR_DXR_cursor_depth (#1949): pick the topmost 3D zone under the pointer. + { + XrRect2Di cursorRects[kNumZones]; + float cursorRots[kNumZones]; + for (uint32_t zi = 0; zi < kNumZones; zi++) { + cursorRects[zi] = g_zonesArr[zi].rect; + cursorRots[zi] = renderer.cubeRotation + g_zonesArr[zi].spinPhase; + } + g_zoneCursor.begin((int32_t)g_windowWidth, (int32_t)g_windowHeight, cursorRects, cursorRots, kNumZones); + } + // Per-zone locate + submit data. The zone structs are chained at BOTH // points (locate and xrEndFrame) — same instances within the frame. XrDisplayZoneDXR zoneStructs[kNumZones]; @@ -1043,7 +1064,13 @@ static void RenderZonesFrame(RenderState& rs, const XrFrameState& frameState) { uint32_t viewCountOutput = 0; XrView zoneViews[8]; for (uint32_t vi = 0; vi < 8; vi++) zoneViews[vi] = {XR_TYPE_VIEW}; + // XR_DXR_cursor_depth (#1949): the hint rides only the locate of the + // zone under the pointer; its canvas is the zone rect. + g_zoneCursor.chain(zi, locateInfo, viewState); XrResult lr = xrLocateViews(xr.session, &locateInfo, &viewState, 8, &viewCountOutput, zoneViews); + if (XR_SUCCEEDED(lr)) { + g_zoneCursor.afterLocate(zi, zoneViews, viewCountOutput); + } if (XR_FAILED(lr) || viewCountOutput == 0) { static bool warned = false; if (!warned) { @@ -1248,6 +1275,8 @@ static void RenderZonesFrame(RenderState& rs, const XrFrameState& frameState) { xrReleaseSwapchainImage(z.swapchain, &ri); } + g_zoneCursor.end(); // pointer over no 3D zone: the OS cursor is the cursor + // Layer list: [projA (zone A chained), projB (zone B chained), strip]. XrCompositionLayerProjection projLayers[kNumZones]; XrCompositionLayerLocal2DDXR stripLayer = {(XrStructureType)XR_TYPE_COMPOSITION_LAYER_LOCAL_2D_DXR}; @@ -1923,6 +1952,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine ShutdownLogging(); return 1; } + // XR_DXR_cursor_depth (#1949): the depth cursor rides the zone under the pointer. + g_zoneCursor.initWin32(g_hasCursorDepthExt ? g_cursorDepthSpecVersion : 0, hwnd, 0.06f); // Create reference spaces if (!CreateSpaces(xr)) { diff --git a/test_apps/handle/cube_zones_d3d11_win/xr_session.cpp b/test_apps/handle/cube_zones_d3d11_win/xr_session.cpp index 3cb80bc90c..800299ae4a 100644 --- a/test_apps/handle/cube_zones_d3d11_win/xr_session.cpp +++ b/test_apps/handle/cube_zones_d3d11_win/xr_session.cpp @@ -8,6 +8,7 @@ #include "xr_session.h" #include "logging.h" #include "dxr_view_config.h" +#include #include // XR_DXR_view_rig: app-local availability flag (see xr_session.h). @@ -18,6 +19,8 @@ ZoneMaskHarness g_zone; // XR_DXR_display_zones (ADR-027) — see xr_session.h. bool g_hasDisplayZonesExt = false; +bool g_hasCursorDepthExt = false; // XR_DXR_cursor_depth (ADR-046, runtime#1949) +uint32_t g_cursorDepthSpecVersion = 0; DisplayZonesHarness g_zones; // INV-1.3 (#715): 3D panel top-left in virtual-desktop pixels (top-down, @@ -112,6 +115,10 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_DXR_LOCAL_3D_ZONE_EXTENSION_NAME) == 0) { g_zone.available = true; } + if (strcmp(ext.extensionName, XR_DXR_CURSOR_DEPTH_EXTENSION_NAME) == 0) { + g_hasCursorDepthExt = true; + g_cursorDepthSpecVersion = ext.extensionVersion; + } if (strcmp(ext.extensionName, XR_DXR_DISPLAY_ZONES_EXTENSION_NAME) == 0) { displayZonesAdvertised = true; } @@ -174,6 +181,15 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (g_hasDisplayZonesExt) { enabledExtensions.push_back(XR_DXR_DISPLAY_ZONES_EXTENSION_NAME); } + if (g_hasCursorDepthExt) { + const char* opt = getenv("DISPLAYXR_CURSOR_DEPTH"); + if (opt != nullptr && strcmp(opt, "0") == 0) { + g_hasCursorDepthExt = false; // opted out: the runtime does no cursor work + } else { + enabledExtensions.push_back(XR_DXR_CURSOR_DEPTH_EXTENSION_NAME); + } + } + LOG_INFO("XR_DXR_cursor_depth: %s (v%u)", g_hasCursorDepthExt ? "ENABLED" : "off", g_cursorDepthSpecVersion); LOG_INFO("Enabling %zu extensions", enabledExtensions.size()); for (const auto& ext : enabledExtensions) { diff --git a/test_apps/handle/cube_zones_d3d11_win/xr_session.h b/test_apps/handle/cube_zones_d3d11_win/xr_session.h index 6bc8fd0408..ecd71d793b 100644 --- a/test_apps/handle/cube_zones_d3d11_win/xr_session.h +++ b/test_apps/handle/cube_zones_d3d11_win/xr_session.h @@ -18,6 +18,7 @@ #include "xr_session_common.h" #include #include +#include #include // INV-1.3 (#715): 3D panel top-left in virtual-desktop pixels (top-down, @@ -52,6 +53,12 @@ extern ZoneMaskHarness g_zone; // single-projection fallback. extern bool g_hasDisplayZonesExt; +// XR_DXR_cursor_depth (ADR-046) available + enabled, and the spec version the +// runtime advertises (v5 = it draws the cursor; zones frames since runtime#1949). +// Opt out with DISPLAYXR_CURSOR_DEPTH=0. +extern bool g_hasCursorDepthExt; +extern uint32_t g_cursorDepthSpecVersion; + struct DisplayZonesHarness { PFN_xrGetDisplayZoneCapabilitiesDXR pfnGetCaps = nullptr; PFN_xrGetDisplayZoneRecommendedViewSizeDXR pfnGetViewSize = nullptr; diff --git a/test_apps/handle/cube_zones_d3d12_win/main.cpp b/test_apps/handle/cube_zones_d3d12_win/main.cpp index 72ce1fc5b2..a87964f247 100644 --- a/test_apps/handle/cube_zones_d3d12_win/main.cpp +++ b/test_apps/handle/cube_zones_d3d12_win/main.cpp @@ -69,6 +69,7 @@ #include "logging.h" #include "projection_depth.h" #include "xr_session.h" +#include "cursor_depth_zones.h" // XR_DXR_cursor_depth on display zones (runtime#1949) #include #include @@ -130,6 +131,15 @@ struct DisplayZone { }; static DisplayZone g_zonesArr[kNumZones]; +// XR_DXR_cursor_depth (ADR-046, runtime#1949): the runtime draws the depth +// cursor in the 3D zone under the pointer. The hint rides that zone's +// zone-scoped locate (cursor_depth_zones.h, dxr::CursorDepth's Win32 adapter +// hides the OS cursor over the client area exactly while willDraw). +// DISPLAYXR_CURSOR_DEPTH=0 opts out; DISPLAYXR_CURSOR_DEPTH_DRAW=app keeps the +// OS cursor (hint + placement only); DISPLAYXR_CURSOR_DEPTH_UV=u,v scripts a +// WINDOW-normalised pointer. +static dxr_zones_cursor::ZoneCursor g_zoneCursor; + // #672 isolation: EXACT Unity multi-zone geometry — 936x1712 portrait window, // two tall bottom-band zones, zone B pinned to the right edge. Reproduces the // Unity provider's layout to test whether zone B drops on geometry alone. @@ -947,6 +957,17 @@ static XrEnvironmentBlendMode ZonesBlendMode() { static void RenderZonesFrame(XrSessionManager& xr, D3D12Renderer& renderer, const XrFrameState& frameState) { + // XR_DXR_cursor_depth (#1949): pick the topmost 3D zone under the pointer. + { + XrRect2Di cursorRects[kNumZones]; + float cursorRots[kNumZones]; + for (uint32_t zi = 0; zi < kNumZones; zi++) { + cursorRects[zi] = g_zonesArr[zi].rect; + cursorRots[zi] = renderer.cubeRotation + g_zonesArr[zi].spinPhase; + } + g_zoneCursor.begin((int32_t)g_windowWidth, (int32_t)g_windowHeight, cursorRects, cursorRots, kNumZones); + } + // Per-zone locate + submit data. The zone structs are chained at BOTH // points (locate and xrEndFrame) — same instances within the frame. XrDisplayZoneDXR zoneStructs[kNumZones]; @@ -995,7 +1016,13 @@ static void RenderZonesFrame(XrSessionManager& xr, D3D12Renderer& renderer, uint32_t viewCountOutput = 0; XrView zoneViews[8]; for (uint32_t vi = 0; vi < 8; vi++) zoneViews[vi] = {XR_TYPE_VIEW}; + // XR_DXR_cursor_depth (#1949): the hint rides only the locate of the + // zone under the pointer; its canvas is the zone rect. + g_zoneCursor.chain(zi, locateInfo, viewState); XrResult lr = xrLocateViews(xr.session, &locateInfo, &viewState, 8, &viewCountOutput, zoneViews); + if (XR_SUCCEEDED(lr)) { + g_zoneCursor.afterLocate(zi, zoneViews, viewCountOutput); + } if (XR_FAILED(lr) || viewCountOutput == 0) { static bool warned = false; if (!warned) { warned = true; @@ -1105,6 +1132,8 @@ static void RenderZonesFrame(XrSessionManager& xr, D3D12Renderer& renderer, xrReleaseSwapchainImage(z.swapchain, &ri); } + g_zoneCursor.end(); // pointer over no 3D zone: the OS cursor is the cursor + // Layer list: [projA (zone A chained), projB (zone B chained)]. XrCompositionLayerProjection projLayers[kNumZones]; XrCompositionLayerLocal2DDXR stripLayer = {(XrStructureType)XR_TYPE_COMPOSITION_LAYER_LOCAL_2D_DXR}; @@ -1270,6 +1299,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine ShutdownLogging(); return 1; } + // XR_DXR_cursor_depth (#1949): the depth cursor rides the zone under the pointer. + g_zoneCursor.initWin32(g_hasCursorDepthExt ? g_cursorDepthSpecVersion : 0, hwnd, 0.06f); if (!CreateSpaces(xr)) { LOG_ERROR("Reference space creation failed"); diff --git a/test_apps/handle/cube_zones_d3d12_win/xr_session.cpp b/test_apps/handle/cube_zones_d3d12_win/xr_session.cpp index 69ce8e636d..86ebd0dc35 100644 --- a/test_apps/handle/cube_zones_d3d12_win/xr_session.cpp +++ b/test_apps/handle/cube_zones_d3d12_win/xr_session.cpp @@ -8,6 +8,7 @@ #include "xr_session.h" #include "logging.h" +#include #include bool g_hasViewRigExt = false; @@ -17,6 +18,8 @@ ZoneMaskHarness g_zone; // XR_DXR_display_zones (ADR-027) — see xr_session.h. bool g_hasDisplayZonesExt = false; +bool g_hasCursorDepthExt = false; // XR_DXR_cursor_depth (ADR-046, runtime#1949) +uint32_t g_cursorDepthSpecVersion = 0; DisplayZonesHarness g_zones; // INV-1.3 (#715): 3D panel top-left in virtual-desktop pixels (top-down, @@ -108,6 +111,10 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_DXR_LOCAL_3D_ZONE_EXTENSION_NAME) == 0) { g_zone.available = true; } + if (strcmp(ext.extensionName, XR_DXR_CURSOR_DEPTH_EXTENSION_NAME) == 0) { + g_hasCursorDepthExt = true; + g_cursorDepthSpecVersion = ext.extensionVersion; + } if (strcmp(ext.extensionName, XR_DXR_DISPLAY_ZONES_EXTENSION_NAME) == 0) { displayZonesAdvertised = true; } @@ -163,6 +170,15 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (g_hasDisplayZonesExt) { enabledExtensions.push_back(XR_DXR_DISPLAY_ZONES_EXTENSION_NAME); } + if (g_hasCursorDepthExt) { + const char* opt = getenv("DISPLAYXR_CURSOR_DEPTH"); + if (opt != nullptr && strcmp(opt, "0") == 0) { + g_hasCursorDepthExt = false; // opted out: the runtime does no cursor work + } else { + enabledExtensions.push_back(XR_DXR_CURSOR_DEPTH_EXTENSION_NAME); + } + } + LOG_INFO("XR_DXR_cursor_depth: %s (v%u)", g_hasCursorDepthExt ? "ENABLED" : "off", g_cursorDepthSpecVersion); LOG_INFO("Enabling %zu extensions", enabledExtensions.size()); for (const auto& ext : enabledExtensions) { diff --git a/test_apps/handle/cube_zones_d3d12_win/xr_session.h b/test_apps/handle/cube_zones_d3d12_win/xr_session.h index e8558b78ee..da632f253f 100644 --- a/test_apps/handle/cube_zones_d3d12_win/xr_session.h +++ b/test_apps/handle/cube_zones_d3d12_win/xr_session.h @@ -23,6 +23,7 @@ #include #include #include +#include // INV-1.3 (#715): 3D panel top-left in virtual-desktop pixels (top-down, // origin = primary top-left); (0,0) = primary/unknown. Filled by @@ -61,6 +62,12 @@ extern ZoneMaskHarness g_zone; // degrade). extern bool g_hasDisplayZonesExt; +// XR_DXR_cursor_depth (ADR-046) available + enabled, and the spec version the +// runtime advertises (v5 = it draws the cursor; zones frames since runtime#1949). +// Opt out with DISPLAYXR_CURSOR_DEPTH=0. +extern bool g_hasCursorDepthExt; +extern uint32_t g_cursorDepthSpecVersion; + struct DisplayZonesHarness { PFN_xrGetDisplayZoneCapabilitiesDXR pfnGetCaps = nullptr; PFN_xrGetDisplayZoneRecommendedViewSizeDXR pfnGetViewSize = nullptr; diff --git a/tests/tests_aux_cursor_depth.cpp b/tests/tests_aux_cursor_depth.cpp index 4fcb0999d4..9989c57568 100644 --- a/tests/tests_aux_cursor_depth.cpp +++ b/tests/tests_aux_cursor_depth.cpp @@ -16,6 +16,8 @@ #include "catch_amalgamated.hpp" #include +#include +#include #include namespace { @@ -1434,3 +1436,69 @@ TEST_CASE("cursor_draw: drawable predicate - the hotspot in ONE tile is enough ( CHECK(out[0].origin[0] > 1.0f); CHECK(out[1].origin[0] <= 1.0f); } + +// One pixel of a cursor whose colour is c and coverage a, composited by "the +// OS" over black and over white, exactly as DrawIconEx does. +static void +os_draw(const uint8_t c[3], float a, uint8_t on_black[4], uint8_t on_white[4]) +{ + for (int k = 0; k < 3; k++) { + on_black[k] = (uint8_t)lrintf(c[k] * a); + on_white[k] = (uint8_t)lrintf(c[k] * a + 255.0f * (1.0f - a)); + } + on_black[3] = on_white[3] = 0; // GDI leaves byte 3 undefined: ignored +} + +TEST_CASE("cursor_draw: unblend recovers premultiplied colour and alpha from the black/white pair") +{ + const uint8_t white[3] = {255, 255, 255}; + const uint8_t orange[3] = {250, 120, 10}; + const float alphas[] = {0.0f, 0.25f, 0.5f, 0.8f, 1.0f}; + for (const uint8_t *c : {white, orange}) { + for (float a : alphas) { + uint8_t b[4], w[4], out[4]; + os_draw(c, a, b, w); + u_cursor_draw_unblend(b, w, 1, out); + CAPTURE(a, (int)c[0]); + CHECK(std::abs((int)out[3] - (int)lrintf(255.0f * a)) <= 1); + for (int k = 0; k < 3; k++) { + // PREMULTIPLIED: colour x coverage, never above alpha. + CHECK(std::abs((int)out[k] - (int)lrintf(c[k] * a)) <= 1); + CHECK(out[k] <= out[3]); + } + if (a >= 0.25f) { + // ...and it un-premultiplies back to the colour drawn. + for (int k = 0; k < 3; k++) { + const int straight = (int)lrintf(255.0f * out[k] / out[3]); + CHECK(std::abs(straight - (int)c[k]) <= (a < 0.5f ? 4 : 2)); + } + } else if (a == 0.0f) { + CHECK(out[0] == 0); // transparent = zero, never garbage + } + } + } +} + +TEST_CASE("cursor_draw: unblend - monochrome AND/XOR cursors, inverted pixels opaque white") +{ + // AND=0 XOR=0: black. AND=0 XOR=1: white. AND=1 XOR=0: transparent. + // AND=1 XOR=1: inverts the screen (white over black, black over white). + const uint8_t black_b[4] = {0, 0, 0, 0}, black_w[4] = {0, 0, 0, 0}; + const uint8_t white_b[4] = {255, 255, 255, 0}, white_w[4] = {255, 255, 255, 0}; + const uint8_t clear_b[4] = {0, 0, 0, 0}, clear_w[4] = {255, 255, 255, 0}; + const uint8_t inv_b[4] = {255, 255, 255, 0}, inv_w[4] = {0, 0, 0, 0}; + uint8_t b[16], w[16], out[16]; + memcpy(b + 0, black_b, 4), memcpy(w + 0, black_w, 4); + memcpy(b + 4, white_b, 4), memcpy(w + 4, white_w, 4); + memcpy(b + 8, clear_b, 4), memcpy(w + 8, clear_w, 4); + memcpy(b + 12, inv_b, 4), memcpy(w + 12, inv_w, 4); + u_cursor_draw_unblend(b, w, 4, out); + CHECK((out[0] == 0 && out[3] == 255)); // opaque black + CHECK((out[4] == 255 && out[5] == 255 && out[7] == 255)); // opaque white + CHECK(out[11] == 0); // transparent + CHECK((out[12] == 255 && out[15] == 255)); // invert -> opaque white + + // MUTANT: taking alpha from GDI's byte 3 (undefined for these cursors) + // would make every one of them invisible - byte 3 is 0 in every input. + CHECK(out[3] == 255); +}