diff --git a/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md b/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md index f5b099a7f2..0b5384de8b 100644 --- a/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md +++ b/docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md @@ -104,3 +104,25 @@ primary takes its phase from `set_present_origin` like any segment; drag snap, t anchor and the hosted window's snap provider follow the owner. Eyes and view routing are unchanged: the primary screen's DP still serves the primary screen. Logic: `comp_segments_owner_*` (unit-tested); detail: `docs/architecture/comp-segments.md`. + +## Amendment 3 (2026-10-09): segments on the service / IPC path + +An `_ipc` client is composited by `displayxr-service`, not in its own process, so the +service has to segment its window and the client's `xrLocateViews` has to learn the table +from the other side of the wire. Decisions: + +- **The service owns the screens.** The screen registry already lives in the service (its + instance's `enumerate_displays`, which `xrEnumerateDisplaysDXR` reaches over IPC), so a + client sends only its session's display pin (`compositor_segments_enable`); the service + builds the client's segment manager from its own registry, on the device that presents. +- **Only the direct single-client pipeline segments** (`pipeline_default_policy_render`, + an `APP_HWND` presenter). The workspace / compose path (`multi_compositor_render`) keeps + one presenter, the service window on the panel, so it stays single-screen: that is a + separate decision (per-screen presenters for the workspace), not a consequence of this one. +- **The table and the routing cross the wire by value** (`compositor_get_segment_metrics`, + `compositor_set_view_routing`, appended to `proto.json`); per-segment views are then + framed client-side from each segment's eyes exactly as in-process, and the service paints + the routed mosaic into the client's atlas. A client that never opts in — every + single-screen box — sends nothing new and is composited byte for byte as before. + +Detail: `docs/architecture/comp-segments.md` § *Service / IPC path*. diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index 79cee5aad7..f47c42bd5d 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -197,7 +197,7 @@ deferred-release list because the immediate context executes in order). What dif one WARN when its segment manager is made (`segments: under the #918 output-device split …`). - **Not segmented on Windows:** zero-copy frames, a zones / Local2D frame (any non-whole-window canvas), the shared-texture path, a pinned session, `DXR_SEGMENTS=0`. - The service / hosted path is not segmented yet (ADR-035 amendment, M6 second half). + The service has its own section below (*Service / IPC path*). - **sim_display** implements both slots on D3D11 too, so a two-monitor Windows box with no vendor hardware exercises the split path (anaglyph on both halves). - **Per-segment views (M3) on Windows** ride the same state-tracker path as Linux: `oxr_system` counts @@ -302,5 +302,46 @@ set). Quads and equirect2 layers are drawn once per segment in each tile, with t segment's view camera, viewport and scissor confined to the segment's rect (the compose pass; the blit fallback draws no quads at all). -**IPC.** In-process only: the service never segments, so a service session always sees -an empty table, one view set and no bindings (its own segmentation is M6). +**IPC.** See *Service / IPC path* below: the D3D11 service publishes the same table for +a direct client, and the client frames per-segment views from it exactly as above. + +## Service / IPC path (Windows, ADR-047 Amendment 3) + +An `_ipc` client (`XRT_FORCE_MODE=ipc`, or any app under a running service) is composited by +`displayxr-service`. The same segment manager weaves its window there: + +- **Opt-in.** At session create the client sends `compositor_segments_enable(pin)` — only when + its system's set capacity is above 1, i.e. the service's registry has two screens with a + D3D11 DP factory (`oxr_system` counts the by-value copy of the service's registry; the + pointers are only NULL-tested). The service answers with its OWN screen list (its instance, + the one `xrEnumerateDisplaysDXR` reports) and keeps a heap copy per client. A single-screen + box never sends it; a client that did not send it is never segmented. +- **Where it weaves.** Only the direct single-client pipeline (`pipeline_default_policy_render`) + for an `APP_HWND` presenter — the app's own window, the only kind that can sit across a seam. + `pipeline_segments_weave` replaces that frame's single `process_atlas`: the client's manager + (`comp_d3d11_segments`, linked from the `comp_d3d11_segs` library), made on `svc_out_device` + (the output device under the #918 split), with the panel DP — bound to the app's HWND — as + the primary and the client's crop (or the egress slot) as the atlas. The window rect is the + app HWND's client area from `ClientToScreen` in the per-monitor-DPI-aware service, i.e. + device px. One client's segment DPs exist at a time: a focus change releases the outgoing + presenter's, and a table older than 250 ms (the client no longer weaves directly — focus + moved, the workspace took over) is not handed out, so that client locates one view set. A hosted client presents into the service's window on the panel, a + present-owner / client-texture client weaves on its own thread and a zones frame keeps the + single DP: none is segmented. +- **Compose / shell mode** (`multi_compositor_render`) is unchanged: one presenter, the + service window on the panel, so a workspace window is never segmented and its table stays + empty. Per-screen workspace presenters are a separate decision. +- **Per-segment views.** `xrLocateViews` fetches the table with `compositor_get_segment_metrics` + (a round trip per locate, only for an opted-in session): the service fills each segment's + eyes at query time — the primary from the panel DP, the others from their segment DPs. The + client then runs the in-process locate unchanged, with two IPC-only differences: the + segment's client-side Kooima FOVs are kept over the device's single-set answer, and a + segment locate never takes the server rig call (`locate_views_rig`), which knows one view + set. `xrEndFrame` sends the routing with `compositor_set_view_routing`, change-only (the + service keeps the last one). The service lays a routed frame into the client's atlas as the + mosaic (`comp_segments_route_place`, `util/comp_segments_route.h`, unit-tested by + `tests/tests_segments_ipc.cpp`); a routed frame never zero-copies. +- **Not yet on the service path:** quads / equirect / cylinder layers in a routed frame are + drawn once per tile with that tile's camera (in-process draws them per segment), and a + second projection layer is routed only where the per-tile pass visits it (segment 0). + diff --git a/docs/architecture/service-architecture.md b/docs/architecture/service-architecture.md index 6ecd1271c8..75db59448d 100644 --- a/docs/architecture/service-architecture.md +++ b/docs/architecture/service-architecture.md @@ -594,6 +594,8 @@ that joined under the shell) fails the `IsWindow` test and logs `swap_chain=NULL (standalone clients are never in `mc->clients[]`) so the first to leave flattens the panel for the other (INFERRED). +**Multi-screen segments (ADR-047 Amendment 3).** A window spanning monitors is woven per screen only on the direct path: when the focused presenter is an `APP_HWND` client that opted in (`compositor_segments_enable`, sent by its oxr session on a box with two DP-backed screens), `pipeline_segments_weave` replaces that frame's single `process_atlas` with the client's own segment manager (the in-process `comp_d3d11_segments`, on `svc_out_device`): the panel DP weaves the primary screen's segment, windowless per-screen DPs the others. The service owns the screen registry, so only the session's display pin crosses IPC; the segment table and the per-segment view routing then cross by value (`compositor_get_segment_metrics`, `compositor_set_view_routing`) and the commit paints the routed atlas mosaic. Render thread under `render_mutex`; the client's `seg_mutex` is a leaf guarding the table it reads at `xrLocateViews`. The workspace path (`multi_compositor_render`) stays one presenter on the panel and never segments. Detail: `docs/architecture/comp-segments.md` § *Service / IPC path*. + ### 4.4 #925 status (correcting the reference doc) S1 bounded waits ✔, S2 audit ✔, **S3 command queue ✔ default on**, **S4 eviction ✔** (tier 1 diff --git a/docs/roadmap/multi-screen.md b/docs/roadmap/multi-screen.md index fde9cc1d69..52c2a81c0e 100644 --- a/docs/roadmap/multi-screen.md +++ b/docs/roadmap/multi-screen.md @@ -208,7 +208,7 @@ multiview + display_info v22. An app may also pin itself to one display | **M3** Per-segment views | Same window: `xrLocateViews` returns 4 views, each half rendered from its own frustum | oxr: per-segment Kooima + eyes from the segment DP; `XrViewDisplayBindingsDXR`; `activeViewCount`; tile routing by segment; legacy stereo = majority segment + 2D blit | none | cube renders the correct perspective on each half; `check_displayxr_app.py` rule for v22 | | **M4 = T1** Mixed vendor on `ds1-linux` | **Leia weave + DS1 tracking on the DS1 half, sim anaglyph on the laptop half** | loader: two plug-ins resident, DPs created from each screen's factory; plug-in display info per screen | leia-linux: `window = 0` always (one line, `leia_sr_linux_sdk.c:1470`); per-DP display state instead of `g_ctx.display` / statics; `srLensDisable` right after `srCreateLens` if the lens must start off. Confirmed by the SR side: a windowless Linux Vulkan weaver fed the DS1 segment + present origin weaves exactly that viewport, votes the lens on once, then the DP owns it. Works on today's SDK for **one** Leia panel. With SR-P0 (verified on the DS1 2026-10-07, branch `ST-5481-weaver-external-routing`): check `SrWeaverRoutingCapabilities` (tag 24) chained on `SrRuntimeCapabilities.pNext`, then chain `SrWeaverRoutingInfo{mode=SR_WEAVER_ROUTING_EXTERNAL}` (tag 22) on `SrWeaverCreateInfoVulkan.pNext`, `window = 0`, present origin per frame, lens via `srLensEnable/Disable` | David eyeballs it; screenshot both halves | | **M5 = T2** Two Leia panels | Each panel tracked by its own camera | per-screen tracker plumbing already in place from M3 | **SR-P1** enumeration + identity, **SR-P2** bind display/weaver/lens/tracker by `displayId`, **SR-P3** N trackers + lens per display (SRService refactor, ST-3160 camera pairing). Plug-in: per-DP SR display + tracker, claims from `srEnumerateDisplays` | needs a second panel on a dev box | -| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). the HWND follows the majority screen (ADR-047 Amendment 2: 20 % margin held 0.5 s, windowless replacement first, rollback on failure; rig-unverified); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed; service/hosted half not started.** | shell with a window straddling two panels | +| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). the HWND follows the majority screen (ADR-047 Amendment 2: 20 % margin held 0.5 s, windowless replacement first, rollback on failure; rig-unverified); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed. Service half: a direct (non-workspace) IPC client's window is segmented by the service's direct pipeline, with per-segment views over IPC (ADR-047 Amendment 3); compose/shell mode and hosted windows stay single-screen.** | shell with a window straddling two panels | Ordering rationale: M0–M3 are hardware-free beyond "two monitors" and exercise everything with sim_display, so the model is proven before any SR dependency. M4 is David's stated diff --git a/src/xrt/compositor/d3d11/CMakeLists.txt b/src/xrt/compositor/d3d11/CMakeLists.txt index 6f2364ae9e..f3175de931 100644 --- a/src/xrt/compositor/d3d11/CMakeLists.txt +++ b/src/xrt/compositor/d3d11/CMakeLists.txt @@ -9,6 +9,27 @@ if(WIN32 AND XRT_HAVE_D3D11) # The cross-adapter atlas bridge is its own static library and lives in # ../xbridge — see that directory's CMakeLists.txt for why. + # Multi-screen M6: the per-screen segment manager and the output-device + # composite unit it blits flat 2D through. Their own static library so the + # D3D11 service compositor (../d3d11_service) can segment IPC clients' + # windows without linking the whole in-process compositor — the same reason + # comp_xbridge is split out. + add_library( + comp_d3d11_segs STATIC + ../d3d_shared/comp_masked_composite_shaders.h + comp_d3d11_outcomp.cpp + comp_d3d11_outcomp.h + comp_d3d11_segments.cpp + comp_d3d11_segments.h + d3d11_layer_shaders.h + ) + target_link_libraries( + comp_d3d11_segs + PUBLIC xrt-interfaces + PRIVATE comp_util aux_util aux_os aux_d3d d3d11 d3dcompiler + ) + target_include_directories(comp_d3d11_segs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) + add_library( comp_d3d11 STATIC ../d3d_shared/comp_equirect2_shaders.h @@ -16,12 +37,8 @@ if(WIN32 AND XRT_HAVE_D3D11) comp_d3d11_compositor.cpp comp_d3d11_compositor.h comp_d3d11_compositor_internals.h - comp_d3d11_outcomp.cpp - comp_d3d11_outcomp.h comp_d3d11_renderer.cpp comp_d3d11_renderer.h - comp_d3d11_segments.cpp - comp_d3d11_segments.h comp_d3d11_state_guard.h comp_d3d11_swapchain.cpp comp_d3d11_swapchain.h @@ -44,6 +61,7 @@ if(WIN32 AND XRT_HAVE_D3D11) # path at all — see docs/investigations/hybrid-igpu-weave.md). aux_d3d comp_xbridge + comp_d3d11_segs xrt-external-stb d3d11 d3d12 diff --git a/src/xrt/compositor/d3d11_service/CMakeLists.txt b/src/xrt/compositor/d3d11_service/CMakeLists.txt index 76b216bfe3..31029f10df 100644 --- a/src/xrt/compositor/d3d11_service/CMakeLists.txt +++ b/src/xrt/compositor/d3d11_service/CMakeLists.txt @@ -73,6 +73,12 @@ target_link_libraries( # guard that builds the bridge is satisfied wherever this library builds at all. target_link_libraries(comp_d3d11_service PRIVATE comp_xbridge) +# Multi-screen M6 on the service path: a direct (non-workspace) IPC client's +# window spanning monitors is woven per screen by the in-process compositor's +# own segment manager (comp_d3d11_segs, split out of comp_d3d11 for exactly +# this). comp_util carries the backend-agnostic segment math (comp_segments.c). +target_link_libraries(comp_d3d11_service PRIVATE comp_d3d11_segs comp_util) + target_include_directories(comp_d3d11_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/../d3d11 diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp index de4cdf26b1..9eb1509974 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp @@ -37,6 +37,12 @@ // #1580: the ONE per-view camera every layer type is projected through. #include "util/comp_layer_view_camera.h" #include "util/comp_dp_factory.h" +// Multi-screen M6/M3 on the service path: the in-process segment manager and +// the shared routing / publish helpers. +#include "util/comp_segments_route.h" +#include "comp_d3d11_outcomp.h" +#include "comp_d3d11_segments.h" +#include "xrt/xrt_screen.h" #include "comp_d3d11_window.h" @@ -952,6 +958,42 @@ struct d3d11_service_compositor uint32_t pipe_tile_rows; bool pipe_frame_ready; + /*! + * Multi-screen M6/M3 on the service path (ADR-047 Amendment 3): this + * client's window, when it spans monitors, is woven per screen by the + * direct pipeline — the in-process compositor's segment manager, made on + * the OUTPUT device. Opt-in: nothing here exists until the client sends + * `compositor_segments_enable` (its oxr session does that only on a box + * with two DP-backed screens), so a single-screen client is untouched. + * + * Threads. `segments`, `seg_outcomp` and `seg_device` are the render + * thread's (it creates, updates and records them holding render_mutex); + * compositor_destroy frees them under render_mutex too. `seg_mutex` + * guards the hand-offs: the screen list (written by the IPC thread), the + * published table (read by the IPC thread at xrLocateViews) and every + * swap of `segments` (the IPC thread reads segment eyes through it). + * Leaf lock: nothing is waited on while holding it. + * + * `seg_route` is the routing the client's last splitting locate handed + * back; it is written and read only on this client's own IPC thread + * (set_view_routing, then layer_commit — one connection, in order). + */ + std::mutex seg_mutex; + struct xrt_screen_list *seg_screens; //!< heap copy; NULL = segmentation not enabled + struct xrt_system_compositor_info *seg_sys_info; //!< heap copy (the DP registry) + uint64_t seg_pinned_display_id; + bool seg_rebuild; //!< a new list arrived: rebuild the manager on the next weave + struct comp_d3d11_segments *segments; + struct comp_d3d11_outcomp *seg_outcomp; //!< flat-2D fill, on @ref seg_device + ID3D11Device *seg_device; //!< the device the two above were made on + struct xrt_segment_metrics seg_pub; //!< the table the last direct weave took + //! When a direct weave of this client last published @ref seg_pub + //! (monotonic ns). A table older than SVC_SEG_PUB_FRESH_NS is not handed + //! out: the client is no longer the direct presenter (focus moved, the + //! workspace took over, its frames stalled), so it locates one view set. + int64_t seg_pub_ns; + struct xrt_segment_view_routing seg_route; + //! #1140: the recipe the per-client atlas ACTUALLY HOLDS — the per-view //! content dims and the view count of the last commit that PAINTED it. //! @@ -13984,6 +14026,206 @@ pipeline_return_app_grant(struct d3d11_multi_compositor *mc, } } +/* + * + * Multi-screen M6/M3 on the service path (ADR-047 Amendment 3): a direct IPC + * client's window that spans monitors, woven per screen. + * + */ + +//! How long a direct weave's segment table stays valid for the client's +//! locates (~15 frames at 60 Hz): long enough for a skipped or slow frame, +//! short enough that a client which lost the panel stops locating per segment. +#define SVC_SEG_PUB_FRESH_NS ((int64_t)250 * 1000 * 1000) + +//! The window's client area in desktop device px. The service is per-monitor +//! DPI aware (dpi_aware.manifest), so this is the space the screen registry's +//! monitor rects are in; GetClientRect / ClientToScreen work cross-process on +//! the app's real HWND and send no message. +static bool +svc_seg_window_rect(HWND hwnd, struct comp_seg_rect *out) +{ + RECT cr; + POINT origin = {0, 0}; + if (hwnd == nullptr || !IsWindow(hwnd) || !GetClientRect(hwnd, &cr) || cr.right <= 0 || cr.bottom <= 0 || + !ClientToScreen(hwnd, &origin)) { + return false; + } + out->x = origin.x; + out->y = origin.y; + out->w = (uint32_t)cr.right; + out->h = (uint32_t)cr.bottom; + return true; +} + +//! Drop the client's segment manager and its flat-2D unit. Render thread (or +//! compositor_destroy), holding render_mutex: no weave is in flight. The +//! pointer is swapped out under seg_mutex, which is what the IPC thread reads +//! segment eyes under, so no reader can still be inside the manager when it +//! is destroyed. +static void +svc_seg_release(struct d3d11_service_compositor *c) +{ + struct comp_d3d11_segments *old = nullptr; + { + std::lock_guard lk(c->seg_mutex); + old = c->segments; + c->segments = nullptr; + struct xrt_segment_metrics none; + memset(&none, 0, sizeof(none)); // memcmp'd: padding too + (void)comp_segments_publish(&c->seg_pub, &none); + } + comp_d3d11_segments_destroy(&old); + comp_d3d11_outcomp_destroy(&c->seg_outcomp); + c->seg_device = nullptr; +} + +/*! + * The direct pipeline's segment path for the focused client: when its window + * spans monitors, weave it per screen into the presenter's back buffer — the + * panel DP (bound to the app's HWND, so it keeps the vendor's phase-snap) on + * the primary screen's segment, a windowless per-screen DP from the registry + * (`create_dp_d3d11_for_screen`, phase from `set_present_origin`) on every + * other, flat 2D where no DP can weave. The in-process D3D11 compositor's own + * manager (comp_d3d11_segments), made on the OUTPUT device so it is right + * under the #918 split too; the atlas it crops is the one the single-DP path + * would have woven (the client's crop, or the egress slot under the split). + * + * Scope: an APP_HWND presenter (a window the app owns, which is what can sit + * across a seam). A hosted client presents into the service's own window on + * the panel, a present-owner / client-texture client weaves on its own, and a + * zones frame keeps the single-DP path — as in-process. A client that never + * sent `compositor_segments_enable` returns false before anything else: the + * frame is the single-DP one, byte for byte. + * + * Every call also publishes the table this weave took (count 0 = one view + * set), for the client's next xrLocateViews. + * + * Render thread, holding render_mutex. The caller has bound @p present_rtv and + * fed @p dp this frame's transparency, encoding and timing. + * + * @return true when the frame was woven per segment (the caller then skips + * its single process_atlas). + */ +static bool +pipeline_segments_weave(struct d3d11_service_system *sys, + struct d3d11_service_compositor *fc, + enum d3d11_presenter_kind kind, + HWND present_hwnd, + ID3D11RenderTargetView *present_rtv, + struct xrt_display_processor_d3d11 *dp, + ID3D11ShaderResourceView *dp_input_srv, + uint32_t view_w, + uint32_t view_h, + uint32_t cols, + uint32_t rows, + uint32_t target_w, + uint32_t target_h) +{ + ID3D11Device *dev = svc_out_device(sys); + ID3D11DeviceContext *ctx = svc_out_context(sys); + bool rebuild = false; + { + std::lock_guard lk(fc->seg_mutex); + if (fc->seg_screens == nullptr) { + return false; // never enabled: the single-DP path, untouched + } + rebuild = fc->seg_rebuild || fc->seg_device != dev; + } + + if (rebuild) { + // A new screen list, or the output device moved (#918 split engaged + // or released): the segment DPs and crops belong to the old device. + svc_seg_release(fc); + struct comp_d3d11_segments *ns = comp_d3d11_segments_create(dev); + bool enabled = false; + { + std::lock_guard lk(fc->seg_mutex); + if (ns != nullptr) { + comp_d3d11_segments_set_screens(ns, fc->seg_screens, fc->seg_sys_info, + fc->seg_pinned_display_id); + enabled = comp_d3d11_segments_enabled(ns); + } + fc->segments = ns; + fc->seg_rebuild = false; + } + if (comp_d3d11_outcomp_create(dev, ctx, &fc->seg_outcomp) != XRT_SUCCESS) { + fc->seg_outcomp = nullptr; // segments that cannot weave then stay black, not flat 2D + } + fc->seg_device = dev; // tried once per device / list, never per frame + if (enabled) { + // Lifecycle (once per manager): the rig's proof that this IPC + // client's segment DPs are made in the service, on the device + // that presents. + U_LOG_W( + "segments: IPC client %p (pid %ld) — a window spanning monitors is woven per screen by the " + "service's direct pipeline; segment DPs on the %s device %p (multi-screen M6, service " + "path)", + (void *)fc, fc->client_pid, sys->split_active ? "OUTPUT" : "app", (void *)dev); + } + } + + struct xrt_segment_metrics m; + memset(&m, 0, sizeof(m)); // memcmp'd by the publish: padding too + bool split = false; + struct comp_seg_rect win = {}; + if (kind == PRESENTER_APP_HWND && fc->segments != nullptr && comp_d3d11_segments_enabled(fc->segments) && + dp != nullptr && dp_input_srv != nullptr && !fc->zone_published && present_rtv != nullptr && + svc_seg_window_rect(present_hwnd, &win)) { + const struct comp_seg_rect canvas = {0, 0, win.w, win.h}; + const uint32_t mode_index = (sys->xdev != nullptr && sys->xdev->hmd != nullptr) + ? sys->xdev->hmd->active_rendering_mode_index + : 0; + split = comp_d3d11_segments_update(fc->segments, &win, &canvas, ctx, mode_index); + // Every segment DP follows the panel's 2D/3D state, like the panel DP. + comp_d3d11_segments_set_display_mode(fc->segments, sys->hardware_display_3d); + if (split) { + (void)comp_d3d11_segments_get_metrics(fc->segments, &win, &canvas, /*primary_has_dp*/ true, &m); + } + } + { + std::lock_guard lk(fc->seg_mutex); + (void)comp_segments_publish(&fc->seg_pub, &m); + fc->seg_pub_ns = (int64_t)os_monotonic_get_ns(); + } + if (!split) { + return false; + } + + struct comp_d3d11_segments_frame sf = {}; + sf.context = ctx; + sf.src_srv = dp_input_srv; + sf.view_width = view_w; + sf.view_height = view_h; + sf.tile_columns = cols; + sf.tile_rows = rows; + sf.src_format = (uint32_t)DXGI_FORMAT_R8G8B8A8_UNORM; + sf.target_rtv = present_rtv; + sf.target_width = target_w; + sf.target_height = target_h; + sf.canvas.x = 0; + sf.canvas.y = 0; + sf.canvas.w = win.w; + sf.canvas.h = win.h; + sf.transparent_background = false; // an APP_HWND presenter is woven opaque (pipeline_dp_set_transparency) + sf.atlas_encoding = (int)service_single_client_atlas_encoding(fc); + sf.primary_dp = dp; + sf.outcomp = fc->seg_outcomp; + comp_d3d11_segments_record(fc->segments, &sf); + + // Leave exactly the state the single-DP path leaves: the back buffer + // bound, full-target viewport and scissor. + ctx->OMSetRenderTargets(1, &present_rtv, nullptr); + D3D11_VIEWPORT full_vp = {}; + full_vp.Width = (float)target_w; + full_vp.Height = (float)target_h; + full_vp.MaxDepth = 1.0f; + ctx->RSSetViewports(1, &full_vp); + D3D11_RECT full_scissor = {0, 0, (LONG)target_w, (LONG)target_h}; + ctx->RSSetScissorRects(1, &full_scissor); + return true; +} + static void pipeline_default_policy_render(struct d3d11_service_system *sys, struct d3d11_multi_compositor *mc, @@ -14033,6 +14275,16 @@ pipeline_default_policy_render(struct d3d11_service_system *sys, // (#939): there both are desktop peers and z-order already shows // the foreground one. pipeline_park_loser_for_winner(mc, was_focused, best); + // Multi-screen M6: the outgoing presenter's segment DPs go with + // the panel — one set of per-screen weavers at a time, rebuilt + // if it takes the panel back (svc_seg_release clears the device + // it was made on, which is the rebuild trigger). + if (was_focused >= 0 && was_focused < D3D11_MULTI_MAX_CLIENTS) { + struct d3d11_service_compositor *oc = mc->clients[was_focused].compositor; + if (oc != nullptr && (oc->segments != nullptr || oc->seg_outcomp != nullptr)) { + svc_seg_release(oc); + } + } // #1018: the INCOMING one may itself be parked — restore it, or // the ready-probe skips it every frame and the panel goes black. // Covers the survivor-fallback path too (holder exits -> next). @@ -14464,10 +14716,15 @@ pipeline_default_policy_render(struct d3d11_service_system *sys, xrt_display_processor_d3d11_set_frame_timing(dp, mc->panel_r_ns, (uint64_t)(U_TIME_1S_IN_NS / sys->refresh_rate)); svc_assert_same_device(dp_input_srv, svc_out_device(sys)); - xrt_display_processor_d3d11_process_atlas(dp, svc_out_context(sys), dp_input_srv, weave_view_w, - weave_view_h, weave_cols, weave_rows, - DXGI_FORMAT_R8G8B8A8_UNORM, target_w, target_h, 0, 0, 0, - 0); + // Multi-screen M6 (ADR-047 Amendment 3): a window across a seam is + // woven per screen; anything else (and every client that never + // enabled segments) takes the single weave below, unchanged. + if (!pipeline_segments_weave(sys, fc, kind, present_hwnd, present_rtv, dp, dp_input_srv, weave_view_w, + weave_view_h, weave_cols, weave_rows, target_w, target_h)) { + xrt_display_processor_d3d11_process_atlas( + dp, svc_out_context(sys), dp_input_srv, weave_view_w, weave_view_h, weave_cols, weave_rows, + DXGI_FORMAT_R8G8B8A8_UNORM, target_w, target_h, 0, 0, 0, 0); + } g_frame_witness_service.count_weave(!witness_take_fresh_paint(fc), sys->hardware_display_3d); } else if (sys->split_active && dp_input_srv != nullptr) { /* @@ -17986,8 +18243,17 @@ struct service_proj_view_blit float dst_w, dst_h; //!< 0 = no scale (source fits the slot) bool is_array; //!< ADR-032 layered source uint32_t array_slice; + //! Multi-screen M3: this view has no place in this frame's atlas (a routed + //! frame's view past every segment's range, or any view past the packed + //! tiles) — the caller draws nothing for it. + bool skip; }; +/*! + * @p packed_tiles is the tile count this commit packs (the active mode's, 1 + * in hardware 2D) — what a routed view's local index must fit (multi-screen + * M3, see the routing block at the end). + */ static void service_proj_view_blit_resolve(struct d3d11_service_system *sys, struct d3d11_service_compositor *c, @@ -17997,6 +18263,7 @@ service_proj_view_blit_resolve(struct d3d11_service_system *sys, bool bridge_override, uint32_t active_vw, uint32_t active_vh, + uint32_t packed_tiles, struct service_proj_view_blit *out) { float src_x = static_cast(layer->data.proj.v[eye].sub.rect.offset.w); @@ -18070,6 +18337,76 @@ service_proj_view_blit_resolve(struct d3d11_service_system *sys, // it needs no change. out->is_array = view_desc->ArraySize > 1; out->array_slice = static_cast(layer->data.proj.v[eye].sub.array_index); + out->skip = false; + + /* + * Multi-screen M3 on the service path: per-segment views. The client's + * last splitting xrLocateViews handed out one view set per window segment + * and sent the routing back (compositor_set_view_routing) ahead of this + * commit. The atlas keeps the active mode's grid; tile j becomes a mosaic + * of every segment's j-th view, each scaled into ITS segment's rect inside + * the tile — the rect the segment crop reads (comp_segments_tile_rect), so + * each segment DP weaves exactly its own views. The same placement the + * in-process renderer paints (comp_d3d11_renderer, set_routed_viewport). + * An inconsistent routing (the mode changed between the locate and the + * commit, a degenerate rect) is dropped for the frame and the views are + * placed unrouted. + */ + if (c->seg_route.count > 0 && packed_tiles > 0) { + uint32_t tw = + bridge_override ? active_vw : static_cast(layer->data.proj.v[0].sub.rect.extent.w); + uint32_t th = + bridge_override ? active_vh : static_cast(layer->data.proj.v[0].sub.rect.extent.h); + if (tw > layout_vw) { + tw = layout_vw; // the content clamp the commit applies after the loop + } + if (th > layout_vh) { + th = layout_vh; + } + if (comp_segments_route_active(&c->seg_route, layer->data.view_count, packed_tiles, tw, th)) { + struct comp_segments_route_slot slot; + if (!comp_segments_route_place(&c->seg_route, layer->data.view_count, packed_tiles, tw, th, eye, + &slot)) { + out->skip = true; + return; + } + uint32_t rx = 0, ry = 0; + u_tiling_view_origin(slot.tile, cols, layout_vw, layout_vh, &rx, &ry); + out->dst_x = static_cast(rx + (uint32_t)slot.rect.x); + out->dst_y = static_cast(ry + (uint32_t)slot.rect.y); + out->dst_w = static_cast(slot.rect.w); // always scaled into the segment rect + out->dst_h = static_cast(slot.rect.h); + return; + } + } + if (eye >= packed_tiles && packed_tiles > 0) { + out->skip = true; // only reachable when a routed range widened the loop + } +} + +/*! + * Multi-screen M3: how many of a projection layer's views this commit walks + * when the client routes per-segment views — the end of the last segment's + * range (each clamped to @p packed_tiles), or 0 when the routing does not fit + * what the layer submitted (the commit then walks the packed tiles only). + */ +static uint32_t +service_seg_route_end(const struct d3d11_service_compositor *c, uint32_t layer_view_count, uint32_t packed_tiles) +{ + const struct xrt_segment_view_routing *r = &c->seg_route; + if (r->count == 0 || r->count > XRT_MAX_SEGMENTS || packed_tiles == 0) { + return 0; + } + uint32_t end = 0; + for (uint32_t k = 0; k < r->count; k++) { + const uint32_t n = r->view_count[k] < packed_tiles ? r->view_count[k] : packed_tiles; + const uint32_t e = r->first_view[k] + n; + if (n == 0 || e > layer_view_count) { + return 0; + } + end = e > end ? e : end; + } + return end > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : end; } /*! @@ -21222,6 +21559,16 @@ compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sy proj_view_count = 1; if (proj_view_count > XRT_MAX_VIEWS) proj_view_count = XRT_MAX_VIEWS; + // Multi-screen M3: the grid this commit packs stays the mode's; a + // routed frame (per-segment views) walks every segment's views, which + // the resolver lays into that grid as a mosaic. Unrouted: identical. + const uint32_t packed_view_count = proj_view_count; + { + const uint32_t route_end = service_seg_route_end(c, layer->data.view_count, packed_view_count); + if (route_end > proj_view_count) { + proj_view_count = route_end; + } + } // #575: remember what this frame actually packed (see eff_view_count // decl). The handoff/crop below derive their grid from this, not the @@ -21234,7 +21581,7 @@ compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sy // resolves the same count in practice, and single-projection frames are // untouched either way.) if (projection_layers_painted == 0) { - eff_view_count = proj_view_count; + eff_view_count = packed_view_count; } // Extract per-view swapchains, textures, and image indices @@ -21772,10 +22119,14 @@ compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sy else if (sys->workspace_mode) zc_reason = "workspace_mode"; else if (color_blocks_zc) zc_reason = "color_needs_encode"; + // Multi-screen M3: a routed frame's atlas is a mosaic the service + // builds; the app's image is never it. + else if (proj_view_count != packed_view_count) + zc_reason = "segment_routed"; if (!has_ui_layers && !has_window_space_layers && !zones_frame && !has_local_2d && - projection_layer_count <= 1 && all_views_zc_eligible && !sys->workspace_mode && - !color_blocks_zc) { + projection_layer_count <= 1 && all_views_zc_eligible && !sys->workspace_mode && !color_blocks_zc && + proj_view_count == packed_view_count) { // Check all views reference the same swapchain image bool all_same = true; for (uint32_t eye = 1; eye < proj_view_count; eye++) { @@ -22020,7 +22371,10 @@ compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sy // passes can never place the same view differently. struct service_proj_view_blit pvb = {}; service_proj_view_blit_resolve(sys, c, layer, eye, &view_descs[eye], bridge_override, active_vw, - active_vh, &pvb); + active_vh, packed_view_count, &pvb); + if (pvb.skip) { + continue; // multi-screen M3: no place for this view in the mosaic + } const float src_x = pvb.src_x; const float src_y = pvb.src_y; const float src_w = pvb.src_w; @@ -22871,7 +23225,11 @@ compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handle_t sy struct service_proj_view_blit pvb = {}; service_proj_view_blit_resolve(sys, c, layer, view_index, &pdesc, - bridge_override, active_vw, active_vh, &pvb); + bridge_override, active_vw, active_vh, + ui_view_count, &pvb); + if (pvb.skip) { + continue; // multi-screen M3: no place in this frame's mosaic + } blit_to_atlas_texture(sys, &c->render, psc->images[pimg].srv.get(), pvb.src_x, pvb.src_y, pvb.src_w, pvb.src_h, pvb.src_tex_w, pvb.src_tex_h, pvb.dst_x, pvb.dst_y, pvb.dst_w, pvb.dst_h, @@ -24402,6 +24760,17 @@ compositor_destroy(struct xrt_compositor *xc) c->workspace_sync_fence.reset(); } + // Multi-screen M6: the segment DPs are the render thread's; tear them down + // under render_mutex so no weave of this client is in flight. + if (c->segments != nullptr || c->seg_outcomp != nullptr) { + render_mutex_fair_lock lock(sys); + svc_seg_release(c); + } + free(c->seg_screens); + c->seg_screens = nullptr; + free(c->seg_sys_info); + c->seg_sys_info = nullptr; + delete c; } @@ -29359,6 +29728,128 @@ comp_d3d11_service_get_client_app_window_metrics(struct xrt_system_compositor *x return true; } +/* + * + * Multi-screen M6/M3 over IPC (ADR-047 Amendment 3). The IPC server is the + * caller: it owns the screen registry handle (the server's instance) and the + * client's connection; everything below runs on that client's IPC thread. + * + */ + +void +comp_d3d11_service_set_client_segment_screens(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, + const struct xrt_screen_list *list, + uint64_t pinned_display_id) +{ + if (xsysc == nullptr || xc == nullptr || list == nullptr || !comp_d3d11_service_is_d3d11_service(xsysc)) { + return; + } + struct d3d11_service_system *sys = d3d11_service_system_from_xrt(xsysc); + struct d3d11_service_compositor *c = d3d11_service_compositor_from_xrt(xc); + + struct xrt_screen_list *copy = U_TYPED_CALLOC(struct xrt_screen_list); + struct xrt_system_compositor_info *info_copy = U_TYPED_CALLOC(struct xrt_system_compositor_info); + if (copy == nullptr || info_copy == nullptr) { + free(copy); + free(info_copy); + return; + } + *copy = *list; + // The registry (iface + instance per screen) as it stands now — a copy, + // like the in-process compositor takes, so a later refresh of the live + // info cannot tear what the render thread builds the manager from. + *info_copy = sys->base.info; + + { + // The render thread rebuilds the manager from this on its next weave + // of this client — never here (the manager is the render thread's). + std::lock_guard lk(c->seg_mutex); + free(c->seg_screens); + c->seg_screens = copy; + free(c->seg_sys_info); + c->seg_sys_info = info_copy; + c->seg_pinned_display_id = pinned_display_id; + c->seg_rebuild = true; + // ...and nothing may frame views from the old table meanwhile. + struct xrt_segment_metrics none; + memset(&none, 0, sizeof(none)); + (void)comp_segments_publish(&c->seg_pub, &none); + } + // Once per session (lifecycle). + U_LOG_W( + "segments: IPC client %p (pid %ld) enabled segments — %u screen(s) in the registry, pinned display " + "0x%016llx", + (void *)c, c->client_pid, list->count, (unsigned long long)pinned_display_id); +} + +bool +comp_d3d11_service_get_client_segment_metrics(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, + struct xrt_segment_metrics *out) +{ + if (out == nullptr) { + return false; + } + memset(out, 0, sizeof(*out)); + if (xsysc == nullptr || xc == nullptr || !comp_d3d11_service_is_d3d11_service(xsysc)) { + return false; + } + struct d3d11_service_compositor *c = d3d11_service_compositor_from_xrt(xc); + { + // Leaf: the table plus every SECONDARY screen's eyes, predicted now + // from its segment DP (comp_d3d11_segments_get_eyes is safe against + // the weave creating or destroying segment DPs; the manager itself is + // only swapped under this lock). + std::lock_guard lk(c->seg_mutex); + if ((int64_t)os_monotonic_get_ns() - c->seg_pub_ns > SVC_SEG_PUB_FRESH_NS) { + return false; // not the direct presenter right now: one view set + } + *out = c->seg_pub; + for (uint32_t k = 0; k < out->count && k < XRT_MAX_SEGMENTS; k++) { + struct xrt_segment_metric *s = &out->seg[k]; + if (!s->is_primary) { + s->have_eyes = comp_d3d11_segments_get_eyes(c->segments, s->screen_id, &s->eyes); + } + } + } + // The PRIMARY screen's eyes are the panel DP's — the same query + // compositor_get_predicted_eye_positions answers, which takes + // render_mutex. Taken AFTER seg_mutex is released: the render thread holds + // render_mutex when it takes seg_mutex, so the two never nest here. + for (uint32_t k = 0; k < out->count && k < XRT_MAX_SEGMENTS; k++) { + struct xrt_segment_metric *s = &out->seg[k]; + if (s->is_primary) { + struct xrt_eye_positions eyes; + memset(&eyes, 0, sizeof(eyes)); + s->have_eyes = + comp_d3d11_service_get_predicted_eye_positions_full_for_client(xsysc, xc, &eyes) && + eyes.valid; + if (s->have_eyes) { + s->eyes = eyes; + } + } + } + return out->count > 0; +} + +void +comp_d3d11_service_set_client_view_routing(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, + const struct xrt_segment_view_routing *routing) +{ + if (xsysc == nullptr || xc == nullptr || routing == nullptr || !comp_d3d11_service_is_d3d11_service(xsysc)) { + return; + } + struct d3d11_service_compositor *c = d3d11_service_compositor_from_xrt(xc); + // This client's IPC thread is the only reader (its next layer_commit): + // one connection, handled in order, so no lock. + c->seg_route = *routing; + if (c->seg_route.count > XRT_MAX_SEGMENTS) { + memset(&c->seg_route, 0, sizeof(c->seg_route)); // never index past the arrays + } +} + void comp_d3d11_service_compositor_set_client_pid(struct xrt_compositor *xc, long pid) { diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.h b/src/xrt/compositor/d3d11_service/comp_d3d11_service.h index fddd5ff0b1..5c303e8a94 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.h +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.h @@ -273,6 +273,53 @@ comp_d3d11_service_get_client_app_window_metrics(struct xrt_system_compositor *x struct xrt_compositor *xc, struct xrt_window_metrics *out_metrics); +struct xrt_screen_list; +struct xrt_segment_metrics; +struct xrt_segment_view_routing; + +/*! + * Multi-screen M6 over IPC (ADR-047 Amendment 3): enable per-screen weaving of + * this client's window. The IPC server passes the SERVICE's screen list (its + * own instance's `enumerate_displays`) and the session's + * `XrSessionDisplayBindingDXR` pin (0 = none; a pinned session is never + * segmented). The render thread builds the client's segment manager from it + * on its next direct weave; a client that never calls this is never + * segmented. + * + * @ingroup comp_d3d11_service + */ +void +comp_d3d11_service_set_client_segment_screens(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, + const struct xrt_screen_list *list, + uint64_t pinned_display_id); + +/*! + * Multi-screen M3 over IPC: the segment table the last direct weave of this + * client took (count 0 = one view set), with every segment's eyes predicted + * now — the primary screen's from the panel DP, the others from their segment + * DPs. What the client's `xrLocateViews` frames per-segment views from. + * + * @return true when the window is woven per segment. + * @ingroup comp_d3d11_service + */ +bool +comp_d3d11_service_get_client_segment_metrics(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, + struct xrt_segment_metrics *out); + +/*! + * Multi-screen M3 over IPC: where each segment's views are in the client's + * next projection layer (what its last splitting `xrLocateViews` handed out; + * count 0 = unrouted). Sent change-only, ahead of the commit it applies to. + * + * @ingroup comp_d3d11_service + */ +void +comp_d3d11_service_set_client_view_routing(struct xrt_system_compositor *xsysc, + struct xrt_compositor *xc, + const struct xrt_segment_view_routing *routing); + /*! * Check if the compositor's window is still valid (not closed by user). * diff --git a/src/xrt/compositor/util/comp_segments_route.h b/src/xrt/compositor/util/comp_segments_route.h new file mode 100644 index 0000000000..c640cd7152 --- /dev/null +++ b/src/xrt/compositor/util/comp_segments_route.h @@ -0,0 +1,162 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-segment view routing and segment-table publishing — the pure + * halves a compositor needs to carry multi-screen M3 (ADR-047 D3). + * @ingroup comp_util + * + * Shared by every compositor that segments a window and routes per-segment + * views into its atlas. Written for the D3D11 service compositor (the + * service / IPC path, where the segment table crosses the process boundary + * and the routing comes back over IPC), deliberately backend-agnostic and + * header-only so it is host-tested (`tests/tests_segments_ipc.cpp`): + * + * - @ref comp_segments_route_place — where one submitted view of a routed + * projection layer lands in the atlas (which tile, which rect in it). + * - @ref comp_segments_publish — the change-only generation bump of the + * published segment table. + * + * The rect mapping is @ref comp_segments_tile_rect — the one the per-segment + * crop (M2) reads with — so a routed view always lands exactly where its + * segment's crop picks it up. Design: docs/architecture/comp-segments.md. + */ +#pragma once + +#include "xrt/xrt_display_metrics.h" +#include "util/comp_segments.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! + * Where a routed view goes. + */ +struct comp_segments_route_slot +{ + uint32_t segment; //!< Which segment of the routing the view belongs to + uint32_t tile; //!< The atlas tile it is painted into (its LOCAL view index) + struct comp_seg_rect rect; //!< The segment's rect inside that tile, tile px +}; + +/*! + * Place submitted view @p view of a projection layer under @p r. + * + * Segment k's local view j (layer view `r->first_view[k] + j`) is painted into + * tile j at segment k's rect inside the tile — the mosaic: the atlas keeps the + * grid of the active mode, each tile holds every segment's j-th view side by + * side. The routing is honoured only while it is consistent with the frame: + * a segment's range must lie inside what the layer submitted, its tiles must + * exist (@p tile_count, the active mode's view count — it can change between + * the locate and the commit across a 2D/3D toggle, in which case only the + * views the tiles can hold are routed) and every rect must be non-degenerate. + * + * @param r The routing the client's last splitting locate handed out. + * @param layer_view_count The views the projection layer submitted. + * @param tile_count The atlas tiles of the active mode (cols x rows). + * @param tile_w, tile_h One view's CONTENT size in the atlas (canvas x mode scale). + * @param view The submitted view to place. + * @param[out] out Filled when the view is routed. + * @return false when the frame is unrouted (count 0, or any inconsistency — + * the caller then places every view as it always did), or when this + * view is past every segment's routed range. + */ +static inline bool +comp_segments_route_place(const struct xrt_segment_view_routing *r, + uint32_t layer_view_count, + uint32_t tile_count, + uint32_t tile_w, + uint32_t tile_h, + uint32_t view, + struct comp_segments_route_slot *out) +{ + if (r == NULL || out == NULL || r->count == 0 || r->count > XRT_MAX_SEGMENTS || tile_count == 0 || + tile_w == 0 || tile_h == 0) { + return false; + } + const struct comp_seg_rect canvas = { + r->canvas.offset.w, + r->canvas.offset.h, + (uint32_t)(r->canvas.extent.w > 0 ? r->canvas.extent.w : 0), + (uint32_t)(r->canvas.extent.h > 0 ? r->canvas.extent.h : 0), + }; + bool found = false; + struct comp_segments_route_slot hit; + memset(&hit, 0, sizeof(hit)); + // Validate EVERY segment, not only the one holding the view: a frame is + // routed all-or-nothing, exactly as the in-process renderer does it. + for (uint32_t k = 0; k < r->count; k++) { + const uint32_t n = r->view_count[k] < tile_count ? r->view_count[k] : tile_count; + if (n == 0 || r->first_view[k] + n > layer_view_count) { + return false; + } + const struct comp_seg_rect seg = { + r->rect[k].offset.w, + r->rect[k].offset.h, + (uint32_t)(r->rect[k].extent.w > 0 ? r->rect[k].extent.w : 0), + (uint32_t)(r->rect[k].extent.h > 0 ? r->rect[k].extent.h : 0), + }; + struct comp_seg_rect tr; + if (!comp_segments_tile_rect(&seg, &canvas, tile_w, tile_h, &tr)) { + return false; + } + if (!found && view >= r->first_view[k] && view < r->first_view[k] + n) { + hit.segment = k; + hit.tile = view - r->first_view[k]; + hit.rect = tr; + found = true; + } + } + if (found) { + *out = hit; + } + return found; +} + +/*! + * Is any view of @p r routed at all (and so must the unrouted placement of + * views past the routed ranges be suppressed)? True exactly when + * @ref comp_segments_route_place would accept the frame. + */ +static inline bool +comp_segments_route_active(const struct xrt_segment_view_routing *r, + uint32_t layer_view_count, + uint32_t tile_count, + uint32_t tile_w, + uint32_t tile_h) +{ + if (r == NULL || r->count == 0) { + return false; + } + struct comp_segments_route_slot s; + return comp_segments_route_place(r, layer_view_count, tile_count, tile_w, tile_h, r->first_view[0], &s); +} + +/*! + * Publish @p next as the segment table readers frame views from, bumping the + * generation only when the table (generation aside) changed — so a reader can + * tell "the same table again" from "a new one" without comparing payloads. + * + * @return true when the published table changed. + */ +static inline bool +comp_segments_publish(struct xrt_segment_metrics *pub, const struct xrt_segment_metrics *next) +{ + struct xrt_segment_metrics m = *next; + m.generation = pub->generation; + const bool changed = memcmp(pub, &m, sizeof(m)) != 0; + if (changed) { + m.generation++; + } + *pub = m; + return changed; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/ipc/client/ipc_client.h b/src/xrt/ipc/client/ipc_client.h index 749831947c..8b85d4975a 100644 --- a/src/xrt/ipc/client/ipc_client.h +++ b/src/xrt/ipc/client/ipc_client.h @@ -154,6 +154,33 @@ struct xrt_window_metrics; bool comp_ipc_client_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_window_metrics *out_metrics); +/*! + * Multi-screen M6/M3 over IPC (ADR-047 Amendment 3) — same gating contract as + * comp_ipc_client_compositor_get_window_metrics (only valid when @p xc is an + * ipc_client_compositor). + * + * - `segments_enable`: ask the service to weave this session's window per + * screen when it spans monitors (the service owns the screen registry; + * @p pinned_display_id is the session's XrSessionDisplayBindingDXR pin, 0 = + * none). Sent once, at session create. + * - `get_segment_metrics`: the segment table the service's last weave of this + * window took, eyes predicted now (count 0 = one view set). False on an + * empty table or a failed call. + * - `set_view_routing`: where each segment's views are in the next frame's + * projection layer (count 0 = unrouted). Best-effort. + */ +struct xrt_segment_metrics; +struct xrt_segment_view_routing; + +void +comp_ipc_client_compositor_segments_enable(struct xrt_compositor *xc, uint64_t pinned_display_id); + +bool +comp_ipc_client_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out_metrics); + +void +comp_ipc_client_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing); + /*! * Phase 2 workspace_sync_fence bridges. Same gating contract as * comp_ipc_client_compositor_get_window_metrics — only valid when @p xc is diff --git a/src/xrt/ipc/client/ipc_client_compositor.c b/src/xrt/ipc/client/ipc_client_compositor.c index f6a20fdb88..3b8ac8c951 100644 --- a/src/xrt/ipc/client/ipc_client_compositor.c +++ b/src/xrt/ipc/client/ipc_client_compositor.c @@ -239,6 +239,63 @@ comp_ipc_client_compositor_get_window_metrics(struct xrt_compositor *xc, struct return out_metrics->valid; } +/* + * Multi-screen M6/M3 over IPC (ADR-047 Amendment 3). Same gating contract as + * comp_ipc_client_compositor_get_window_metrics. See ipc_client.h. + */ +void +comp_ipc_client_compositor_segments_enable(struct xrt_compositor *xc, uint64_t pinned_display_id) +{ + if (xc == NULL) { + return; + } + struct ipc_client_compositor *icc = ipc_client_compositor(xc); + if (icc == NULL || icc->ipc_c == NULL) { + return; + } + xrt_result_t xret = ipc_call_compositor_segments_enable(icc->ipc_c, pinned_display_id); + if (xret != XRT_SUCCESS) { + U_LOG_W( + "multi-screen: compositor_segments_enable failed (%d) — this session's window is woven by one DP", + (int)xret); + } +} + +bool +comp_ipc_client_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out_metrics) +{ + if (out_metrics == NULL) { + return false; + } + memset(out_metrics, 0, sizeof(*out_metrics)); + if (xc == NULL) { + return false; + } + struct ipc_client_compositor *icc = ipc_client_compositor(xc); + if (icc == NULL || icc->ipc_c == NULL) { + return false; + } + xrt_result_t xret = ipc_call_compositor_get_segment_metrics(icc->ipc_c, out_metrics); + if (xret != XRT_SUCCESS || out_metrics->count > XRT_MAX_SEGMENTS) { + memset(out_metrics, 0, sizeof(*out_metrics)); // one view set + return false; + } + return out_metrics->count > 0; +} + +void +comp_ipc_client_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing) +{ + if (xc == NULL || routing == NULL) { + return; + } + struct ipc_client_compositor *icc = ipc_client_compositor(xc); + if (icc == NULL || icc->ipc_c == NULL) { + return; + } + (void)ipc_call_compositor_set_view_routing(icc->ipc_c, routing); +} + /* * Push the session's eye-tracking control mode (MANAGED=0 / MANUAL=1) to the * out-of-process display processor (#522). Same gating contract as diff --git a/src/xrt/ipc/server/ipc_server_handler.c b/src/xrt/ipc/server/ipc_server_handler.c index 8cad4fbe16..97b8d09353 100644 --- a/src/xrt/ipc/server/ipc_server_handler.c +++ b/src/xrt/ipc/server/ipc_server_handler.c @@ -3072,6 +3072,84 @@ ipc_handle_compositor_get_predicted_eye_positions(volatile struct ipc_client_sta return XRT_SUCCESS; } +/* + * Multi-screen M6/M3 over IPC (ADR-047 Amendment 3): per-screen weaving of a + * direct client's window, and per-segment views for its xrLocateViews. The + * service owns the screen registry (its own instance), so the client sends + * only its session's display pin; the table and the routing then cross the + * wire by value. Off the D3D11 service compositor these are inert: enable is + * a no-op, the table is empty (one view set), the routing is dropped. + */ +static_assert(sizeof(struct ipc_compositor_get_segment_metrics_reply) <= IPC_BUF_SIZE, + "compositor_get_segment_metrics reply exceeds IPC_BUF_SIZE"); +static_assert(sizeof(struct ipc_compositor_set_view_routing_msg) <= IPC_BUF_SIZE, + "compositor_set_view_routing message exceeds IPC_BUF_SIZE"); + +xrt_result_t +ipc_handle_compositor_segments_enable(volatile struct ipc_client_state *ics, uint64_t pinned_display_id) +{ + IPC_TRACE_MARKER(); + +#if defined(XRT_HAVE_D3D11_SERVICE_COMPOSITOR) + if (ics->server != NULL && ics->server->xsysc != NULL && ics->xc != NULL && + comp_d3d11_service_is_d3d11_service(ics->server->xsysc)) { + struct xrt_screen_list *list = U_TYPED_CALLOC(struct xrt_screen_list); + if (list == NULL) { + return XRT_ERROR_ALLOCATION; + } + if (xrt_instance_enumerate_displays(ics->server->xinst, list) != XRT_SUCCESS) { + memset(list, 0, sizeof(*list)); // no screens: the manager stays disabled + } + comp_d3d11_service_set_client_segment_screens(ics->server->xsysc, (struct xrt_compositor *)ics->xc, + list, pinned_display_id); + free(list); + } +#else + (void)ics; + (void)pinned_display_id; +#endif + + return XRT_SUCCESS; +} + +xrt_result_t +ipc_handle_compositor_get_segment_metrics(volatile struct ipc_client_state *ics, + struct xrt_segment_metrics *out_metrics) +{ + IPC_TRACE_MARKER(); + + memset(out_metrics, 0, sizeof(*out_metrics)); +#if defined(XRT_HAVE_D3D11_SERVICE_COMPOSITOR) + if (ics->server != NULL && ics->server->xsysc != NULL && ics->xc != NULL) { + (void)comp_d3d11_service_get_client_segment_metrics(ics->server->xsysc, + (struct xrt_compositor *)ics->xc, out_metrics); + } +#else + (void)ics; +#endif + + return XRT_SUCCESS; +} + +xrt_result_t +ipc_handle_compositor_set_view_routing(volatile struct ipc_client_state *ics, + const struct xrt_segment_view_routing *routing) +{ + IPC_TRACE_MARKER(); + +#if defined(XRT_HAVE_D3D11_SERVICE_COMPOSITOR) + if (ics->server != NULL && ics->server->xsysc != NULL && ics->xc != NULL) { + comp_d3d11_service_set_client_view_routing(ics->server->xsysc, (struct xrt_compositor *)ics->xc, + routing); + } +#else + (void)ics; + (void)routing; +#endif + + return XRT_SUCCESS; +} + xrt_result_t ipc_handle_compositor_predict_frame(volatile struct ipc_client_state *ics, int64_t *out_frame_id, diff --git a/src/xrt/ipc/shared/proto.json b/src/xrt/ipc/shared/proto.json index ef7266079a..143606c3d0 100644 --- a/src/xrt/ipc/shared/proto.json +++ b/src/xrt/ipc/shared/proto.json @@ -1326,5 +1326,23 @@ "out": [ {"name": "value", "type": "uint32_t"} ] + }, + + "compositor_segments_enable": { + "in": [ + {"name": "pinned_display_id", "type": "uint64_t"} + ] + }, + + "compositor_get_segment_metrics": { + "out": [ + {"name": "metrics", "type": "struct xrt_segment_metrics"} + ] + }, + + "compositor_set_view_routing": { + "in": [ + {"name": "routing", "type": "struct xrt_segment_view_routing"} + ] } } diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index d9b6c7b233..dd7f4cf9b0 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -2885,6 +2885,15 @@ struct oxr_session struct xrt_segment_view_routing seg_route; //! Last per-segment view layout logged (one INFO line per change). struct xrt_segment_view_routing seg_route_logged; + /*! + * Multi-screen over IPC (ADR-047 Amendment 3): this service session asked + * the service to segment its window (compositor_segments_enable), so its + * locates fetch the service's segment table and its frames send the + * routing — only on a box whose service can segment at all. + */ + bool seg_ipc; + //! The routing last sent to the service (it is sent change-only). + struct xrt_segment_view_routing seg_route_sent; #ifdef XRT_OS_ANDROID //! The last ANativeWindow this session published into `android_globals` diff --git a/src/xrt/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index f75f3b26f0..e7e9b80794 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -292,6 +292,40 @@ oxr_segment_views_route_take(struct xrt_segment_view_routing *frame, struct xrt_ memset(frame, 0, sizeof(*frame)); } +/*! + * Must the routing @p next be sent to an out-of-process compositor that last + * received @p sent? (Multi-screen M3 over IPC, ADR-047 Amendment 3: the + * service keeps the last routing it was given, so the client sends it only on + * a change — a window that stays split costs no round trip per frame, and the + * first unrouted frame after a split is sent once, which clears it.) + * + * Compares only the fields that mean something for @p next's count: entries + * past the count are don't-care (the take resets them, but a sender must not + * re-send because of them). + */ +static inline bool +oxr_segment_views_route_differs(const struct xrt_segment_view_routing *sent, + const struct xrt_segment_view_routing *next) +{ + if (sent->count != next->count) { + return true; + } + if (next->count == 0) { + return false; + } + if (memcmp(&sent->canvas, &next->canvas, sizeof(next->canvas)) != 0) { + return true; + } + for (uint32_t k = 0; k < next->count && k < XRT_MAX_SEGMENTS; k++) { + if (sent->screen_id[k] != next->screen_id[k] || sent->first_view[k] != next->first_view[k] || + sent->view_count[k] != next->view_count[k] || + memcmp(&sent->rect[k], &next->rect[k], sizeof(next->rect[k])) != 0) { + return true; + } + } + return false; +} + /*! * Which located view view @p i carries: itself while active, view 0 for the * inactive tail `[active_view_count, reported)` — the ADR-041 alias, the same diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 3b3fd9ef2b..b8161a668f 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -131,6 +131,14 @@ struct xrt_window_metrics; bool comp_ipc_client_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_window_metrics *out_metrics); +// Multi-screen M6/M3 over IPC (ADR-047 Amendment 3) — same linkage pattern. +// The table / routing travel by value; see ipc_client.h. +struct xrt_segment_metrics; +void +comp_ipc_client_compositor_segments_enable(struct xrt_compositor *xc, uint64_t pinned_display_id); +bool +comp_ipc_client_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out_metrics); + // IPC-client DP tracking state (#441 Phase 2) — same linkage pattern as // above. Consumed ONLY for the derived isTracking value + tracking-state // event; view poses stay server-computed (ipc_try_get_sr_view_poses). @@ -3471,8 +3479,12 @@ locate_views_one(struct oxr_logger *log, #else const bool zone_chained = false; #endif - const bool rig_route_native = - (rig_active || view_raw != NULL || zone_chained) && !sess->is_bridge_relay && sess->xcn != NULL; + // Multi-screen over IPC (ADR-047 Amendment 3): a per-segment locate is + // framed HERE, from the segment's own eyes and canvas the service sent + // (seg); the server's rig call computes one view set for the whole window + // and would overwrite it, so a segment locate never takes it. + const bool rig_route_native = (rig_active || view_raw != NULL || zone_chained) && !sess->is_bridge_relay && + sess->xcn != NULL && seg == NULL; const bool rig_route_bridge = view_raw != NULL && sess->is_bridge_relay; if ((rig_route_native || rig_route_bridge) && ipc_service_mode) { struct ipc_view_rig_info rig_info = {0}; @@ -3674,9 +3686,13 @@ locate_views_one(struct oxr_logger *log, // fovs[0..active_view_count-1] (see #246). Entries beyond active are // filled by the device's get_view_poses and must not be overwritten. if (!ipc_rig_done) { + // Multi-screen over IPC: a segment's views are computed client-side + // from that segment's eyes (the server knows one view set only), so + // its Kooima FOVs are the answer, not the device's. + const bool seg_ipc_fov = seg != NULL && sess->seg_ipc; struct xrt_fov kooima_fovs[XRT_MAX_VIEWS]; uint32_t fov_save_count = (active_view_count < view_count) ? active_view_count : view_count; - if (have_kooima_fov && (have_view_state || rig_active)) { + if (have_kooima_fov && (have_view_state || rig_active || seg_ipc_fov)) { for (uint32_t ei = 0; ei < fov_save_count; ei++) { kooima_fovs[ei] = fovs[ei]; } @@ -3696,7 +3712,7 @@ locate_views_one(struct oxr_logger *log, // 3D state (non-IPC mode) or a chained view rig. In IPC mode, the // server already computes 3D-adjusted Kooima FOVs and returns them // via get_view_poses — don't override those. - if (have_kooima_fov && (have_view_state || rig_active)) { + if (have_kooima_fov && (have_view_state || rig_active || seg_ipc_fov)) { for (uint32_t ei = 0; ei < fov_save_count; ei++) { fovs[ei] = kooima_fovs[ei]; } @@ -4156,13 +4172,18 @@ locate_views_one(struct oxr_logger *log, */ //! The compositor's segment table, when this session's compositor segments its -//! window (in-process Vulkan on desktop Linux, in-process D3D11 on Windows). -//! Everything else — the other native compositors, IPC/service sessions (the -//! service never segments) — answers false and keeps one view set. +//! window (in-process Vulkan on desktop Linux, in-process D3D11 on Windows, the +//! D3D11 service for a Windows IPC session that enabled it — one round trip, +//! only on a box with two DP-backed screens). Everything else — the other +//! native compositors, every other service session — answers false and keeps +//! one view set. static bool locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics *out) { memset(out, 0, sizeof(*out)); + if (sess->seg_ipc && sess->xcn != NULL) { + return comp_ipc_client_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; + } #ifdef XRT_HAVE_VK_NATIVE_COMPOSITOR if (sess->xcn != NULL && sess->is_vk_native_compositor) { return comp_vk_native_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; @@ -6290,6 +6311,24 @@ oxr_session_create(struct oxr_logger *log, } #endif +#ifdef XRT_OS_WINDOWS + // Multi-screen M6/M3 over IPC (ADR-047 Amendment 3): the D3D11 service + // weaves a direct client's window per screen the same way. It owns the + // screen registry, so only the session's pin crosses the wire. Asked for + // only where a window can split at all (two DP-backed screens: the set + // capacity), so a single-screen box never sends it and never pays the + // per-locate table fetch. + if (sys->xsysc != NULL && sys->xsysc->info.is_service_mode && sys->xsysc->xmcc == NULL && sess->xcn != NULL && + !sess->is_bridge_relay && !xsi.is_workspace_controller && sys->multiview_set_capacity > 1) { + uint64_t pinned = 0; +#ifdef OXR_HAVE_DXR_display_info + pinned = bound_display_id; +#endif + comp_ipc_client_compositor_segments_enable(&sess->xcn->base, pinned); + sess->seg_ipc = true; + } +#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/src/xrt/state_trackers/oxr/oxr_session_frame_end.c b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c index eb21ccc555..6f02abda67 100644 --- a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c +++ b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c @@ -36,9 +36,18 @@ #include "oxr_chain.h" #include "oxr_xret.h" #include "oxr_view_config_rule.h" +#include "oxr_segment_views.h" #include +// Multi-screen M3 over IPC (ADR-047 Amendment 3): the routing hand-off to the +// D3D11 service. Declared here rather than via ipc_client.h (st_oxr does not +// pull the ipc_client include path); the runtime links ipc_client. +struct xrt_compositor; +struct xrt_segment_view_routing; +void +comp_ipc_client_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing); + #include #include @@ -3165,6 +3174,18 @@ oxr_session_frame_end(struct oxr_logger *log, struct oxr_session *sess, const Xr comp_metal_compositor_set_view_routing(&sess->xcn->base, &route); } #endif + // Multi-screen M3 over IPC (ADR-047 Amendment 3): the same hand-off to the + // D3D11 service, change-only — the service keeps the last routing, so a + // window that stays split costs no extra round trip per frame. Sent ahead + // of this frame's layer_commit on the same connection, so it applies to it. + if (sess->seg_ipc && sess->xcn != NULL) { + struct xrt_segment_view_routing route; + oxr_session_take_segment_view_routing(sess, &route); + if (oxr_segment_views_route_differs(&sess->seg_route_sent, &route)) { + comp_ipc_client_compositor_set_view_routing(&sess->xcn->base, &route); + sess->seg_route_sent = route; + } + } xrt_result_t xret; xret = xrt_comp_layer_begin(xc, &data); diff --git a/src/xrt/state_trackers/oxr/oxr_system.c b/src/xrt/state_trackers/oxr/oxr_system.c index 039a952b6d..8b0fa06366 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -171,9 +171,9 @@ oxr_system_fill_in( /* * Multi-screen M3: advertise one view set per segment only where a * window can ever be split — the in-process Vulkan compositor on desktop - * Linux, the in-process D3D11 compositor on Windows or the in-process - * Metal compositor on macOS, with at least two - * registry screens that have a DP factory. A single-screen box, a service + * Linux, the in-process D3D11 compositor or the D3D11 service on Windows, + * or the in-process Metal compositor on macOS, with at least two registry + * screens that have a DP factory. A single-screen box, a Linux service * session and every other platform keep the pre-M3 count, so existing * multiview apps and the CTS see no change. */ @@ -201,9 +201,17 @@ oxr_system_fill_in( // capacity is per system, so a D3D12 / GL / Vulkan session on the // same box advertises the doubled count too and never splits — the // tail views then repeat view 0, which the contract allows. + // + // Service sessions too (ADR-047 Amendment 3): the D3D11 service + // segments a direct IPC client's window the same way. The registry + // counted here is then the SERVICE's — `xrt_system_compositor_info` + // crosses the wire by value, so each entry's factory pointer is a + // service-process address: never called here, only tested for NULL + // ("the service can make a D3D11 DP for that screen"). A + // single-screen box still counts 1 and keeps the pre-M3 count. const char *segs_env = getenv("DXR_SEGMENTS"); const bool segments_off = segs_env != NULL && segs_env[0] == '0'; - if (sys->xsysc != NULL && !sys->xsysc->info.is_service_mode && !segments_off) { + if (sys->xsysc != NULL && !segments_off) { can_segment = true; const struct xrt_dp_factory_registry *reg = &sys->xsysc->info.dp_registry; for (uint32_t e = 0; e < reg->entry_count && e < XRT_DP_REGISTRY_MAX_ENTRIES; e++) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ae335ba9a0..1797ea885f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -65,6 +65,7 @@ set(tests tests_oxr_layer_space tests_oxr_view_config_rule tests_oxr_segment_views + tests_segments_ipc tests_target_screen_pin tests_oxr_legacy_mode_rule tests_oxr_mode_fillable_rule @@ -614,6 +615,21 @@ target_sources(tests_oxr_segment_views PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/sta target_include_directories(tests_oxr_segment_views PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/state_trackers/oxr) target_link_libraries(tests_oxr_segment_views PRIVATE xrt-interfaces aux_math) +# Multi-screen on the service / IPC path (ADR-047 Amendment 3): the routed-view +# mosaic placement and the change-only table publish (compositor/util), the +# client's change-only routing send (oxr), and -- where the IPC protocol is +# generated -- that the two new wire messages fit one IPC buffer. +target_sources(tests_segments_ipc PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/compositor/util/comp_segments.c) +target_include_directories( + tests_segments_ipc PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/compositor + ${PROJECT_SOURCE_DIR}/src/xrt/state_trackers/oxr + ) +target_link_libraries(tests_segments_ipc PRIVATE xrt-interfaces aux_util) +if(TARGET ipc_shared) + target_link_libraries(tests_segments_ipc PRIVATE ipc_shared) + target_compile_definitions(tests_segments_ipc PRIVATE DXR_TESTS_HAVE_IPC_PROTOCOL) +endif() + # Multi-screen M4 — DXR_SCREEN_PLUGIN (per-screen plug-in pin) and the per-monitor # winner rule of the DP registry, pure functions of target_screen_pin.c. target_sources(tests_target_screen_pin PRIVATE ${PROJECT_SOURCE_DIR}/src/xrt/targets/common/target_screen_pin.c) diff --git a/tests/tests_ipc_proto.py b/tests/tests_ipc_proto.py index b53f97a3f9..8f956e8915 100644 --- a/tests/tests_ipc_proto.py +++ b/tests/tests_ipc_proto.py @@ -148,6 +148,23 @@ def test_default_name_unchanged(self): call = self._call("weave_get_fence") self.assertEqual(call.out_handles.arg_names, ("handles", "handle_count")) + def test_segment_calls_are_appended(self): + # Multi-screen on the service path (ADR-047 Amendment 3): the three + # calls ride at the END of proto.json, so every earlier command keeps + # its enum value (append-only; the u_git_tag gate refuses a skewed + # client/service pair at connect anyway). + names = [c.name for c in self.p.calls] + tail = ["compositor_segments_enable", "compositor_get_segment_metrics", "compositor_set_view_routing"] + self.assertEqual(names[-3:], tail) + enable = self._call("compositor_segments_enable") + self.assertEqual([(a.name, a.typename) for a in enable.in_args], [("pinned_display_id", "uint64_t")]) + get = self._call("compositor_get_segment_metrics") + self.assertEqual([(a.name, a.typename) for a in get.out_args], + [("metrics", "struct xrt_segment_metrics")]) + route = self._call("compositor_set_view_routing") + self.assertEqual([(a.name, a.typename) for a in route.in_args], + [("routing", "struct xrt_segment_view_routing")]) + if __name__ == "__main__": if len(sys.argv) < 2: diff --git a/tests/tests_segments_ipc.cpp b/tests/tests_segments_ipc.cpp new file mode 100644 index 0000000000..57a657745c --- /dev/null +++ b/tests/tests_segments_ipc.cpp @@ -0,0 +1,289 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Multi-screen segments on the service / IPC path (ADR-047 Amendment 3): + * the routed-view placement the D3D11 service paints its atlas mosaic + * with, the change-only publish of the segment table, the client's + * change-only routing send, and the wire messages that carry both. + */ + +#include "util/comp_segments_route.h" +#include "oxr_segment_views.h" + +#ifdef DXR_TESTS_HAVE_IPC_PROTOCOL +#include "shared/ipc_protocol.h" +#include "ipc_protocol_generated.h" +#endif + +#include "catch_amalgamated.hpp" + +#include + +namespace { + +/* + * A 1920x1080 window across a seam at window x = 1200: segment 0 (the + * primary screen) is 1200 px wide, segment 1 720 px. Two views per segment + * (stereo per screen), so the layer carries 4 views and the atlas keeps the + * 2-tile stereo grid. + */ +xrt_segment_view_routing +two_screen_routing() +{ + xrt_segment_view_routing r; + std::memset(&r, 0, sizeof(r)); + r.count = 2; + r.screen_id[0] = 0x1001; + r.screen_id[1] = 0x2002; + r.rect[0] = {{0, 0}, {1200, 1080}}; + r.rect[1] = {{1200, 0}, {720, 1080}}; + r.first_view[0] = 0; + r.view_count[0] = 2; + r.first_view[1] = 2; + r.view_count[1] = 2; + r.canvas = {{0, 0}, {1920, 1080}}; + return r; +} + +// One tile = the window at half scale (a 2x1 stereo atlas of 960x540 views). +const uint32_t TILE_W = 960; +const uint32_t TILE_H = 540; + +comp_seg_rect +expected_tile_rect(const xrt_rect &seg, const xrt_rect &canvas) +{ + const comp_seg_rect s = {seg.offset.w, seg.offset.h, (uint32_t)seg.extent.w, (uint32_t)seg.extent.h}; + const comp_seg_rect c = {canvas.offset.w, canvas.offset.h, (uint32_t)canvas.extent.w, + (uint32_t)canvas.extent.h}; + comp_seg_rect out{}; + REQUIRE(comp_segments_tile_rect(&s, &c, TILE_W, TILE_H, &out)); + return out; +} + +} // namespace + +TEST_CASE("segments ipc: routed views land as a mosaic, at the crop's rect") +{ + const xrt_segment_view_routing r = two_screen_routing(); + const comp_seg_rect r0 = expected_tile_rect(r.rect[0], r.canvas); + const comp_seg_rect r1 = expected_tile_rect(r.rect[1], r.canvas); + + // The two rects share the seam column inside a tile: no gap, no overlap. + CHECK(r0.x == 0); + CHECK(r0.x + (int32_t)r0.w == r1.x); + CHECK(r1.x + (int32_t)r1.w == (int32_t)TILE_W); + + REQUIRE(comp_segments_route_active(&r, 4, 2, TILE_W, TILE_H)); + + struct + { + uint32_t view, segment, tile; + } cases[] = {{0, 0, 0}, {1, 0, 1}, {2, 1, 0}, {3, 1, 1}}; + for (const auto &c : cases) { + comp_segments_route_slot s{}; + REQUIRE(comp_segments_route_place(&r, 4, 2, TILE_W, TILE_H, c.view, &s)); + CHECK(s.segment == c.segment); + CHECK(s.tile == c.tile); + const comp_seg_rect &want = c.segment == 0 ? r0 : r1; + CHECK(s.rect.x == want.x); + CHECK(s.rect.y == want.y); + CHECK(s.rect.w == want.w); + CHECK(s.rect.h == want.h); + } +} + +TEST_CASE("segments ipc: hardware 2D packs one tile; each segment's first view lands in it") +{ + xrt_segment_view_routing r = two_screen_routing(); + // The locate in a 1-view mode hands each segment one view. + r.view_count[0] = 1; + r.first_view[1] = 1; + r.view_count[1] = 1; + comp_segments_route_slot s{}; + REQUIRE(comp_segments_route_place(&r, 2, 1, TILE_W, TILE_H, 0, &s)); + CHECK(s.segment == 0); + CHECK(s.tile == 0); + REQUIRE(comp_segments_route_place(&r, 2, 1, TILE_W, TILE_H, 1, &s)); + CHECK(s.segment == 1); + CHECK(s.tile == 0); +} + +TEST_CASE("segments ipc: a 3D routing over a 1-tile commit routes only what the tiles hold") +{ + // The mode flipped to 2D between the locate and the commit: two views per + // segment were located, one tile is packed. Each segment's FIRST view is + // routed; its second has no tile. + const xrt_segment_view_routing r = two_screen_routing(); + comp_segments_route_slot s{}; + REQUIRE(comp_segments_route_place(&r, 4, 1, TILE_W, TILE_H, 0, &s)); + CHECK(s.tile == 0); + CHECK_FALSE(comp_segments_route_place(&r, 4, 1, TILE_W, TILE_H, 1, &s)); // the caller skips it + REQUIRE(comp_segments_route_place(&r, 4, 1, TILE_W, TILE_H, 2, &s)); + CHECK(s.segment == 1); + CHECK(s.tile == 0); + CHECK_FALSE(comp_segments_route_place(&r, 4, 1, TILE_W, TILE_H, 3, &s)); +} + +TEST_CASE("segments ipc: an inconsistent routing is dropped for the whole frame") +{ + comp_segments_route_slot s{}; + + SECTION("unrouted") + { + xrt_segment_view_routing r; + std::memset(&r, 0, sizeof(r)); + CHECK_FALSE(comp_segments_route_active(&r, 2, 2, TILE_W, TILE_H)); + CHECK_FALSE(comp_segments_route_place(&r, 2, 2, TILE_W, TILE_H, 0, &s)); + } + SECTION("a segment's range past what the layer submitted") + { + // The app submitted one stereo set (2 views) for a frame located per + // segment: segment 1's views do not exist — not even segment 0's + // views may be routed (all or nothing). + const xrt_segment_view_routing r = two_screen_routing(); + CHECK_FALSE(comp_segments_route_active(&r, 2, 2, TILE_W, TILE_H)); + CHECK_FALSE(comp_segments_route_place(&r, 2, 2, TILE_W, TILE_H, 0, &s)); + } + SECTION("a degenerate segment rect") + { + xrt_segment_view_routing r = two_screen_routing(); + r.rect[1] = {{1920, 0}, {0, 1080}}; + CHECK_FALSE(comp_segments_route_active(&r, 4, 2, TILE_W, TILE_H)); + } + SECTION("a count past the arrays") + { + xrt_segment_view_routing r = two_screen_routing(); + r.count = XRT_MAX_SEGMENTS + 1; + CHECK_FALSE(comp_segments_route_active(&r, 4, 2, TILE_W, TILE_H)); + } + SECTION("no tiles") + { + const xrt_segment_view_routing r = two_screen_routing(); + CHECK_FALSE(comp_segments_route_active(&r, 4, 0, TILE_W, TILE_H)); + CHECK_FALSE(comp_segments_route_active(&r, 4, 2, 0, TILE_H)); + } +} + +TEST_CASE("segments ipc: the published table bumps its generation only on a change") +{ + xrt_segment_metrics pub; + std::memset(&pub, 0, sizeof(pub)); + xrt_segment_metrics next; + std::memset(&next, 0, sizeof(next)); + + // Empty -> empty: nothing happened. + CHECK_FALSE(comp_segments_publish(&pub, &next)); + CHECK(pub.generation == 0); + + next.count = 2; + next.seg[0].screen_id = 0x1001; + next.seg[1].screen_id = 0x2002; + next.window_pixel_width = 1920; + CHECK(comp_segments_publish(&pub, &next)); + CHECK(pub.generation == 1); + CHECK(pub.count == 2); + + // The same table again (whatever generation the caller left in it). + next.generation = 77; + CHECK_FALSE(comp_segments_publish(&pub, &next)); + CHECK(pub.generation == 1); + + // A move. + next.window_screen_left = 100; + CHECK(comp_segments_publish(&pub, &next)); + CHECK(pub.generation == 2); + CHECK(pub.window_screen_left == 100); + + // Back to one view set. + std::memset(&next, 0, sizeof(next)); + CHECK(comp_segments_publish(&pub, &next)); + CHECK(pub.count == 0); + CHECK(pub.generation == 3); +} + +TEST_CASE("segments ipc: the client sends the routing only when it changes") +{ + xrt_segment_view_routing sent; + std::memset(&sent, 0, sizeof(sent)); + xrt_segment_view_routing next; + std::memset(&next, 0, sizeof(next)); + + // Unrouted -> unrouted: nothing to send, ever (the single-screen case). + CHECK_FALSE(oxr_segment_views_route_differs(&sent, &next)); + + next = two_screen_routing(); + CHECK(oxr_segment_views_route_differs(&sent, &next)); + sent = next; + CHECK_FALSE(oxr_segment_views_route_differs(&sent, &next)); + + // A resize moves the seam. + next.rect[0].extent.w = 1100; + CHECK(oxr_segment_views_route_differs(&sent, &next)); + next = sent; + + // A mode change re-ranges the views. + next.view_count[0] = 1; + CHECK(oxr_segment_views_route_differs(&sent, &next)); + next = sent; + + // The first unrouted frame after a split is sent once (it clears the + // service's routing)... + xrt_segment_view_routing none; + std::memset(&none, 0, sizeof(none)); + CHECK(oxr_segment_views_route_differs(&sent, &none)); + sent = none; + CHECK_FALSE(oxr_segment_views_route_differs(&sent, &none)); + + // ...and stale entries past the count never trigger a send. + xrt_segment_view_routing junk; + std::memset(&junk, 0, sizeof(junk)); + junk.first_view[1] = 9; + junk.screen_id[0] = 42; + CHECK_FALSE(oxr_segment_views_route_differs(&sent, &junk)); +} + +#ifdef DXR_TESTS_HAVE_IPC_PROTOCOL +TEST_CASE("segments ipc: the wire messages fit one IPC buffer and carry the payload verbatim") +{ + STATIC_REQUIRE(sizeof(ipc_compositor_get_segment_metrics_reply) <= IPC_BUF_SIZE); + STATIC_REQUIRE(sizeof(ipc_compositor_set_view_routing_msg) <= IPC_BUF_SIZE); + STATIC_REQUIRE(sizeof(ipc_compositor_segments_enable_msg) <= IPC_BUF_SIZE); + + // The table crosses by value (the generated proxy memcpys the struct): + // a full two-segment table with eyes survives byte for byte. + xrt_segment_metrics m; + std::memset(&m, 0, sizeof(m)); + m.count = 2; + for (uint32_t k = 0; k < 2; k++) { + m.seg[k].screen_id = 0x1000 + k; + m.seg[k].window_rect = {{(int)(k * 1200), 0}, {k == 0 ? 1200 : 720, 1080}}; + m.seg[k].screen_width_m = 0.3442f; + m.seg[k].nominal_viewer_z_m = 0.65f; + m.seg[k].is_primary = k == 0; + m.seg[k].has_dp = true; + m.seg[k].woven = true; + m.seg[k].eyes.count = 2; + m.seg[k].eyes.valid = true; + m.seg[k].eyes.eyes[1].x = 0.032f; + m.seg[k].have_eyes = true; + } + m.generation = 5; + + ipc_compositor_get_segment_metrics_reply reply; + std::memset(&reply, 0, sizeof(reply)); + reply.result = XRT_SUCCESS; + reply.metrics = m; + ipc_compositor_get_segment_metrics_reply wire; + std::memcpy(&wire, &reply, sizeof(wire)); + CHECK(std::memcmp(&wire.metrics, &m, sizeof(m)) == 0); + + const xrt_segment_view_routing r = two_screen_routing(); + ipc_compositor_set_view_routing_msg msg; + std::memset(&msg, 0, sizeof(msg)); + msg.routing = r; + ipc_compositor_set_view_routing_msg wire_msg; + std::memcpy(&wire_msg, &msg, sizeof(wire_msg)); + CHECK(std::memcmp(&wire_msg.routing, &r, sizeof(r)) == 0); +} +#endif