Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 51 additions & 6 deletions common/cursor_depth.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +18,8 @@
* // setPlatform(dxr::cursorPlatformX11(dpy, win))
* cursor.setRayCast([&](const dxr::CursorRay& r) -> std::optional<float> {
* float t; return myScene.raycast(r.origin, r.dir, t) ? std::optional<float>(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);
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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<XrVector3f> hit = hitTest(uv);
if (hit) {
Expand All @@ -420,7 +450,7 @@ class CursorDepth
if (layer) {
m_source = detail::makeXr<XrCursorDepthSourceDXR>(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) {
Expand Down Expand Up @@ -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<XrVector3f> hitTest(XrVector2f uv) const
{
if (m_prevCount < 2) return std::nullopt; // 2D or no previous locate: nothing to hint
Expand Down Expand Up @@ -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;
Expand Down
88 changes: 87 additions & 1 deletion tests/cursor_depth_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<const Base *>(li.next); b != nullptr; b = static_cast<const Base *>(b->next))
if (b->type == XR_TYPE_CURSOR_DEPTH_SOURCE_DXR) return reinterpret_cast<const XrCursorDepthSourceDXR *>(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<float> {
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()
{
Expand All @@ -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;
}
Loading