diff --git a/CMakeLists.txt b/CMakeLists.txt index de17242..8b03cfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -211,7 +211,10 @@ else() # XR_DXR_cursor_depth.h SPEC_VERSION 5 (the runtime-drawn cursor's # XrCursorDepthDrawInfoDXR / XrCursorDepthDrawStateDXR), which # common/cursor_depth.h needs. - GIT_TAG af2de88348ab062a21344bf4d27c19eba40dfd8a + # Bumped 2026-10-12 to displayxr-extensions@main (runtime 1d7fd8b2a): + # XR_DXR_cursor_depth.h SPEC_VERSION 7 (XR_CURSOR_DEPTH_SOURCE_MEASURED_DISPARITY_DXR, + # ADR-046 Amendment 3), which CursorDepth::useMeasuredDisparity() sends. + GIT_TAG 722b47b6e555991487df1d7880ccb94b1e8fdaaa ) FetchContent_GetProperties(displayxr_extensions_headers) if(NOT displayxr_extensions_headers_POPULATED) diff --git a/README.md b/README.md index 3cd157b..717700d 100644 --- a/README.md +++ b/README.md @@ -778,6 +778,13 @@ cursor.afterLocate(views.data(), activeViewCount);// hides the OS cursor exactly views through the cursor's footprint and keeps the hit nearest the viewer), `setHitTest` (full control: you get the views, the UV and the ready-made rays), or `useDepthLayer(true)` (spec v2: the runtime reads the depth you submit with `XR_KHR_composition_layer_depth`). + For stereo content with **no depth at all** (SBS / stereo video, stereo photos, camera pairs), + `useMeasuredDisparity(true)` (spec v7, ADR-046 Amendment 3): the runtime block-matches a + cursor-sized patch between the two outermost views you submit and lifts the cursor over the + nearest *confident* disparity; flat or repetitive texture leaves the cursor on the glass (the OS + cursor). The request is per frame, so turning it off costs nothing from the next frame. Below v7 + it is not sent (the ray cast / hit test, if set, is the fallback). In-process native compositors + only; IPC/service and display-zones frames report no content. - **Look:** `setSprite(swapchain, hotSpot, sizeMeters, aspect)`; the default (`XR_NULL_HANDLE`) is the runtime's own look, the OS cursor's shape or the classic arrow. The footprint follows the sprite: the arrow's is the tip plus a ring around its body, the same rule as the runtime diff --git a/common/cursor_depth.h b/common/cursor_depth.h index ff7c0d9..14e3519 100644 --- a/common/cursor_depth.h +++ b/common/cursor_depth.h @@ -3,7 +3,7 @@ /*! * @file * @brief `dxr::CursorDepth` — the app-side helper for the depth-aware cursor - * (`XR_DXR_cursor_depth`, ADR-046 + Amendment 2). + * (`XR_DXR_cursor_depth`, ADR-046 + Amendments 2 and 3). * * The runtime DRAWS the depth cursor (spec v5, Amendment 2 §B); the app only * owns the depth under it. What is left for an app is bookkeeping that every @@ -18,7 +18,8 @@ * // setPlatform(dxr::cursorPlatformX11(dpy, win)) * cursor.setRayCast([&](const dxr::CursorRay& r) -> std::optional { * float t; return myScene.raycast(r.origin, r.dir, t) ? std::optional(t) : std::nullopt; - * }); // or useDepthLayer(true), or setHitTest(...) + * }); // or useDepthLayer(true), useMeasuredDisparity(true), + * // or setHitTest(...) * // per frame, around xrLocateViews: * cursor.chain(locateInfo, viewState); * xrLocateViews(session, &locateInfo, &viewState, cap, &n, views); @@ -63,6 +64,17 @@ namespace dxr { +/*! + * XR_CURSOR_DEPTH_SOURCE_MEASURED_DISPARITY_DXR (spec v7, ADR-046 Amendment 3). + * Spelled as a value so a consumer whose vendored header is v5/v6 still + * compiles; the helper only sends it to a runtime that reported v7+. + */ +inline constexpr XrCursorDepthSourceKindDXR kCursorDepthSourceMeasuredDisparity = (XrCursorDepthSourceKindDXR)2; +#if XR_DXR_cursor_depth_SPEC_VERSION >= 7 +static_assert(kCursorDepthSourceMeasuredDisparity == XR_CURSOR_DEPTH_SOURCE_MEASURED_DISPARITY_DXR, + "MEASURED_DISPARITY is source kind 2"); +#endif + // --------------------------------------------------------------------------- // Pure geometry // --------------------------------------------------------------------------- @@ -368,7 +380,20 @@ class CursorDepth * submits (XR_KHR_composition_layer_depth, projection layer in the locate * space). No hit test runs. Ignored below v2. */ - void useDepthLayer(bool on) { m_depthLayer = on; } + void useDepthLayer(bool on) { setSource(Source::DepthLayer, on); } + + /*! + * Spec v7 (ADR-046 Amendment 3): for stereo content with NO depth (SBS / + * stereo video, stereo photos, camera pairs). The runtime measures the + * disparity under the cursor between the two outermost submitted views; + * no hit test runs and the hint carries no point. Low confidence (flat + * or repetitive texture) = the display plane = the OS cursor stays. + * The request is per frame, so toggling this off stops all runtime cost + * from the next frame. Below v7 it is ignored (the ray cast / hit test, + * if any, is used instead). Replaces useDepthLayer(true) and vice versa. + */ + void useMeasuredDisparity(bool on) { setSource(Source::MeasuredDisparity, on); } + bool measuredDisparity() const { return m_sourceKind == Source::MeasuredDisparity; } void setRayCast(RayCastFn fn) { m_rayCast = std::move(fn); } void setHitTest(HitTestFn fn) { m_hitTest = std::move(fn); } @@ -408,7 +433,12 @@ class CursorDepth m_hint.cursorHeight = m_height; m_hint.hasContent = XR_FALSE; m_hint.nearestPoint = {0.0f, 0.0f, 0.0f}; - const bool layer = m_depthLayer && m_spec >= 2; + XrCursorDepthSourceKindDXR runtimeSource = XR_CURSOR_DEPTH_SOURCE_APP_POINT_DXR; + if (m_sourceKind == Source::DepthLayer && m_spec >= 2) + runtimeSource = XR_CURSOR_DEPTH_SOURCE_SUBMITTED_DEPTH_DXR; + else if (m_sourceKind == Source::MeasuredDisparity && m_spec >= 7) + runtimeSource = kCursorDepthSourceMeasuredDisparity; + const bool layer = runtimeSource != XR_CURSOR_DEPTH_SOURCE_APP_POINT_DXR; if (!layer) { std::optional hit = hitTest(uv); if (hit) { @@ -420,7 +450,7 @@ class CursorDepth if (layer) { m_source = detail::makeXr(XR_TYPE_CURSOR_DEPTH_SOURCE_DXR); m_source.next = tail; - m_source.source = XR_CURSOR_DEPTH_SOURCE_SUBMITTED_DEPTH_DXR; + m_source.source = runtimeSource; tail = &m_source; } if (m_chainOptions && m_spec >= 3) { @@ -490,6 +520,21 @@ class CursorDepth const XrCursorDepthDrawStateDXR &drawState() const { return m_drawState; } private: + //! Who finds the content under the cursor. + enum class Source + { + AppPoint, //!< the ray cast / hit test (or none) + DepthLayer, //!< spec v2: SUBMITTED_DEPTH + MeasuredDisparity, //!< spec v7: MEASURED_DISPARITY + }; + void setSource(Source s, bool on) + { + if (on) + m_sourceKind = s; + else if (m_sourceKind == s) + m_sourceKind = Source::AppPoint; + } + std::optional hitTest(XrVector2f uv) const { if (m_prevCount < 2) return std::nullopt; // 2D or no previous locate: nothing to hint @@ -533,7 +578,7 @@ class CursorDepth bool m_inCanvas = false; float m_height = 0.03f; XrVector2f m_bodyOffset = kCursorArrowBodyOffset; - bool m_depthLayer = false; + Source m_sourceKind = Source::AppPoint; bool m_chainOptions = false; bool m_wantPlacement = false; bool m_hintChained = false; diff --git a/tests/cursor_depth_test.cpp b/tests/cursor_depth_test.cpp index 4ed1401..cd94b99 100644 --- a/tests/cursor_depth_test.cpp +++ b/tests/cursor_depth_test.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /*! * @file - * @brief Contract test for cursor_depth.h (dxr::CursorDepth, XR_DXR_cursor_depth v5). + * @brief Contract test for cursor_depth.h (dxr::CursorDepth, XR_DXR_cursor_depth v5-v7). * * The pure parts: pointer -> canvas UV, the footprint (same rule as the * runtime / web SDK), footprint rays against a known Kooima pair (built the @@ -417,6 +417,91 @@ testNearestHit() CHECK(casts == 0 && c.hint().hasContent == XR_FALSE, "2D: no rays"); } +static const XrCursorDepthSourceDXR * +findSource(const XrViewLocateInfo &li) +{ + for (const Base *b = static_cast(li.next); b != nullptr; b = static_cast(b->next)) + if (b->type == XR_TYPE_CURSOR_DEPTH_SOURCE_DXR) return reinterpret_cast(b); + return nullptr; +} + +static void +testMeasuredDisparity() +{ + XrView v[2] = {kooima({-0.032f, 0, 0.6f}), kooima({0.032f, 0, 0.6f})}; + int casts = 0; + auto rayCast = [&](const dxr::CursorRay &) -> std::optional { + casts++; + return 1.0f; + }; + { // v7: the source struct carries MEASURED_DISPARITY; no app hit test, no point. + dxr::CursorDepth c; + c.enable(7); + c.setPointerUV(0.4f, 0.6f); + c.setRayCast(rayCast); + c.useMeasuredDisparity(true); + CHECK(c.measuredDisparity(), "measured source selected"); + c.afterLocate(v, 2); + casts = 0; + XrViewLocateInfo li{XR_TYPE_VIEW_LOCATE_INFO}; + XrViewState vs{XR_TYPE_VIEW_STATE}; + c.chain(li, vs); + const XrCursorDepthSourceDXR *src = findSource(li); + CHECK(src && src->source == XR_CURSOR_DEPTH_SOURCE_MEASURED_DISPARITY_DXR, "v7: source = MEASURED (2)"); + CHECK(casts == 0 && c.hint().hasContent == XR_FALSE, "v7 measured: no app hit test, no point"); + CHECK(c.hint().cursorUV.x == 0.4f && c.hint().cursorUV.y == 0.6f, "cursorUV still sent"); + + // Pointer off the canvas: nothing chained on the locate (zero runtime cost). + c.setPointerUV(-0.1f, 0.5f); + XrViewLocateInfo li2{XR_TYPE_VIEW_LOCATE_INFO}; + c.chain(li2, vs); + CHECK(li2.next == nullptr && findSource(li2) == nullptr, "off the canvas: no request"); + + // Off again: the next frame carries no source and the hit test is back. + c.setPointerUV(0.4f, 0.6f); + c.useMeasuredDisparity(false); + XrViewLocateInfo li3{XR_TYPE_VIEW_LOCATE_INFO}; + c.chain(li3, vs); + CHECK(findSource(li3) == nullptr && casts == 20, "measured off: no source, ray cast resumes"); + } + { // v6 runtime: measured is not understood -> not sent; the ray cast is the fallback. + dxr::CursorDepth c; + c.enable(6); + c.setPointerUV(0.5f, 0.5f); + c.setRayCast(rayCast); + c.useMeasuredDisparity(true); + c.afterLocate(v, 2); + casts = 0; + XrViewLocateInfo li{XR_TYPE_VIEW_LOCATE_INFO}; + XrViewState vs{XR_TYPE_VIEW_STATE}; + c.chain(li, vs); + CHECK(findSource(li) == nullptr && casts == 20, "v6: no measured request, ray cast used"); + } + { // The two runtime sources replace each other; turning off the inactive one is a no-op. + dxr::CursorDepth c; + c.enable(7); + c.setPointerUV(0.5f, 0.5f); + c.useDepthLayer(true); + c.useMeasuredDisparity(true); + XrViewLocateInfo li{XR_TYPE_VIEW_LOCATE_INFO}; + XrViewState vs{XR_TYPE_VIEW_STATE}; + c.chain(li, vs); + const XrCursorDepthSourceDXR *src = findSource(li); + CHECK(src && src->source == XR_CURSOR_DEPTH_SOURCE_MEASURED_DISPARITY_DXR, "last call wins (measured)"); + c.useDepthLayer(false); + XrViewLocateInfo li2{XR_TYPE_VIEW_LOCATE_INFO}; + c.chain(li2, vs); + src = findSource(li2); + CHECK(src && src->source == XR_CURSOR_DEPTH_SOURCE_MEASURED_DISPARITY_DXR, + "useDepthLayer(false) leaves measured selected"); + c.useDepthLayer(true); + XrViewLocateInfo li3{XR_TYPE_VIEW_LOCATE_INFO}; + c.chain(li3, vs); + src = findSource(li3); + CHECK(src && src->source == XR_CURSOR_DEPTH_SOURCE_SUBMITTED_DEPTH_DXR, "last call wins (depth layer)"); + } +} + int main() { @@ -427,6 +512,7 @@ main() testChainOrder(); testShowHide(); testNearestHit(); + testMeasuredDisparity(); if (g_failures == 0) std::printf("cursor_depth_test: all checks passed\n"); return g_failures == 0 ? 0 : 1; }