diff --git a/docs/README.md b/docs/README.md index 55215c7492..f28f8e4f3e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,6 +39,7 @@ Build apps for 3D displays using the OpenXR standard. - [XR_DXR_depth_budget](specs/extensions/XR_DXR_depth_budget.md) — rear depth budget: how far behind the display plane a transparent app may render (ADR-040) - [XR_EXT_view_configuration_views_change](specs/extensions/XR_EXT_view_configuration_views_change.md) — **Khronos** extension: adoption note for the live-`recommendedImageRect*` doorbell (move `subImage.imageRect`, never reallocate; kill switches; the two bespoke zone events it soft-deprecates) - [Kooima Projection](architecture/kooima-projection.md) — N-view Kooima math and projection pipelines +- [Window Segments](architecture/comp-segments.md) — a window spanning screens, woven per screen by each screen's own DP (multi-screen M2) --- diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md new file mode 100644 index 0000000000..cd4e663d71 --- /dev/null +++ b/docs/architecture/comp-segments.md @@ -0,0 +1,158 @@ +# Window segments: one window, one DP per screen + +*How the compositor weaves a window that spans several screens (multi-screen M2). +Companion to ADR-047 (`docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md`, lands with PR #1849) +decision D2 and the multi-screen plan (`docs/roadmap/multi-screen.md`, lands with PR #1849). +Code: `src/xrt/compositor/util/comp_segments.{h,c}` (backend-agnostic math and +policy, unit-tested by `tests/tests_comp_segments.cpp`) and +`src/xrt/compositor/vk_native/comp_vk_native_segments.{h,c}` (the Vulkan +in-process compositor's use of it).* + +## What a segment is + +A **screen** is an OS monitor in the per-monitor DP registry; each one is won by +a plug-in, which can make a display processor (DP) for it. A **segment** is the +part of a window's canvas that lands on one screen: + +``` +segment = canvas ∩ screen +``` + +Each segment has: + +| field | meaning | +|---|---| +| `window_rect` | the segment in **window pixels** — the canvas handed to `process_atlas` | +| `screen_rect` | the segment in **that screen's pixels** — where it lands on the panel | +| `present_origin` | the window's origin relative to that screen (`set_present_origin`; may be negative). The DP adds the canvas offset: `present_origin + window_rect.origin = screen_rect.origin`, the phase input | +| `screen_1to1` | whether window pixels reach that screen unresampled (desktop rect == device mode), or unknown | + +The table is ordered left to right by desktop position (the order M3's +per-segment views will use). The rect math is `u_multi_display_compute_slices`, +half-open, so a window flush to a seam belongs to exactly one screen. + +## Who weaves what + +- The **primary screen** is the system-default screen, the one the session's own + DP (`comp_vk_native_compositor::display_processor`) was made for. Its segment is + woven by that DP, which also keeps owning everything view-related: eye positions, + window metrics, Kooima. M2 renders **one view pair** for the whole window; per-segment + views are M3. +- Every **other screen** gets a segment DP from that screen's registry factory: + `xrt_plugin_iface::create_dp_vk_for_screen` with an `xrt_screen_binding` + (monitor id, desktop rect, native px, mm, serial) — only that slot: a plug-in + without it gets a flat 2D view on its other screens, never a plain `create_dp_vk` + DP (which would describe the wrong panel and may clear the whole target). It is + created against the #868 runtime-owned queue, like the primary. + It is windowless (NULL window): its phase is `set_present_origin`, fed per frame, + ADR-033. A plug-in's DP made for a screen describes that screen + (`get_display_dimensions` / `get_display_pixel_info`). +- A window entirely on the primary screen — the common case — never leaves the + single-DP path: `comp_segments_table_is_split()` is false and the frame is + byte-for-byte what shipped before. + +## The frame (split path) + +`comp_vk_native_segments_record()`, called where the single-DP `process_atlas` +was: + +1. **Clear** the target (whatever no segment covers would otherwise be undefined). +2. **Crop per segment.** The atlas holds the whole canvas; each woven segment gets + its own image with exactly its views (each tile cut to the segment, edges rounded + independently so neighbours share the seam column). Crop before the DP is the + law (ADR-030): a DP's atlas holds exactly its canvas. +3. **Weave**, primary first, then left to right. Each DP gets `canvas = segment`; + between two DPs the target goes `PRESENT_SRC_KHR → COLOR_ATTACHMENT_OPTIMAL` + with its contents preserved. +4. **Flat 2D** for every segment that cannot be woven and for canvas no segment + covers: one view (the middle one, the left eye of a stereo pair), scaled from the + view tile to the window rect — the same content and seam registration as the + shipped #1654 band. + +It leaves the target in `PRESENT_SRC_KHR`, exactly as the single-DP path does, so +the Local2D composite, the HUD and the present after it are unchanged. + +## Who sets viewport and scissor + +The **DP**, from the canvas. A Vulkan DP records its own render pass, so the +compositor cannot set dynamic state inside it; what it guarantees is the canvas +argument, the cropped atlas and the target layout (`COLOR_ATTACHMENT_OPTIMAL` on +entry). The contract a segment DP must honour (`create_dp_vk_for_screen` doc): + +- **render area, viewport AND scissor = the canvas.** A windowless weaver's default + scissor is its panel rect, so a backbuffer wider than the panel is clipped unless + both are set (multi-screen plan, risk 7); +- **not the first writer**: preserve the target outside the canvas. + +sim_display implements both (a screen-bound DP uses a render-pass variant starting +in `COLOR_ATTACHMENT_OPTIMAL` with the clear confined to the render area). A DP +built before M2 may still clear its whole target, which is why the primary runs +first. + +**sim_display behaviour change for every caller, on purpose.** sim's viewport and +scissor now follow the canvas for the unbound (primary) DP too — the split path +needs it, and it is what the `process_atlas` contract always said. It is identical +to before whenever the canvas is the whole target (every single-screen in-process +frame); it differs when the canvas is smaller — the one-or-two-frame window-shrink +transient before the swapchain is recreated, the shared-texture path, and a +`comp_multi` zone canvas — where sim used to stretch the canvas-sized atlas across +the whole target. The interlace phase does NOT change for the unbound DP: it stays +`canvas_offset_x`; only a screen-bound segment DP takes its phase from +`set_present_origin`. + +## The 1:1 policy + +A lenticular weave is a ~1-pixel-period pattern: if the display server resamples +the window on its way to the panel, every view leaks into its neighbour +(#1595, #1831 — refuse rather than resample). Anaglyph, side-by-side and blend do +not care. So the gate is per segment and per DP capability +(`comp_segments_decide`): + +| DP for the segment | screen 1:1 | rendered | +|---|---|---| +| none | — | flat 2D | +| needs 1:1 | no | flat 2D | +| needs 1:1 | yes / unknown | woven | +| tolerates a resample | any | woven | + +"Unknown never degrades." The capability is `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE` +in `xrt_dp_scanout_caps::flags`, read through the existing `get_scanout_caps` slot +(the flags word was `reserved[0]`, so every older plug-in reads as "needs 1:1"). +The primary segment keeps the session-level gates, which now also let a tolerant +primary DP weave (the X11 gate of #1831 and the resample reasons of the Wayland +gate of #1595). On `ds1-linux` both monitors are resampled under XWayland, which is +why M2 could not be shown there without this. + +## Lifecycle + +Segment DPs follow the table with hysteresis (`comp_segments_lifecycle`): created +once a segment has existed for 2 consecutive metric updates, destroyed once it has +been gone for 30 (~0.5 s at 60 Hz), so a drag back and forth across a seam never +rebuilds DPs. A failed create is not retried until the segment goes away. Until a +DP exists the segment is flat 2D. A table change is logged once at INFO +(`segments: window … -> N segment(s); [i] screen … canvas … origin …`); a DP create +or destroy is one WARN. + +## Scope and switches + +- In-process Vulkan compositor, desktop Linux, **X11/XWayland**: root coordinates + are desktop-absolute, the same space as the registry's RandR rects. Native Wayland + stays primary-only — the geometry payload does not describe the other monitors + (`docs/specs/runtime/wayland-window-geometry.md` §5). +- **One vendor**: all screens must belong to the primary screen's plug-in. A + mixed layout (a Leia DS1 next to a sim_display laptop panel) keeps the single-DP + path; mixing vendors is M4. +- Not segmented: zero-copy frames, a self-submitting DP or one without a render + pass, a session pinned with `XrSessionDisplayBindingDXR`, the shared-texture path. +- `DXR_SEGMENTS=0` turns it off. +- Capture: with the window split, the post-compose atlas capture also writes each + segment's DP input as `.seg.png`. + +## What M3 adds + +Per-segment **views**: `xrLocateViews` returns a view pair per segment (eyes from +that segment's DP, Kooima from the segment rect relative to its own screen), +`XrViewDisplayBindingsDXR` tells the app which views belong to which segment, and the +compositor routes each segment's tiles to its DP instead of cropping one shared +pair. The segment table, the lifecycle and the per-segment DPs here are what it +builds on. diff --git a/docs/reference/xrt_plugin_iface.md b/docs/reference/xrt_plugin_iface.md index 94a534cb3c..9d0807c498 100644 --- a/docs/reference/xrt_plugin_iface.md +++ b/docs/reference/xrt_plugin_iface.md @@ -301,6 +301,56 @@ append. Reference implementation: `sim_display_plugin.c` (EDID mm, else its `SIM_DISPLAY_WIDTH_M/HEIGHT_M` defaults; its usual nominal viewer; `SIM_DISPLAY_FAKE_TRACKING` semantics unchanged). +### `create_dp_vk_for_screen` (multi-screen M2) + +```c +xrt_result_t (*create_dp_vk_for_screen)(struct xrt_plugin_instance *inst, + void *vk_bundle, + void *vk_cmd_pool, + void *window_handle, + int32_t target_format, + const struct xrt_screen_binding *binding, + struct xrt_display_processor **out_xdp); +``` + +Make a Vulkan DP for ONE screen your plug-in won. A window that spans screens is woven per +**segment** (canvas ∩ screen, [`comp-segments.md`](../architecture/comp-segments.md)); the +session's own DP keeps the system-default screen's segment and this slot makes the DP for each +other screen. Same contract as `create_dp_vk`, plus: + +- `binding` names the screen: monitor id, desktop rect, native (device-mode) pixels, EDID mm, + desktop scale, your claim's serial, a vendor display id (0 until SR-P1/P2 supply one), the + output name. Clamp reads to `binding->struct_size`. Answer `get_display_dimensions` / + `get_display_pixel_info` for THAT screen, with the screen origin = the binding's desktop + origin. +- `window_handle` is NULL on desktop Linux: a segment DP is windowless, its phase comes from + `set_present_origin` (ADR-033) — the window's origin relative to THIS screen, to which you add + the canvas offset. +- Each `process_atlas` hands you `canvas = the segment` (a sub-rect of the target) and an atlas + that holds exactly that segment's views (the runtime crops per segment — crop before the DP is + the law). **Confine render area, viewport AND scissor to the canvas** (a windowless weaver's + default scissor is its panel rect — multi-screen plan risk 7), and **do not assume you are the + first writer** to the target this frame: the target arrives in `COLOR_ATTACHMENT_OPTIMAL` + holding other segments' pixels; leave it in `PRESENT_SRC_KHR` like any VK DP. +- Several instances coexist in one process, one per screen. + +NULL (or an older `struct_size`) → your screens other than the session's primary one get a +flat 2D view. There is deliberately no fallback to `create_dp_vk`: that DP describes your one +panel, not the screen, and nothing binds it to the canvas / not-first-writer contract above, so +it could clear the segment the primary DP just wove. +Appended after `get_display_info_for_monitor` per ADR-020 (`struct_size`-gated, no +`XRT_PLUGIN_API_VERSION_CURRENT` bump; ABI stays 5); guard with +`#ifdef XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN`. M2 only segments a window whose screens all +belong to the ACTIVE plug-in; mixed vendors are M4. Reference implementation: +`sim_display_dp_factory_vk_for_screen` (`drivers/sim_display/sim_display_processor.c`). + +**The 1:1 capability.** Report through the existing `get_scanout_caps` slot whether your output +survives a display-server resample: `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE` in +`xrt_dp_scanout_caps::flags` (carved from the former `reserved[0]`, so an older plug-in reads +as "needs 1:1"). A lenticular weave leaves it clear; the runtime then paints flat 2D on a +segment whose screen is resampled, and the #1595 / #1831 session gates degrade as before. +sim_display sets it for every output but INTERLACED. + ### `set_pose_source` ```c @@ -684,7 +734,8 @@ Flags (`XRT_PLUGIN_STEREO_CAMERA_*`) mirror the XR bits: `SHARED_WITH_EYE_TRACKI **Slot order.** The six slots are the LAST members of the iface, appended (ADR-020) after everything that was on `main` before them: `… vk_bundle_fn_table_offset` → `create_dp_d3d11_lift` (ADR-042) → `get_platform_state` (ADR-045) → `stereo_camera_enumerate … stereo_camera_close` -→ `get_display_info_for_monitor` (multi-screen M1, now the last member). +→ `get_display_info_for_monitor` (multi-screen M1) → `create_dp_vk_for_screen` (multi-screen M2, +now the last member). `tests_stereo_camera` pins that order (lift right after the vk fingerprint, platform-state right after lift, the camera block right after platform-state and ending the struct), so a reorder or a slot squeezed in between fails on the host, not on a vendor box. diff --git a/docs/roadmap/linux-support.md b/docs/roadmap/linux-support.md index 348809c366..6d2477c2a0 100644 --- a/docs/roadmap/linux-support.md +++ b/docs/roadmap/linux-support.md @@ -807,6 +807,24 @@ source comments and, until this section, in no document at all. overlay, and a window entirely off the panel skips the weave; kill switch `DXR_SPAN_2D=0` (either name works on both paths), headless proof `weave_probe_vk_linux --span2d`. + **Multi-screen M2 (X11/XWayland, in-process Vulkan) replaces the band with a + real segment wherever the other monitor has a display processor.** A window + spanning screens is split into segments (`canvas ∩ screen`, + `util/comp_segments.h`); each segment is woven by its own screen's DP + (created from that screen's registry factory, `create_dp_vk_for_screen`) + with `canvas = segment`, a pre-cropped atlas and its own present origin, and + composited into the one surface (`comp_vk_native_segments.c`). Only a + segment with no DP (a monitor no plug-in claimed, a mixed-vendor layout — + that is M4) or whose DP needs 1:1 pixels on a resampled screen keeps the flat + 2D view. sim_display's anaglyph tolerates a resample, so with + `DXR_PLUGIN_EXCLUSIVE=sim-display` a window across eDP-1 and the DS1 is + anaglyph on both halves. The window entirely on the primary screen is the + shipped single-DP path, unchanged. Native Wayland keeps the band until the + geometry service publishes every monitor + (`docs/specs/runtime/wayland-window-geometry.md` §5); the X11 1:1 gate + (#1831) and the Wayland resample reasons of #1595 no longer degrade a DP + that declares `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE`. Kill switch + `DXR_SEGMENTS=0`. Design: `docs/architecture/comp-segments.md`. - **Flat regions on the weave service are painted flat, not wished (spec v8, browser#88).** The caller's flat regions — each submit's `XrWeaveSubmitFlatRegionsDXR` plus the sticky screen-space diff --git a/docs/specs/runtime/wayland-window-geometry.md b/docs/specs/runtime/wayland-window-geometry.md index af6fba447d..7a1294db18 100644 --- a/docs/specs/runtime/wayland-window-geometry.md +++ b/docs/specs/runtime/wayland-window-geometry.md @@ -366,6 +366,40 @@ is what lets any package ship it and any runtime consume it. - Mutter emits geometry transactionally with its own redraw, so tracking during interactive drags is expected to be at least as good as the X11 per-frame poll; validate visually (phase lock while dragging). +- **Multi-screen segments need every monitor, not just the window's own + (multi-screen M2 follow-up, ADR-047 D2).** M2 weaves a window that spans + screens per segment — `canvas ∩ screen`, each by that screen's display + processor with its own present origin — on X11/XWayland, where root + coordinates are desktop-absolute and RandR gives every monitor's rect in the + same space (`docs/architecture/comp-segments.md`). Native Wayland stays on + the primary-segment-only behaviour (#1654 bands), because this payload cannot + place the second segment: + - `monitor` describes only the window's OWN monitor. The rect of the part of + the window that lands on another monitor, in THAT monitor's device px, + needs that monitor's stage rect and its own `device_scale` — a + mixed-scale layout (this box: 1.6667 laptop + 2.0 DS1) makes a single + factor wrong for one of the two. + - The runtime's screen registry is keyed by connector / RandR output name, + and the payload names no connector, so even the window's own monitor is + matched only by its device size today (`wr.monitor_width_px == + panel_px_w`), which is ambiguous for two identical panels. + + **Required extension change** (not installed on any dev box; the runtime + keeps refusing until a publisher announces it): add a top-level + `monitors: [{connector, x, y, w, h, scale, device_scale}]` array — every + monitor in stage coordinates, `connector` = the DRM connector / Mutter + monitor connector (`Meta.MonitorManager` → `get_monitor_for_connector`, + e.g. `"HDMI-1"`, which is what the registry's `device_name` holds) — plus + `monitor.connector` on each window. Additive, so the schema stays + `version: 1` and an older consumer ignores it; bump the extension version + so `org.displayxr.WindowGeometry1` capability probing can tell. The consumer + then intersects the window's `buffer` rect with each monitor in stage + coordinates, converts each piece to that monitor's device px with that + monitor's `device_scale`, and hands `comp_segments` a segment table in + which every rect is already per screen. Note that Mutter paints a straddling + surface at ONE buffer scale (the window's main monitor's), so the part on + the other monitor is resampled: the per-segment 1:1 policy applies (a + resample-tolerant DP weaves it, a lenticular weave gets flat 2D). ## 6. Capture exclusion — `org.displayxr.CaptureExclusion1` (extension version 2) diff --git a/src/xrt/compositor/CMakeLists.txt b/src/xrt/compositor/CMakeLists.txt index a151e10634..fe164f1a31 100644 --- a/src/xrt/compositor/CMakeLists.txt +++ b/src/xrt/compositor/CMakeLists.txt @@ -189,6 +189,8 @@ if(XRT_HAVE_VULKAN) util/comp_render_helpers.h util/comp_scratch.c util/comp_scratch.h + util/comp_segments.c + util/comp_segments.h util/comp_semaphore.h util/comp_semaphore.c util/comp_swapchain.h diff --git a/src/xrt/compositor/util/comp_segments.c b/src/xrt/compositor/util/comp_segments.c new file mode 100644 index 0000000000..19250c9963 --- /dev/null +++ b/src/xrt/compositor/util/comp_segments.c @@ -0,0 +1,513 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Window segments (multi-screen M2) — see comp_segments.h. + * @ingroup comp_util + */ + +#include "util/comp_segments.h" + +#include "util/u_multi_display.h" + +#include +#include + + +/* + * + * Segment table. + * + */ + +enum comp_seg_1to1 +comp_segments_screen_1to1(const struct comp_segments_screen *s) +{ + if (s == NULL || s->native_w == 0 || s->native_h == 0 || s->desktop.w == 0 || s->desktop.h == 0) { + return COMP_SEG_1TO1_UNKNOWN; + } + return (s->native_w == s->desktop.w && s->native_h == s->desktop.h) ? COMP_SEG_1TO1_YES : COMP_SEG_1TO1_NO; +} + +static bool +seg_before(const struct comp_segment *a, const struct comp_segment *b, const struct comp_seg_rect *window_desktop) +{ + // Desktop position of each segment = window origin + window-px offset. + const int64_t ax = (int64_t)window_desktop->x + a->window_rect.x; + const int64_t bx = (int64_t)window_desktop->x + b->window_rect.x; + if (ax != bx) { + return ax < bx; + } + return a->window_rect.y < b->window_rect.y; +} + +uint32_t +comp_segments_compute(const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + const struct comp_segments_screen *screens, + uint32_t screen_count, + struct comp_segment_table *out) +{ + if (out == NULL) { + return 0; + } + memset(out, 0, sizeof(*out)); + if (window_desktop == NULL || screens == NULL || screen_count == 0 || window_desktop->w == 0 || + window_desktop->h == 0) { + return 0; + } + if (screen_count > COMP_SEGMENTS_MAX_SCREENS) { + screen_count = COMP_SEGMENTS_MAX_SCREENS; + } + + // The canvas inside the window, window px. Zero = the whole window. + struct comp_seg_rect cv = {0, 0, window_desktop->w, window_desktop->h}; + if (canvas != NULL && canvas->w > 0 && canvas->h > 0) { + cv = *canvas; + } + + // The canvas on the desktop, as the slice math's half-open rect. + const struct u_md_rect canvas_desktop = { + .left = window_desktop->x + cv.x, + .top = window_desktop->y + cv.y, + .right = window_desktop->x + cv.x + (int32_t)cv.w, + .bottom = window_desktop->y + cv.y + (int32_t)cv.h, + }; + + /* + * Mirrored outputs (two screens with the SAME desktop rect) would yield + * two overlapping segments for one region. Keep one per rect: the primary + * if it is among them, else the first listed. map[] takes a slice's + * monitor index back to the caller's screen index. + */ + struct u_md_rect monitors[COMP_SEGMENTS_MAX_SCREENS]; + uint32_t map[COMP_SEGMENTS_MAX_SCREENS]; + uint32_t mon_count = 0; + for (uint32_t i = 0; i < screen_count; i++) { + const struct comp_seg_rect *d = &screens[i].desktop; + const struct u_md_rect r = {d->x, d->y, d->x + (int32_t)d->w, d->y + (int32_t)d->h}; + uint32_t dup = UINT32_MAX; + for (uint32_t j = 0; j < mon_count; j++) { + if (monitors[j].left == r.left && monitors[j].top == r.top && monitors[j].right == r.right && + monitors[j].bottom == r.bottom) { + dup = j; + break; + } + } + if (dup == UINT32_MAX) { + monitors[mon_count] = r; + map[mon_count] = i; + mon_count++; + } else if (screens[i].is_primary && !screens[map[dup]].is_primary) { + map[dup] = i; // the primary wins a mirror + } + } + + struct u_md_slice slices[U_MD_MAX_SLICES]; + const uint32_t n = u_multi_display_compute_slices(canvas_desktop, monitors, mon_count, slices, + U_MD_MAX_SLICES < COMP_SEGMENTS_MAX ? U_MD_MAX_SLICES + : COMP_SEGMENTS_MAX); + + for (uint32_t i = 0; i < n; i++) { + const struct u_md_slice *s = &slices[i]; + const uint32_t si = map[s->monitor_index]; + const struct comp_segments_screen *scr = &screens[si]; + struct comp_segment *g = &out->seg[out->count++]; + g->screen_index = si; + g->screen_id = scr->id; + g->is_primary = scr->is_primary; + g->has_dp_factory = scr->has_dp_factory; + // atlas_x/y are relative to the CANVAS; the segment rect is window px. + g->window_rect.x = cv.x + s->atlas_x; + g->window_rect.y = cv.y + s->atlas_y; + g->window_rect.w = s->atlas_w; + g->window_rect.h = s->atlas_h; + g->screen_rect.x = s->canvas_offset_x; + g->screen_rect.y = s->canvas_offset_y; + g->screen_rect.w = s->atlas_w; + g->screen_rect.h = s->atlas_h; + g->present_origin_x = window_desktop->x - scr->desktop.x; + g->present_origin_y = window_desktop->y - scr->desktop.y; + g->screen_1to1 = comp_segments_screen_1to1(scr); + } + + // Left to right (insertion sort; at most COMP_SEGMENTS_MAX entries). + for (uint32_t i = 1; i < out->count; i++) { + struct comp_segment key = out->seg[i]; + uint32_t j = i; + while (j > 0 && seg_before(&key, &out->seg[j - 1], window_desktop)) { + out->seg[j] = out->seg[j - 1]; + j--; + } + out->seg[j] = key; + } + return out->count; +} + +bool +comp_segments_table_is_split(const struct comp_segment_table *t, uint32_t canvas_w, uint32_t canvas_h) +{ + if (t == NULL || t->count == 0) { + // On no screen at all: nothing to split; the single-DP path stands. + return false; + } + if (t->count > 1) { + return true; + } + const struct comp_segment *g = &t->seg[0]; + if (!g->is_primary) { + return true; + } + // One primary segment. Clipped by the screen edge (window hanging off the + // desktop) is still the single-DP case: the off-desktop part is invisible, + // and this is exactly what shipped. So only a non-primary screen splits. + (void)canvas_w; + (void)canvas_h; + return false; +} + +enum comp_seg_render +comp_segments_decide(bool have_dp, bool dp_tolerates_resample, enum comp_seg_1to1 screen_1to1) +{ + if (!have_dp) { + return COMP_SEG_RENDER_FLAT_2D; + } + if (screen_1to1 == COMP_SEG_1TO1_NO && !dp_tolerates_resample) { + return COMP_SEG_RENDER_FLAT_2D; + } + return COMP_SEG_RENDER_WEAVE; +} + +static void +sort_i32(int32_t *v, uint32_t n) +{ + for (uint32_t i = 1; i < n; i++) { + int32_t k = v[i]; + uint32_t j = i; + while (j > 0 && v[j - 1] > k) { + v[j] = v[j - 1]; + j--; + } + v[j] = k; + } +} + +uint32_t +comp_segments_uncovered(const struct comp_segment_table *t, + const struct comp_seg_rect *canvas, + struct comp_seg_rect *out, + uint32_t cap) +{ + if (canvas == NULL || out == NULL || cap == 0 || canvas->w == 0 || canvas->h == 0) { + return 0; + } + const int32_t cx0 = canvas->x, cy0 = canvas->y; + const int32_t cx1 = canvas->x + (int32_t)canvas->w, cy1 = canvas->y + (int32_t)canvas->h; + const uint32_t n = t != NULL ? t->count : 0; + + // Row bands at every segment's top/bottom edge. + int32_t ys[2 * COMP_SEGMENTS_MAX + 2]; + uint32_t yn = 0; + ys[yn++] = cy0; + ys[yn++] = cy1; + for (uint32_t i = 0; i < n; i++) { + const struct comp_seg_rect *r = &t->seg[i].window_rect; + if (r->y > cy0 && r->y < cy1) { + ys[yn++] = r->y; + } + const int32_t b = r->y + (int32_t)r->h; + if (b > cy0 && b < cy1) { + ys[yn++] = b; + } + } + sort_i32(ys, yn); + + uint32_t count = 0; + for (uint32_t b = 0; b + 1 < yn; b++) { + const int32_t y0 = ys[b], y1 = ys[b + 1]; + if (y1 <= y0) { + continue; + } + // Segments spanning this band, as sorted x intervals. + int32_t xs0[COMP_SEGMENTS_MAX], xs1[COMP_SEGMENTS_MAX]; + uint32_t xn = 0; + for (uint32_t i = 0; i < n; i++) { + const struct comp_seg_rect *r = &t->seg[i].window_rect; + if (r->w == 0 || r->h == 0 || r->y > y0 || r->y + (int32_t)r->h < y1) { + continue; + } + uint32_t j = xn++; + while (j > 0 && xs0[j - 1] > r->x) { + xs0[j] = xs0[j - 1]; + xs1[j] = xs1[j - 1]; + j--; + } + xs0[j] = r->x; + xs1[j] = r->x + (int32_t)r->w; + } + int32_t x = cx0; + for (uint32_t i = 0; i <= xn; i++) { + const int32_t gap_end = i < xn ? (xs0[i] < cx1 ? xs0[i] : cx1) : cx1; + if (gap_end > x && count < cap) { + out[count++] = (struct comp_seg_rect){x, y0, (uint32_t)(gap_end - x), (uint32_t)(y1 - y0)}; + } + if (i < xn && xs1[i] > x) { + x = xs1[i]; + } + } + } + return count; +} + +bool +comp_segments_table_equal(const struct comp_segment_table *a, const struct comp_segment_table *b) +{ + if (a == NULL || b == NULL) { + return a == b; + } + if (a->count != b->count) { + return false; + } + for (uint32_t i = 0; i < a->count; i++) { + const struct comp_segment *x = &a->seg[i]; + const struct comp_segment *y = &b->seg[i]; + if (x->screen_id != y->screen_id || x->window_rect.x != y->window_rect.x || + x->window_rect.y != y->window_rect.y || x->window_rect.w != y->window_rect.w || + x->window_rect.h != y->window_rect.h || x->present_origin_x != y->present_origin_x || + x->present_origin_y != y->present_origin_y) { + return false; + } + } + return true; +} + +void +comp_segments_table_format(const struct comp_segment_table *t, char *buf, size_t size) +{ + if (buf == NULL || size == 0) { + return; + } + buf[0] = '\0'; + if (t == NULL) { + return; + } + size_t used = 0; + int w = snprintf(buf, size, "%u segment(s)", t->count); + if (w < 0) { + return; + } + used = (size_t)w < size ? (size_t)w : size - 1; + for (uint32_t i = 0; i < t->count && used + 1 < size; i++) { + const struct comp_segment *g = &t->seg[i]; + w = snprintf(buf + used, size - used, "; [%u] screen 0x%016llx%s canvas %d,%d %ux%u origin %d,%d 1:1=%s", i, + (unsigned long long)g->screen_id, g->is_primary ? " (primary)" : "", g->window_rect.x, + g->window_rect.y, g->window_rect.w, g->window_rect.h, g->present_origin_x, + g->present_origin_y, + g->screen_1to1 == COMP_SEG_1TO1_YES ? "yes" + : g->screen_1to1 == COMP_SEG_1TO1_NO ? "no" + : "unknown"); + if (w < 0) { + return; + } + used += (size_t)w; + if (used >= size) { + used = size - 1; + } + } +} + + +/* + * + * Lifecycle. + * + */ + +void +comp_segments_lifecycle_init(struct comp_segments_lifecycle *lc, uint32_t create_after, uint32_t destroy_after) +{ + if (lc == NULL) { + return; + } + memset(lc, 0, sizeof(*lc)); + lc->create_after = create_after > 0 ? create_after : COMP_SEGMENTS_DEFAULT_CREATE_AFTER; + lc->destroy_after = destroy_after > 0 ? destroy_after : COMP_SEGMENTS_DEFAULT_DESTROY_AFTER; +} + +static struct comp_segments_slot * +lc_find(struct comp_segments_lifecycle *lc, uint64_t id, bool add) +{ + struct comp_segments_slot *free_slot = NULL; + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + struct comp_segments_slot *s = &lc->slots[i]; + if (s->used && s->screen_id == id) { + return s; + } + if (!s->used && free_slot == NULL) { + free_slot = s; + } + } + if (!add || free_slot == NULL) { + return NULL; + } + memset(free_slot, 0, sizeof(*free_slot)); + free_slot->used = true; + free_slot->screen_id = id; + return free_slot; +} + +static void +actions_push(struct comp_segments_actions *out, uint64_t id, enum comp_seg_action a) +{ + if (out->count < COMP_SEGMENTS_MAX_SCREENS) { + out->items[out->count].screen_id = id; + out->items[out->count].action = a; + out->count++; + } +} + +void +comp_segments_lifecycle_update(struct comp_segments_lifecycle *lc, + const struct comp_segment_table *t, + struct comp_segments_actions *out) +{ + if (out != NULL) { + memset(out, 0, sizeof(*out)); + } + if (lc == NULL || out == NULL) { + return; + } + + // Mark the screens with a non-empty, non-primary, DP-capable segment. + bool seen[COMP_SEGMENTS_MAX_SCREENS] = {false}; + if (t != NULL) { + for (uint32_t i = 0; i < t->count; i++) { + const struct comp_segment *g = &t->seg[i]; + if (g->is_primary || !g->has_dp_factory || g->window_rect.w == 0 || g->window_rect.h == 0) { + continue; + } + struct comp_segments_slot *s = lc_find(lc, g->screen_id, true); + if (s == NULL) { + continue; // out of slots: that segment stays 2D + } + seen[s - lc->slots] = true; + } + } + + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + struct comp_segments_slot *s = &lc->slots[i]; + if (!s->used) { + continue; + } + if (seen[i]) { + s->absent_streak = 0; + if (s->present_streak < UINT32_MAX) { + s->present_streak++; + } + if (!s->live && !s->failed && s->present_streak >= lc->create_after) { + actions_push(out, s->screen_id, COMP_SEG_ACTION_CREATE); + } + } else { + s->present_streak = 0; + s->failed = false; // gone: a later return may try again + if (s->absent_streak < UINT32_MAX) { + s->absent_streak++; + } + if (s->live && s->absent_streak >= lc->destroy_after) { + actions_push(out, s->screen_id, COMP_SEG_ACTION_DESTROY); + s->live = false; + } + if (!s->live) { + s->used = false; // nothing left to track + } + } + } +} + +void +comp_segments_lifecycle_set_created(struct comp_segments_lifecycle *lc, uint64_t screen_id, bool ok) +{ + if (lc == NULL) { + return; + } + struct comp_segments_slot *s = lc_find(lc, screen_id, false); + if (s == NULL) { + return; + } + s->live = ok; + s->failed = !ok; +} + +bool +comp_segments_lifecycle_is_live(const struct comp_segments_lifecycle *lc, uint64_t screen_id) +{ + if (lc == NULL) { + return false; + } + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + if (lc->slots[i].used && lc->slots[i].screen_id == screen_id) { + return lc->slots[i].live; + } + } + return false; +} + +void +comp_segments_lifecycle_drain(struct comp_segments_lifecycle *lc, struct comp_segments_actions *out) +{ + if (out != NULL) { + memset(out, 0, sizeof(*out)); + } + if (lc == NULL) { + return; + } + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + struct comp_segments_slot *s = &lc->slots[i]; + if (s->used && s->live && out != NULL) { + actions_push(out, s->screen_id, COMP_SEG_ACTION_DESTROY); + } + memset(s, 0, sizeof(*s)); + } +} + + +/* + * + * Deferred release. + * + */ + +bool +comp_segments_retire_push(struct comp_segments_retire *r, uint32_t kind, uint64_t item) +{ + if (r == NULL || r->count >= COMP_SEGMENTS_RETIRE_MAX) { + return false; + } + r->kinds[r->count] = kind; + r->items[r->count] = item; + r->count++; + return true; +} + +uint32_t +comp_segments_retire_take(struct comp_segments_retire *r, + bool safe, + uint32_t *out_kinds, + uint64_t *out_items, + uint32_t cap) +{ + if (r == NULL || !safe || out_kinds == NULL || out_items == NULL || cap == 0) { + return 0; + } + const uint32_t n = r->count < cap ? r->count : cap; + for (uint32_t i = 0; i < n; i++) { + out_kinds[i] = r->kinds[i]; + out_items[i] = r->items[i]; + } + for (uint32_t i = n; i < r->count; i++) { + r->kinds[i - n] = r->kinds[i]; + r->items[i - n] = r->items[i]; + } + r->count -= n; + return n; +} diff --git a/src/xrt/compositor/util/comp_segments.h b/src/xrt/compositor/util/comp_segments.h new file mode 100644 index 0000000000..5f9ae23e37 --- /dev/null +++ b/src/xrt/compositor/util/comp_segments.h @@ -0,0 +1,381 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Window segments: one window canvas split across the screens it + * covers, each piece woven by the display processor of its own screen + * (multi-screen M2, ADR-047 D2). + * @ingroup comp_util + * + * A **segment** is `canvas ∩ screen`: the part of a window's canvas that lands + * on one screen. Each segment gets its own display processor (created from + * that screen's registry factory), its own present origin (the window's origin + * relative to THAT screen) and its own canvas rect (the segment, in window + * pixels). The compositor hands each DP its segment and composites the results + * into the one surface it presents. Design note: `docs/architecture/comp-segments.md`. + * + * This file is the backend-agnostic half — pure integer geometry and policy, + * no graphics-API types, so it is unit-tested headless + * (`tests/tests_comp_segments.cpp`): + * + * - @ref comp_segments_compute — the segment table for one window placement. + * - @ref comp_segments_screen_1to1 / @ref comp_segments_decide — the per- + * segment refuse-rather-than-resample policy (weave, or a flat 2D blit). + * - @ref comp_segments_lifecycle — hysteresis for creating and destroying a + * segment's DP, so a window dragged across a seam never thrashes DPs. + * + * The rect math is @ref u_multi_display_compute_slices (half-open, so a seam + * belongs to exactly one screen). + */ + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//! Max screens the segment math considers (= XRT_SCREEN_LIST_MAX). +#define COMP_SEGMENTS_MAX_SCREENS 16 + +//! Max segments one window is split into (= U_MD_MAX_SLICES). +#define COMP_SEGMENTS_MAX 8 + +//! Size of @ref comp_segments_screen::plugin_id, incl. the NUL. +#define COMP_SEGMENTS_ID_MAX 64 + +//! Default hysteresis: metric updates a segment must exist before its DP is created. +#define COMP_SEGMENTS_DEFAULT_CREATE_AFTER 2 + +//! Default hysteresis: metric updates a segment must be gone before its DP is destroyed +//! (~0.5 s at 60 Hz). +#define COMP_SEGMENTS_DEFAULT_DESTROY_AFTER 30 + +/*! + * A rectangle: origin + size. Signed origin (screens sit at negative desktop + * coordinates; a window can hang off the left of a screen). + */ +struct comp_seg_rect +{ + int32_t x; + int32_t y; + uint32_t w; + uint32_t h; +}; + +/*! + * One screen as the segment math needs it — a projection of `xrt_screen`. + */ +struct comp_segments_screen +{ + //! Screen (registry monitor) id; never 0. + uint64_t id; + + //! Desktop rect, in the space the window position is read in (X11: the root). + struct comp_seg_rect desktop; + + //! The connector's device mode; 0 = unknown. + uint32_t native_w; + uint32_t native_h; + + //! The screen the session's primary display processor weaves (the system default). + bool is_primary; + + //! The screen's winning plug-in can make a VK display processor for it. + bool has_dp_factory; + + //! Discovery id of the screen's plug-in ("" = none). + char plugin_id[COMP_SEGMENTS_ID_MAX]; +}; + +/*! + * Whether pixels a window draws on a screen reach it unresampled. + */ +enum comp_seg_1to1 +{ + //! Nothing to compare (device mode or desktop size unknown). Never degrades. + COMP_SEG_1TO1_UNKNOWN = 0, + //! The screen's desktop rect IS its device mode: window px == device px. + COMP_SEG_1TO1_YES = 1, + //! The display server resamples this screen's window pixels. + COMP_SEG_1TO1_NO = 2, +}; + +/*! + * What a segment is rendered with this frame. + */ +enum comp_seg_render +{ + //! Its screen's display processor weaves it. + COMP_SEG_RENDER_WEAVE = 0, + //! A flat 2D blit of one view (no DP, or a DP that needs 1:1 on a resampled screen). + COMP_SEG_RENDER_FLAT_2D = 1, +}; + +/*! + * One segment. + */ +struct comp_segment +{ + //! Index into the screen array handed to @ref comp_segments_compute. + uint32_t screen_index; + //! That screen's id. + uint64_t screen_id; + //! The segment is on the primary screen (woven by the session's primary DP). + bool is_primary; + //! The screen's plug-in can make a DP for it. + bool has_dp_factory; + + //! The segment in WINDOW pixels — the canvas handed to process_atlas. + struct comp_seg_rect window_rect; + + //! The segment in its SCREEN's pixels — where it lands on that screen. + struct comp_seg_rect screen_rect; + + //! The window's origin relative to the screen (`set_present_origin`; may be + //! negative). The DP adds the canvas offset: phase = present origin + + //! canvas offset = @ref screen_rect origin. + int32_t present_origin_x; + int32_t present_origin_y; + + //! @ref comp_seg_1to1 of the screen. + enum comp_seg_1to1 screen_1to1; +}; + +/*! + * The segment table for one window placement. Ordered left to right by desktop + * position (then top to bottom), which is the order M3's per-segment views use. + */ +struct comp_segment_table +{ + uint32_t count; + struct comp_segment seg[COMP_SEGMENTS_MAX]; +}; + +/*! + * Is @p s 1:1 — its desktop rect equal to its device mode? + */ +enum comp_seg_1to1 +comp_segments_screen_1to1(const struct comp_segments_screen *s); + +/*! + * Compute the segment table. + * + * @param window_desktop The window's client area in desktop coordinates. + * @param canvas The canvas inside the window, window pixels; NULL or a + * zero size = the whole window. + * @param screens The screens. + * @param screen_count Number of screens. + * @param[out] out The table (always written; count 0 when the window is + * on no screen). + * @return out->count. + */ +uint32_t +comp_segments_compute(const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + const struct comp_segments_screen *screens, + uint32_t screen_count, + struct comp_segment_table *out); + +/*! + * True when @p t is anything other than "one segment, on the primary screen, + * covering the whole canvas" — i.e. when the compositor must leave its + * single-DP path. A window entirely on the primary screen (the common case) + * returns false, which is what keeps that case byte-for-byte unchanged. + * + * @param canvas_w/h The canvas size the table was computed for. + */ +bool +comp_segments_table_is_split(const struct comp_segment_table *t, uint32_t canvas_w, uint32_t canvas_h); + +/*! + * The refuse-rather-than-resample policy for one segment. + * + * - No DP for the segment → flat 2D. + * - The screen is known NOT 1:1 and the DP does not tolerate a resample (a + * lenticular weave, sim_display INTERLACED) → flat 2D. + * - Otherwise (1:1, unknown, or a DP whose output survives a resample such as + * anaglyph) → weave. Unknown never degrades. + */ +enum comp_seg_render +comp_segments_decide(bool have_dp, bool dp_tolerates_resample, enum comp_seg_1to1 screen_1to1); + +//! Max rectangles @ref comp_segments_uncovered emits. +#define COMP_SEGMENTS_MAX_UNCOVERED 32 + +/*! + * The parts of @p canvas (window px) that no segment covers — canvas on a + * monitor no plug-in claimed, or off every screen. The compositor paints them + * flat 2D, the same as a segment it cannot weave (#1654's off-panel rule). + * Disjoint rectangles, row bands top to bottom, left to right within a band. + * + * @return the number written (excess dropped at @p cap). + */ +uint32_t +comp_segments_uncovered(const struct comp_segment_table *t, + const struct comp_seg_rect *canvas, + struct comp_seg_rect *out, + uint32_t cap); + +/*! + * Equal geometry and screens (used to log a table change once). + */ +bool +comp_segments_table_equal(const struct comp_segment_table *a, const struct comp_segment_table *b); + +/*! + * One-line description of @p t for the change log. Always NUL-terminates. + */ +void +comp_segments_table_format(const struct comp_segment_table *t, char *buf, size_t size); + + +/* + * + * DP lifecycle with hysteresis. + * + */ + +/*! + * What the lifecycle wants done with one screen's segment DP. + */ +enum comp_seg_action +{ + COMP_SEG_ACTION_NONE = 0, + //! Create the DP now (the segment has existed long enough). + COMP_SEG_ACTION_CREATE = 1, + //! Destroy the DP now (the segment has been gone long enough). + COMP_SEG_ACTION_DESTROY = 2, +}; + +//! @private Per-screen lifecycle slot. +struct comp_segments_slot +{ + uint64_t screen_id; + uint32_t present_streak; + uint32_t absent_streak; + //! A DP exists for the screen. + bool live; + //! Creation failed while the segment existed; not retried until it is gone. + bool failed; + bool used; +}; + +/*! + * Hysteresis for secondary segment DPs. The primary screen's DP is owned by + * the session and is not tracked here. + * + * Per metric update, a screen with a non-empty non-primary segment counts up a + * present streak and one without counts up an absent streak. The DP is + * created once the present streak reaches `create_after` and destroyed once + * the absent streak reaches `destroy_after` — so a window dragged back and + * forth across a seam keeps its DPs instead of rebuilding them every frame. + */ +struct comp_segments_lifecycle +{ + uint32_t create_after; + uint32_t destroy_after; + struct comp_segments_slot slots[COMP_SEGMENTS_MAX_SCREENS]; +}; + +/*! + * Actions out of one @ref comp_segments_lifecycle_update. + */ +struct comp_segments_actions +{ + uint32_t count; + struct + { + uint64_t screen_id; + enum comp_seg_action action; + } items[COMP_SEGMENTS_MAX_SCREENS]; +}; + +/*! + * Initialise. 0 for either threshold selects the default. + */ +void +comp_segments_lifecycle_init(struct comp_segments_lifecycle *lc, uint32_t create_after, uint32_t destroy_after); + +/*! + * Advance one metric update with the current table; fill @p out with the + * creates/destroys due now. The caller performs each and reports a create's + * outcome with @ref comp_segments_lifecycle_set_created (a destroy is assumed + * done). + */ +void +comp_segments_lifecycle_update(struct comp_segments_lifecycle *lc, + const struct comp_segment_table *t, + struct comp_segments_actions *out); + +/*! + * Report the outcome of a CREATE action. + */ +void +comp_segments_lifecycle_set_created(struct comp_segments_lifecycle *lc, uint64_t screen_id, bool ok); + +/*! + * Does a DP exist for @p screen_id? + */ +bool +comp_segments_lifecycle_is_live(const struct comp_segments_lifecycle *lc, uint64_t screen_id); + +/*! + * Every live slot as a DESTROY action, slots cleared (teardown). + */ +void +comp_segments_lifecycle_drain(struct comp_segments_lifecycle *lc, struct comp_segments_actions *out); + + + +/* + * + * Deferred release. + * + */ + +//! Capacity of a @ref comp_segments_retire list. +#define COMP_SEGMENTS_RETIRE_MAX 64 + +/*! + * Objects a segment no longer needs, held until no submitted work can still + * reference them. The in-process Vulkan compositor's repaint ("fill") parks on + * its fence with the compositor lock RELEASED (#1264 S1), so the app thread can + * run a metric update — and want to destroy a segment DP or an input image — + * while that fill's command buffer is still executing on the GPU. Releases go + * here instead and are handed back only when the caller says it is safe. + * + * Items are opaque 64-bit handles with a caller-defined kind. + */ +struct comp_segments_retire +{ + uint32_t count; + uint32_t kinds[COMP_SEGMENTS_RETIRE_MAX]; + uint64_t items[COMP_SEGMENTS_RETIRE_MAX]; +}; + +/*! + * Queue @p item. False when the list is full (the caller must then either + * release now, if that is safe, or keep the object). + */ +bool +comp_segments_retire_push(struct comp_segments_retire *r, uint32_t kind, uint64_t item); + +/*! + * Hand back everything queued, oldest first, and empty the list — but only when + * @p safe; otherwise nothing moves and 0 is returned. At most @p cap items are + * returned per call (the rest stay queued). + */ +uint32_t +comp_segments_retire_take(struct comp_segments_retire *r, + bool safe, + uint32_t *out_kinds, + uint64_t *out_items, + uint32_t cap); + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/compositor/vk_native/CMakeLists.txt b/src/xrt/compositor/vk_native/CMakeLists.txt index 65ccba9137..fc178137ad 100644 --- a/src/xrt/compositor/vk_native/CMakeLists.txt +++ b/src/xrt/compositor/vk_native/CMakeLists.txt @@ -34,6 +34,8 @@ if((WIN32 OR APPLE OR ANDROID OR (XRT_HAVE_LINUX AND XRT_HAVE_XCB)) AND XRT_HAVE comp_vk_native_equirect2.h comp_vk_native_renderer.c comp_vk_native_renderer.h + comp_vk_native_segments.c + comp_vk_native_segments.h comp_vk_native_split.cpp comp_vk_native_split.h comp_vk_native_swapchain.c diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c index c64be07375..f733902858 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -18,6 +18,7 @@ #include "comp_vk_native_target.h" #include "comp_vk_native_renderer.h" #include "comp_vk_native_alpha_probe.h" +#include "comp_vk_native_segments.h" #include "util/comp_swapchain_ring.h" #include "util/comp_zone_tier1.h" @@ -27,6 +28,7 @@ // -> publish -> dump). Only the typed get_background_preview call stays here. #include "util/comp_rear_budget.h" #include "util/comp_lazy_transparency.h" +#include "util/comp_segments.h" #ifdef XRT_OS_WINDOWS #include "util/comp_display_refresh_win.h" #endif @@ -511,6 +513,17 @@ struct comp_vk_native_compositor //! Generic Vulkan display processor (vendor-agnostic weaving). struct xrt_display_processor *display_processor; + //! The DP's output survives a display-server resample + //! (XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE). Read at DP creation and again + //! whenever the head's rendering mode changes (sim_display's answer + //! follows its output mode: INTERLACED needs 1:1) — never per frame. + //! Relaxes the #1595 / #1831 refuse-rather-than-resample gates (M2). + bool dp_tolerates_resample; + //! The rendering-mode index @ref dp_tolerates_resample was last read at. + uint32_t dp_caps_mode_index; + //! vk_make_dp_vk called the factory (whether or not it succeeded). + bool dp_create_attempted; + /*! * #1484 — the atlas encoding last DECLARED to `display_processor` via base * slot 14, as an `int` so `-1` can mean "unknown / never declared". The @@ -593,6 +606,12 @@ struct comp_vk_native_compositor //! Display refresh rate in Hz. float display_refresh_rate; + //! #1264 S1: a repaint ("fill") is parked in vkWaitForFences with + //! c->mutex RELEASED, its command buffer still executing. Set and cleared + //! under c->mutex. Anything that frees an object a weave references must + //! check it (multi-screen M2: the segment manager defers its releases). + bool fill_parked; + //! Last present origin handed to the DP (panel-relative px) — an origin //! change means the window is being dragged; the target clamps its queue //! shallow for the duration so the weave phase stays snapped (#912). @@ -690,6 +709,23 @@ struct comp_vk_native_compositor //! #1831: the X11 arm of the 1:1 gate has been evaluated (once — its //! inputs are the static desktop resolution in sys_info). bool x11_1to1_evaluated; + + /*! + * Multi-screen M2 (ADR-047 D2): per-screen segment DPs for a window that + * spans screens. @ref segments is created lazily, on the first weave + * after @ref comp_vk_native_compositor_set_screens, once the DP command + * pool exists. The window's desktop rect is cached by + * @ref vk_update_present_origin from the same metrics read the present + * origin uses, so segmentation costs no window-system round trip. + */ + struct comp_vk_native_segments *segments; + struct xrt_screen_list *seg_screens; //!< heap copy; NULL = never set + uint64_t seg_pinned_display_id; + struct comp_seg_rect seg_window; + bool have_seg_window; + //! set_screens() handed over a new list; the manager is rebuilt on the + //! next weave with no fill in flight. + bool seg_rebuild; #endif #ifdef XRT_OS_ANDROID @@ -4357,6 +4393,42 @@ vk_native_dispatch_capture(struct comp_vk_native_compositor *c, uint32_t mode_fi U_LOG_W("Atlas capture failed (mode=%u path=%s)", c->capture_intent.mode, c->capture_intent.path); } +#ifdef XRT_OS_LINUX_DESKTOP + // Multi-screen M2: with the window split across screens, also write each + // segment's own DP input — `.seg.png` — so a capture shows the + // per-screen canvases, not only the whole-window atlas they were cut from. + if (ok && mode_filter == MCP_CAPTURE_MODE_POST_COMPOSE && c->segments != NULL) { + const char *path = c->capture_intent.path; + // The extension's dot only — not one in a directory name. + const char *dot = strrchr(path, '.'); + const char *slash = strrchr(path, '/'); + if (dot != NULL && slash != NULL && dot < slash) { + dot = NULL; + } + const size_t stem = dot != NULL ? (size_t)(dot - path) : strlen(path); + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX; i++) { + VkImage img = VK_NULL_HANDLE; + uint32_t w = 0, h = 0; + uint64_t screen_id = 0; + bool woven = false; + if (!comp_vk_native_segments_get_capture(c->segments, i, &img, &w, &h, &screen_id, &woven)) { + break; + } + if (!woven || img == VK_NULL_HANDLE) { + U_LOG_W("Atlas capture: segment %u (screen 0x%016llx) is flat 2D this frame — no DP input", + i, (unsigned long long)screen_id); + continue; + } + char seg_path[MCP_CAPTURE_PATH_MAX + 16]; + snprintf(seg_path, sizeof(seg_path), "%.*s.seg%u.png", (int)stem, path, i); + const bool seg_ok = vk_native_dump_image_to_png( + c, img, w, h, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, u_image_capture_raw_alpha(), seg_path); + U_LOG_W("Atlas capture: segment %u (screen 0x%016llx) DP input %ux%u -> %s%s", i, + (unsigned long long)screen_id, w, h, seg_path, seg_ok ? "" : " FAILED"); + } + } +#endif u_capture_intent_complete(&c->capture_intent, &c->mcp_capture, ok); } @@ -4710,7 +4782,11 @@ vk_linux_update_surface_not_1to1(struct comp_vk_native_compositor *c) // scale 1, i.e. a surface twice the output. A surface INSIDE the frame // (a title bar above it) is fine. Unknown never degrades. const bool fits_surface = wr.surface_within_frame; - const bool not_1to1 = !on_panel || !fits_window || !fits_surface; + // Multi-screen M2: (b) and (c) are resamples; a DP whose output survives + // one (XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE) is not degraded by them. + // (a) stays: off the panel, native Wayland has no segment to weave into. + const bool tolerant = c->display_processor != NULL && c->dp_tolerates_resample; + const bool not_1to1 = !on_panel || (!tolerant && (!fits_window || !fits_surface)); if (not_1to1 == c->linux_surface_not_1to1) { return; @@ -4809,6 +4885,37 @@ vk_linux_apply_not_1to1(struct comp_vk_native_compositor *c, bool not_1to1) * Evaluated once: its inputs are the desktop resolution taken at system * creation, which is static for the life of the compositor on X11. */ +/*! + * Multi-screen M2 review: re-read the DP's resample tolerance when the head's + * rendering mode changes. sim_display's answer follows its output mode (the + * 1/2/3 keys / V): switching to INTERLACED on a scaled desktop must degrade + * (#1595 / #1831 — a resampled interlace is a double image), switching back to + * anaglyph must weave again. A change re-arms the X11 gate's one-shot; the + * Wayland arm reads the flag on every evaluation. One vtable call per MODE + * CHANGE, never per frame. + */ +static void +vk_dp_refresh_resample_caps(struct comp_vk_native_compositor *c) +{ + if (c->display_processor == NULL || c->xdev == NULL || c->xdev->hmd == NULL) { + return; + } + const uint32_t idx = c->xdev->hmd->active_rendering_mode_index; + if (idx == c->dp_caps_mode_index) { + return; + } + c->dp_caps_mode_index = idx; + const bool tol = + xrt_display_processor_vk_tolerates_resample((struct xrt_display_processor_vk *)c->display_processor); + if (tol == c->dp_tolerates_resample) { + return; + } + U_LOG_I("1:1 gate: rendering mode %u — the display processor's output %s a resample now; re-evaluating", + idx, tol ? "tolerates" : "does NOT tolerate"); + c->dp_tolerates_resample = tol; + c->x11_1to1_evaluated = false; +} + static void vk_x11_update_not_1to1(struct comp_vk_native_compositor *c) { @@ -4816,6 +4923,14 @@ vk_x11_update_not_1to1(struct comp_vk_native_compositor *c) if (c->x11_1to1_evaluated || !c->sys_info_set) { return; } + // Multi-screen M2: the verdict depends on the DP's resample tolerance, so + // wait until this compositor has tried to create its DP (it is made + // lazily); a failed create evaluates with "needs 1:1". + if (c->display_processor == NULL && c->dp_factory_available && !c->dp_create_attempted) { + return; + } + // One-shot per tolerance: vk_dp_refresh_resample_caps re-arms it when a + // mode switch changes the DP's answer. c->x11_1to1_evaluated = true; #ifdef DXR_HAVE_DIRECT_SCANOUT // A leased connector is scanned out by us, not composited: no resample. @@ -4855,6 +4970,26 @@ vk_x11_update_not_1to1(struct comp_vk_native_compositor *c) return; } + /* + * Multi-screen M2: the gate protects a weave that a resample destroys. + * A DP whose output survives one (sim_display anaglyph/SBS/blend — + * XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE) keeps weaving: there is no + * interlace to lose, and degrading it would hide the very 3D a scaled + * dev box can show. Every other DP — every lenticular weave — degrades as + * before (an absent or silent caps slot reads as "needs 1:1"). + */ + if (c->display_processor != NULL && c->dp_tolerates_resample) { + U_LOG_W("X11 1:1 gate: the panel '%s' is resampled (%ux%u in the X root, %ux%u device px), but this " + "display processor's output tolerates a resample — weaving%s. (#1831, multi-screen M2)", + si->display_device_name, x11_w, x11_h, dev_w, dev_h, + c->linux_surface_not_1to1 ? " again (the mode switched to one that does)" : " anyway"); + vk_linux_apply_not_1to1(c, false); + return; + } + if (c->linux_surface_not_1to1) { + return; // already presenting 2D for this reason + } + char scale_str[48] = ""; if (si->display_desktop_scale > 0.0f) { (void)snprintf(scale_str, sizeof(scale_str), " at %.0f%% scale", @@ -5078,6 +5213,74 @@ vk_dp_canvas_rect(struct comp_vk_native_compositor *c) return r; } +#ifdef XRT_OS_LINUX_DESKTOP +/*! + * Multi-screen M2: does THIS weave take the per-screen segment path? + * + * Runs the segment manager's metric update (segment table + DP lifecycle) + * and answers true only when the window covers a screen other than the + * primary one. Everything that cannot be segmented today answers false and + * keeps the shipped single-DP path, byte for byte: + * + * - native Wayland: the geometry service reports a rect relative to the + * window's own monitor, in that monitor's device px — not a desktop rect + * (docs/specs/runtime/wayland-window-geometry.md, M2 follow-up); + * - zero-copy (no compositor-owned atlas to crop per segment); + * - a self-submitting DP or one with no render pass (the split path records + * every segment DP into our command buffer, sharing our framebuffer); + * - no screens handed over, one screen, a pinned session, mixed vendors. + */ +static bool +vk_segments_frame_update(struct comp_vk_native_compositor *c, + const struct xrt_rect *dp_canvas, + bool zero_copy, + bool dp_self_submits, + VkRenderPass dp_render_pass) +{ + if (c->seg_screens == NULL || c->display_processor == NULL || c->cmd_pool == VK_NULL_HANDLE || + !c->have_seg_window) { + return false; + } +#ifdef XRT_HAVE_WAYLAND + if (c->use_wayland) { + return false; + } +#endif + // A new screen list (set_screens) rebuilds the manager — but never under + // a parked fill, whose command buffer may still use its DPs and images. + if (c->seg_rebuild && !c->fill_parked) { + comp_vk_native_segments_destroy(&c->segments); + c->seg_rebuild = false; + } + if (c->segments != NULL && c->seg_rebuild) { + return false; // old manager, new list: sit this frame out + } + if (c->segments == NULL) { + c->segments = comp_vk_native_segments_create(&c->vk, c->cmd_pool, c->repaint_queue); + if (c->segments == NULL) { + return false; + } + comp_vk_native_segments_set_screens(c->segments, c->seg_screens, &c->sys_info, + c->seg_pinned_display_id); + } + if (!comp_vk_native_segments_enabled(c->segments) || zero_copy || dp_self_submits || + dp_render_pass == VK_NULL_HANDLE) { + return false; + } + struct comp_seg_rect canvas = { + dp_canvas->offset.w, + dp_canvas->offset.h, + (uint32_t)(dp_canvas->extent.w > 0 ? dp_canvas->extent.w : 0), + (uint32_t)(dp_canvas->extent.h > 0 ? dp_canvas->extent.h : 0), + }; + return comp_vk_native_segments_update(c->segments, &c->seg_window, &canvas, + (int32_t)VK_FORMAT_B8G8R8A8_UNORM, !c->fill_parked, + (c->xdev != NULL && c->xdev->hmd != NULL) + ? c->xdev->hmd->active_rendering_mode_index + : 0); +} +#endif + /*! * #1484 — `DXR_VK_ATLAS_ENCODING`: diagnostic override for the ADR-021 atlas- * encoding declaration below. Values: @@ -5156,6 +5359,26 @@ vk_native_atlas_encoding(const struct comp_vk_native_compositor *c) return XRT_ATLAS_ENCODING_ENCODED; } +#ifdef XRT_OS_LINUX_DESKTOP +/*! + * Multi-screen M2: the encoding segment DPs are told — exactly what + * @ref vk_dp_assert_atlas_encoding tells the primary, DXR_VK_ATLAS_ENCODING + * included; -1 when the override says never to call the slot. + */ +static int +vk_segments_atlas_encoding(struct comp_vk_native_compositor *c) +{ + const int over = dxr_vk_atlas_encoding_override(); + if (over == DXR_VK_ATLAS_ENCODING_DISABLED) { + return -1; + } + if (over != DXR_VK_ATLAS_ENCODING_NO_OVERRIDE) { + return over; + } + return (int)vk_native_atlas_encoding(c); +} +#endif + /*! * #1484 — assert @p dp's atlas encoding, the way the D3D11 service's * `svc_client_weave_dp_assert_state()` does. Same contract: every weave site @@ -6256,13 +6479,51 @@ vk_dp_weave_and_present(struct comp_vk_native_compositor *c, // precede process_atlas; no-op on no change. vk_dp_assert_atlas_encoding(c, c->display_processor, vk_native_atlas_encoding(c)); - xrt_display_processor_process_atlas( - c->display_processor, dp_self_submits ? VK_NULL_HANDLE : cmd, - (VkImage_XDP)(uintptr_t)src_image_u64, (VkImageView)(uintptr_t)src_view_u64, view_width, - view_height, tc, tr, (VkFormat_XDP)view_format, target_fb, - (VkImage_XDP)(uintptr_t)target_image, tgt_width, tgt_height, - (VkFormat_XDP)comp_vk_native_target_get_format(c->target), dp_canvas.offset.w, - dp_canvas.offset.h, (uint32_t)dp_canvas.extent.w, (uint32_t)dp_canvas.extent.h); +#ifdef XRT_OS_LINUX_DESKTOP + // Multi-screen M2: a window spanning screens is woven per + // segment, each by its own screen's DP. A window on the primary + // screen only never takes this branch (byte-for-byte the single- + // DP call below). + const bool seg_split = + vk_segments_frame_update(c, &dp_canvas, zero_copy, dp_self_submits, dp_render_pass); +#else + const bool seg_split = false; +#endif + if (seg_split) { +#ifdef XRT_OS_LINUX_DESKTOP + struct comp_vk_native_segments_frame sf = { + .cmd = cmd, + .is_repaint = is_repaint, + .src_image = (VkImage)(uintptr_t)src_image_u64, + .src_format = (VkFormat)view_format, + .view_width = view_width, + .view_height = view_height, + .tile_columns = tc, + .tile_rows = tr, + .target_fb = target_fb, + .target_image = (VkImage)(uintptr_t)target_image, + .target_view = (VkImageView)(uintptr_t)target_view, + .target_width = tgt_width, + .target_height = tgt_height, + .target_format = (VkFormat)comp_vk_native_target_get_format(c->target), + .canvas = {dp_canvas.offset.w, dp_canvas.offset.h, + (uint32_t)(dp_canvas.extent.w > 0 ? dp_canvas.extent.w : (int)tgt_width), + (uint32_t)(dp_canvas.extent.h > 0 ? dp_canvas.extent.h : (int)tgt_height)}, + .transparent_background = c->transparent_background, + .atlas_encoding = vk_segments_atlas_encoding(c), + .primary_dp = c->display_processor, + }; + (void)comp_vk_native_segments_record(c->segments, &sf); +#endif + } else { + xrt_display_processor_process_atlas( + c->display_processor, dp_self_submits ? VK_NULL_HANDLE : cmd, + (VkImage_XDP)(uintptr_t)src_image_u64, (VkImageView)(uintptr_t)src_view_u64, view_width, + view_height, tc, tr, (VkFormat_XDP)view_format, target_fb, + (VkImage_XDP)(uintptr_t)target_image, tgt_width, tgt_height, + (VkFormat_XDP)comp_vk_native_target_get_format(c->target), dp_canvas.offset.w, + dp_canvas.offset.h, (uint32_t)dp_canvas.extent.w, (uint32_t)dp_canvas.extent.h); + } /* * #1394 — the DP is allowed to say "that frame did not happen". @@ -6677,10 +6938,12 @@ vk_dp_weave_and_present(struct comp_vk_native_compositor *c, const uint32_t gen_park = comp_vk_native_target_get_generation(c->target); const uint64_t serial_park = c->present_serial; + c->fill_parked = true; os_mutex_unlock(&c->mutex); vk->vkWaitForFences(vk->device, 1, fence_p, VK_TRUE, UINT64_MAX); vk->vkResetFences(vk->device, 1, fence_p); os_mutex_lock(&c->mutex); + c->fill_parked = false; // GPU is done — the command buffer is free regardless of // what the validation below decides. vk->vkFreeCommandBuffers(vk->device, cmd_pool, 1, &cmd); @@ -7874,6 +8137,7 @@ vk_compositor_layer_commit_locked(struct xrt_compositor *xc, // decides whether this frame weaves at all, so it must precede the layout. // Cheap (a non-blocking D-Bus pump the metrics path already performs) and // only acts on a transition. + vk_dp_refresh_resample_caps(c); vk_linux_update_surface_not_1to1(c); #ifdef XRT_HAVE_WAYLAND if (!c->use_wayland) @@ -8945,6 +9209,12 @@ vk_compositor_destroy(struct xrt_compositor *xc) xrt_display_processor_clear_local_zone_mask(c->display_processor); c->zone_published = false; } +#ifdef XRT_OS_LINUX_DESKTOP + // Multi-screen M2: the segment DPs go first (they share the DP pool). + comp_vk_native_segments_destroy(&c->segments); + free(c->seg_screens); + c->seg_screens = NULL; +#endif xrt_display_processor_destroy(&c->display_processor); // Destroy shared texture resources @@ -9253,6 +9523,7 @@ vk_make_dp_vk(struct comp_vk_native_compositor *c, "(app queue %p stays the app frame's)", (void *)c->repaint_queue, (void *)saved_main_queue); } + c->dp_create_attempted = true; xrt_result_t dp_ret = factory(&c->vk, (void *)(uintptr_t)c->cmd_pool, dp_window_handle, (int32_t)VK_FORMAT_B8G8R8A8_UNORM, &c->display_processor); c->vk.main_queue->queue = saved_main_queue; @@ -9269,6 +9540,11 @@ vk_make_dp_vk(struct comp_vk_native_compositor *c, (void)u_weave_scope_report(xrt_display_processor_vk_get_weave_scope( (struct xrt_display_processor_vk *)c->display_processor), "VK", /* panel_scoped */ false); + c->dp_tolerates_resample = xrt_display_processor_vk_tolerates_resample( + (struct xrt_display_processor_vk *)c->display_processor); + c->dp_caps_mode_index = (c->xdev != NULL && c->xdev->hmd != NULL) + ? c->xdev->hmd->active_rendering_mode_index + : UINT32_MAX; } } else { U_LOG_W("No VK display processor factory provided"); @@ -11837,6 +12113,16 @@ vk_update_present_origin(struct comp_vk_native_compositor *c) c->last_window_px_w = m.window_pixel_width; c->last_window_px_h = m.window_pixel_height; c->have_last_window_size = true; +#ifdef XRT_OS_LINUX_DESKTOP + // Multi-screen M2: the window's desktop rect, for the segment table. + // X11 root coordinates are desktop-absolute — the same space the + // screen registry's RandR rects are in. + c->seg_window.x = m.window_screen_left; + c->seg_window.y = m.window_screen_top; + c->seg_window.w = m.window_pixel_width; + c->seg_window.h = m.window_pixel_height; + c->have_seg_window = true; +#endif } /* @@ -12065,6 +12351,42 @@ comp_vk_native_compositor_set_system_devices(struct xrt_compositor *xc, #endif } +void +comp_vk_native_compositor_set_screens(struct xrt_compositor *xc, + const struct xrt_screen_list *list, + uint64_t pinned_display_id) +{ +#ifdef XRT_OS_LINUX_DESKTOP + if (xc == NULL || list == NULL) { + return; + } + struct comp_vk_native_compositor *c = vk_comp(xc); + struct xrt_screen_list *copy = U_TYPED_CALLOC(struct xrt_screen_list); + if (copy == NULL) { + return; + } + *copy = *list; + + // #1394: never block the app thread on a wedged weave. + if (vk_weave_blocked(c)) { + free(copy); + return; + } + os_mutex_lock(&c->mutex); + // The segment manager is rebuilt on the next weave with no fill in flight + // (destroying it here could free DPs a parked fill is still executing). + c->seg_rebuild = true; + free(c->seg_screens); + c->seg_screens = copy; + c->seg_pinned_display_id = pinned_display_id; + os_mutex_unlock(&c->mutex); +#else + (void)xc; + (void)list; + (void)pinned_display_id; +#endif +} + void comp_vk_native_compositor_set_sys_info(struct xrt_compositor *xc, const struct xrt_system_compositor_info *info) diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.h b/src/xrt/compositor/vk_native/comp_vk_native_compositor.h index a9ca482abf..74f5782efa 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.h @@ -237,6 +237,23 @@ void comp_vk_native_compositor_set_sys_info(struct xrt_compositor *xc, const struct xrt_system_compositor_info *info); +struct xrt_screen_list; + +/*! + * Multi-screen M2: hand the compositor the system's screens so a window that + * spans several of them is woven per segment, each by its own screen's display + * processor (ADR-047 D2). Call after @ref comp_vk_native_compositor_set_sys_info + * (it reads the DP registry from there). @p pinned_display_id is the session's + * `XrSessionDisplayBindingDXR` (0 = none); a pinned session is never segmented. + * Desktop Linux X11/XWayland only; a no-op elsewhere. + * + * @ingroup comp_vk_native + */ +void +comp_vk_native_compositor_set_screens(struct xrt_compositor *xc, + const struct xrt_screen_list *list, + uint64_t pinned_display_id); + /*! * Set legacy app tile scaling flag (gates 1/2/3 key mode selection). * diff --git a/src/xrt/compositor/vk_native/comp_vk_native_segments.c b/src/xrt/compositor/vk_native/comp_vk_native_segments.c new file mode 100644 index 0000000000..d4944361f7 --- /dev/null +++ b/src/xrt/compositor/vk_native/comp_vk_native_segments.c @@ -0,0 +1,996 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen segment display processors (multi-screen M2) — see + * comp_vk_native_segments.h. + * @ingroup comp_vk_native + */ + +#include "comp_vk_native_segments.h" + +#include "xrt/xrt_display_processor_vk.h" +#include "xrt/xrt_limits.h" +#include "xrt/xrt_plugin.h" + +#include "vk/vk_helpers.h" + +#include "util/u_logging.h" +#include "util/u_misc.h" + +#include +#include +#include +#include + + +/* + * + * State. + * + */ + +//! One cropped DP input image. +struct seg_crop +{ + VkImage image; + VkDeviceMemory memory; + VkImageView view; + uint32_t w; + uint32_t h; + VkFormat format; +}; + +//! Which frame class records into a crop image (see seg_screen_state::crop). +enum seg_frame_class +{ + SEG_CLASS_APP = 0, + SEG_CLASS_REPAINT = 1, + SEG_CLASS_COUNT = 2, +}; + +//! What a retired handle is (comp_segments_retire kinds). +enum seg_retire_kind +{ + SEG_RETIRE_DP = 1, + SEG_RETIRE_VIEW = 2, + SEG_RETIRE_IMAGE = 3, + SEG_RETIRE_MEMORY = 4, +}; + +//! Per-screen state: the screen's segment DP (NULL for the primary screen, +//! whose DP the session owns) and the cropped DP input for its segment. +struct seg_screen_state +{ + struct xrt_display_processor *dp; + bool tolerates_resample; + //! The one-time DP setup (no 2D-under backdrop) is done. + bool configured; + //! Last atlas encoding / transparency declared to @ref dp (-1 = never), + //! re-declared on change, like the primary's (#1484, #573). + int encoding_latched; + int transparent_latched; + + /*! + * One DP input image PER FRAME CLASS. The repaint ("fill") parks on its + * fence with the compositor lock released (#1264 S1), so an app frame can + * record while the fill's command buffer is still executing: sharing one + * image would let the app's copy overwrite what the in-flight fill samples + * (and a resize would destroy it). Each class only ever touches its own, + * and each class waits its own previous submission before recording, so a + * class may recreate its own image immediately. + */ + struct seg_crop crop[SEG_CLASS_COUNT]; +}; + +//! What the last split frame did with one segment (atlas capture). +struct seg_capture +{ + uint32_t screen_index; + bool woven; + uint32_t w; + uint32_t h; +}; + +struct comp_vk_native_segments +{ + struct vk_bundle *vk; + VkCommandPool cmd_pool; + //! #868: the runtime-owned queue a DP must capture at creation (see + //! vk_make_dp_vk); VK_NULL_HANDLE = none, the DP sees the app's queue. + VkQueue dp_queue; + + bool enabled; + + uint32_t screen_count; + struct comp_segments_screen screens[COMP_SEGMENTS_MAX_SCREENS]; + struct xrt_screen_binding bindings[COMP_SEGMENTS_MAX_SCREENS]; + //! Owning plug-in iface / instance per screen (registry). + const struct xrt_plugin_iface *iface[COMP_SEGMENTS_MAX_SCREENS]; + struct xrt_plugin_instance *inst[COMP_SEGMENTS_MAX_SCREENS]; + struct seg_screen_state st[COMP_SEGMENTS_MAX_SCREENS]; + + struct comp_segments_lifecycle lc; + struct comp_segment_table table; + struct comp_segment_table logged_table; + bool have_logged_table; + bool logged_split; + + struct seg_capture cap[COMP_SEGMENTS_MAX]; + uint32_t cap_count; + + //! Releases that must wait until no fill is in flight (see + //! comp_segments_retire). Drained by update() when the caller says it is + //! safe, and by destroy. + struct comp_segments_retire retire; + //! True only inside an update() the caller declared release-safe, and in + //! destroy — the debug assert in release_now() checks it. + bool release_safe; + + //! Rendering-mode index the segment DPs' tolerance was last read at. + uint32_t mode_index; +}; + + +/* + * + * Helpers. + * + */ + +static int +screen_index_of(const struct comp_vk_native_segments *segs, uint64_t id) +{ + for (uint32_t i = 0; i < segs->screen_count; i++) { + if (segs->screens[i].id == id) { + return (int)i; + } + } + return -1; +} + +/*! + * Destroy one handle NOW. Only legal when no submitted work can reference it: + * a fill parked with the lock released may still be executing a command buffer + * that does. + */ +static void +release_now(struct comp_vk_native_segments *segs, uint32_t kind, uint64_t item) +{ + assert(segs->release_safe && "segments: release while a fill may be in flight"); + struct vk_bundle *vk = segs->vk; + switch (kind) { + case SEG_RETIRE_DP: { + struct xrt_display_processor *dp = (struct xrt_display_processor *)(uintptr_t)item; + xrt_display_processor_destroy(&dp); + break; + } + case SEG_RETIRE_VIEW: vk->vkDestroyImageView(vk->device, (VkImageView)(uintptr_t)item, NULL); break; + case SEG_RETIRE_IMAGE: vk->vkDestroyImage(vk->device, (VkImage)(uintptr_t)item, NULL); break; + case SEG_RETIRE_MEMORY: vk->vkFreeMemory(vk->device, (VkDeviceMemory)(uintptr_t)item, NULL); break; + default: break; + } +} + +/*! + * Release @p item, deferred onto the retire list. A full list (a pathological + * pile-up while a fill stays parked) leaks the handle with one WARN rather than + * free something the GPU may still read. + */ +static void +release_deferred(struct comp_vk_native_segments *segs, uint32_t kind, uint64_t item) +{ + if (item == 0) { + return; + } + if (!comp_segments_retire_push(&segs->retire, kind, item)) { + static bool warned = false; + if (!warned) { + warned = true; + U_LOG_W("segments: retire list full — leaking a released object rather than freeing it under " + "an in-flight fill"); + } + } +} + +//! Hand everything retired back to Vulkan, if @p safe. +static void +retire_drain(struct comp_vk_native_segments *segs, bool safe) +{ + uint32_t kinds[COMP_SEGMENTS_RETIRE_MAX]; + uint64_t items[COMP_SEGMENTS_RETIRE_MAX]; + const uint32_t n = comp_segments_retire_take(&segs->retire, safe, kinds, items, COMP_SEGMENTS_RETIRE_MAX); + for (uint32_t i = 0; i < n; i++) { + release_now(segs, kinds[i], items[i]); + } +} + +//! Retire a crop image (deferred: another frame class may still read it). +static void +crop_retire(struct comp_vk_native_segments *segs, struct seg_crop *c) +{ + release_deferred(segs, SEG_RETIRE_VIEW, (uint64_t)(uintptr_t)c->view); + release_deferred(segs, SEG_RETIRE_IMAGE, (uint64_t)(uintptr_t)c->image); + release_deferred(segs, SEG_RETIRE_MEMORY, (uint64_t)(uintptr_t)c->memory); + memset(c, 0, sizeof(*c)); +} + +/*! + * Destroy a crop image immediately. Only for the recording class's OWN image: + * that class waited its previous submission before this frame, and no other + * class ever touches it. + */ +static void +crop_destroy_own(struct vk_bundle *vk, struct seg_crop *c) +{ + if (c->view != VK_NULL_HANDLE) { + vk->vkDestroyImageView(vk->device, c->view, NULL); + } + if (c->image != VK_NULL_HANDLE) { + vk->vkDestroyImage(vk->device, c->image, NULL); + } + if (c->memory != VK_NULL_HANDLE) { + vk->vkFreeMemory(vk->device, c->memory, NULL); + } + memset(c, 0, sizeof(*c)); +} + +/*! + * (Re)create a segment's DP input image at exactly @p w x @p h — a DP reads its + * atlas as `tile_columns * view_width` wide, so the image must be exactly the + * segment's tile grid (the same reason the single-DP path crops, ADR-030). + */ +static bool +crop_ensure(struct vk_bundle *vk, struct seg_crop *c, uint32_t w, uint32_t h, VkFormat format) +{ + if (c->image != VK_NULL_HANDLE && c->w == w && c->h == h && c->format == format) { + return true; + } + crop_destroy_own(vk, c); + + VkImageCreateInfo ici = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {w, h, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + // TRANSFER_SRC: a DP may read its input with a transfer (the Leia + // Linux DP's 2D path blits it), and the atlas capture reads it back. + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + if (vk->vkCreateImage(vk->device, &ici, NULL, &c->image) != VK_SUCCESS) { + c->image = VK_NULL_HANDLE; + return false; + } + + VkMemoryRequirements req; + vk->vkGetImageMemoryRequirements(vk->device, c->image, &req); + VkPhysicalDeviceMemoryProperties props; + vk->vkGetPhysicalDeviceMemoryProperties(vk->physical_device, &props); + uint32_t type = UINT32_MAX; + for (uint32_t i = 0; i < props.memoryTypeCount; i++) { + if ((req.memoryTypeBits & (1u << i)) && + (props.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { + type = i; + break; + } + } + if (type == UINT32_MAX) { + crop_destroy_own(vk, c); + return false; + } + VkMemoryAllocateInfo mai = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = req.size, + .memoryTypeIndex = type, + }; + if (vk->vkAllocateMemory(vk->device, &mai, NULL, &c->memory) != VK_SUCCESS || + vk->vkBindImageMemory(vk->device, c->image, c->memory, 0) != VK_SUCCESS) { + crop_destroy_own(vk, c); + return false; + } + + VkImageViewCreateInfo vci = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = c->image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = format, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; + if (vk->vkCreateImageView(vk->device, &vci, NULL, &c->view) != VK_SUCCESS) { + crop_destroy_own(vk, c); + return false; + } + c->w = w; + c->h = h; + c->format = format; + return true; +} + +static void +dp_release(struct comp_vk_native_segments *segs, uint32_t i) +{ + struct seg_screen_state *st = &segs->st[i]; + if (st->dp != NULL) { + U_LOG_I("segments: retiring the segment DP for screen 0x%016llx ('%s')", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name); + release_deferred(segs, SEG_RETIRE_DP, (uint64_t)(uintptr_t)st->dp); + st->dp = NULL; + } + st->configured = false; + st->encoding_latched = -1; + st->transparent_latched = -1; + st->tolerates_resample = false; + for (uint32_t k = 0; k < SEG_CLASS_COUNT; k++) { + crop_retire(segs, &st->crop[k]); + } +} + +/*! + * Create screen @p i's segment DP through the plug-in's per-screen factory. + * Windowless (NULL window) on purpose: a segment DP's phase comes from + * set_present_origin (ADR-033). + * + * There is deliberately NO fallback to the plain `create_dp_vk`: that DP would + * describe the plug-in's process-wide panel instead of this screen, and + * nothing promises it honours a sub-rect canvas or leaves the rest of the + * target alone — it could clear the primary's segment after the primary wove. + * A plug-in without the slot gets flat 2D on its other screens + * (has_dp_factory is false, so this is never reached for it). + */ +static bool +dp_create(struct comp_vk_native_segments *segs, uint32_t i, int32_t target_format) +{ + struct seg_screen_state *st = &segs->st[i]; + const struct xrt_plugin_iface *iface = segs->iface[i]; + xrt_result_t xret = XRT_ERROR_DEVICE_CREATION_FAILED; + const char *how = "none"; + + if (xrt_plugin_iface_has_create_dp_vk_for_screen(iface)) { + how = "create_dp_vk_for_screen"; + /* + * #868: same queue swap as vk_make_dp_vk — a vendor DP captures + * vk->main_queue once, at creation, for its internal submits, and the + * weave also runs on the repaint thread; the runtime-owned queue keeps + * those submits off the app's queue. Restored immediately. + */ + VkQueue saved = segs->vk->main_queue->queue; + if (segs->dp_queue != VK_NULL_HANDLE) { + segs->vk->main_queue->queue = segs->dp_queue; + } + xret = iface->create_dp_vk_for_screen(segs->inst[i], segs->vk, (void *)(uintptr_t)segs->cmd_pool, NULL, + target_format, &segs->bindings[i], &st->dp); + segs->vk->main_queue->queue = saved; + } + + if (xret != XRT_SUCCESS || st->dp == NULL || st->dp->process_atlas == NULL) { + if (st->dp != NULL) { + xrt_display_processor_destroy(&st->dp); + } + U_LOG_W("segments: could not create a DP for screen 0x%016llx ('%s', plug-in '%s') via %s (%d) — " + "that segment stays flat 2D", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, + segs->screens[i].plugin_id, how, (int)xret); + return false; + } + st->tolerates_resample = xrt_display_processor_vk_tolerates_resample((struct xrt_display_processor_vk *)st->dp); + st->configured = false; + st->encoding_latched = -1; + st->transparent_latched = -1; + // Lifecycle event (hysteresis-gated), not per frame. + U_LOG_W("segments: created a segment DP for screen 0x%016llx ('%s', plug-in '%s') via %s — %s", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, segs->screens[i].plugin_id, + how, st->tolerates_resample ? "tolerates a resample" : "needs 1:1 pixels"); + return true; +} + +static int32_t +scale_round(int64_t v, int64_t num, int64_t den) +{ + // round(v * num / den), v/num/den >= 0 + return den > 0 ? (int32_t)((v * num + den / 2) / den) : 0; +} + +/*! + * A segment's rect in VIEW-TILE pixels (the atlas holds the canvas at view + * resolution). Edges are rounded independently, so two segments sharing a + * seam share the tile column too: no gap, no overlap. + */ +static bool +segment_tile_rect(const struct comp_seg_rect *wr, + const struct comp_seg_rect *canvas, + uint32_t view_w, + uint32_t view_h, + int32_t *x0, + int32_t *y0, + uint32_t *w, + uint32_t *h) +{ + if (canvas->w == 0 || canvas->h == 0) { + return false; + } + const int64_t rx = (int64_t)wr->x - canvas->x; + const int64_t ry = (int64_t)wr->y - canvas->y; + if (rx < 0 || ry < 0) { + return false; + } + const int32_t ax = scale_round(rx, view_w, canvas->w); + const int32_t ay = scale_round(ry, view_h, canvas->h); + int32_t bx = scale_round(rx + wr->w, view_w, canvas->w); + int32_t by = scale_round(ry + wr->h, view_h, canvas->h); + if (bx > (int32_t)view_w) { + bx = (int32_t)view_w; + } + if (by > (int32_t)view_h) { + by = (int32_t)view_h; + } + if (ax < 0 || ay < 0 || bx <= ax || by <= ay) { + return false; + } + *x0 = ax; + *y0 = ay; + *w = (uint32_t)(bx - ax); + *h = (uint32_t)(by - ay); + return true; +} + +static void +image_barrier(struct vk_bundle *vk, + VkCommandBuffer cmd, + VkImage image, + VkImageLayout from, + VkImageLayout to, + VkAccessFlags src_access, + VkAccessFlags dst_access, + VkPipelineStageFlags src_stage, + VkPipelineStageFlags dst_stage) +{ + VkImageMemoryBarrier b = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = from, + .newLayout = to, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; + vk->vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b); +} + + +/* + * + * Exported. + * + */ + +struct comp_vk_native_segments * +comp_vk_native_segments_create(struct vk_bundle *vk, VkCommandPool cmd_pool, VkQueue dp_queue) +{ + struct comp_vk_native_segments *segs = U_TYPED_CALLOC(struct comp_vk_native_segments); + if (segs == NULL) { + return NULL; + } + segs->vk = vk; + segs->cmd_pool = cmd_pool; + segs->dp_queue = dp_queue; + comp_segments_lifecycle_init(&segs->lc, 0, 0); + return segs; +} + +void +comp_vk_native_segments_destroy(struct comp_vk_native_segments **segs_ptr) +{ + if (segs_ptr == NULL || *segs_ptr == NULL) { + return; + } + struct comp_vk_native_segments *segs = *segs_ptr; + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + dp_release(segs, i); + } + // The caller guarantees nothing in flight (see the header). + segs->release_safe = true; + retire_drain(segs, true); + segs->release_safe = false; + free(segs); + *segs_ptr = NULL; +} + +void +comp_vk_native_segments_set_screens(struct comp_vk_native_segments *segs, + const struct xrt_screen_list *list, + const struct xrt_system_compositor_info *info, + uint64_t pinned_display_id) +{ + if (segs == NULL) { + return; + } + // A new screen set invalidates every segment DP built for the old one. + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + dp_release(segs, i); + } + comp_segments_lifecycle_init(&segs->lc, 0, 0); + segs->enabled = false; + segs->screen_count = 0; + segs->have_logged_table = false; + segs->logged_split = false; + segs->cap_count = 0; + memset(&segs->table, 0, sizeof(segs->table)); + + const char *env = getenv("DXR_SEGMENTS"); + if (env != NULL && env[0] == '0') { + U_LOG_W("segments: DXR_SEGMENTS=0 — a window spanning screens keeps the single-DP path"); + return; + } + if (list == NULL || info == NULL || list->count < 2) { + return; // one screen: nothing to segment + } + if (pinned_display_id != 0) { + U_LOG_I("segments: session pinned to display 0x%016llx (XrSessionDisplayBindingDXR) — no segmentation", + (unsigned long long)pinned_display_id); + return; + } + + // The primary: the system-default screen, woven by the session's own DP. + const struct xrt_dp_registry_entry *primary_entry = NULL; + for (uint32_t i = 0; i < list->count && i < XRT_SCREEN_LIST_MAX; i++) { + if ((list->screens[i].flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) == 0) { + continue; + } + for (uint32_t e = 0; e < info->dp_registry.entry_count; e++) { + if (info->dp_registry.entries[e].monitor_id == list->screens[i].id) { + primary_entry = &info->dp_registry.entries[e]; + break; + } + } + } + if (primary_entry == NULL) { + U_LOG_I("segments: the DP registry does not know the system-default screen — no segmentation"); + return; + } + + uint32_t n = 0; + bool mixed = false; + for (uint32_t i = 0; i < list->count && i < XRT_SCREEN_LIST_MAX && n < COMP_SEGMENTS_MAX_SCREENS; i++) { + const struct xrt_screen *s = &list->screens[i]; + if (s->desktop_width == 0 || s->desktop_height == 0) { + continue; // no desktop placement (pure-DRM record): cannot be segmented against + } + const struct xrt_dp_registry_entry *e = NULL; + for (uint32_t k = 0; k < info->dp_registry.entry_count; k++) { + if (info->dp_registry.entries[k].monitor_id == s->id) { + e = &info->dp_registry.entries[k]; + break; + } + } + + struct comp_segments_screen *cs = &segs->screens[n]; + memset(cs, 0, sizeof(*cs)); + cs->id = s->id; + cs->desktop.x = s->desktop_left; + cs->desktop.y = s->desktop_top; + cs->desktop.w = s->desktop_width; + cs->desktop.h = s->desktop_height; + cs->native_w = s->native_width; + cs->native_h = s->native_height; + cs->is_primary = (e == primary_entry); + snprintf(cs->plugin_id, sizeof(cs->plugin_id), "%s", s->plugin_id); + + segs->iface[n] = NULL; + segs->inst[n] = NULL; + if (e != NULL && !cs->is_primary) { + const bool same_vendor = strcmp(e->plugin_id, primary_entry->plugin_id) == 0; + if (!same_vendor) { + mixed = true; + } else { + segs->iface[n] = (const struct xrt_plugin_iface *)e->owning_iface; + segs->inst[n] = (struct xrt_plugin_instance *)e->owning_instance; + // The per-screen slot, and only it (see dp_create). The + // registry's VK factory being set is what says the claim + // covers Vulkan and the vk_bundle ABI matched (#1243). + cs->has_dp_factory = e->dp_factory_vk != NULL && + xrt_plugin_iface_has_create_dp_vk_for_screen(segs->iface[n]); + } + } + + struct xrt_screen_binding *b = &segs->bindings[n]; + memset(b, 0, sizeof(*b)); + b->struct_size = (uint32_t)sizeof(*b); + b->monitor_id = s->id; + b->desktop_left = s->desktop_left; + b->desktop_top = s->desktop_top; + b->desktop_width = s->desktop_width; + b->desktop_height = s->desktop_height; + b->native_pixel_width = s->native_width; + b->native_pixel_height = s->native_height; + b->physical_width_mm = s->physical_width_mm; + b->physical_height_mm = s->physical_height_mm; + b->desktop_scale = s->desktop_scale; + snprintf(b->device_name, sizeof(b->device_name), "%s", s->device_name); + if (e != NULL) { + snprintf(b->serial, sizeof(b->serial), "%s", e->serial); + } + n++; + } + segs->screen_count = n; + + if (mixed) { + // One-off session setup line. Mixed vendors (a Leia panel next to a + // sim_display laptop panel) are multi-screen M4: the vendor DP's + // behaviour on a sub-rect canvas next to another vendor's segment is + // not established yet, so the window keeps the shipped single-DP + // path rather than risk a broken weave on the 3D panel. + U_LOG_W("segments: the screens belong to different plug-ins (primary '%s') — mixed-vendor segments are " + "multi-screen M4; a window spanning screens keeps the single-DP path", + primary_entry->plugin_id); + return; + } + segs->enabled = n >= 2; + if (segs->enabled) { + U_LOG_W("segments: %u screens, plug-in '%s' — a window spanning screens is woven per screen " + "(multi-screen M2)", + n, primary_entry->plugin_id); + } +} + +bool +comp_vk_native_segments_enabled(const struct comp_vk_native_segments *segs) +{ + return segs != NULL && segs->enabled; +} + +bool +comp_vk_native_segments_update(struct comp_vk_native_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + int32_t target_format, + bool release_safe, + uint32_t mode_index) +{ + if (segs == NULL) { + return false; + } + // Retired DPs / images go back to Vulkan only when no fill is in flight. + segs->release_safe = release_safe; + retire_drain(segs, release_safe); + segs->release_safe = false; + if (!segs->enabled || window_desktop == NULL) { + return false; + } + + comp_segments_compute(window_desktop, canvas, segs->screens, segs->screen_count, &segs->table); + + struct comp_segments_actions act; + comp_segments_lifecycle_update(&segs->lc, &segs->table, &act); + for (uint32_t a = 0; a < act.count; a++) { + const int i = screen_index_of(segs, act.items[a].screen_id); + if (i < 0) { + continue; + } + if (act.items[a].action == COMP_SEG_ACTION_CREATE) { + const bool ok = dp_create(segs, (uint32_t)i, target_format); + comp_segments_lifecycle_set_created(&segs->lc, act.items[a].screen_id, ok); + } else if (act.items[a].action == COMP_SEG_ACTION_DESTROY) { + dp_release(segs, (uint32_t)i); + } + } + + const uint32_t cw = canvas != NULL && canvas->w > 0 ? canvas->w : window_desktop->w; + const uint32_t ch = canvas != NULL && canvas->h > 0 ? canvas->h : window_desktop->h; + const bool split = comp_segments_table_is_split(&segs->table, cw, ch); + + // A DP's resample tolerance can follow the rendering mode (sim_display: + // INTERLACED needs 1:1). Re-ask on a mode change — never per frame. + if (mode_index != segs->mode_index) { + segs->mode_index = mode_index; + for (uint32_t i = 0; i < segs->screen_count; i++) { + if (segs->st[i].dp != NULL) { + segs->st[i].tolerates_resample = xrt_display_processor_vk_tolerates_resample( + (struct xrt_display_processor_vk *)segs->st[i].dp); + } + } + } + + // One INFO line per table change while split, and one on leaving it — + // a drag produces a burst, a still window logs once. Never WARN. + if (split && (!segs->have_logged_table || !comp_segments_table_equal(&segs->table, &segs->logged_table))) { + // The capability can follow a runtime mode switch (sim's 1/2/3 keys): + // re-ask on change, never per frame. + for (uint32_t i = 0; i < segs->screen_count; i++) { + if (segs->st[i].dp != NULL) { + segs->st[i].tolerates_resample = xrt_display_processor_vk_tolerates_resample( + (struct xrt_display_processor_vk *)segs->st[i].dp); + } + } + char buf[768]; + comp_segments_table_format(&segs->table, buf, sizeof(buf)); + U_LOG_I("segments: window %d,%d %ux%u -> %s", window_desktop->x, window_desktop->y, window_desktop->w, + window_desktop->h, buf); + segs->logged_table = segs->table; + segs->have_logged_table = true; + } else if (!split && segs->logged_split) { + U_LOG_I("segments: window %d,%d %ux%u is on the primary screen only — single-DP path", + window_desktop->x, window_desktop->y, window_desktop->w, window_desktop->h); + segs->have_logged_table = false; + } + segs->logged_split = split; + if (!split) { + segs->cap_count = 0; + } + return split; +} + +bool +comp_vk_native_segments_record(struct comp_vk_native_segments *segs, + const struct comp_vk_native_segments_frame *f) +{ + if (segs == NULL || f == NULL || f->cmd == VK_NULL_HANDLE || f->target_image == VK_NULL_HANDLE || + f->src_image == VK_NULL_HANDLE || f->tile_columns == 0 || f->tile_rows == 0) { + return false; + } + struct vk_bundle *vk = segs->vk; + VkCommandBuffer cmd = f->cmd; + const struct comp_segment_table *t = &segs->table; + const uint32_t cls = f->is_repaint ? SEG_CLASS_REPAINT : SEG_CLASS_APP; + + /* + * 1. Decide each segment: which DP (if any) and whether it may weave. + */ + struct xrt_display_processor *dp_of[COMP_SEGMENTS_MAX] = {0}; + struct seg_screen_state *st_of[COMP_SEGMENTS_MAX] = {0}; + bool weave[COMP_SEGMENTS_MAX] = {false}; + int32_t tx[COMP_SEGMENTS_MAX], ty[COMP_SEGMENTS_MAX]; + uint32_t tw[COMP_SEGMENTS_MAX], th[COMP_SEGMENTS_MAX]; + bool have_tile[COMP_SEGMENTS_MAX] = {false}; + + for (uint32_t k = 0; k < t->count; k++) { + const struct comp_segment *g = &t->seg[k]; + have_tile[k] = + segment_tile_rect(&g->window_rect, &f->canvas, f->view_width, f->view_height, &tx[k], &ty[k], &tw[k], &th[k]); + const int i = (int)g->screen_index; + st_of[k] = (i >= 0 && (uint32_t)i < segs->screen_count) ? &segs->st[i] : NULL; + if (g->is_primary) { + // The primary screen's 1:1 gate is the session-level one + // (#1595 / #1831), already applied to the frame's layout. + dp_of[k] = f->primary_dp; + weave[k] = f->primary_dp != NULL; + } else if (st_of[k] != NULL) { + dp_of[k] = st_of[k]->dp; + weave[k] = comp_segments_decide(dp_of[k] != NULL, st_of[k]->tolerates_resample, g->screen_1to1) == + COMP_SEG_RENDER_WEAVE; + } + if (!have_tile[k] || st_of[k] == NULL) { + weave[k] = false; + } + } + + /* + * 2. Clear the target. A segment DP confines its render pass to its + * canvas, so whatever no segment covers (the part of the window on no + * screen) would otherwise be undefined. + */ + { + image_barrier(vk, cmd, f->target_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + const VkClearColorValue clear = {.float32 = {0.0f, 0.0f, 0.0f, f->transparent_background ? 0.0f : 1.0f}}; + const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vk->vkCmdClearColorImage(cmd, f->target_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + image_barrier(vk, cmd, f->target_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); + } + + /* + * 3. Crop each woven segment's views out of the atlas — crop before the + * DP is the law (ADR-030): a DP's atlas holds exactly its canvas. + */ + image_barrier(vk, cmd, f->src_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + for (uint32_t k = 0; k < t->count; k++) { + if (!weave[k]) { + continue; + } + struct seg_screen_state *st = st_of[k]; + const uint32_t cw = tw[k] * f->tile_columns; + const uint32_t ch = th[k] * f->tile_rows; + struct seg_crop *crop = &st->crop[cls]; + if (!crop_ensure(vk, crop, cw, ch, f->src_format)) { + weave[k] = false; // no input image: flat 2D instead + continue; + } + image_barrier(vk, cmd, crop->image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + VkImageCopy regions[XRT_MAX_VIEWS]; + uint32_t rc = 0; + for (uint32_t row = 0; row < f->tile_rows; row++) { + for (uint32_t col = 0; col < f->tile_columns && rc < XRT_MAX_VIEWS; col++) { + regions[rc++] = (VkImageCopy){ + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffset = {(int32_t)(col * f->view_width) + tx[k], + (int32_t)(row * f->view_height) + ty[k], 0}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffset = {(int32_t)(col * tw[k]), (int32_t)(row * th[k]), 0}, + .extent = {tw[k], th[k], 1}, + }; + } + } + vk->vkCmdCopyImage(cmd, f->src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, crop->image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rc, regions); + image_barrier(vk, cmd, crop->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } + image_barrier(vk, cmd, f->src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + /* + * 4. Weave: the primary first (a DP built before M2 may still clear the + * whole target, so it must not run after a sibling), then left to right. + * Each gets canvas = its segment; a DP confines viewport AND scissor to + * it (plan risk 7). Between two DPs the target goes back to + * COLOR_ATTACHMENT_OPTIMAL with its contents preserved. + */ + bool any_woven = false; + for (uint32_t pass = 0; pass < 2; pass++) { + for (uint32_t k = 0; k < t->count; k++) { + const struct comp_segment *g = &t->seg[k]; + if (!weave[k] || g->is_primary != (pass == 0)) { + continue; + } + struct xrt_display_processor *dp = dp_of[k]; + struct seg_screen_state *st = st_of[k]; + if (any_woven) { + image_barrier(vk, cmd, f->target_image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); + } + if (!g->is_primary) { + struct xrt_display_processor_vk *vdp = (struct xrt_display_processor_vk *)dp; + if (!st->configured) { + st->configured = true; + xrt_display_processor_set_background_2d(dp, VK_NULL_HANDLE, 0, 0); + } + // Same declarations the primary gets, on change only: the + // atlas encoding (DXR_VK_ATLAS_ENCODING honoured by the + // caller; -1 = do not declare) and the session's + // transparency, which can toggle mid-session. + if (f->atlas_encoding >= 0 && st->encoding_latched != f->atlas_encoding) { + st->encoding_latched = f->atlas_encoding; + xrt_display_processor_set_atlas_encoding(dp, + (enum xrt_atlas_encoding)f->atlas_encoding); + } + if (st->transparent_latched != (int)f->transparent_background) { + st->transparent_latched = (int)f->transparent_background; + xrt_display_processor_vk_set_transparent_background(vdp, f->transparent_background, + false); + } + // Phase only where window px ARE device px; a resampled + // screen's origin is in the wrong units (and only a + // resample-tolerant DP weaves there, which has no phase). + if (g->screen_1to1 != COMP_SEG_1TO1_NO) { + xrt_display_processor_vk_set_present_origin(vdp, g->present_origin_x, + g->present_origin_y); + } + xrt_display_processor_set_target_color_view(dp, f->target_view); + } + xrt_display_processor_process_atlas( + dp, cmd, (VkImage_XDP)st->crop[cls].image, st->crop[cls].view, tw[k], th[k], f->tile_columns, + f->tile_rows, (VkFormat_XDP)f->src_format, f->target_fb, (VkImage_XDP)f->target_image, + f->target_width, f->target_height, (VkFormat_XDP)f->target_format, g->window_rect.x, + g->window_rect.y, g->window_rect.w, g->window_rect.h); + any_woven = true; + } + } + if (!any_woven) { + image_barrier(vk, cmd, f->target_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); + } + + /* + * 5. Flat 2D for every segment that could not be woven, and for the + * canvas no segment covers (a monitor no plug-in claimed, or off every + * screen): one view (the middle one — the left eye of a stereo pair), + * linearly scaled from the view tile to the window rect. Same content and + * seam registration as the shipped off-panel band (#1654). + */ + struct comp_seg_rect flat_dst[COMP_SEGMENTS_MAX + COMP_SEGMENTS_MAX_UNCOVERED]; + uint32_t flat_n = 0; + for (uint32_t k = 0; k < t->count; k++) { + if (!weave[k] && have_tile[k]) { + flat_dst[flat_n++] = t->seg[k].window_rect; + } + } + flat_n += comp_segments_uncovered(t, &f->canvas, &flat_dst[flat_n], COMP_SEGMENTS_MAX_UNCOVERED); + if (flat_n > 0) { + const uint32_t views = f->tile_columns * f->tile_rows; + const uint32_t vi = views > 0 ? (views - 1) / 2 : 0; + const int32_t base_x = (int32_t)((vi % f->tile_columns) * f->view_width); + const int32_t base_y = (int32_t)((vi / f->tile_columns) * f->view_height); + image_barrier(vk, cmd, f->src_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + image_barrier(vk, cmd, f->target_image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + for (uint32_t k = 0; k < flat_n; k++) { + const struct comp_seg_rect *d = &flat_dst[k]; + int32_t sx = 0, sy = 0; + uint32_t sw = 0, sh = 0; + if (!segment_tile_rect(d, &f->canvas, f->view_width, f->view_height, &sx, &sy, &sw, &sh)) { + continue; // thinner than one source pixel + } + VkImageBlit blit = { + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffsets = {{base_x + sx, base_y + sy, 0}, + {base_x + sx + (int32_t)sw, base_y + sy + (int32_t)sh, 1}}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffsets = {{d->x, d->y, 0}, {d->x + (int32_t)d->w, d->y + (int32_t)d->h, 1}}, + }; + vk->vkCmdBlitImage(cmd, f->src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, f->target_image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); + } + image_barrier(vk, cmd, f->target_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, 0, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); + image_barrier(vk, cmd, f->src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } + + // What the capture can show for this frame. + segs->cap_count = 0; + for (uint32_t k = 0; k < t->count && segs->cap_count < COMP_SEGMENTS_MAX; k++) { + struct seg_capture *c = &segs->cap[segs->cap_count++]; + c->screen_index = t->seg[k].screen_index; + c->woven = weave[k]; + c->w = weave[k] ? tw[k] * f->tile_columns : 0; + c->h = weave[k] ? th[k] * f->tile_rows : 0; + } + return any_woven; +} + +bool +comp_vk_native_segments_get_capture(const struct comp_vk_native_segments *segs, + uint32_t index, + VkImage *out_image, + uint32_t *out_w, + uint32_t *out_h, + uint64_t *out_screen_id, + bool *out_woven) +{ + if (segs == NULL || index >= segs->cap_count) { + return false; + } + const struct seg_capture *c = &segs->cap[index]; + if (c->screen_index >= segs->screen_count) { + return false; + } + const struct seg_screen_state *st = &segs->st[c->screen_index]; + // The capture fires on app frames: the app class's image. + *out_image = c->woven ? st->crop[SEG_CLASS_APP].image : VK_NULL_HANDLE; + *out_w = c->w; + *out_h = c->h; + *out_screen_id = segs->screens[c->screen_index].id; + *out_woven = c->woven; + return true; +} diff --git a/src/xrt/compositor/vk_native/comp_vk_native_segments.h b/src/xrt/compositor/vk_native/comp_vk_native_segments.h new file mode 100644 index 0000000000..dfcab24548 --- /dev/null +++ b/src/xrt/compositor/vk_native/comp_vk_native_segments.h @@ -0,0 +1,187 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen segment display processors for the in-process Vulkan + * compositor (multi-screen M2, ADR-047 D2). + * @ingroup comp_vk_native + * + * A window whose canvas covers several screens is woven per segment: each + * segment (canvas ∩ screen, see util/comp_segments.h) gets the display + * processor of ITS screen, a pre-cropped atlas holding exactly that segment's + * views, `canvas = segment rect`, and its own present origin; the results are + * composited into the one presented surface. A segment with no DP for its + * screen (a plug-in without `create_dp_vk_for_screen`), or whose DP needs 1:1 pixels on a resampled screen, gets a flat 2D + * blit of one view instead. + * + * The session's primary DP (`comp_vk_native_compositor::display_processor`) + * keeps weaving the primary screen's segment, and it keeps owning everything + * view-related (eye positions, window metrics) — per-segment views are M3. + * A window entirely on the primary screen never enters this module's record + * path: that case stays byte-for-byte the single-DP path. + * + * Scope: desktop Linux, X11/XWayland (root coordinates are desktop-absolute). + * Native Wayland stays primary-only until the window-geometry service reports + * a desktop rect (docs/specs/runtime/wayland-window-geometry.md, follow-up). + * Design note: docs/architecture/comp-segments.md. + */ + +#pragma once + +#include "xrt/xrt_compiler.h" +#include "xrt/xrt_compositor.h" +#include "xrt/xrt_display_processor.h" +#include "xrt/xrt_screen.h" +#include "xrt/xrt_vulkan_includes.h" + +#include "util/comp_segments.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct vk_bundle; +struct comp_vk_native_segments; + +/*! + * Create the segment manager. Nothing is allocated on the GPU until a segment + * needs it. + * + * @param vk The compositor's bundle (borrowed; outlives this object). + * @param cmd_pool Pool handed to segment DP factories (borrowed). + * @param dp_queue #868 runtime-owned queue the DPs must capture at creation + * (VK_NULL_HANDLE = none). + */ +struct comp_vk_native_segments * +comp_vk_native_segments_create(struct vk_bundle *vk, VkCommandPool cmd_pool, VkQueue dp_queue); + +/*! + * Tear down every segment DP and image (including everything still on the + * retire list). The caller guarantees no submitted work still references them: + * the repaint thread is stopped and every fence waited. + */ +void +comp_vk_native_segments_destroy(struct comp_vk_native_segments **segs_ptr); + +/*! + * Hand over the system's screens and DP registry. Segmentation is enabled only + * when at least two screens are listed, the registry knows the system-default + * screen, and @p pinned_display_id is 0 (`XrSessionDisplayBindingDXR` pins a + * session to one display, which turns segmentation off). + * + * @param list The screens (xrEnumerateDisplaysDXR's list). + * @param info System compositor info: the DP registry + active plug-in. + * @param pinned_display_id The session's display binding, 0 = none. + */ +void +comp_vk_native_segments_set_screens(struct comp_vk_native_segments *segs, + const struct xrt_screen_list *list, + const struct xrt_system_compositor_info *info, + uint64_t pinned_display_id); + +/*! + * Is segmentation possible at all for this session? + */ +bool +comp_vk_native_segments_enabled(const struct comp_vk_native_segments *segs); + +/*! + * One metric update: recompute the segment table for the window, run the DP + * lifecycle (create/destroy secondary DPs with hysteresis), log a table change + * once at INFO. Call once per weave, before deciding which path records. + * + * @param window_desktop The window's client area in desktop coordinates. + * @param canvas The canvas in window px (`vk_dp_canvas_rect`). + * @param target_format Swapchain format, for segment DP creation. + * @param release_safe No repaint ("fill") is parked with its command buffer + * in flight (#1264 S1 fence-park). Only then are retired + * DPs / images handed back to Vulkan; otherwise they + * wait on the retire list. + * @param mode_index The head's active rendering-mode index; a change + * re-reads each segment DP's resample tolerance. + * @return true when this frame must take the split path + * (@ref comp_vk_native_segments_record); false = the single-DP path. + */ +bool +comp_vk_native_segments_update(struct comp_vk_native_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + int32_t target_format, + bool release_safe, + uint32_t mode_index); + +/*! + * Everything the split path needs about this frame. + */ +struct comp_vk_native_segments_frame +{ + VkCommandBuffer cmd; + + //! A repaint replay (#868) rather than an app frame: selects the frame + //! class's own DP input images, so an app frame never writes what an + //! in-flight fill reads. + bool is_repaint; + + //! The DP's input this frame (atlas cropped to content), SHADER_READ_ONLY_OPTIMAL. + VkImage src_image; + VkFormat src_format; + uint32_t view_width; + uint32_t view_height; + uint32_t tile_columns; + uint32_t tile_rows; + + //! The target, in COLOR_ATTACHMENT_OPTIMAL on entry; left in PRESENT_SRC_KHR. + VkFramebuffer target_fb; + VkImage target_image; + VkImageView target_view; + uint32_t target_width; + uint32_t target_height; + VkFormat target_format; + + //! The canvas the atlas holds, window px (whole-canvas view dims above). + struct comp_seg_rect canvas; + + //! Clear colour alpha for the target outside every segment; also + //! re-declared to segment DPs when it changes. + bool transparent_background; + + //! The atlas encoding to declare to segment DPs (`enum + //! xrt_atlas_encoding`), or -1 to declare nothing (DXR_VK_ATLAS_ENCODING=off). + int atlas_encoding; + + //! The session's primary DP (weaves the primary screen's segment). The + //! caller has already fed it this frame's present origin, background, + //! timing and atlas encoding. + struct xrt_display_processor *primary_dp; +}; + +/*! + * Record the split frame: clear the target, crop each segment's views out of + * the atlas, run each segment's DP over its canvas (primary first, then left + * to right; viewport + scissor = segment), and blit flat 2D into the segments + * that cannot be woven. Leaves the target in PRESENT_SRC_KHR — the state the + * single-DP path leaves it in — so everything after the weave is unchanged. + * + * @return true if at least one DP wove. + */ +bool +comp_vk_native_segments_record(struct comp_vk_native_segments *segs, + const struct comp_vk_native_segments_frame *f); + +/*! + * The cropped DP input of segment @p index of the LAST split frame (for the + * atlas capture: one PNG per segment proves the canvases). False past the end + * or when the last frame was not split. + */ +bool +comp_vk_native_segments_get_capture(const struct comp_vk_native_segments *segs, + uint32_t index, + VkImage *out_image, + uint32_t *out_w, + uint32_t *out_h, + uint64_t *out_screen_id, + bool *out_woven); + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/drivers/sim_display/sim_display_interface.h b/src/xrt/drivers/sim_display/sim_display_interface.h index 2bb00592c1..da7003b578 100644 --- a/src/xrt/drivers/sim_display/sim_display_interface.h +++ b/src/xrt/drivers/sim_display/sim_display_interface.h @@ -25,6 +25,8 @@ struct xrt_display_processor_d3d12; struct xrt_display_processor_metal; struct xrt_display_processor_gl; struct vk_bundle; +struct xrt_plugin_instance; +struct xrt_screen_binding; /*! * @defgroup drv_sim_display Simulation 3D Display Driver @@ -372,6 +374,24 @@ sim_display_dp_factory_vk(void *vk_bundle, int32_t target_format, struct xrt_display_processor **out_xdp); +/*! + * Per-screen twin of @ref sim_display_dp_factory_vk (multi-screen M2) — + * `xrt_plugin_iface::create_dp_vk_for_screen`. The DP describes the bound + * screen (its EDID size, native pixels, desktop origin), confines its output to + * the canvas it is handed, and preserves the target outside it, so several of + * these can weave one window's segments into one surface. + * + * @ingroup drv_sim_display + */ +xrt_result_t +sim_display_dp_factory_vk_for_screen(struct xrt_plugin_instance *inst, + void *vk_bundle, + void *vk_cmd_pool, + void *window_handle, + int32_t target_format, + const struct xrt_screen_binding *binding, + struct xrt_display_processor **out_xdp); + /*! * Create a simulation Metal display processor. * diff --git a/src/xrt/drivers/sim_display/sim_display_plugin.c b/src/xrt/drivers/sim_display/sim_display_plugin.c index d1b4fd2d9f..464595d421 100644 --- a/src/xrt/drivers/sim_display/sim_display_plugin.c +++ b/src/xrt/drivers/sim_display/sim_display_plugin.c @@ -364,6 +364,13 @@ static struct xrt_plugin_iface g_sim_display_iface = { /* Multi-screen M1: per-monitor display info for every monitor sim wins. */ .get_display_info_for_monitor = sim_display_plugin_get_display_info_for_monitor, + + /* Multi-screen M2: one VK DP per screen a spanning window covers. */ +#if defined(XRT_HAVE_VULKAN) || !defined(_WIN32) + .create_dp_vk_for_screen = sim_display_dp_factory_vk_for_screen, +#else + .create_dp_vk_for_screen = NULL, +#endif }; diff --git a/src/xrt/drivers/sim_display/sim_display_processor.c b/src/xrt/drivers/sim_display/sim_display_processor.c index 49d170d1d4..e21b8ea2df 100644 --- a/src/xrt/drivers/sim_display/sim_display_processor.c +++ b/src/xrt/drivers/sim_display/sim_display_processor.c @@ -16,10 +16,12 @@ #include "sim_display_interface.h" #include "sim_display_zone_common.h" +#include "sim_display_scanout_common.h" #include "xrt/xrt_display_processor.h" #include "xrt/xrt_display_processor_vk.h" // the snap_window_rect slot (#1588 / #1609) #include "xrt/xrt_display_metrics.h" +#include "xrt/xrt_plugin.h" // xrt_screen_binding (multi-screen M2) #include "vk/vk_helpers.h" #include "util/u_debug.h" @@ -163,6 +165,37 @@ struct sim_display_processor //! SIM_DISPLAY_FAKE_LENS — the fake lens state (true = 3D, the default). bool fake_lens_3d; + + /*! + * Multi-screen M2: per-instance screen. Set when the DP was created for a + * screen (`create_dp_vk_for_screen`); then the DP describes THAT screen + * (`get_display_dimensions` / `get_display_pixel_info`) instead of the + * process-wide panel, and it is a SEGMENT DP — it must not assume it is + * the first writer to the target, so a sub-rect canvas preserves the + * pixels outside it (@ref render_pass_preserve). + */ + bool screen_bound; + uint64_t screen_monitor_id; + float screen_w_m, screen_h_m; + uint32_t screen_px_w, screen_px_h; + int32_t screen_left, screen_top; + + //! Sub-rect render pass for a segment DP: initialLayout + //! COLOR_ATTACHMENT_OPTIMAL (the compositor's contract before every + //! process_atlas) and a clear confined to the render area, so the rest + //! of the target — other segments — survives. Compatible with + //! @ref render_pass, so the same pipelines and framebuffers serve both. + VkRenderPass render_pass_preserve; + + //! set_present_origin (runtime#757 / ADR-033): the window's origin on + //! this DP's screen. The interlaced output's phase input; every other + //! mode is position-preserving and ignores it. + bool have_present_origin; + int32_t present_origin_x, present_origin_y; + + //! get_scanout_caps: last flags reported (log on change only). + bool caps_reported; + uint32_t caps_last_flags; }; static inline struct sim_display_processor * @@ -247,7 +280,14 @@ sim_dp_process_atlas(struct xrt_display_processor *xdp, if (changed) { float panel_w_m = 0.0f, panel_h_m = 0.0f; uint32_t panel_px_w = 0, panel_px_h = 0; - sim_display_get_panel_metrics(&panel_w_m, &panel_h_m, &panel_px_w, &panel_px_h); + if (sdp->screen_bound) { + panel_w_m = sdp->screen_w_m; + panel_h_m = sdp->screen_h_m; + panel_px_w = sdp->screen_px_w; + panel_px_h = sdp->screen_px_h; + } else { + sim_display_get_panel_metrics(&panel_w_m, &panel_h_m, &panel_px_w, &panel_px_h); + } const bool fills_panel = panel_px_w == target_width && panel_px_h == target_height; const bool at_origin = canvas_offset_x == 0 && canvas_offset_y == 0; @@ -267,10 +307,10 @@ sim_dp_process_atlas(struct xrt_display_processor *xdp, canvas_offset_x, canvas_offset_y, canvas_width, canvas_height, at_origin ? " (panel origin)" : " (displaced — phase-critical for a real weaver)", view_width, view_height, tile_columns, tile_rows, - phase_visible ? "SIM_DISPLAY_OUTPUT=interlaced USES the offset as the " - "interlace phase, so a phase error is visible in the output." - : "This mode ignores the offset (sim_display has no " - "set_present_origin slot), so phase errors are INVISIBLE in " + phase_visible ? "SIM_DISPLAY_OUTPUT=interlaced USES the present origin (else the " + "offset) as the interlace phase, so a phase error is visible in the " + "output." + : "This mode ignores the phase, so phase errors are INVISIBLE in " "what it draws — run SIM_DISPLAY_OUTPUT=interlaced to see them."); sdp->geom_reported = true; @@ -317,18 +357,67 @@ sim_dp_process_atlas(struct xrt_display_processor *xdp, vk->vkUpdateDescriptorSets(vk->device, 1, &write, 0, NULL); + /* + * The canvas: where in the target this DP writes (multi-screen M2 — a + * window split across screens hands each screen's DP its own segment, + * and the atlas it is handed holds exactly that segment's views). A zero + * size is the ABI's "fills the full target" sentinel. Clamped to the + * target: a canvas outside it is not a rect any weaver can honour. + */ + int32_t cx = canvas_offset_x, cy = canvas_offset_y; + uint32_t cw = canvas_width, ch = canvas_height; + if (cw == 0 || ch == 0) { + cx = 0; + cy = 0; + cw = target_width; + ch = target_height; + } + if (cx < 0) { + cw = (uint32_t)((int64_t)cw + cx > 0 ? (int64_t)cw + cx : 0); + cx = 0; + } + if (cy < 0) { + ch = (uint32_t)((int64_t)ch + cy > 0 ? (int64_t)ch + cy : 0); + cy = 0; + } + if ((uint32_t)cx >= target_width || (uint32_t)cy >= target_height || cw == 0 || ch == 0) { + return; // nothing of the canvas is in the target + } + if ((uint32_t)cx + cw > target_width) { + cw = target_width - (uint32_t)cx; + } + if ((uint32_t)cy + ch > target_height) { + ch = target_height - (uint32_t)cy; + } + const bool full_target = cx == 0 && cy == 0 && cw == target_width && ch == target_height; + // Begin render pass. Clear to alpha=0 when a transparent background was // requested so any region the weave doesn't fully overwrite stays // see-through instead of opaque black (issue #392). + // + // A full-target canvas is exactly the pre-M2 pass. A sub-rect canvas + // always confines the draw (viewport + scissor) to the canvas; a SEGMENT + // DP additionally confines the clear and the layout contract to it, so + // the segments a sibling DP already wove survive. + // + // Confining the draw is deliberate for EVERY caller, bound or not: it is + // the DP contract (process_atlas: the canvas is the region the DP + // writes; comp_multi passes its zone-3D rect so "the DP confines the + // weave to it"), and the split path needs it from the unbound primary. + // It differs from pre-M2 only when the canvas is smaller than the + // target — the window-shrink transient before the swapchain catches up, + // the shared-texture path, a comp_multi zone canvas — where the old pass + // stretched a canvas-sized atlas over the whole target. + const bool preserve = !full_target && sdp->screen_bound && sdp->render_pass_preserve != VK_NULL_HANDLE; VkClearValue clear_value = {.color = {{0.0f, 0.0f, 0.0f, sdp->transparent_bg ? 0.0f : 1.0f}}}; VkRenderPassBeginInfo rp_begin = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, - .renderPass = sdp->render_pass, + .renderPass = preserve ? sdp->render_pass_preserve : sdp->render_pass, .framebuffer = target_fb, .renderArea = { - .offset = {0, 0}, - .extent = {target_width, target_height}, + .offset = {preserve ? cx : 0, preserve ? cy : 0}, + .extent = {preserve ? cw : target_width, preserve ? ch : target_height}, }, .clearValueCount = 1, .pClearValues = &clear_value, @@ -347,28 +436,39 @@ sim_dp_process_atlas(struct xrt_display_processor *xdp, .inv_tile_rows = 1.0f / (float)tile_rows, .tile_columns = (float)tile_columns, .tile_rows = (float)tile_rows, - // #817: the interlace phase is the target's panel-relative X origin. - .phase_px = (float)canvas_offset_x, + // #817: the interlace phase. The panel column of a fragment is + // gl_FragCoord.x (target px, so it already includes the canvas + // offset) + the target's origin on the panel. A SEGMENT DP (bound to + // a screen, multi-screen M2) knows that origin exactly from + // set_present_origin. The unbound DP — every pre-M2 caller — keeps + // the pre-M2 proxy (the canvas offset) byte-for-byte, whatever origin + // it was fed: it reports its panel at (0,0), so an origin relative to + // a panel elsewhere would put the phase on the wrong lattice. + .phase_px = (sdp->screen_bound && sdp->have_present_origin) ? (float)sdp->present_origin_x + : (float)canvas_offset_x, .period_px = (float)sdp->interlace_period_px, }; vk->vkCmdPushConstants(cmd_buffer, sdp->pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); - // Set dynamic viewport and scissor + // Set dynamic viewport and scissor — BOTH to the canvas (multi-screen + // plan risk 7: a viewport alone lets a fullscreen triangle's clip + // guard band bleed, a scissor alone squeezes nothing). A full-target + // canvas is the pre-M2 state exactly. VkViewport viewport = { - .x = 0.0f, - .y = 0.0f, - .width = (float)target_width, - .height = (float)target_height, + .x = (float)cx, + .y = (float)cy, + .width = (float)cw, + .height = (float)ch, .minDepth = 0.0f, .maxDepth = 1.0f, }; vk->vkCmdSetViewport(cmd_buffer, 0, 1, &viewport); VkRect2D scissor = { - .offset = {0, 0}, - .extent = {target_width, target_height}, + .offset = {cx, cy}, + .extent = {cw, ch}, }; vk->vkCmdSetScissor(cmd_buffer, 0, 1, &scissor); @@ -443,6 +543,23 @@ create_pipeline_resources(struct sim_display_processor *sdp, int32_t target_form return false; } + // Multi-screen M2: the segment variant. Starts from the layout the + // compositor guarantees before every process_atlas instead of UNDEFINED + // (which would let the driver discard what sibling segment DPs wrote); + // the clear is confined to the render area. Same attachment format and + // sample count => render-pass compatible with the one above. + if (sdp->screen_bound) { + VkAttachmentDescription keep = color_attachment; + keep.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + VkRenderPassCreateInfo keep_info = rp_info; + keep_info.pAttachments = &keep; + ret = vk->vkCreateRenderPass(vk->device, &keep_info, NULL, &sdp->render_pass_preserve); + if (ret != VK_SUCCESS) { + U_LOG_E("sim_display: Failed to create the segment render pass: %d", ret); + return false; + } + } + // 2. Create descriptor set layout (1 combined image sampler for atlas) VkDescriptorSetLayoutBinding binding = { .binding = 0, @@ -674,8 +791,106 @@ create_pipeline_resources(struct sim_display_processor *sdp, int32_t target_form } -// #856: panel geometry -> compositor computes window-scoped Kooima. -SIM_ZONE_DEFINE_PANEL_METRIC_FNS(sim_dp, xrt_display_processor) +/* + * #856: panel geometry -> compositor computes window-scoped Kooima. + * + * Multi-screen M2: a DP created for a screen answers for THAT screen — its + * EDID size, its native pixels and its desktop origin. An unbound DP (the + * session's primary, from the plain factory) answers exactly as before, from + * the process-wide panel at the desktop origin. + */ +static bool +sim_dp_get_display_dimensions(struct xrt_display_processor *xdp, float *out_width_m, float *out_height_m) +{ + struct sim_display_processor *sdp = sim_display_processor(xdp); + if (out_width_m == NULL || out_height_m == NULL) { + return false; + } + if (sdp->screen_bound) { + *out_width_m = sdp->screen_w_m; + *out_height_m = sdp->screen_h_m; + } else { + sim_display_get_panel_metrics(out_width_m, out_height_m, NULL, NULL); + } + return (*out_width_m > 0.0f && *out_height_m > 0.0f); +} + +static bool +sim_dp_get_display_pixel_info(struct xrt_display_processor *xdp, + uint32_t *out_pixel_width, + uint32_t *out_pixel_height, + int32_t *out_screen_left, + int32_t *out_screen_top) +{ + struct sim_display_processor *sdp = sim_display_processor(xdp); + if (out_pixel_width == NULL || out_pixel_height == NULL) { + return false; + } + int32_t left = 0, top = 0; // unbound: sim models a single panel at the desktop origin + if (sdp->screen_bound) { + *out_pixel_width = sdp->screen_px_w; + *out_pixel_height = sdp->screen_px_h; + left = sdp->screen_left; + top = sdp->screen_top; + } else { + sim_display_get_panel_metrics(NULL, NULL, out_pixel_width, out_pixel_height); + } + if (out_screen_left != NULL) { + *out_screen_left = left; + } + if (out_screen_top != NULL) { + *out_screen_top = top; + } + return (*out_pixel_width > 0 && *out_pixel_height > 0); +} + +/*! + * set_present_origin (runtime#757 / ADR-033): the window's client-area origin + * on this DP's screen. Sticky. Only the interlaced output consumes it (as its + * phase); every other mode draws the same pixels wherever the window is. + */ +static void +sim_dp_set_present_origin(struct xrt_display_processor_vk *xdp, int32_t panel_x, int32_t panel_y) +{ + struct sim_display_processor *sdp = (struct sim_display_processor *)xdp; + sdp->have_present_origin = true; + sdp->present_origin_x = panel_x; + sdp->present_origin_y = panel_y; +} + +/*! + * get_scanout_caps: the weave scope (SIM_DISPLAY_WEAVE_SCOPE, canvas by + * default) and, for multi-screen M2's per-segment 1:1 gate, whether the + * output survives a display-server resample. Every sim output does except + * INTERLACED, whose 1-pixel period is exactly what a resample destroys. + * Read from the CURRENT output mode on every call (the 1/2/3 keys can change + * it), so the runtime re-queries when it re-evaluates a segment. Logs on + * change only. + */ +static bool +sim_dp_get_scanout_caps(struct xrt_display_processor_vk *xdp, struct xrt_dp_scanout_caps *out_caps) +{ + struct sim_display_processor *sdp = (struct sim_display_processor *)xdp; + if (out_caps == NULL || out_caps->struct_size < XRT_DP_SCANOUT_CAPS_SIZE_V1) { + return false; + } + const enum xrt_dp_weave_scope scope = sim_scanout_scope_from_env(); + const bool interlaced = sim_display_get_output_mode() == SIM_DISPLAY_OUTPUT_INTERLACED; + const uint32_t flags = interlaced ? 0u : XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE; + out_caps->weave_scope = (uint32_t)scope; + out_caps->flags = flags; + for (size_t i = 0; i < sizeof(out_caps->reserved) / sizeof(out_caps->reserved[0]); i++) { + out_caps->reserved[i] = 0; + } + if (!sdp->caps_reported || sdp->caps_last_flags != flags) { + sdp->caps_reported = true; + sdp->caps_last_flags = flags; + U_LOG_I("sim_display VK DP %p: weave scope '%s', %s", (void *)sdp, xrt_dp_weave_scope_name(scope), + interlaced ? "INTERLACED needs 1:1 pixels (a resample destroys the pattern)" + : "output survives a display-server resample"); + } + return true; +} static bool sim_dp_get_predicted_eye_positions(struct xrt_display_processor *xdp, struct xrt_eye_positions *out) @@ -764,6 +979,9 @@ sim_dp_destroy(struct xrt_display_processor *xdp) if (sdp->render_pass != VK_NULL_HANDLE) { vk->vkDestroyRenderPass(vk->device, sdp->render_pass, NULL); } + if (sdp->render_pass_preserve != VK_NULL_HANDLE) { + vk->vkDestroyRenderPass(vk->device, sdp->render_pass_preserve, NULL); + } } free(sdp); @@ -1001,11 +1219,47 @@ sim_dp_clear_local_zone_mask(struct xrt_display_processor *xdp) * */ -xrt_result_t -sim_display_processor_create(enum sim_display_output_mode mode, - struct vk_bundle *vk, - int32_t target_format, - struct xrt_display_processor **out_xdp) +/*! + * Seed the per-instance screen from a binding (multi-screen M2). Physical size + * from the EDID mm, else the process-wide panel's (the SIM_DISPLAY_* env / + * default); pixels from the device mode, else the desktop size; origin = the + * screen's desktop origin. + */ +static void +sim_dp_bind_screen(struct sim_display_processor *sdp, const struct xrt_screen_binding *b) +{ + if (b == NULL || b->struct_size < offsetof(struct xrt_screen_binding, desktop_scale)) { + return; + } + float def_w_m = 0.0f, def_h_m = 0.0f; + uint32_t def_px_w = 0, def_px_h = 0; + sim_display_get_panel_metrics(&def_w_m, &def_h_m, &def_px_w, &def_px_h); + + sdp->screen_bound = true; + sdp->screen_monitor_id = b->monitor_id; + sdp->screen_left = b->desktop_left; + sdp->screen_top = b->desktop_top; + sdp->screen_px_w = b->native_pixel_width != 0 ? b->native_pixel_width : b->desktop_width; + sdp->screen_px_h = b->native_pixel_height != 0 ? b->native_pixel_height : b->desktop_height; + if (sdp->screen_px_w == 0 || sdp->screen_px_h == 0) { + sdp->screen_px_w = def_px_w; + sdp->screen_px_h = def_px_h; + } + if (b->physical_width_mm != 0 && b->physical_height_mm != 0) { + sdp->screen_w_m = (float)b->physical_width_mm / 1000.0f; + sdp->screen_h_m = (float)b->physical_height_mm / 1000.0f; + } else { + sdp->screen_w_m = def_w_m; + sdp->screen_h_m = def_h_m; + } +} + +static xrt_result_t +sim_display_processor_create_impl(enum sim_display_output_mode mode, + struct vk_bundle *vk, + int32_t target_format, + const struct xrt_screen_binding *binding, + struct xrt_display_processor **out_xdp) { if (out_xdp == NULL) { return XRT_ERROR_DEVICE_CREATION_FAILED; @@ -1015,6 +1269,7 @@ sim_display_processor_create(enum sim_display_output_mode mode, if (sdp == NULL) { return XRT_ERROR_ALLOCATION; } + sim_dp_bind_screen(sdp, binding); // ADR-020 rule 1: advertise the vtable size so the runtime knows which // slots this plug-in actually built (calloc already zeroed reserved_0). @@ -1025,6 +1280,8 @@ sim_display_processor_create(enum sim_display_output_mode mode, sdp->base_vk.base.struct_size = (uint32_t)sizeof(struct xrt_display_processor_vk); sdp->base_vk.snap_window_rect = sim_dp_snap_window_rect; // #1588 / #1609 sdp->base_vk.set_transparency_active = sim_dp_set_transparency_active; // lazy transparency + sdp->base_vk.set_present_origin = sim_dp_set_present_origin; // ADR-033 / multi-screen M2 + sdp->base_vk.get_scanout_caps = sim_dp_get_scanout_caps; // multi-screen M2 1:1 gate sdp->base_vk.base.destroy = sim_dp_destroy; sdp->base_vk.base.get_render_pass = sim_dp_get_render_pass; sdp->base_vk.base.get_predicted_eye_positions = sim_dp_get_predicted_eye_positions; @@ -1077,7 +1334,10 @@ sim_display_processor_create(enum sim_display_output_mode mode, return XRT_ERROR_VULKAN; } - // Set the initial output mode (atomic global read by process_atlas each frame) + // Set the initial output mode (atomic global read by process_atlas each + // frame). The output mode is SESSION state — one atlas, one rendering + // mode, switched by the 1/2/3 keys — so a segment DP follows it rather + // than carrying its own (multi-screen M2). sim_display_set_output_mode(mode); U_LOG_W("Created sim display processor (all %d pipelines), initial mode: %s", SIM_DP_PIPELINE_COUNT, @@ -1088,10 +1348,25 @@ sim_display_processor_create(enum sim_display_output_mode mode, mode == SIM_DISPLAY_OUTPUT_INTERLACED ? "Interlaced" : mode == SIM_DISPLAY_OUTPUT_PASSTHROUGH ? "Passthrough" : "Blend"); + if (sdp->screen_bound) { + U_LOG_W("sim_display: segment DP %p for screen 0x%016llx — %ux%u px, %.3f x %.3f m, desktop origin (%d, %d)", + (void *)sdp, (unsigned long long)sdp->screen_monitor_id, sdp->screen_px_w, sdp->screen_px_h, + (double)sdp->screen_w_m, (double)sdp->screen_h_m, sdp->screen_left, sdp->screen_top); + } + *out_xdp = &sdp->base_vk.base; return XRT_SUCCESS; } +xrt_result_t +sim_display_processor_create(enum sim_display_output_mode mode, + struct vk_bundle *vk, + int32_t target_format, + struct xrt_display_processor **out_xdp) +{ + return sim_display_processor_create_impl(mode, vk, target_format, NULL, out_xdp); +} + /* * @@ -1114,3 +1389,24 @@ sim_display_dp_factory_vk(void *vk_bundle_ptr, return sim_display_processor_create(mode, vk, target_format, out_xdp); } + +xrt_result_t +sim_display_dp_factory_vk_for_screen(struct xrt_plugin_instance *inst, + void *vk_bundle_ptr, + void *vk_cmd_pool, + void *window_handle, + int32_t target_format, + const struct xrt_screen_binding *binding, + struct xrt_display_processor **out_xdp) +{ + (void)inst; + (void)vk_cmd_pool; + (void)window_handle; + if (binding == NULL) { + return XRT_ERROR_DEVICE_CREATION_FAILED; + } + struct vk_bundle *vk = (struct vk_bundle *)vk_bundle_ptr; + // The CURRENT session mode: creating a segment DP must not reset what the + // user switched to (create() republishes the mode it is given). + return sim_display_processor_create_impl(sim_display_get_output_mode(), vk, target_format, binding, out_xdp); +} diff --git a/src/xrt/include/xrt/xrt_display_processor_vk.h b/src/xrt/include/xrt/xrt_display_processor_vk.h index 8366959561..5581434e36 100644 --- a/src/xrt/include/xrt/xrt_display_processor_vk.h +++ b/src/xrt/include/xrt/xrt_display_processor_vk.h @@ -1157,6 +1157,25 @@ xrt_display_processor_vk_get_weave_scope(struct xrt_display_processor_vk *xdp) return xrt_dp_weave_scope_clamp(caps.weave_scope); } +/*! + * Does the DP's output survive a display-server resample + * (@ref XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE)? Every "didn't answer" case — + * absent slot, NULL, false return — is false: the conservative reading, under + * which the runtime's 1:1 gates behave exactly as they did before the flag. + * + * @public @memberof xrt_display_processor_vk + */ +static inline bool +xrt_display_processor_vk_tolerates_resample(struct xrt_display_processor_vk *xdp) +{ + struct xrt_dp_scanout_caps caps; + xrt_dp_scanout_caps_init(&caps); + if (!xrt_display_processor_vk_get_scanout_caps(xdp, &caps)) { + return false; + } + return (caps.flags & XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE) != 0; +} + /*! * @copydoc xrt_display_processor_vk::get_background_preview * diff --git a/src/xrt/include/xrt/xrt_display_scanout.h b/src/xrt/include/xrt/xrt_display_scanout.h index ef1df93a9d..efb47dcc70 100644 --- a/src/xrt/include/xrt/xrt_display_scanout.h +++ b/src/xrt/include/xrt/xrt_display_scanout.h @@ -112,10 +112,32 @@ struct xrt_dp_scanout_caps //! @ref XRT_DP_WEAVE_SCOPE_CANVAS (see @ref xrt_dp_weave_scope_clamp). uint32_t weave_scope; + //! `XRT_DP_SCANOUT_FLAG_*` (multi-screen M2). Carved out of the former + //! `reserved[0]`: same size and offsets, and a plug-in built before the + //! field existed wrote 0 here — which is the conservative default (the + //! weave needs 1:1 pixels), so nothing changes for it. + uint32_t flags; + //! Reserved for future scanout capabilities. Plug-ins MUST write 0. - uint32_t reserved[6]; + uint32_t reserved[5]; }; +/*! + * @ref xrt_dp_scanout_caps::flags: the DP's output survives the display + * server resampling the window (a fractionally scaled desktop, an X11 window + * under XWayland at a non-integer scale). True for outputs that are not + * pixel-period patterns — anaglyph, side-by-side, blend, passthrough. False + * (the default, bit clear) for a lenticular weave or any 1-pixel-period + * interlace, which a resample turns into a uniform double image. + * + * The runtime's refuse-rather-than-resample gates (#1595 Wayland, #1831 X11, + * and the per-segment gate of multi-screen M2) degrade a session or a segment + * to flat 2D only when the DP does NOT set this bit. + * + * @ingroup xrt_iface + */ +#define XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE (1u << 0) + /*! * Size of the V1 caps shape. Plug-ins accept any caller `struct_size >= ` this * and reject only callers older than the slot itself. diff --git a/src/xrt/include/xrt/xrt_plugin.h b/src/xrt/include/xrt/xrt_plugin.h index d62d91e70f..d3e1034305 100644 --- a/src/xrt/include/xrt/xrt_plugin.h +++ b/src/xrt/include/xrt/xrt_plugin.h @@ -234,6 +234,60 @@ struct xrt_display_physical uint32_t native_pixel_height; }; +/*! + * The screen a display processor is created for (multi-screen M2, ADR-047 + * D2): which monitor, where it sits on the desktop, and its physical facts. + * Handed by pointer to `xrt_plugin_iface::create_dp_vk_for_screen` so the DP + * can describe THAT screen (its own panel size, pixel size and desktop + * origin) instead of the plug-in's one process-wide panel. + * + * The runtime sets `struct_size` to its own `sizeof`; a plug-in MUST NOT + * read past it. Grows by appending (never passed as an array). + */ +struct xrt_screen_binding +{ + /*! `sizeof(struct xrt_screen_binding)` at the runtime's compile time. */ + uint32_t struct_size; + + /*! Reserved for alignment. Must be 0. */ + uint32_t reserved_0; + + /*! Registry monitor id (`xrt_display_descriptor::monitor_id`). */ + uint64_t monitor_id; + + /*! The monitor's desktop rect, in the space the OS places windows in + * (X11: the root window). */ + int32_t desktop_left; + int32_t desktop_top; + uint32_t desktop_width; + uint32_t desktop_height; + + /*! The connector's device (native) mode; 0 = unknown. Equal to the + * desktop size exactly when window pixels reach the panel unresampled. */ + uint32_t native_pixel_width; + uint32_t native_pixel_height; + + /*! EDID physical size, millimetres; 0 = unknown. */ + uint32_t physical_width_mm; + uint32_t physical_height_mm; + + /*! Desktop compositor scale for this output; 0 = unknown. */ + float desktop_scale; + + /*! Reserved for alignment. Must be 0. */ + uint32_t reserved_1; + + /*! Vendor display identity for the plug-in's own SDK (e.g. the LeiaSR + * `displayId` of SR-P1/P2); 0 = none. Opaque to the runtime. */ + uint64_t display_id; + + /*! Vendor serial from the winning claim ("" = n/a). */ + char serial[64]; + + /*! OS output name (X11: RandR output, else the DRM connector); "" = unknown. */ + char device_name[64]; +}; + /*! * How sure a plug-in is that a monitor is its hardware. The runtime * resolves competing claims for the same monitor by highest confidence @@ -1182,6 +1236,45 @@ struct xrt_plugin_iface const struct xrt_display_descriptor *display, const struct xrt_display_physical *physical, struct xrt_plugin_display_info *out_info); + + /*! + * Create a Vulkan display processor for ONE screen this plug-in won in + * the per-monitor registry (multi-screen M2, ADR-047 D2: a window that + * spans several screens is woven per segment by each screen's own DP). + * + * Same contract as @ref create_dp_vk (`xrt_dp_factory_vk_fn_t`), plus: + * - @p inst is this plug-in's instance (NULL for a plug-in without + * instance state). + * - @p binding names the screen (monitor id, desktop rect, native px, + * mm, serial, vendor display id); never NULL. The DP should answer + * `get_display_dimensions` / `get_display_pixel_info` for THAT screen, + * with `out_screen_left/top` = the binding's desktop origin. + * - @p window_handle is NULL on desktop Linux: a segment DP is + * windowless and gets its phase from `set_present_origin` (ADR-033). + * - The compositor calls it with a canvas that is the segment (a + * sub-rect of the target) and a pre-cropped atlas holding exactly that + * segment's views; the DP MUST confine its output — render area, + * viewport AND scissor — to the canvas (multi-screen plan risk 7), and + * must not assume it is the first writer to the target this frame. + * - Several instances may coexist in one process, one per screen. + * + * Optional. NULL (or a plug-in whose `struct_size` predates this field) + * ⟹ the plug-in's screens other than the session's primary one get a + * flat 2D view, never a @ref create_dp_vk DP: that DP describes the + * plug-in's one panel and is not bound by the canvas / not-first-writer + * contract above, so it could wipe the segment woven before it. + * Appended per ADR-020 (append-only within a major; gated by + * @ref struct_size; no XRT_PLUGIN_API_VERSION_CURRENT bump) after + * @ref get_display_info_for_monitor. Announced by + * @ref XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN. + */ + xrt_result_t (*create_dp_vk_for_screen)(struct xrt_plugin_instance *inst, + void *vk_bundle, + void *vk_cmd_pool, + void *window_handle, + int32_t target_format, + const struct xrt_screen_binding *binding, + struct xrt_display_processor **out_xdp); }; /*! @@ -1229,6 +1322,27 @@ xrt_plugin_iface_has_display_info_for_monitor(const struct xrt_plugin_iface *ifa iface->get_display_info_for_monitor != NULL; } +/*! + * Defined when @ref xrt_plugin_iface carries @ref + * xrt_plugin_iface::create_dp_vk_for_screen and this header defines @ref + * xrt_screen_binding (multi-screen M2), so a plug-in built against an older + * runtime header can #ifdef-guard implementing it. + */ +#define XRT_PLUGIN_IFACE_HAS_CREATE_DP_FOR_SCREEN 1 + +/*! + * True when @p iface implements @ref xrt_plugin_iface::create_dp_vk_for_screen + * (and its struct_size covers the slot). + */ +static inline bool +xrt_plugin_iface_has_create_dp_vk_for_screen(const struct xrt_plugin_iface *iface) +{ + return iface != NULL && + iface->struct_size >= offsetof(struct xrt_plugin_iface, create_dp_vk_for_screen) + + sizeof(iface->create_dp_vk_for_screen) && + iface->create_dp_vk_for_screen != NULL; +} + /*! * True when @p iface fills the whole stereo camera slot set (and its * struct_size covers it). diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index f33ded58b4..27247b34da 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -5624,6 +5624,24 @@ oxr_session_create(struct oxr_logger *log, } #endif +#if defined(XRT_HAVE_VK_NATIVE_COMPOSITOR) && defined(XRT_OS_LINUX_DESKTOP) + // Multi-screen M2: the in-process Vulkan compositor weaves a window that + // spans screens per segment, each with its own screen's DP; it needs the + // screen list (and stays single-DP for a session pinned to one display). + if (sess->is_vk_native_compositor && sess->xcn != NULL) { + struct xrt_screen_list *screens = U_TYPED_CALLOC(struct xrt_screen_list); + if (screens != NULL) { + oxr_system_get_screens(sys, screens); + uint64_t pinned = 0; +#ifdef OXR_HAVE_DXR_display_info + pinned = bound_display_id; +#endif + comp_vk_native_compositor_set_screens(&sess->xcn->base, screens, pinned); + free(screens); + } + } +#endif + #ifdef OXR_HAVE_DXR_depth_budget // XR_DXR_depth_budget: the create-info chain is gone by locate time, and // the rear-depth DEFAULT is a function of transparency, so remember it. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1db7356e37..6603a74489 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,6 +56,7 @@ set(tests tests_aux_weave_flat tests_aux_display_mode_hold tests_comp_rear_budget + tests_comp_segments tests_comp_lazy_transparency tests_comp_overlay_2d_vk tests_oxr_weave_latch @@ -589,6 +590,13 @@ target_sources( target_include_directories(tests_comp_rear_budget PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/compositor) target_link_libraries(tests_comp_rear_budget PRIVATE xrt-interfaces aux_os xrt-external-stb) +# Multi-screen M2: the segment table, the per-segment 1:1 policy and the DP +# lifecycle hysteresis. Pure math -- compiled into the unit like +# tests_comp_rear_budget instead of linking the Vulkan-gated comp_util. +target_sources(tests_comp_segments PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/compositor/util/comp_segments.c) +target_include_directories(tests_comp_segments PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/compositor) +target_link_libraries(tests_comp_segments PRIVATE xrt-interfaces aux_util) + if(XRT_BUILD_DRIVER_HANDTRACKING) target_link_libraries( tests_levenbergmarquardt diff --git a/tests/tests_comp_segments.cpp b/tests/tests_comp_segments.cpp new file mode 100644 index 0000000000..05a1f726ce --- /dev/null +++ b/tests/tests_comp_segments.cpp @@ -0,0 +1,424 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief comp_segments (multi-screen M2): segment table math, the per-segment + * 1:1 policy and the DP lifecycle hysteresis. + */ + +#include "util/comp_segments.h" + +#include "catch_amalgamated.hpp" + +#include + +namespace { + +// ds1-linux as M0/M1 report it: the laptop panel at the origin, the DS1 to +// its right. eDP-1 is scaled (desktop != device mode), HDMI-1 is 1:1. +comp_segments_screen +screen(uint64_t id, int32_t x, int32_t y, uint32_t w, uint32_t h, uint32_t nw, uint32_t nh, bool primary) +{ + comp_segments_screen s{}; + s.id = id; + s.desktop = {x, y, w, h}; + s.native_w = nw; + s.native_h = nh; + s.is_primary = primary; + s.has_dp_factory = true; + std::strcpy(s.plugin_id, "sim-display"); + return s; +} + +const uint64_t EDP = 0x1001; +const uint64_t DS1 = 0x2002; + +} // namespace + +TEST_CASE("comp_segments: 1:1 classification") +{ + auto a = screen(EDP, 0, 0, 3456, 2160, 3456, 2160, true); + CHECK(comp_segments_screen_1to1(&a) == COMP_SEG_1TO1_YES); + auto b = screen(EDP, 0, 0, 4148, 2592, 3456, 2160, true); + CHECK(comp_segments_screen_1to1(&b) == COMP_SEG_1TO1_NO); + auto c = screen(EDP, 0, 0, 3456, 2160, 0, 0, true); + CHECK(comp_segments_screen_1to1(&c) == COMP_SEG_1TO1_UNKNOWN); + CHECK(comp_segments_screen_1to1(nullptr) == COMP_SEG_1TO1_UNKNOWN); +} + +TEST_CASE("comp_segments: table") +{ + // DS1 is the primary (the active plug-in's panel); eDP to its left. + comp_segments_screen scr[2] = { + screen(EDP, 0, 0, 3456, 2160, 3456, 2160, false), + screen(DS1, 3456, 0, 3840, 2160, 3840, 2160, true), + }; + comp_segment_table t{}; + + SECTION("window entirely on the primary -> one primary segment, not split") + { + comp_seg_rect win{4000, 100, 1280, 720}; + REQUIRE(comp_segments_compute(&win, nullptr, scr, 2, &t) == 1); + CHECK(t.seg[0].is_primary); + CHECK(t.seg[0].screen_id == DS1); + CHECK(t.seg[0].window_rect.x == 0); + CHECK(t.seg[0].window_rect.w == 1280); + CHECK(t.seg[0].present_origin_x == 4000 - 3456); + CHECK(t.seg[0].present_origin_y == 100); + CHECK(t.seg[0].screen_rect.x == 4000 - 3456); + CHECK_FALSE(comp_segments_table_is_split(&t, 1280, 720)); + } + + SECTION("window straddling the seam -> two segments, left to right, each with its own origin") + { + comp_seg_rect win{3000, 200, 1280, 720}; // 456 px on eDP, 824 on the DS1 + REQUIRE(comp_segments_compute(&win, nullptr, scr, 2, &t) == 2); + CHECK(comp_segments_table_is_split(&t, 1280, 720)); + + const comp_segment &l = t.seg[0]; + CHECK(l.screen_id == EDP); + CHECK_FALSE(l.is_primary); + CHECK(l.window_rect.x == 0); + CHECK(l.window_rect.y == 0); + CHECK(l.window_rect.w == 456); + CHECK(l.window_rect.h == 720); + CHECK(l.screen_rect.x == 3000); + CHECK(l.screen_rect.y == 200); + CHECK(l.present_origin_x == 3000); + CHECK(l.present_origin_y == 200); + + const comp_segment &r = t.seg[1]; + CHECK(r.screen_id == DS1); + CHECK(r.is_primary); + CHECK(r.window_rect.x == 456); + CHECK(r.window_rect.w == 824); + CHECK(r.screen_rect.x == 0); + // phase = present origin + canvas offset = the segment's screen position + CHECK(r.present_origin_x + r.window_rect.x == r.screen_rect.x); + CHECK(l.present_origin_x + l.window_rect.x == l.screen_rect.x); + // the two segments tile the window exactly + CHECK(l.window_rect.w + r.window_rect.w == win.w); + } + + SECTION("window flush to the seam belongs to one screen only") + { + comp_seg_rect win{3456 - 1280, 0, 1280, 720}; + REQUIRE(comp_segments_compute(&win, nullptr, scr, 2, &t) == 1); + CHECK(t.seg[0].screen_id == EDP); + // entirely on a non-primary screen is still a split (the primary DP + // does not weave it) + CHECK(comp_segments_table_is_split(&t, 1280, 720)); + } + + SECTION("canvas sub-rect is honoured in window px") + { + comp_seg_rect win{3000, 0, 1280, 720}; + comp_seg_rect canvas{100, 50, 1000, 600}; // desktop 3100..4100 + REQUIRE(comp_segments_compute(&win, &canvas, scr, 2, &t) == 2); + CHECK(t.seg[0].window_rect.x == 100); + CHECK(t.seg[0].window_rect.y == 50); + CHECK(t.seg[0].window_rect.w == 356); + CHECK(t.seg[1].window_rect.x == 456); + CHECK(t.seg[1].window_rect.w == 644); + CHECK(t.seg[0].present_origin_x == 3000); // still the WINDOW's origin + } + + SECTION("window on no screen -> empty, not split") + { + comp_seg_rect win{-5000, -5000, 100, 100}; + CHECK(comp_segments_compute(&win, nullptr, scr, 2, &t) == 0); + CHECK_FALSE(comp_segments_table_is_split(&t, 100, 100)); + } + + SECTION("mirrored outputs give one segment per region, the primary winning") + { + comp_segments_screen m[3] = { + screen(0x77, 3456, 0, 3840, 2160, 3840, 2160, false), // mirror of the DS1, listed first + scr[0], + scr[1], + }; + comp_seg_rect win{3000, 200, 1280, 720}; + REQUIRE(comp_segments_compute(&win, nullptr, m, 3, &t) == 2); + CHECK(t.seg[0].screen_id == EDP); + CHECK(t.seg[1].screen_id == DS1); + CHECK(t.seg[1].is_primary); + CHECK(t.seg[1].screen_index == 2); + } + + SECTION("degenerate inputs") + { + comp_seg_rect win{0, 0, 0, 0}; + CHECK(comp_segments_compute(&win, nullptr, scr, 2, &t) == 0); + comp_seg_rect ok{0, 0, 10, 10}; + CHECK(comp_segments_compute(&ok, nullptr, scr, 0, &t) == 0); + CHECK(comp_segments_compute(&ok, nullptr, nullptr, 2, &t) == 0); + } + + SECTION("equality + format") + { + comp_seg_rect win{3000, 200, 1280, 720}; + comp_segment_table a{}, b{}; + comp_segments_compute(&win, nullptr, scr, 2, &a); + comp_segments_compute(&win, nullptr, scr, 2, &b); + CHECK(comp_segments_table_equal(&a, &b)); + win.x += 1; + comp_segments_compute(&win, nullptr, scr, 2, &b); + CHECK_FALSE(comp_segments_table_equal(&a, &b)); + char buf[512]; + comp_segments_table_format(&a, buf, sizeof(buf)); + CHECK(std::strstr(buf, "2 segment(s)") != nullptr); + char tiny[8]; + comp_segments_table_format(&a, tiny, sizeof(tiny)); + CHECK(std::strlen(tiny) < sizeof(tiny)); + } +} + +TEST_CASE("comp_segments: uncovered canvas") +{ + comp_seg_rect out[COMP_SEGMENTS_MAX_UNCOVERED]; + comp_seg_rect canvas{0, 0, 1280, 720}; + + SECTION("fully covered -> nothing") + { + comp_segment_table t{}; + t.count = 2; + t.seg[0].window_rect = {0, 0, 456, 720}; + t.seg[1].window_rect = {456, 0, 824, 720}; + CHECK(comp_segments_uncovered(&t, &canvas, out, COMP_SEGMENTS_MAX_UNCOVERED) == 0); + } + + SECTION("no segments -> the whole canvas") + { + comp_segment_table t{}; + REQUIRE(comp_segments_uncovered(&t, &canvas, out, COMP_SEGMENTS_MAX_UNCOVERED) == 1); + CHECK(out[0].x == 0); + CHECK(out[0].w == 1280); + CHECK(out[0].h == 720); + } + + SECTION("a window hanging below a short screen") + { + // left screen covers the full height, right screen only the top 500 px + comp_segment_table t{}; + t.count = 2; + t.seg[0].window_rect = {0, 0, 456, 720}; + t.seg[1].window_rect = {456, 0, 824, 500}; + REQUIRE(comp_segments_uncovered(&t, &canvas, out, COMP_SEGMENTS_MAX_UNCOVERED) == 1); + CHECK(out[0].x == 456); + CHECK(out[0].y == 500); + CHECK(out[0].w == 824); + CHECK(out[0].h == 220); + } + + SECTION("a gap between two screens") + { + comp_segment_table t{}; + t.count = 2; + t.seg[0].window_rect = {0, 0, 400, 720}; + t.seg[1].window_rect = {500, 0, 780, 720}; + REQUIRE(comp_segments_uncovered(&t, &canvas, out, COMP_SEGMENTS_MAX_UNCOVERED) == 1); + CHECK(out[0].x == 400); + CHECK(out[0].w == 100); + CHECK(out[0].h == 720); + } + + SECTION("area is conserved") + { + comp_segment_table t{}; + t.count = 2; + t.seg[0].window_rect = {100, 50, 300, 200}; + t.seg[1].window_rect = {600, 300, 400, 400}; + const uint32_t n = comp_segments_uncovered(&t, &canvas, out, COMP_SEGMENTS_MAX_UNCOVERED); + uint64_t area = 0; + for (uint32_t i = 0; i < n; i++) { + area += (uint64_t)out[i].w * out[i].h; + } + CHECK(area == 1280ull * 720 - 300ull * 200 - 400ull * 400); + } +} + +TEST_CASE("comp_segments: 1:1 policy") +{ + // No DP at all: always flat 2D. + CHECK(comp_segments_decide(false, true, COMP_SEG_1TO1_YES) == COMP_SEG_RENDER_FLAT_2D); + // A lenticular weave on a resampled screen: refused. + CHECK(comp_segments_decide(true, false, COMP_SEG_1TO1_NO) == COMP_SEG_RENDER_FLAT_2D); + // Anaglyph survives a resample: woven even on a scaled screen (ds1-linux). + CHECK(comp_segments_decide(true, true, COMP_SEG_1TO1_NO) == COMP_SEG_RENDER_WEAVE); + // 1:1 or unknown never degrades. + CHECK(comp_segments_decide(true, false, COMP_SEG_1TO1_YES) == COMP_SEG_RENDER_WEAVE); + CHECK(comp_segments_decide(true, false, COMP_SEG_1TO1_UNKNOWN) == COMP_SEG_RENDER_WEAVE); +} + +namespace { + +comp_segment_table +straddle_table() +{ + comp_segment_table t{}; + t.count = 2; + t.seg[0].screen_id = EDP; + t.seg[0].has_dp_factory = true; + t.seg[0].window_rect = {0, 0, 456, 720}; + t.seg[1].screen_id = DS1; + t.seg[1].is_primary = true; + t.seg[1].has_dp_factory = true; + t.seg[1].window_rect = {456, 0, 824, 720}; + return t; +} + +comp_segment_table +primary_only_table() +{ + comp_segment_table t{}; + t.count = 1; + t.seg[0].screen_id = DS1; + t.seg[0].is_primary = true; + t.seg[0].has_dp_factory = true; + t.seg[0].window_rect = {0, 0, 1280, 720}; + return t; +} + +int +count_action(const comp_segments_actions &a, comp_seg_action what, uint64_t id) +{ + int n = 0; + for (uint32_t i = 0; i < a.count; i++) { + if (a.items[i].action == what && a.items[i].screen_id == id) { + n++; + } + } + return n; +} + +} // namespace + +TEST_CASE("comp_segments: lifecycle hysteresis") +{ + comp_segments_lifecycle lc; + comp_segments_lifecycle_init(&lc, 2, 3); + comp_segments_actions act{}; + const comp_segment_table split = straddle_table(); + const comp_segment_table single = primary_only_table(); + + SECTION("defaults") + { + comp_segments_lifecycle d; + comp_segments_lifecycle_init(&d, 0, 0); + CHECK(d.create_after == COMP_SEGMENTS_DEFAULT_CREATE_AFTER); + CHECK(d.destroy_after == COMP_SEGMENTS_DEFAULT_DESTROY_AFTER); + } + + SECTION("create after N updates, never for the primary") + { + comp_segments_lifecycle_update(&lc, &split, &act); + CHECK(act.count == 0); // 1 update: not yet + comp_segments_lifecycle_update(&lc, &split, &act); + REQUIRE(act.count == 1); + CHECK(count_action(act, COMP_SEG_ACTION_CREATE, EDP) == 1); + CHECK(count_action(act, COMP_SEG_ACTION_CREATE, DS1) == 0); + comp_segments_lifecycle_set_created(&lc, EDP, true); + CHECK(comp_segments_lifecycle_is_live(&lc, EDP)); + comp_segments_lifecycle_update(&lc, &split, &act); + CHECK(act.count == 0); // live: no second create + } + + SECTION("a one-frame touch never creates") + { + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_update(&lc, &single, &act); + comp_segments_lifecycle_update(&lc, &split, &act); + CHECK(act.count == 0); + } + + SECTION("destroy only after M empty updates; a return in between keeps the DP") + { + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_set_created(&lc, EDP, true); + + comp_segments_lifecycle_update(&lc, &single, &act); + comp_segments_lifecycle_update(&lc, &single, &act); + CHECK(act.count == 0); + comp_segments_lifecycle_update(&lc, &split, &act); // back across the seam + CHECK(act.count == 0); + CHECK(comp_segments_lifecycle_is_live(&lc, EDP)); + + comp_segments_lifecycle_update(&lc, &single, &act); + comp_segments_lifecycle_update(&lc, &single, &act); + CHECK(act.count == 0); + comp_segments_lifecycle_update(&lc, &single, &act); + REQUIRE(act.count == 1); + CHECK(count_action(act, COMP_SEG_ACTION_DESTROY, EDP) == 1); + CHECK_FALSE(comp_segments_lifecycle_is_live(&lc, EDP)); + } + + SECTION("a failed create is not retried until the segment goes away") + { + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_update(&lc, &split, &act); + REQUIRE(count_action(act, COMP_SEG_ACTION_CREATE, EDP) == 1); + comp_segments_lifecycle_set_created(&lc, EDP, false); + for (int i = 0; i < 5; i++) { + comp_segments_lifecycle_update(&lc, &split, &act); + CHECK(act.count == 0); + } + comp_segments_lifecycle_update(&lc, &single, &act); + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_update(&lc, &split, &act); + CHECK(count_action(act, COMP_SEG_ACTION_CREATE, EDP) == 1); + } + + SECTION("a screen without a DP factory is never created") + { + comp_segment_table t = split; + t.seg[0].has_dp_factory = false; + for (int i = 0; i < 5; i++) { + comp_segments_lifecycle_update(&lc, &t, &act); + CHECK(act.count == 0); + } + } + + SECTION("drain destroys every live DP") + { + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_update(&lc, &split, &act); + comp_segments_lifecycle_set_created(&lc, EDP, true); + comp_segments_lifecycle_drain(&lc, &act); + REQUIRE(act.count == 1); + CHECK(count_action(act, COMP_SEG_ACTION_DESTROY, EDP) == 1); + CHECK_FALSE(comp_segments_lifecycle_is_live(&lc, EDP)); + } +} + +TEST_CASE("comp_segments: deferred release") +{ + comp_segments_retire r{}; + uint32_t kinds[4]; + uint64_t items[4]; + + REQUIRE(comp_segments_retire_push(&r, 1, 0xA)); + REQUIRE(comp_segments_retire_push(&r, 2, 0xB)); + REQUIRE(comp_segments_retire_push(&r, 3, 0xC)); + + // A fill is in flight: nothing is handed back, nothing is lost. + CHECK(comp_segments_retire_take(&r, false, kinds, items, 4) == 0); + CHECK(r.count == 3); + + // Safe: oldest first, capped, the rest stays queued. + REQUIRE(comp_segments_retire_take(&r, true, kinds, items, 2) == 2); + CHECK(kinds[0] == 1); + CHECK(items[0] == 0xA); + CHECK(items[1] == 0xB); + CHECK(r.count == 1); + REQUIRE(comp_segments_retire_take(&r, true, kinds, items, 4) == 1); + CHECK(items[0] == 0xC); + CHECK(r.count == 0); + + // Full list refuses instead of overwriting. + for (uint32_t i = 0; i < COMP_SEGMENTS_RETIRE_MAX; i++) { + REQUIRE(comp_segments_retire_push(&r, 0, i)); + } + CHECK_FALSE(comp_segments_retire_push(&r, 0, 999)); + CHECK_FALSE(comp_segments_retire_push(nullptr, 0, 1)); +} diff --git a/tests/tests_dp_weave_scope.cpp b/tests/tests_dp_weave_scope.cpp index 92156004a5..0e237e130b 100644 --- a/tests/tests_dp_weave_scope.cpp +++ b/tests/tests_dp_weave_scope.cpp @@ -66,11 +66,21 @@ TEST_CASE("dp_weave_scope: caps init stamps the caller's size") CHECK(caps.struct_size == sizeof(struct xrt_dp_scanout_caps)); CHECK(caps.weave_scope == XRT_DP_WEAVE_SCOPE_CANVAS); + CHECK(caps.flags == 0); // conservative default: the weave needs 1:1 for (size_t i = 0; i < sizeof(caps.reserved) / sizeof(caps.reserved[0]); i++) { CHECK(caps.reserved[i] == 0); } } +TEST_CASE("dp_weave_scope: flags were carved out of reserved[0] (multi-screen M2)") +{ + // A plug-in built before `flags` wrote 0 into reserved[0] at this offset, + // which reads as "needs 1:1" — the behaviour it always had. + CHECK(offsetof(struct xrt_dp_scanout_caps, flags) == 8); + CHECK(offsetof(struct xrt_dp_scanout_caps, reserved) == 12); + CHECK(sizeof(struct xrt_dp_scanout_caps) == XRT_DP_SCANOUT_CAPS_SIZE_V1); +} + TEST_CASE("dp_weave_scope: V1 size constant matches the struct") { CHECK(sizeof(struct xrt_dp_scanout_caps) == XRT_DP_SCANOUT_CAPS_SIZE_V1); diff --git a/tests/tests_stereo_camera.cpp b/tests/tests_stereo_camera.cpp index 49f30997da..d784f11071 100644 --- a/tests/tests_stereo_camera.cpp +++ b/tests/tests_stereo_camera.cpp @@ -313,10 +313,13 @@ TEST_CASE("plug-in iface: camera slots are appended after lift and platform-stat CHECK(offsetof(xrt_plugin_iface, get_platform_state) == lift_end); CHECK(offsetof(xrt_plugin_iface, stereo_camera_enumerate) == platform_state_end); // Multi-screen M1 appended get_display_info_for_monitor right after the - // camera block; it now ends the struct. + // camera block, and M2 create_dp_vk_for_screen right after that; it now + // ends the struct. CHECK(offsetof(xrt_plugin_iface, get_display_info_for_monitor) == offsetof(xrt_plugin_iface, stereo_camera_close) + sizeof(void *)); - CHECK(offsetof(xrt_plugin_iface, get_display_info_for_monitor) + sizeof(void *) == sizeof(xrt_plugin_iface)); + CHECK(offsetof(xrt_plugin_iface, create_dp_vk_for_screen) == + offsetof(xrt_plugin_iface, get_display_info_for_monitor) + sizeof(void *)); + CHECK(offsetof(xrt_plugin_iface, create_dp_vk_for_screen) + sizeof(void *) == sizeof(xrt_plugin_iface)); xrt_plugin_iface iface{}; iface.struct_size = (uint32_t)offsetof(xrt_plugin_iface, stereo_camera_enumerate);