From f924882032202e4ffb20210bcb0dbdff1cd40c22 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 4 Oct 2026 21:27:44 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(vk=20dp,=20weave/linux):=20set=5Foverl?= =?UTF-8?q?ay=5F2d=20VK=20slot=20=E2=80=94=202D=20under=20the=20lens=20on?= =?UTF-8?q?=20Vulkan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append xrt_display_processor_vk::set_overlay_2d + set_overlay_2d_filter_strength (XRT_DP_VK_HAS_OVERLAY_2D[_FILTER_STRENGTH], ADR-020 append, no ABI major) — the Vulkan twin of the D3D11 slots from #1814/#1822. Same contract: stateless per frame, layer target-sized, premultiplied, ENCODED sRGB, RGBA8/BGRA8; true = the DP composites it inside the next process_atlas, false/absent = the runtime keeps its post-weave blend. VK specifics: a VkImageView + VkFormat, in SHADER_READ_ONLY_OPTIMAL when process_atlas' command buffer executes. First producer: the desktop-Linux weave engine. When the frame weaves and the v4 overlay is exactly output-sized, it acquires the overlay and transitions it before the self-submitting split, offers it to the DP (v15 strength first, v14 unchanged hint from the submit), and skips its own full-window blend when the DP takes it. Off-panel bands and flat regions are painted over the weave afterwards, so the layer is redrawn over those rects only, keeping 2D readable across a seam as before. One WARN on each change of who composites. The IPC handler forwards overlay_unchanged / overlay_filter_strength on both Linux submit paths (they were already on the wire). sim_display has no slot, so its path is unchanged: weave_present_vk_linux --headless 90 PASSes (HUD composited) with the runtime-blend verdict logged. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/adr/ADR-027-display-zones.md | 8 +- docs/specs/extensions/XR_DXR_weave.md | 12 +- src/xrt/compositor/multi/comp_multi_private.h | 25 +++ .../compositor/multi/comp_multi_weave_linux.c | 124 +++++++++++--- .../include/xrt/xrt_display_processor_vk.h | 158 +++++++++++++++++- src/xrt/ipc/server/ipc_server_handler.c | 4 + 6 files changed, 305 insertions(+), 26 deletions(-) diff --git a/docs/adr/ADR-027-display-zones.md b/docs/adr/ADR-027-display-zones.md index 474c73609..e09aaebd5 100644 --- a/docs/adr/ADR-027-display-zones.md +++ b/docs/adr/ADR-027-display-zones.md @@ -59,7 +59,13 @@ issues: [439, 396] > - First producer: the `XR_DXR_weave` v4 overlay (`XrWeaveSubmitOverlaysDXR`) on the D3D11 > service's full-window weave paths (v3 batch, v6); the legacy single-rect path keeps the > blit. Other APIs and the in-process compositors' Local2D-over path follow the same slot -> shape when needed. 2D-*under*-3D (`set_background_2d`) is unchanged. +> shape when needed. **Vulkan (2026-10):** the appended VK-variant slot +> `xrt_display_processor_vk::set_overlay_2d` (+ `set_overlay_2d_filter_strength`, +> `XRT_DP_VK_HAS_OVERLAY_2D[_FILTER_STRENGTH]`) carries the same contract with a +> `VkImageView` + `VkFormat`; the layer must be target-sized and in +> `SHADER_READ_ONLY_OPTIMAL` when process_atlas' command buffer executes. First producer: the +> desktop-Linux weave engine (`comp_multi_weave_linux.c`); off-panel bands and flat regions +> painted after the weave get the layer redrawn over them, so 2D stays readable across a seam. 2D-*under*-3D (`set_background_2d`) is unchanged. > - Browser use (decided 2026-10): the layer is the **whole flat page** with the woven tiles > as alpha-0 holes, not only the 2D lifted over tiles — the aliasing users saw was page text > around the canvas. While any tile exists the browser submits every frame; a submit whose diff --git a/docs/specs/extensions/XR_DXR_weave.md b/docs/specs/extensions/XR_DXR_weave.md index e4f3da7a2..5f199002e 100644 --- a/docs/specs/extensions/XR_DXR_weave.md +++ b/docs/specs/extensions/XR_DXR_weave.md @@ -230,8 +230,10 @@ typedef struct XrWeaveSubmitOverlayUnchangedDXR { the viewer moves enough to matter. - **Content-only.** The overlay is composited every frame either way; this only gates re-deriving work from it. -- D3D11 service path today (`set_overlay_2d(..., layer_unchanged)`); the Vulkan weave engines - accept and ignore it until their DP slot exists. +- D3D11 service path (`set_overlay_2d(..., layer_unchanged)`) and the desktop-Linux Vulkan + weave engine (the appended VK DP slot `xrt_display_processor_vk::set_overlay_2d`, same + argument); the macOS / Android Vulkan weave engines accept and ignore it until they call that + slot. ### Filter strength (v15) @@ -251,9 +253,9 @@ typedef struct XrWeaveSubmitOverlayFilterDXR { vendor value tuned by eye on its panel (Leia SR: 0.6). The runtime passes "not set", never a number of its own. - `filterStrength` outside [0,1] or NaN is `XR_ERROR_VALIDATION_FAILURE`. -- Per submit, no latch. Carried to the DP by the appended D3D11 slot - `set_overlay_2d_filter_strength` (negative = default), called before `set_overlay_2d` each - frame the layer goes to the DP. +- Per submit, no latch. Carried to the DP by the appended slot + `set_overlay_2d_filter_strength` (D3D11, and the VK variant on desktop Linux; negative = + default), called before `set_overlay_2d` each frame the layer goes to the DP. diff --git a/src/xrt/compositor/multi/comp_multi_private.h b/src/xrt/compositor/multi/comp_multi_private.h index 03f0ad06e..3b55f3870 100644 --- a/src/xrt/compositor/multi/comp_multi_private.h +++ b/src/xrt/compositor/multi/comp_multi_private.h @@ -935,6 +935,20 @@ struct multi_compositor bool mono_in_2d; bool mono_active; + /*! + * @name 2D under the lens (ADR-027 Amendment, XR_DXR_weave v14/v15) + * Per-submit overlay hints, set by comp_multi_weave_linux_set_overlay_hints + * right before each submit (never sticky), forwarded to the DP with the + * overlay when its set_overlay_2d slot takes it. @c overlay_in_dp is the + * last verdict (DP composites vs runtime post-weave blend) so the WARN + * fires on a change only. + * @{ + */ + bool overlay_unchanged; + float overlay_filter_strength; + uint8_t overlay_in_dp; //!< 0 = no overlay seen yet, 1 = runtime blend, 2 = DP composites + //! @} + //! @name Flat regions (spec v8, browser#88, on desktop Linux) //! The sticky screen-space latch (xrWeaveSetScreenFlatRegionsDXR), //! absolute screen device px; a SET, count 0 clears. Unioned with @@ -1572,6 +1586,17 @@ comp_multi_weave_linux_request_display_mode(struct multi_compositor *mc, bool en */ void comp_multi_weave_linux_set_mono_in_2d(struct xrt_compositor *xc, bool mono_in_2d); + +/*! + * XR_DXR_weave v14/v15 overlay hints of the NEXT submit (same per-submit shape + * as comp_multi_weave_linux_set_mono_in_2d): @p overlay_unchanged = the overlay + * holds the previous accepted submit's pixels; @p filter_strength = the app's + * lens-filter strength, [0,1], negative = the DP's default. Used only when the + * DP composites the overlay inside the weave (set_overlay_2d, ADR-027 + * Amendment); the runtime's own post-weave blend ignores both. + */ +void +comp_multi_weave_linux_set_overlay_hints(struct xrt_compositor *xc, bool overlay_unchanged, float filter_strength); /*! @} */ #endif // XRT_OS_LINUX_DESKTOP diff --git a/src/xrt/compositor/multi/comp_multi_weave_linux.c b/src/xrt/compositor/multi/comp_multi_weave_linux.c index b31f8beff..3d446aba6 100644 --- a/src/xrt/compositor/multi/comp_multi_weave_linux.c +++ b/src/xrt/compositor/multi/comp_multi_weave_linux.c @@ -2126,6 +2126,43 @@ weave_run_frame(struct vk_bundle *vk, skip_weave = true; } + // 2D under the lens (ADR-027 Amendment): offer the v4 overlay to the DP so + // it composites the layer INSIDE the weave and band-limits it for the lens, + // instead of the post-weave blend below (which aliases per eye wherever the + // lens is on). Only when this frame weaves and the layer is exactly the + // output (= the process_atlas target) size — the slot's contract. The layer + // is taken from the producer and put in SHADER_READ_ONLY here, BEFORE the + // self-submitting split below, so it is ready when the DP's reads execute + // whichever command buffer they land in. A DP that declines (no slot, no SR + // compose, transparency, 2D content, ...) leaves the post-weave blend to do + // exactly what it did before; the layer is then already acquired. + struct xrt_display_processor_vk *dp_vk = (struct xrt_display_processor_vk *)mc->weave.dp; + bool ov_acquired = false; // ov taken from the producer and in SHADER_READ_ONLY_OPTIMAL + bool ov_in_dp = false; // the DP composites ov in this frame's process_atlas + if (ov != NULL && !skip_weave && ov->w == mc->weave.out_w && ov->h == mc->weave.out_h && + xrt_display_processor_vk_supports_overlay_2d(dp_vk)) { + weave_acquire_slot(vk, cmd, ov, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + weave_layout_barrier(vk, cmd, ov->image, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + ov->first_use = false; + ov_acquired = true; + // v15 strength first: the D3D11 order, and a DP may key its weave on both. + xrt_display_processor_vk_set_overlay_2d_filter_strength(dp_vk, mc->weave.overlay_filter_strength); + ov_in_dp = + xrt_display_processor_vk_set_overlay_2d(dp_vk, ov->view, (VkFormat_XDP)ov->format, ov->w, ov->h, + XRT_ATLAS_ENCODING_ENCODED, mc->weave.overlay_unchanged); + } + if (ov != NULL) { + const uint8_t verdict = ov_in_dp ? 2u : 1u; + if (verdict != mc->weave.overlay_in_dp) { + mc->weave.overlay_in_dp = verdict; + U_LOG_W("weave: 2D overlay (%ux%u) %s", ov->w, ov->h, + ov_in_dp ? "composited by the display processor INSIDE the weave (2D under the lens)" + : "blended post-weave by the runtime (the DP did not take it this frame)"); + } + } + // SELF-SUBMITTING DP ORDERING (Android #1036's one-frame trail fix): a // DP that submits its own batch during process_atlas would otherwise // execute BEFORE this frame's blits (still unsubmitted in cmd). Flush @@ -2233,9 +2270,9 @@ weave_run_frame(struct vk_bundle *vk, // input is window-sized in register with the output; otherwise (v6 / // legacy, where the input holds views, not a page) the flat view the // off-panel bands use. + struct u_wl_rect_px flat[WEAVE_FLAT_MAX_RECTS]; + const uint32_t flat_count = weave_flat_rects(mc, nview, flat_rect_count, flat_rects, flat); { - struct u_wl_rect_px flat[WEAVE_FLAT_MAX_RECTS]; - const uint32_t flat_count = weave_flat_rects(mc, nview, flat_rect_count, flat_rects, flat); if (flat_count > 0) { const bool page_source = !nview && rect_count > 0; if (page_source) { @@ -2269,16 +2306,34 @@ weave_run_frame(struct vk_bundle *vk, // v4 overlay atlas (browser#18): composite the caller's window-sized // premultiplied 2D atlas OVER the woven output — not woven, drawn after - // process_atlas onto the same attachment. + // process_atlas onto the same attachment. When the DP already composited it + // inside the weave (ov_in_dp), only the off-panel bands and flat regions + // painted over the weave above need it again: those were cleared and + // re-painted flat, so the layer is redrawn there (and only there), keeping + // 2D readable on both sides of a seam exactly as before. if (ov != NULL) { - if (weave_blend_begin(vk, mc, &blend_begun)) { - weave_acquire_slot(vk, cmd, ov, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_ACCESS_SHADER_READ_BIT); - weave_layout_barrier(vk, cmd, ov->image, VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0, VK_ACCESS_SHADER_READ_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - ov->first_use = false; + struct u_wl_rect_px redo[ARRAY_SIZE(offpanel) + WEAVE_FLAT_MAX_RECTS]; + uint32_t redo_count = 0; + if (ov_in_dp) { + for (uint32_t i = 0; i < offpanel_count && redo_count < ARRAY_SIZE(redo); i++) { + redo[redo_count++] = offpanel[i]; + } + for (uint32_t i = 0; i < flat_count && redo_count < ARRAY_SIZE(redo); i++) { + redo[redo_count++] = flat[i]; + } + } + const bool draw = ov_in_dp ? redo_count > 0 : true; + if (draw && weave_blend_begin(vk, mc, &blend_begun)) { + if (!ov_acquired) { + weave_acquire_slot(vk, cmd, ov, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_ACCESS_SHADER_READ_BIT); + weave_layout_barrier(vk, cmd, ov->image, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0, + VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + ov->first_use = false; + ov_acquired = true; + } weave_layout_barrier(vk, cmd, mc->weave.out_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, @@ -2287,14 +2342,29 @@ weave_run_frame(struct vk_bundle *vk, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); - vk_local2d_composite_flatten_draw(&mc->weave.overlay_blend, vk, cmd, mc->weave.out_fb, - mc->weave.out_w, mc->weave.out_h, - ov->view, // - 0, 0, mc->weave.out_w, - mc->weave.out_h, // dst = full window - 0.0f, 0.0f, 1.0f, 1.0f, // src = whole atlas - /*unpremultiplied*/ false); - + if (!ov_in_dp) { + vk_local2d_composite_flatten_draw(&mc->weave.overlay_blend, vk, cmd, mc->weave.out_fb, + mc->weave.out_w, mc->weave.out_h, + ov->view, // + 0, 0, mc->weave.out_w, + mc->weave.out_h, // dst = full window + 0.0f, 0.0f, 1.0f, 1.0f, // src = whole atlas + /*unpremultiplied*/ false); + } else { + // The layer is output-sized (checked before it went to the DP), + // so each rect samples it 1:1 at its own position. + for (uint32_t i = 0; i < redo_count; i++) { + const struct u_wl_rect_px r = redo[i]; + vk_local2d_composite_flatten_draw( + &mc->weave.overlay_blend, vk, cmd, mc->weave.out_fb, mc->weave.out_w, + mc->weave.out_h, ov->view, r.x, r.y, (uint32_t)r.w, (uint32_t)r.h, + (float)r.x / (float)ov->w, (float)r.y / (float)ov->h, + (float)r.w / (float)ov->w, (float)r.h / (float)ov->h, + /*unpremultiplied*/ false); + } + } + } + if (ov_acquired) { weave_layout_barrier(vk, cmd, ov->image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_READ_BIT, 0, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, @@ -2885,6 +2955,22 @@ comp_multi_weave_linux_set_mono_in_2d(struct xrt_compositor *xc, bool mono_in_2d os_mutex_unlock(&mc->weave.mutex); } +void +comp_multi_weave_linux_set_overlay_hints(struct xrt_compositor *xc, bool overlay_unchanged, float filter_strength) +{ + struct multi_compositor *mc = multi_compositor(xc); + if (mc == NULL || mc->msc == NULL) { + return; + } + weave_ensure_mutex(mc); + os_mutex_lock(&mc->weave.mutex); + mc->weave.overlay_unchanged = overlay_unchanged; + // Out of range (incl. NaN) = not set by the app: the DP's own default. + mc->weave.overlay_filter_strength = + (filter_strength >= 0.0f && filter_strength <= 1.0f) ? filter_strength : -1.0f; + os_mutex_unlock(&mc->weave.mutex); +} + void comp_multi_weave_fini(struct multi_compositor *mc) { diff --git a/src/xrt/include/xrt/xrt_display_processor_vk.h b/src/xrt/include/xrt/xrt_display_processor_vk.h index 5c13ff219..836695956 100644 --- a/src/xrt/include/xrt/xrt_display_processor_vk.h +++ b/src/xrt/include/xrt/xrt_display_processor_vk.h @@ -631,6 +631,83 @@ struct xrt_display_processor_vk * opaque and the DP may idle its transparency work. */ void (*set_transparency_active)(struct xrt_display_processor_vk *xdp, bool active); + + /*! + * The runtime's 2D over-layer for the NEXT @ref xrt_display_processor::process_atlas + * (ADR-027 Amendment, "2D under the lens") — the Vulkan twin of + * @ref xrt_display_processor_d3d11::set_overlay_2d. + * + * 2D composited as plain pixels AFTER the weave aliases per eye wherever the + * lens is on; only the DP knows the per-subpixel lens phase needed to + * band-limit it. Returning true says the DP composites @p overlay_view + * (premultiplied "over", in encoded space, onto the encoded woven result) + * inside that process_atlas, and the runtime then does NOT composite the + * layer itself. Returning false (or an absent slot) keeps the runtime's + * post-weave composite exactly as before, so a DP without the capability is + * unchanged. + * + * ## Contract + * + * - **Stateless, per frame.** Called before every process_atlas that carries + * an overlay, with the layer for THAT call only; a process_atlas not + * preceded by a call has no layer. The DP consumes and clears it in + * process_atlas (also on an early return), so it can never leak into a + * later weave. + * - **Layer:** a 2D image view of exactly the process_atlas TARGET size + * (@p width x @p height = target_width x target_height; texel (0,0) is + * the target's top-left), RGBA8 or BGRA8 (`_UNORM`, or the `_SRGB` view of + * one), premultiplied alpha, values encoded sRGB as they would be written + * to an 8-bit swapchain (@p encoding = XRT_ATLAS_ENCODING_ENCODED; a DP + * returns false for anything else), alpha = 2D coverage. + * - **Synchronisation:** the image is in + * `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`, owned by the queue family of + * process_atlas' command buffer, with its writes made visible to + * fragment-shader reads, by the time that command buffer executes, and + * stays alive until it has completed. The DP reads it only; it never + * transitions or releases it. + * - The process_atlas target-layout contract is unchanged (a + * command-recording DP still returns the target in PRESENT_SRC_KHR). + * + * Optional — absent slot (older plug-in `struct_size`) or NULL ⟹ the + * runtime composites the layer post-weave. Appended after + * @ref set_transparency_active per ADR-020 (append-only within a major). + * + * @param xdp Pointer to self. + * @param overlay_view VkImageView of the layer (never VK_NULL_HANDLE). + * @param format VkFormat of @p overlay_view. + * @param width Layer width in pixels. + * @param height Layer height in pixels. + * @param encoding Colour state of the layer (v1: ENCODED). + * @param layer_unchanged The caller declared the layer's pixels identical to + * the previous call's (XR_DXR_weave v14): a DP that + * caches a lens prefilter of the layer may reuse it. + * Advisory — false is always correct, only slower. + * @return true when the DP composites the layer in the next process_atlas. + */ + bool (*set_overlay_2d)(struct xrt_display_processor_vk *xdp, + VkImageView overlay_view, + VkFormat_XDP format, + uint32_t width, + uint32_t height, + enum xrt_atlas_encoding encoding, + bool layer_unchanged); + + /*! + * Lens-filter strength for the 2D over-layer of the NEXT process_atlas + * (XR_DXR_weave v15) — the Vulkan twin of + * @ref xrt_display_processor_d3d11::set_overlay_2d_filter_strength. + * 0 = no lens filtering, 1 = the DP's full filter, NEGATIVE = the DP's own + * default. Called right before @ref set_overlay_2d whenever an overlay goes + * to the DP, so a DP that keeps the value sticky must take it from here + * every frame. + * + * Optional — absent slot or NULL ⟹ the DP's default is used. Appended per + * ADR-020. + * + * @param xdp Pointer to self. + * @param strength [0,1], or negative for the DP's default. + */ + void (*set_overlay_2d_filter_strength)(struct xrt_display_processor_vk *xdp, float strength); }; /*! @@ -741,7 +818,9 @@ XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_vk, get_background_previ XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_vk, get_last_frame_dropped) == sizeof(struct xrt_display_processor) + 12 * sizeof(void *), XRT_DP_ABI_MSG); XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_vk, snap_window_rect) == sizeof(struct xrt_display_processor) + 13 * sizeof(void *), XRT_DP_ABI_MSG); XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_vk, set_transparency_active) == sizeof(struct xrt_display_processor) + 14 * sizeof(void *), XRT_DP_ABI_MSG); -XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_vk) == sizeof(struct xrt_display_processor) + 15 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_vk, set_overlay_2d) == sizeof(struct xrt_display_processor) + 15 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_vk, set_overlay_2d_filter_strength) == sizeof(struct xrt_display_processor) + 16 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_vk) == sizeof(struct xrt_display_processor) + 17 * sizeof(void *), XRT_DP_ABI_MSG); /*! * Defined when this header carries the @ref @@ -768,6 +847,21 @@ XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_vk) == sizeof(struct xrt_d * appended slot. */ #define XRT_DP_VK_HAS_TRANSPARENCY_ACTIVE 1 + +/*! + * Defined when this header carries the @ref + * xrt_display_processor_vk::set_overlay_2d slot (ADR-027 Amendment, "2D under + * the lens" on Vulkan), so a plug-in built against an older runtime can + * #ifdef-guard its implementation. Purely additive (ADR-020). + */ +#define XRT_DP_VK_HAS_OVERLAY_2D 1 + +/*! + * Defined when this header carries the @ref + * xrt_display_processor_vk::set_overlay_2d_filter_strength slot + * (XR_DXR_weave v15). Purely additive (ADR-020). + */ +#define XRT_DP_VK_HAS_OVERLAY_2D_FILTER_STRENGTH 1 // clang-format on /*! @@ -1201,6 +1295,68 @@ xrt_display_processor_vk_set_transparency_active(struct xrt_display_processor_vk return true; } +/*! + * Does this DP carry @ref xrt_display_processor_vk::set_overlay_2d (its + * `base.struct_size` covers the slot and the pointer is set)? Lets a caller + * skip preparing the layer for a DP that could never take it. + * + * @public @memberof xrt_display_processor_vk + */ +static inline bool +xrt_display_processor_vk_supports_overlay_2d(struct xrt_display_processor_vk *xdp) +{ + if (xdp == NULL) { + return false; + } + const char *slot_end = (const char *)&xdp->set_overlay_2d + sizeof(xdp->set_overlay_2d); + return slot_end <= (const char *)xdp + xdp->base.struct_size && xdp->set_overlay_2d != NULL; +} + +/*! + * @copydoc xrt_display_processor_vk::set_overlay_2d + * + * Returns false if not supported (the plug-in's `base.struct_size` doesn't cover + * the slot, or the pointer is NULL) — the caller then composites the layer + * post-weave itself, as before. + * + * @public @memberof xrt_display_processor_vk + */ +static inline bool +xrt_display_processor_vk_set_overlay_2d(struct xrt_display_processor_vk *xdp, + VkImageView overlay_view, + VkFormat_XDP format, + uint32_t width, + uint32_t height, + enum xrt_atlas_encoding encoding, + bool layer_unchanged) +{ + if (!xrt_display_processor_vk_supports_overlay_2d(xdp)) { + return false; + } + return xdp->set_overlay_2d(xdp, overlay_view, format, width, height, encoding, layer_unchanged); +} + +/*! + * @copydoc xrt_display_processor_vk::set_overlay_2d_filter_strength + * + * No-op if not supported (the DP's default strength applies). + * + * @public @memberof xrt_display_processor_vk + */ +static inline void +xrt_display_processor_vk_set_overlay_2d_filter_strength(struct xrt_display_processor_vk *xdp, float strength) +{ + if (xdp == NULL) { + return; + } + const char *slot_end = + (const char *)&xdp->set_overlay_2d_filter_strength + sizeof(xdp->set_overlay_2d_filter_strength); + if (slot_end > (const char *)xdp + xdp->base.struct_size || xdp->set_overlay_2d_filter_strength == NULL) { + return; + } + xdp->set_overlay_2d_filter_strength(xdp, strength); +} + #ifdef __cplusplus } #endif diff --git a/src/xrt/ipc/server/ipc_server_handler.c b/src/xrt/ipc/server/ipc_server_handler.c index a49ca1acc..704523a83 100644 --- a/src/xrt/ipc/server/ipc_server_handler.c +++ b/src/xrt/ipc/server/ipc_server_handler.c @@ -6678,6 +6678,8 @@ ipc_handle_weave_submit(volatile struct ipc_client_state *ics, #ifdef XRT_OS_LINUX_DESKTOP // v13: per submit — a submit without XrWeaveSubmitMonoIn2DDXR clears it. comp_multi_weave_linux_set_mono_in_2d(ics->xc, args->mono_in_2d != 0); + // v14/v15: per submit, read only when the DP composites the overlay. + comp_multi_weave_linux_set_overlay_hints(ics->xc, args->overlay_unchanged != 0, args->overlay_filter_strength); #endif bool ok = comp_multi_weave_submit( // ics->xc, handles[0], // @@ -6995,6 +6997,8 @@ ipc_handle_weave_submit_dmabuf(volatile struct ipc_client_state *ics, struct xrt_weave_woven_origin woven = {0}; // v13: per submit — a submit without XrWeaveSubmitMonoIn2DDXR clears it. comp_multi_weave_linux_set_mono_in_2d(ics->xc, args->mono_in_2d != 0); + // v14/v15: per submit, read only when the DP composites the overlay. + comp_multi_weave_linux_set_overlay_hints(ics->xc, args->overlay_unchanged != 0, args->overlay_filter_strength); bool ok = comp_multi_weave_submit_dmabuf( // ics->xc, &in_desc, // args->have_overlay ? &overlay_desc : NULL, // From e91656aee81c73dcc75d2aa5f2e60227e27a2854 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 4 Oct 2026 21:42:21 -0700 Subject: [PATCH 2/2] test(vk dp): pin the set_overlay_2d append + struct_size gate; lazy-transparency size check follows the append MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests_comp_lazy_transparency pinned the VK variant's total size at 15 appended slots, which the set_overlay_2d append (ADR-020) correctly grows to 17 — it now checks that the next slot follows set_transparency_active directly. tests_comp_overlay_2d_vk pins both new slots' offsets and that a plug-in whose struct_size ends before a slot is never called through it. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/CMakeLists.txt | 1 + tests/tests_comp_lazy_transparency.cpp | 4 +- tests/tests_comp_overlay_2d_vk.cpp | 150 +++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 tests/tests_comp_overlay_2d_vk.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9ab14e607..0b5829ce4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,7 @@ set(tests tests_aux_display_mode_hold tests_comp_rear_budget tests_comp_lazy_transparency + tests_comp_overlay_2d_vk tests_oxr_weave_latch tests_oxr_layer_space tests_oxr_view_config_rule diff --git a/tests/tests_comp_lazy_transparency.cpp b/tests/tests_comp_lazy_transparency.cpp index 7938e8470..945a15ccc 100644 --- a/tests/tests_comp_lazy_transparency.cpp +++ b/tests/tests_comp_lazy_transparency.cpp @@ -60,7 +60,9 @@ size_without_slot() TEST_CASE("lazy_transparency: the slot is appended, not inserted") { CHECK(size_without_slot() == sizeof(struct xrt_display_processor) + 14 * sizeof(void *)); - CHECK(sizeof(struct xrt_display_processor_vk) == sizeof(struct xrt_display_processor) + 15 * sizeof(void *)); + // Later appends (set_overlay_2d, ADR-027 Amendment) follow it directly. + CHECK(offsetof(struct xrt_display_processor_vk, set_overlay_2d) == + sizeof(struct xrt_display_processor) + 15 * sizeof(void *)); } TEST_CASE("lazy_transparency: an older plug-in is unsupported and never called") diff --git a/tests/tests_comp_overlay_2d_vk.cpp b/tests/tests_comp_overlay_2d_vk.cpp new file mode 100644 index 000000000..e1eb0a097 --- /dev/null +++ b/tests/tests_comp_overlay_2d_vk.cpp @@ -0,0 +1,150 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief The Vulkan DP's 2D over-layer slots (ADR-027 Amendment, "2D under + * the lens"): append position and the struct_size back-compat gate. + * + * An older plug-in's `base.struct_size` ends before `set_overlay_2d`; the bytes + * past it belong to the plug-in. The runtime must read "unsupported" — never + * call through them — and keep compositing the layer post-weave itself. + */ + +#include "catch_amalgamated.hpp" + +#include "xrt/xrt_display_processor_vk.h" + +#include + +namespace { + +int g_overlay_calls = 0; +int g_strength_calls = 0; +float g_last_strength = 0.0f; +bool g_last_unchanged = false; +bool g_accept = true; + +bool +fake_set_overlay(struct xrt_display_processor_vk *xdp, + VkImageView view, + VkFormat_XDP format, + uint32_t width, + uint32_t height, + enum xrt_atlas_encoding encoding, + bool layer_unchanged) +{ + (void)xdp; + (void)view; + (void)format; + (void)width; + (void)height; + (void)encoding; + g_overlay_calls++; + g_last_unchanged = layer_unchanged; + return g_accept; +} + +void +fake_set_strength(struct xrt_display_processor_vk *xdp, float strength) +{ + (void)xdp; + g_strength_calls++; + g_last_strength = strength; +} + +struct xrt_display_processor_vk +make_dp(uint32_t struct_size, bool with_fns) +{ + struct xrt_display_processor_vk dp; + std::memset(&dp, 0, sizeof(dp)); + dp.base.struct_size = struct_size; + dp.set_overlay_2d = with_fns ? fake_set_overlay : nullptr; + dp.set_overlay_2d_filter_strength = with_fns ? fake_set_strength : nullptr; + return dp; +} + +void +reset() +{ + g_overlay_calls = 0; + g_strength_calls = 0; + g_last_strength = 0.0f; + g_last_unchanged = false; + g_accept = true; +} + +// A non-null handle value; the fakes never dereference it. +VkImageView +some_view() +{ + VkImageView v; + std::memset(&v, 0x5a, sizeof(v)); + return v; +} + +} // namespace + +TEST_CASE("overlay_2d_vk: both slots are appended after set_transparency_active") +{ + const size_t base = sizeof(struct xrt_display_processor); + CHECK(offsetof(struct xrt_display_processor_vk, set_overlay_2d) == base + 15 * sizeof(void *)); + CHECK(offsetof(struct xrt_display_processor_vk, set_overlay_2d_filter_strength) == base + 16 * sizeof(void *)); + CHECK(sizeof(struct xrt_display_processor_vk) == base + 17 * sizeof(void *)); +} + +TEST_CASE("overlay_2d_vk: an older plug-in is unsupported and never called") +{ + reset(); + // Real function pointers sit behind struct_size: a NULL-only check would call them. + struct xrt_display_processor_vk dp = + make_dp((uint32_t)offsetof(struct xrt_display_processor_vk, set_overlay_2d), /*with_fns=*/true); + CHECK_FALSE(xrt_display_processor_vk_supports_overlay_2d(&dp)); + CHECK_FALSE( + xrt_display_processor_vk_set_overlay_2d(&dp, some_view(), 44, 16, 16, XRT_ATLAS_ENCODING_ENCODED, false)); + xrt_display_processor_vk_set_overlay_2d_filter_strength(&dp, 0.5f); + CHECK(g_overlay_calls == 0); + CHECK(g_strength_calls == 0); +} + +TEST_CASE("overlay_2d_vk: a plug-in with the overlay slot but not the strength slot") +{ + reset(); + struct xrt_display_processor_vk dp = make_dp( + (uint32_t)offsetof(struct xrt_display_processor_vk, set_overlay_2d_filter_strength), /*with_fns=*/true); + CHECK(xrt_display_processor_vk_supports_overlay_2d(&dp)); + xrt_display_processor_vk_set_overlay_2d_filter_strength(&dp, 0.5f); + CHECK(g_strength_calls == 0); + CHECK(xrt_display_processor_vk_set_overlay_2d(&dp, some_view(), 44, 16, 16, XRT_ATLAS_ENCODING_ENCODED, true)); + CHECK(g_overlay_calls == 1); + CHECK(g_last_unchanged); +} + +TEST_CASE("overlay_2d_vk: NULL slots or NULL DP are unsupported") +{ + reset(); + struct xrt_display_processor_vk dp = make_dp(sizeof(struct xrt_display_processor_vk), /*with_fns=*/false); + CHECK_FALSE(xrt_display_processor_vk_supports_overlay_2d(&dp)); + CHECK_FALSE( + xrt_display_processor_vk_set_overlay_2d(&dp, some_view(), 44, 16, 16, XRT_ATLAS_ENCODING_ENCODED, false)); + xrt_display_processor_vk_set_overlay_2d_filter_strength(&dp, 0.5f); + CHECK_FALSE(xrt_display_processor_vk_supports_overlay_2d(nullptr)); + CHECK_FALSE(xrt_display_processor_vk_set_overlay_2d(nullptr, some_view(), 44, 16, 16, + XRT_ATLAS_ENCODING_ENCODED, false)); + xrt_display_processor_vk_set_overlay_2d_filter_strength(nullptr, 0.5f); + CHECK(g_overlay_calls == 0); + CHECK(g_strength_calls == 0); +} + +TEST_CASE("overlay_2d_vk: a current plug-in's verdict is passed through") +{ + reset(); + struct xrt_display_processor_vk dp = make_dp(sizeof(struct xrt_display_processor_vk), /*with_fns=*/true); + xrt_display_processor_vk_set_overlay_2d_filter_strength(&dp, -1.0f); + CHECK(g_strength_calls == 1); + CHECK(g_last_strength == -1.0f); + CHECK(xrt_display_processor_vk_set_overlay_2d(&dp, some_view(), 44, 16, 16, XRT_ATLAS_ENCODING_ENCODED, false)); + g_accept = false; // a DP that declines: the runtime blends the layer itself + CHECK_FALSE( + xrt_display_processor_vk_set_overlay_2d(&dp, some_view(), 44, 16, 16, XRT_ATLAS_ENCODING_ENCODED, false)); + CHECK(g_overlay_calls == 2); +}