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
28 changes: 27 additions & 1 deletion docs/adr/ADR-046-depth-aware-cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ sprite and the depth, and the runtime owns the drawing. Apps get the same contra
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
`willDraw = XR_FALSE`; display zones; multi-screen routed frames draw per tile without the
`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).

Expand Down Expand Up @@ -669,6 +669,32 @@ sprite and the depth, and the runtime owns the drawing. Apps get the same contra
- **Found in passing (Metal, #1941):** on the Vulkan-app-on-Metal fallback a custom sprite is
handed over as the client swapchain and cast to the native Metal type — runtime#1948.

#### §B as built in display-zones frames (`XR_DXR_cursor_depth` spec v6, unchanged; runtime#1949)

- **One cursor per frame, in the zone under the pointer.** The hint already rides the
zone-scoped locate of that zone (§2.1 of the spec): the zone rect is that locate's canvas, so
`cursorUV`, the placement and its *unit* (zone height × t) are all zone-relative. The arming
locate now also records the `XrDisplayZoneDXR::zoneId` it carried.
- **State tracker.** In a zones frame `xrEndFrame` maps the placement through the views of the
frame's projection layer whose chained `XrDisplayZoneDXR` has that `zoneId` (its submitted
poses and fovs, same-space rule unchanged), so the map is ZONE-normalised; the request
(`u_cursor_draw_request`) gains `zone_valid` / `zone_id` / `zone_rect` (the rect as SUBMITTED —
authoritative per ADR-027). No such zone, a zone-scoped hint in a frame without zones, or a
zones frame whose hint was not zone-scoped: nothing drawn, `willDraw = XR_FALSE` on the next
locates, one-time WARN — the same OS-cursor fallback as before. No new struct, no spec bump:
`willDraw` already tells the app frame by frame.
- **Compositors (Metal, vk_native, GL).** One pure helper, `u_cursor_draw_into_zone()`, re-expresses
the zone-normalised views in the tile exactly the way each compositor places the zone's own
layer (zone rect scaled by tile / window), and returns the zone's sub-rect as a clip; the draw
is scissored to tile ∩ zone, so a cursor at a zone edge never bleeds into a 2D zone or a
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.
- **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.

### Rollout

1. **Groundwork:**
Expand Down
12 changes: 10 additions & 2 deletions docs/specs/extensions/XR_DXR_cursor_depth.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,23 @@ display processor, one sprite per view tile, depth test off, never occluded, alp
Each view's copy is projected with **that view's submitted pose and fov from the frame's first
projection layer**, so the projection layer must be in the **same `XrSpace`** as the locate
(otherwise nothing is drawn and later locates report `willDraw = XR_FALSE` until a frame's layer
is in the locate space again). Display-zones frames are not drawn yet (same fallback).
is in the locate space again).

**Display zones.** One cursor per frame, in the 3D zone whose zone-scoped locate carried the
hint (the zone under the pointer; `cursorUV` is relative to that zone). It is projected with
**the views of the projection layer chained with that zone's `XrDisplayZoneDXR` (same
`zoneId`)**, drawn only inside that zone's rect as submitted, never into a 2D zone or another
3D zone. If the frame has no 3D zone with that `zoneId`, the hint did not ride a zone-scoped
locate in a zones frame, or a zone-scoped hint arrives in a frame without zones, nothing is
drawn and later locates report `willDraw = XR_FALSE` (the fallback above).

### 2.6 `XrCursorDepthDrawStateDXR` (output, spec v5)

Chained on `XrViewState::next`; filled on every locate that chains it. `willDraw` is `XR_TRUE`
only when this locate chained a hint carrying a visible `XrCursorDepthDrawInfoDXR`, the placement
is active (the §3 policy: content pops out under the cursor), and the session's compositor can
draw it. **Hide the OS cursor over the canvas exactly while `willDraw` is `XR_TRUE`.** Whenever
the runtime cannot draw (another backend, a space mismatch, a zones frame), it reports
the runtime cannot draw (another backend, a space mismatch, a zones frame without the hinted zone), it reports
`XR_FALSE`, so the fallback is always the flat OS cursor, never no cursor. The runtime never
touches the OS cursor itself.

Expand Down
53 changes: 53 additions & 0 deletions src/xrt/auxiliary/util/u_cursor_depth.c
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,59 @@ u_cursor_draw_view_from_placement(const struct u_cursor_depth_view *view,
return true;
}

bool
u_cursor_draw_into_zone(struct u_cursor_draw_request *req, uint32_t target_w, uint32_t target_h, float out_clip[4])
{
out_clip[0] = 0.0f;
out_clip[1] = 0.0f;
out_clip[2] = 1.0f;
out_clip[3] = 1.0f;
if (!req->zone_valid) {
return true;
}
if (target_w == 0 || target_h == 0 || req->zone_rect.extent.w <= 0 || req->zone_rect.extent.h <= 0) {
return false;
}
// The zone's sub-rect of the tile, exactly as its layer is placed.
const float zx = (float)req->zone_rect.offset.w / (float)target_w;
const float zy = (float)req->zone_rect.offset.h / (float)target_h;
const float zw = (float)req->zone_rect.extent.w / (float)target_w;
const float zh = (float)req->zone_rect.extent.h / (float)target_h;

const float x0 = zx > 0.0f ? zx : 0.0f;
const float y0 = zy > 0.0f ? zy : 0.0f;
const float x1 = zx + zw < 1.0f ? zx + zw : 1.0f;
const float y1 = zy + zh < 1.0f ? zy + zh : 1.0f;
if (!(x1 > x0) || !(y1 > y0)) {
return false;
}

const uint32_t n = req->view_count < XRT_MAX_VIEWS ? req->view_count : XRT_MAX_VIEWS;
for (uint32_t i = 0; i < n; i++) {
struct u_cursor_draw_view *v = &req->view[i];
v->origin[0] = zx + v->origin[0] * zw;
v->origin[1] = zy + v->origin[1] * zh;
v->axis_x[0] *= zw;
v->axis_x[1] *= zh;
v->axis_y[0] *= zw;
v->axis_y[1] *= zh;
}
out_clip[0] = x0;
out_clip[1] = y0;
out_clip[2] = x1 - x0;
out_clip[3] = y1 - y0;
return true;
}

uint32_t
u_cursor_draw_canvas_px_h(const struct u_cursor_draw_request *req, uint32_t canvas_px_h)
{
if (req->zone_valid && req->zone_rect.extent.h > 0) {
return (uint32_t)req->zone_rect.extent.h;
}
return canvas_px_h;
}

void
u_cursor_draw_corners(
const struct u_cursor_draw_view *v, float height, float aspect, float hot_u, float hot_v, float out_xy[4][2])
Expand Down
42 changes: 42 additions & 0 deletions src/xrt/auxiliary/util/u_cursor_depth.h
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,21 @@ struct u_cursor_draw_request
float hot_u, hot_v;
//! Sprite HEIGHT on the panel, metres; <= 0 = the OS cursor's size.
float size_m;

/*!
* XR_DXR_display_zones (#1949): the hint rode the zone-scoped locate of
* this 3D zone, and the views above are that zone's submitted views - so
* they are ZONE-normalised (0..1 spans the zone's sub-rect of each tile)
* and one unit is the ZONE's height. The compositor re-expresses them in
* its tile with u_cursor_draw_into_zone() and clips the draw to the zone.
* False (zero-initialised) = the whole tile, as before.
*/
bool zone_valid;
//! The app's zone id (debug / logs only).
uint32_t zone_id;
//! The zone rect as submitted, client-window pixels, origin top-left -
//! the same space every compositor places XRT_LAYER_ZONE_3D layers in.
struct xrt_rect zone_rect;
};

/*!
Expand Down Expand Up @@ -530,6 +545,33 @@ void
u_cursor_draw_corners(
const struct u_cursor_draw_view *v, float height, float aspect, float hot_u, float hot_v, float out_xy[4][2]);

/*!
* XR_DXR_display_zones (#1949): re-express a zone request in TILE-normalised
* coordinates, the way the compositor places the zone's own layer - the zone
* rect scaled by tile / @p target (the window the zone rects are authored
* against; in a zones frame the tile spans it).
*
* Rewrites every view of @p req in place (origin into the zone's sub-rect,
* axes scaled by its extent) and returns the sub-rect, clipped to the tile, in
* @p out_clip (x, y, w, h; tile-normalised, origin top-left): the draw must be
* scissored to it, so a cursor at a zone edge never spills into a 2D zone or
* a neighbouring 3D zone. A request without a zone is left untouched and
* clips to the whole tile.
*
* @return false if the zone cannot be placed (no target size, or the zone lies
* entirely outside the window): draw nothing.
*/
bool
u_cursor_draw_into_zone(struct u_cursor_draw_request *req, uint32_t target_w, uint32_t target_h, float out_clip[4]);

/*!
* The height, in the compositor's target pixels, of the canvas the request's
* unit was built for: the zone's height for a zone request, else
* @p canvas_px_h (the window / canvas the compositor already uses).
*/
uint32_t
u_cursor_draw_canvas_px_h(const struct u_cursor_draw_request *req, uint32_t canvas_px_h);

/*!
* Rasterise the classic arrow (white fill, dark one-px outline, tip at the
* top-left corner) into a @p w x @p h RGBA8 image with PREMULTIPLIED alpha.
Expand Down
34 changes: 28 additions & 6 deletions src/xrt/compositor/gl/comp_gl_compositor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6053,8 +6053,19 @@ gl_cursor_update_default(struct comp_gl_compositor *c, float backing_scale)
* target).
*/
static bool
gl_cursor_draw(struct comp_gl_compositor *c, const struct u_cursor_draw_request *req)
gl_cursor_draw(struct comp_gl_compositor *c,
const struct u_cursor_draw_request *req_in,
uint32_t zones_target_w,
uint32_t zones_target_h)
{
// #1949: a zone request's views are zone-normalised - re-express them in
// the tile the way the zone's own layer is placed, and clip to the zone.
struct u_cursor_draw_request zreq = *req_in;
const struct u_cursor_draw_request *req = &zreq;
float clip[4];
if (!u_cursor_draw_into_zone(&zreq, zones_target_w, zones_target_h, clip)) {
return false;
}
if (!gl_cursor_ensure_program(c)) {
return false;
}
Expand All @@ -6074,6 +6085,7 @@ gl_cursor_draw(struct comp_gl_compositor *c, const struct u_cursor_draw_request
if (canvas_px_h == 0) {
canvas_px_h = c->eff_rows * c->eff_tile_h;
}
canvas_px_h = u_cursor_draw_canvas_px_h(req, canvas_px_h); // a zone's own height
if (canvas_px_h == 0) {
return false;
}
Expand Down Expand Up @@ -6156,7 +6168,16 @@ gl_cursor_draw(struct comp_gl_compositor *c, const struct u_cursor_draw_request
bh = c->eff_tile_h;
}
glViewport((GLint)bx, (GLint)by, (GLsizei)bw, (GLsizei)bh);
glScissor((GLint)bx, (GLint)by, (GLsizei)bw, (GLsizei)bh);
// Clipped to the zone in a zones frame (#1949): the clip is
// top-left-origin, the tile box bottom-left.
const GLint sx0 = (GLint)bx + (GLint)lroundf(clip[0] * (float)bw);
const GLint sx1 = (GLint)bx + (GLint)lroundf((clip[0] + clip[2]) * (float)bw);
const GLint sy0 = (GLint)by + (GLint)lroundf((1.0f - clip[1] - clip[3]) * (float)bh);
const GLint sy1 = (GLint)by + (GLint)lroundf((1.0f - clip[1]) * (float)bh);
if (sx1 <= sx0 || sy1 <= sy0) {
continue;
}
glScissor(sx0, sy0, (GLsizei)(sx1 - sx0), (GLsizei)(sy1 - sy0));

float xy[4][2];
u_cursor_draw_corners(&req->view[i], height, aspect, hot_u, hot_v, xy);
Expand Down Expand Up @@ -6238,16 +6259,17 @@ gl_compositor_layer_commit_locked(struct xrt_compositor *xc, xrt_graphics_sync_h
/*
* 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. Zones
* frames are refused by the state tracker already; belt and braces.
* XrCursorDepthDrawInfoDXR. The request covers this commit only.
*/
struct u_cursor_draw_request cursor_req = {};
bool cursor_pending = false;
c->cursor_draw.pending = false;
if (c->cursor_draw.req.requested) {
cursor_req = c->cursor_draw.req;
c->cursor_draw.req.requested = false;
cursor_pending = !c->zones_frame && u_cursor_draw_should_draw(&cursor_req);
// A zones frame draws only a zone request (#1949; the state tracker
// sends nothing else there) and vice versa; belt and braces.
cursor_pending = c->zones_frame == cursor_req.zone_valid && u_cursor_draw_should_draw(&cursor_req);
c->cursor_draw.pending = cursor_pending;
c->cursor_draw.pending_source_srgb =
cursor_req.image == NULL || gl_swapchain_is_srgb(gl_swapchain(cursor_req.image));
Expand Down Expand Up @@ -7086,7 +7108,7 @@ gl_compositor_layer_commit_locked(struct xrt_compositor *xc, xrt_graphics_sync_h
#ifdef XRT_OS_WINDOWS
cursor_routed = seg_routed_frame;
#endif
if (cursor_pending && !cursor_routed && gl_cursor_draw(c, &cursor_req)) {
if (cursor_pending && !cursor_routed && gl_cursor_draw(c, &cursor_req, zones_target_w, zones_target_h)) {
window_space_drew = true; // republish the compose target
}

Expand Down
41 changes: 33 additions & 8 deletions src/xrt/compositor/metal/comp_metal_compositor.m
Original file line number Diff line number Diff line change
Expand Up @@ -4134,8 +4134,20 @@ - (BOOL)wantsUpdateLayer
uint32_t atlas_view_w,
uint32_t atlas_view_h,
bool compose_linear,
uint32_t canvas_px_h)
uint32_t canvas_px_h,
uint32_t zones_target_w,
uint32_t zones_target_h)
{
// #1949: a zone request's views are zone-normalised - re-express them in
// the tile the way the zone's own layer is placed, and clip to the zone.
struct u_cursor_draw_request zreq = *req;
float clip[4];
if (!u_cursor_draw_into_zone(&zreq, zones_target_w, zones_target_h, clip)) {
return;
}
req = &zreq;
canvas_px_h = u_cursor_draw_canvas_px_h(req, canvas_px_h);

if (!metal_cursor_ensure_pipelines(c)) {
return;
}
Expand Down Expand Up @@ -4209,9 +4221,20 @@ - (BOOL)wantsUpdateLayer
MTLViewport vp = {(double)ox, (double)oy, (double)atlas_view_w, (double)atlas_view_h, 0.0, 1.0};
[encoder setViewport:vp];
// 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).
MTLScissorRect sr = {ox, oy, (ox + atlas_view_w > att_w) ? (att_w - ox) : atlas_view_w,
(oy + atlas_view_h > att_h) ? (att_h - oy) : atlas_view_h};
// into the neighbour view (the DP would weave it into the wrong eye),
// nor - in a zones frame - out of its zone (#1949).
const uint32_t tw = (ox + atlas_view_w > att_w) ? (att_w - ox) : atlas_view_w;
const uint32_t th = (oy + atlas_view_h > att_h) ? (att_h - oy) : atlas_view_h;
const uint32_t cx0 = (uint32_t)lroundf(clip[0] * (float)atlas_view_w);
const uint32_t cy0 = (uint32_t)lroundf(clip[1] * (float)atlas_view_h);
uint32_t cx1 = (uint32_t)lroundf((clip[0] + clip[2]) * (float)atlas_view_w);
uint32_t cy1 = (uint32_t)lroundf((clip[1] + clip[3]) * (float)atlas_view_h);
cx1 = cx1 < tw ? cx1 : tw;
cy1 = cy1 < th ? cy1 : th;
if (cx1 <= cx0 || cy1 <= cy0) {
continue;
}
MTLScissorRect sr = {ox + cx0, oy + cy0, cx1 - cx0, cy1 - cy0};
[encoder setScissorRect:sr];

float xy[4][2];
Expand Down Expand Up @@ -5392,11 +5415,13 @@ - (BOOL)wantsUpdateLayer
// never occluded - and before the display processor. Inside this
// encoder, so the /tmp/dxr_atlas_trigger dump below shows it.
if (cursor_pending) {
if (zones_frame) {
// The state tracker already refuses zones frames; belt and braces.
// A zones frame draws only a zone request (the state tracker
// sends nothing else there) and vice versa; belt and braces.
if (zones_frame != cursor_req.zone_valid) {
if (!c->cursor_draw.warned_zones) {
c->cursor_draw.warned_zones = true;
U_LOG_W("cursor_draw: zones frame - cursor not drawn (one-time warning)");
U_LOG_W("cursor_draw: zones frame / zone request mismatch - cursor not drawn "
"(one-time warning)");
}
} else {
uint32_t canvas_px_h = 0;
Expand All @@ -5406,7 +5431,7 @@ - (BOOL)wantsUpdateLayer
canvas_px_h = (uint32_t)output_texture.height;
}
metal_cursor_draw(c, encoder, &cursor_req, atlas_cols, atlas_rows, atlas_view_w, atlas_view_h,
compose_linear, canvas_px_h);
compose_linear, canvas_px_h, zones_target_w, zones_target_h);
}
}

Expand Down
26 changes: 21 additions & 5 deletions src/xrt/compositor/vk_native/comp_vk_native_compositor.c
Original file line number Diff line number Diff line change
Expand Up @@ -9449,24 +9449,39 @@ vk_shared_submit_and_wait(struct comp_vk_native_compositor *c, const VkSubmitInf
* per-view placement into this frame's renderer cursor - which image, its
* size in canvas units, and each view's corners in its tile.
*
* @param target_w The target (window) width in backing pixels - with @p canvas_px_h, what
* a zone rect is scaled against (#1949).
* @param canvas_px_h The canvas (window) height in backing pixels: the size
* the Kooima views were built for, so one unit = canvas height.
* @return false if nothing can be drawn this frame (the frame is untouched).
*/
static bool
vk_cursor_resolve(struct comp_vk_native_compositor *c,
const struct u_cursor_draw_request *req,
const struct u_cursor_draw_request *req_in,
uint32_t target_w,
uint32_t canvas_px_h,
struct comp_vk_native_cursor_draw *out)
{
if (c->zones_frame) {
// The state tracker already refuses zones frames; belt and braces.
// A zones frame draws only a zone request (the state tracker sends
// nothing else there) and vice versa; 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: zones frame - cursor not drawn (one-time warning)");
U_LOG_W(
"cursor_draw: zones frame / zone request mismatch - cursor not drawn (one-time warning)");
}
return false;
}
// #1949: a zone request's views are zone-normalised - re-express them in
// the tile the way the zone's own layer is placed (zone rect scaled by
// tile / target, the renderer's zone pass), and clip to the zone.
struct u_cursor_draw_request zreq = *req_in;
const struct u_cursor_draw_request *req = &zreq;
float clip[4];
if (!u_cursor_draw_into_zone(&zreq, target_w, canvas_px_h, clip)) {
return false;
}
canvas_px_h = u_cursor_draw_canvas_px_h(req, canvas_px_h);
if (c->eff_layout.route_count > 0) {
// Multi-screen M3: a tile is a per-segment mosaic, not one view.
if (!c->cursor_draw.warned_routed) {
Expand Down Expand Up @@ -9521,6 +9536,7 @@ vk_cursor_resolve(struct comp_vk_native_compositor *c,
}
const float height = size_m / canvas_h_m; // sprite height, in units

memcpy(out->clip, clip, sizeof(out->clip));
out->view_count = req->view_count < XRT_MAX_VIEWS ? req->view_count : XRT_MAX_VIEWS;
for (uint32_t i = 0; i < out->view_count; i++) {
u_cursor_draw_corners(&req->view[i], height, aspect, hot_u, hot_v, out->corners[i]);
Expand Down Expand Up @@ -9835,7 +9851,7 @@ vk_compositor_layer_commit_locked(struct xrt_compositor *xc,
struct u_cursor_draw_request cursor_req = c->cursor_draw.req;
c->cursor_draw.req.requested = false;
if (u_cursor_draw_should_draw(&cursor_req)) {
(void)vk_cursor_resolve(c, &cursor_req, tgt_height, &cursor_draw);
(void)vk_cursor_resolve(c, &cursor_req, tgt_width, tgt_height, &cursor_draw);
}
}

Expand Down
Loading
Loading