diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index 6cd55bf44e..3769848493 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -181,6 +181,14 @@ deferred-release list because the immediate context executes in order). What dif The service / hosted path is not segmented yet (ADR-035 amendment, M6 second half). - **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 + the registry's D3D11 factories for the view-set capacity, `xrLocateViews` reads + `comp_d3d11_compositor_get_segment_metrics` (the last weave's table + each segment's eyes, the + secondary screen's from its own DP — on a Leia panel the pinned nominal viewer), `xrEndFrame` hands + the routing to `comp_d3d11_compositor_set_view_routing`, and the renderer paints each segment's + views at that segment's rect inside every tile (the mosaic; quads and equirect2 once per segment + with that segment's camera). A routed frame is never zero-copy. This is what makes an untracked + second panel render from its own viewer instead of the tracked panel's. ## Per-segment views (M3) diff --git a/docs/roadmap/multi-screen.md b/docs/roadmap/multi-screen.md index f835aa8a3c..f950c79e54 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). HWND stays with the primary screen's DP (majority hand-off is a follow-up); 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). HWND stays with the primary screen's DP (majority hand-off is a follow-up); 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; service/hosted half not started.** | 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/comp_d3d11_compositor.cpp b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp index 14b9ae0f63..35490c29b2 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp @@ -504,6 +504,16 @@ struct comp_d3d11_compositor uint64_t seg_pinned_display_id; struct comp_seg_rect seg_window; //!< the window's client area in desktop device px bool have_seg_window; + /*! + * Multi-screen M3: what the app thread reads (xrLocateViews) and hands + * back (xrEndFrame) while the weave thread owns the segment manager — + * a small lock of its own, never c->mutex (the repaint thread holds that + * across its whole replay). + */ + std::mutex seg_pub_mutex; + struct xrt_segment_metrics seg_pub; //!< the table the last weave took (count 0 = one view set) + struct xrt_segment_view_routing seg_route; //!< the routing the last locate handed out + struct comp_d3d11_eff_layout seg_route_logged; //!< change-only logging of the routed layout //! System devices (for qwerty driver keyboard input and display mode toggle). struct xrt_system_devices *xsysd; @@ -2156,6 +2166,85 @@ d3d11_hash_target(ID3D11Device *device, * re-reads an app-owned texture and never ticks a per-frame state machine — see * d3d11_composite_zone_mask, and the D3D12 leg for what happens when it does. */ +/*! + * Multi-screen M3: turn the routing the last xrLocateViews handed out into + * per-segment tile rects on this frame's effective layout. The D3D11 twin of + * vk_route_effective_layout: on any inconsistency (a segment's views past + * what the layer submitted, a degenerate rect) the routing is dropped for the + * frame and the tiles are painted unrouted. + */ +static void +d3d11_route_effective_layout(struct comp_d3d11_compositor *c, uint32_t layer_view_count) +{ + struct xrt_segment_view_routing r; + { + std::lock_guard seg_lock(c->seg_pub_mutex); + r = c->seg_route; + } + struct comp_d3d11_eff_layout *L = &c->eff_layout; + L->route_count = 0; + if (r.count == 0 || r.count > XRT_MAX_SEGMENTS || L->views == 0 || L->tile_w == 0 || L->tile_h == 0) { + return; + } + 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), + }; + for (uint32_t k = 0; k < r.count; k++) { + // Across a 2D<->3D toggle the mode (tile count) can change between + // the locate and this commit: route the views the tiles can hold. + const uint32_t n = r.view_count[k] < L->views ? r.view_count[k] : L->views; + if (n == 0 || r.first_view[k] + n > layer_view_count) { + return; + } + 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, L->tile_w, L->tile_h, &tr)) { + return; + } + L->route[k].first_view = r.first_view[k]; + L->route[k].view_count = n; + L->route[k].x = tr.x; + L->route[k].y = tr.y; + L->route[k].w = tr.w; + L->route[k].h = tr.h; + } + L->route_count = r.count; + + // One INFO line per routing change (a move, a resize, a mode switch). + if (memcmp(L->route, c->seg_route_logged.route, sizeof(L->route)) != 0 || + L->route_count != c->seg_route_logged.route_count || L->tile_w != c->seg_route_logged.tile_w || + L->tile_h != c->seg_route_logged.tile_h) { + c->seg_route_logged = *L; + U_LOG_I("segments: per-segment views routed — %u segment(s) x %u view(s), tile %ux%u; [0] views %u.. " + "at %d,%d %ux%u; [1] views %u.. at %d,%d %ux%u", + L->route_count, L->views, L->tile_w, L->tile_h, L->route[0].first_view, L->route[0].x, + L->route[0].y, L->route[0].w, L->route[0].h, L->route_count > 1 ? L->route[1].first_view : 0, + L->route_count > 1 ? L->route[1].x : 0, L->route_count > 1 ? L->route[1].y : 0, + L->route_count > 1 ? L->route[1].w : 0, L->route_count > 1 ? L->route[1].h : 0); + } +} + +//! The projection-class layer's submitted view count (what routing may index). +static uint32_t +d3d11_projection_layer_view_count(const struct comp_layer_accum *layers) +{ + for (uint32_t i = 0; i < layers->layer_count; i++) { + const enum xrt_layer_type t = layers->layers[i].data.type; + if (t == XRT_LAYER_PROJECTION || t == XRT_LAYER_PROJECTION_DEPTH) { + return layers->layers[i].data.view_count; + } + } + return 0; +} + static bool d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint) { @@ -2591,15 +2680,19 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint) bool seg_split = false; if (!zero_copy && !c->split_active && !eff_canvas.valid && c->have_seg_window && c->seg_screens != nullptr && atlas_srv != nullptr) { - if (c->seg_rebuild) { - comp_d3d11_segments_destroy(&c->segments); - c->seg_rebuild = false; - } - if (c->segments == nullptr) { - c->segments = comp_d3d11_segments_create(d3d11_out_device(c)); - if (c->segments != nullptr) { - comp_d3d11_segments_set_screens(c->segments, c->seg_screens, c->seg_sys_info, - c->seg_pinned_display_id); + if (c->seg_rebuild || c->segments == nullptr) { + // The app thread reads c->segments for eyes (M3): swap under the lock. + std::lock_guard seg_lock(c->seg_pub_mutex); + if (c->seg_rebuild) { + comp_d3d11_segments_destroy(&c->segments); + c->seg_rebuild = false; + } + if (c->segments == nullptr) { + c->segments = comp_d3d11_segments_create(d3d11_out_device(c)); + if (c->segments != nullptr) { + comp_d3d11_segments_set_screens(c->segments, c->seg_screens, c->seg_sys_info, + c->seg_pinned_display_id); + } } } if (comp_d3d11_segments_enabled(c->segments)) { @@ -2613,6 +2706,29 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint) } } + /* + * Multi-screen M3: publish the table this weave took, for the next + * xrLocateViews to frame per-segment views from. Nothing is published + * (count 0 = one view set) unless the window really is woven per segment. + */ + { + struct xrt_segment_metrics m; + memset(&m, 0, sizeof(m)); + if (seg_split) { + const struct comp_seg_rect canvas = {0, 0, c->seg_window.w, c->seg_window.h}; + (void)comp_d3d11_segments_get_metrics(c->segments, &c->seg_window, &canvas, + c->display_processor != nullptr, &m); + } + std::lock_guard seg_lock(c->seg_pub_mutex); + m.generation = c->seg_pub.generation; + struct xrt_segment_metrics prev = c->seg_pub; + prev.generation = m.generation; + if (memcmp(&prev, &m, sizeof(m)) != 0) { + m.generation++; + } + c->seg_pub = m; + } + if (seg_split) { struct comp_d3d11_segments_frame sf = {}; sf.context = d3d11_out_context(c); @@ -3464,6 +3580,8 @@ d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl // and the capture providers — they must all agree on the frame's // geometry. comp_d3d11_renderer_compute_effective_layout(c->renderer, &c->layer_accum, &c->eff_layout); + // Multi-screen M3: per-segment views -> a mosaic per tile (no-op unless routed). + d3d11_route_effective_layout(c, d3d11_projection_layer_view_count(&c->layer_accum)); // Zero-copy check: can we pass the app's swapchain directly to the DP? // @@ -3568,6 +3686,13 @@ d3d11_compositor_layer_commit(struct xrt_compositor *xc, xrt_graphics_sync_handl } + // Multi-screen M3: a routed frame is a mosaic the renderer must paint; the + // app's swapchain image can never be handed to the DP as-is. + if (c->eff_layout.route_count > 0) { + zero_copy = false; + zc_srv = nullptr; + } + // No CPU-side wait for the app's GPU work here, by design: the app renders // on this same immediate context, and D3D11 executes it in submission order, // so every read of an app swapchain image below is already ordered after @@ -8390,6 +8515,50 @@ comp_d3d11_compositor_set_screens(struct xrt_compositor *xc, free(c->seg_sys_info); c->seg_sys_info = info_copy; c->seg_pinned_display_id = pinned_display_id; + // ...and nothing may frame views from the old table meanwhile. + std::lock_guard seg_lock(c->seg_pub_mutex); + memset(&c->seg_pub, 0, sizeof(c->seg_pub)); +} + +extern "C" bool +comp_d3d11_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out) +{ + if (out == nullptr) { + return false; + } + memset(out, 0, sizeof(*out)); + if (xc == nullptr) { + return false; + } + struct comp_d3d11_compositor *c = d3d11_comp(xc); + std::lock_guard seg_lock(c->seg_pub_mutex); + *out = c->seg_pub; + // The eyes are predicted NOW, per segment: the primary from the session's + // own DP, every other screen from its segment DP (guarded against the + // weave destroying it: the manager is swapped under this same lock). + 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 = c->display_processor != nullptr && + xrt_display_processor_d3d11_get_predicted_eye_positions(c->display_processor, + &s->eyes) && + s->eyes.valid; + } else { + s->have_eyes = comp_d3d11_segments_get_eyes(c->segments, s->screen_id, &s->eyes); + } + } + return out->count > 0; +} + +extern "C" void +comp_d3d11_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing) +{ + if (xc == nullptr || routing == nullptr) { + return; + } + struct comp_d3d11_compositor *c = d3d11_comp(xc); + std::lock_guard seg_lock(c->seg_pub_mutex); + c->seg_route = *routing; } extern "C" void diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.h b/src/xrt/compositor/d3d11/comp_d3d11_compositor.h index f9fc9e3e92..15f43bde68 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.h @@ -244,6 +244,28 @@ comp_d3d11_compositor_set_screens(struct xrt_compositor *xc, const struct xrt_system_compositor_info *info, uint64_t pinned_display_id); +/*! + * Multi-screen M3 (Windows): the segment table the last weave took, with each + * segment's eyes predicted NOW (the primary from the session's DP, the others + * from their segment DP), for xrLocateViews to frame per-segment views. + * Thread-safe against the weave. `out->count == 0` = one view set. + * + * @ingroup comp_d3d11 + */ +bool +comp_d3d11_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out); + +/*! + * Multi-screen M3 (Windows): which views of the next frame belong to which + * window segment — what the app's last xrLocateViews handed out. The renderer + * paints each segment's views into that segment's rect of every tile (a + * mosaic) and the segment path crops them out per screen. Thread-safe. + * + * @ingroup comp_d3d11 + */ +void +comp_d3d11_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing); + /*! * XR_DXR_depth_budget: whether this session asked for the rear depth budget. * diff --git a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp index b3b9731d72..1ba61f1cd1 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp @@ -1629,6 +1629,9 @@ comp_d3d11_renderer_compute_effective_layout(struct comp_d3d11_renderer *rendere } out_layout->views = views; + // Multi-screen M3: routing is decided by the compositor, after this. + out_layout->route_count = 0; + memset(out_layout->route, 0, sizeof(out_layout->route)); if (views == 1) { // Mono content: one tile spanning the full content region (the // paint box additionally caps to the window target — see @@ -1702,6 +1705,52 @@ set_view_viewport(struct comp_d3d11_renderer *renderer, internals.context->RSSetViewports(1, &viewport); } +/*! + * Multi-screen M3: the viewport for segment @p k's local view @p view_index — + * the segment's rect inside tile @p view_index (the mosaic). False when the + * segment has no view @p view_index or an empty rect; the caller then draws + * nothing for that segment in this tile. + */ +static bool +set_routed_viewport(struct comp_d3d11_renderer *renderer, + uint32_t view_index, + uint32_t k, + const struct comp_d3d11_eff_layout *layout, + uint32_t target_width, + uint32_t target_height) +{ + if (k >= layout->route_count || k >= XRT_MAX_SEGMENTS || view_index >= layout->route[k].view_count || + layout->route[k].w == 0 || layout->route[k].h == 0) { + return false; + } + auto internals = get_internals(renderer->c); + float tx, ty, tw, th; + get_view_tile_box(renderer, view_index, layout, target_width, target_height, &tx, &ty, &tw, &th); + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = tx + static_cast(layout->route[k].x); + viewport.TopLeftY = ty + static_cast(layout->route[k].y); + viewport.Width = static_cast(layout->route[k].w); + viewport.Height = static_cast(layout->route[k].h); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + internals.context->RSSetViewports(1, &viewport); + return true; +} + +//! Multi-screen M3: the end of the last routed view range (0 = unrouted). +static uint32_t +routed_view_end(const struct comp_d3d11_eff_layout *layout) +{ + uint32_t n = 0; + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t end = layout->route[k].first_view + layout->route[k].view_count; + if (end > n) { + n = end; + } + } + return n > XRT_MAX_VIEWS ? XRT_MAX_VIEWS : n; +} + extern "C" xrt_result_t comp_d3d11_renderer_draw_projection_pass(struct comp_d3d11_renderer *renderer, struct comp_layer_accum *layers, @@ -1822,13 +1871,19 @@ comp_d3d11_renderer_draw_projection_pass(struct comp_d3d11_renderer *renderer, have_wm ? xrt_vec3{canvas->window_center_offset_x_m, canvas->window_center_offset_y_m, canvas->window_center_offset_z_m} : xrt_vec3{0.0f, 0.0f, 0.0f}; + // Multi-screen M3: under routing a quad / equirect2 is drawn once per + // routed view, with THAT view's camera — so resolve every routed view's + // camera, not only the first tile's worth. + const uint32_t routed_end = routed_view_end(layout); + const uint32_t cam_count = routed_end > effective_views ? routed_end : effective_views; struct comp_layer_view_camera cameras[XRT_MAX_VIEWS] = {}; - for (uint32_t view = 0; view < effective_views && view < XRT_MAX_VIEWS; view++) { + for (uint32_t view = 0; view < cam_count && view < XRT_MAX_VIEWS; view++) { comp_layer_view_camera_select_eyes(layers, view, eyes, effective_views, have_wm ? &canvas_center : nullptr, have_wm ? canvas->window_width_m : 0.0f, have_wm ? canvas->window_height_m : 0.0f, &cameras[view]); } + const bool routed = layout->route_count > 0; for (uint32_t view_index = 0; view_index < effective_views; view_index++) { set_view_viewport(renderer, view_index, layout, target_width, target_height); @@ -1881,11 +1936,29 @@ comp_d3d11_renderer_draw_projection_pass(struct comp_d3d11_renderer *renderer, ID3D11BlendState *bs = blend_state_for(renderer, mode); if (bs != renderer->blend_opaque) { internals.context->OMSetBlendState(bs, nullptr, 0xFFFFFFFF); + } + if (routed) { + // Multi-screen M3: the tile is a mosaic — segment k's + // local view `view_index` (layer view first_view + + // view_index) lands at the segment's rect inside this + // tile. The blend mode was decided once for the tile + // above and is shared by its segments. + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t lv = layout->route[k].first_view + view_index; + if (lv >= layer->data.view_count || + !set_routed_viewport(renderer, view_index, k, layout, target_width, + target_height)) { + continue; + } + render_projection_layer(renderer, layer, lv, mode); + } + set_view_viewport(renderer, view_index, layout, target_width, target_height); + } else { render_projection_layer(renderer, layer, view_index, mode); + } + if (bs != renderer->blend_opaque) { internals.context->OMSetBlendState(renderer->blend_opaque, nullptr, 0xFFFFFFFF); - } else { - render_projection_layer(renderer, layer, view_index, mode); } break; } @@ -1952,8 +2025,26 @@ comp_d3d11_renderer_draw_projection_pass(struct comp_d3d11_renderer *renderer, // outcomes, not errors, and a quad that did not // paint this tile must not turn a following // projection layer into a non-first one. - if (render_quad_layer(renderer, layer, view_index, effective_views, - &cameras[view_index].pose, &cameras[view_index].fov)) { + if (routed) { + // Multi-screen M3: once per segment, with that segment's + // view camera, confined to its rect in this tile. + bool drew = false; + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t lv = layout->route[k].first_view + view_index; + if (lv >= XRT_MAX_VIEWS || + !set_routed_viewport(renderer, view_index, k, layout, target_width, + target_height)) { + continue; + } + drew |= render_quad_layer(renderer, layer, lv, cam_count, &cameras[lv].pose, + &cameras[lv].fov); + } + set_view_viewport(renderer, view_index, layout, target_width, target_height); + if (drew) { + comp_layer_tile_mark_composited(&tile); + } + } else if (render_quad_layer(renderer, layer, view_index, effective_views, + &cameras[view_index].pose, &cameras[view_index].fov)) { comp_layer_tile_mark_composited(&tile); } break; @@ -1989,8 +2080,25 @@ comp_d3d11_renderer_draw_projection_pass(struct comp_d3d11_renderer *renderer, * erasing it. */ const enum comp_layer_blend_mode mode = comp_layer_blend_mode(layer->data.flags); - if (render_equirect2_layer(renderer, layer, view_index, effective_views, - &cameras[view_index].pose, &cameras[view_index].fov, mode)) { + if (routed) { + bool drew = false; + for (uint32_t k = 0; k < layout->route_count && k < XRT_MAX_SEGMENTS; k++) { + const uint32_t lv = layout->route[k].first_view + view_index; + if (lv >= XRT_MAX_VIEWS || + !set_routed_viewport(renderer, view_index, k, layout, target_width, + target_height)) { + continue; + } + drew |= render_equirect2_layer(renderer, layer, lv, cam_count, &cameras[lv].pose, + &cameras[lv].fov, mode); + } + set_view_viewport(renderer, view_index, layout, target_width, target_height); + if (drew) { + comp_layer_tile_mark_composited(&tile); + } + } else if (render_equirect2_layer(renderer, layer, view_index, effective_views, + &cameras[view_index].pose, &cameras[view_index].fov, + mode)) { comp_layer_tile_mark_composited(&tile); } break; diff --git a/src/xrt/compositor/d3d11/comp_d3d11_renderer.h b/src/xrt/compositor/d3d11/comp_d3d11_renderer.h index 3c589a72d4..e1a1a28f73 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.h @@ -42,6 +42,26 @@ struct comp_d3d11_eff_layout uint32_t rows; //!< atlas tile rows uint32_t tile_w; //!< per-tile width in pixels uint32_t tile_h; //!< per-tile height in pixels + + /*! + * Multi-screen M3 (Windows): per-segment views. 0 = one view set fills + * every tile (the only case before M3). Otherwise the projection layer + * carries one view set per window segment and each tile is a MOSAIC: + * local view j of segment k is placed at that segment's rect inside tile + * j, so cropping a segment's rect out of every tile + * (comp_d3d11_segments) yields exactly that segment's own views. Quads + * and equirect2 layers are drawn once per segment with that segment's + * camera; zones are not routed. Filled by the compositor after + * comp_d3d11_renderer_compute_effective_layout (which zeroes it). + */ + uint32_t route_count; + struct + { + uint32_t first_view; //!< first layer view of the segment + uint32_t view_count; //!< its views (<= @ref views) + int32_t x, y; //!< the segment's rect inside a tile, tile px + uint32_t w, h; + } route[XRT_MAX_SEGMENTS]; }; /*! diff --git a/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp b/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp index 27451c53cc..6964b2236f 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp @@ -65,6 +65,7 @@ struct comp_d3d11_segments uint32_t screen_count; struct comp_segments_screen screens[COMP_SEGMENTS_MAX_SCREENS]; struct xrt_screen_binding bindings[COMP_SEGMENTS_MAX_SCREENS]; + struct xrt_screen_info info[COMP_SEGMENTS_MAX_SCREENS]; //!< per-screen size + nominal viewer (M3) 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]; @@ -405,6 +406,7 @@ comp_d3d11_segments_set_screens(struct comp_d3d11_segments *segs, if (e != nullptr) { snprintf(b->serial, sizeof(b->serial), "%s", e->serial); } + segs->info[n] = s->info; n++; } segs->screen_count = n; @@ -701,6 +703,73 @@ comp_d3d11_segments_set_display_mode(struct comp_d3d11_segments *segs, bool enab } } +extern "C" bool +comp_d3d11_segments_get_metrics(const struct comp_d3d11_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + bool primary_has_dp, + struct xrt_segment_metrics *out) +{ + memset(out, 0, sizeof(*out)); + if (segs == nullptr || !segs->enabled || !segs->logged_split || window_desktop == nullptr || + canvas == nullptr) { + return false; + } + const struct comp_segment_table *t = &segs->table; + if (t->count == 0 || t->count > XRT_MAX_SEGMENTS) { + // More screens than view sets: the window keeps one view set (M2). + return false; + } + for (uint32_t k = 0; k < t->count; k++) { + const struct comp_segment *g = &t->seg[k]; + if (g->screen_index >= segs->screen_count) { + return false; + } + const uint32_t i = g->screen_index; + const struct seg_screen_state *st = &segs->st[i]; + struct xrt_segment_metric *m = &out->seg[k]; + m->screen_id = g->screen_id; + m->window_rect.offset.w = g->window_rect.x; + m->window_rect.offset.h = g->window_rect.y; + m->window_rect.extent.w = (int)g->window_rect.w; + m->window_rect.extent.h = (int)g->window_rect.h; + m->screen_rect.offset.w = g->screen_rect.x; + m->screen_rect.offset.h = g->screen_rect.y; + m->screen_rect.extent.w = (int)g->screen_rect.w; + m->screen_rect.extent.h = (int)g->screen_rect.h; + m->screen_desktop_left = segs->screens[i].desktop.x; + m->screen_desktop_top = segs->screens[i].desktop.y; + m->screen_desktop_width = segs->screens[i].desktop.w; + m->screen_desktop_height = segs->screens[i].desktop.h; + m->screen_width_m = segs->info[i].width_m; + m->screen_height_m = segs->info[i].height_m; + m->nominal_viewer_x_m = segs->info[i].nominal_viewer_x_m; + m->nominal_viewer_y_m = segs->info[i].nominal_viewer_y_m; + m->nominal_viewer_z_m = segs->info[i].nominal_viewer_z_m; + m->is_primary = g->is_primary; + if (g->is_primary) { + m->has_dp = primary_has_dp; + m->tolerates_resample = false; // the primary keeps the session-level gates + m->woven = primary_has_dp; + } else { + m->has_dp = st->dp != nullptr; + m->tolerates_resample = st->tolerates_resample; + m->woven = comp_segments_decide(m->has_dp, st->tolerates_resample, g->screen_1to1) == + COMP_SEG_RENDER_WEAVE; + } + } + out->count = t->count; + out->canvas.offset.w = canvas->x; + out->canvas.offset.h = canvas->y; + out->canvas.extent.w = (int)canvas->w; + out->canvas.extent.h = (int)canvas->h; + out->window_screen_left = window_desktop->x; + out->window_screen_top = window_desktop->y; + out->window_pixel_width = window_desktop->w; + out->window_pixel_height = window_desktop->h; + return true; +} + extern "C" bool comp_d3d11_segments_get_eyes(struct comp_d3d11_segments *segs, uint64_t screen_id, struct xrt_eye_positions *out) { @@ -711,7 +780,7 @@ comp_d3d11_segments_get_eyes(struct comp_d3d11_segments *segs, uint64_t screen_i os_mutex_lock(&segs->dp_mutex); const int i = screen_index_of(segs, screen_id); if (i >= 0 && segs->st[i].dp != nullptr) { - ok = xrt_display_processor_d3d11_get_predicted_eye_positions(segs->st[i].dp, out); + ok = xrt_display_processor_d3d11_get_predicted_eye_positions(segs->st[i].dp, out) && out->valid; } os_mutex_unlock(&segs->dp_mutex); return ok; diff --git a/src/xrt/compositor/d3d11/comp_d3d11_segments.h b/src/xrt/compositor/d3d11/comp_d3d11_segments.h index 52b1bc1d32..187e39814b 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_segments.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_segments.h @@ -27,6 +27,7 @@ #include "xrt/xrt_compiler.h" #include "xrt/xrt_compositor.h" +#include "xrt/xrt_display_metrics.h" #include "xrt/xrt_display_processor_d3d11.h" #include "xrt/xrt_screen.h" #include "util/comp_segments.h" @@ -152,6 +153,22 @@ comp_d3d11_segments_set_display_mode(struct comp_d3d11_segments *segs, bool enab bool comp_d3d11_segments_get_eyes(struct comp_d3d11_segments *segs, uint64_t screen_id, struct xrt_eye_positions *out); +/*! + * Multi-screen M3: the last update's segment table as per-segment view + * metrics (geometry, each screen's physical size + nominal viewer, whether it + * is woven). Eyes are NOT filled — they are predicted per query + * (@ref comp_d3d11_segments_get_eyes). + * + * @return false when the last update did not split the window, or split it + * into more than XRT_MAX_SEGMENTS segments (one view set then). + */ +bool +comp_d3d11_segments_get_metrics(const struct comp_d3d11_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + bool primary_has_dp, + struct xrt_segment_metrics *out); + #ifdef __cplusplus } #endif diff --git a/src/xrt/include/xrt/xrt_display_metrics.h b/src/xrt/include/xrt/xrt_display_metrics.h index 4ac6c94276..43bea61042 100644 --- a/src/xrt/include/xrt/xrt_display_metrics.h +++ b/src/xrt/include/xrt/xrt_display_metrics.h @@ -140,7 +140,7 @@ struct xrt_window_metrics /*! * One window segment — the part of a window's canvas on one screen — as the * view math needs it (multi-screen M3). Produced by an in-process compositor - * that segments its window (Vulkan native, desktop Linux). + * that segments its window (Vulkan native on desktop Linux, D3D11 on Windows). * * @ingroup xrt_iface */ diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 083eb1c7e8..6c8ea6afd2 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -4156,9 +4156,9 @@ locate_views_one(struct oxr_logger *log, */ //! The compositor's segment table, when this session's compositor segments its -//! window (in-process Vulkan, desktop Linux). 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). +//! Everything else — the other native compositors, IPC/service sessions (the +//! service never segments) — answers false and keeps one view set. static bool locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics *out) { @@ -4167,9 +4167,13 @@ locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics if (sess->xcn != NULL && sess->is_vk_native_compositor) { return comp_vk_native_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; } -#else - (void)sess; #endif +#if defined(XRT_HAVE_D3D11_NATIVE_COMPOSITOR) && defined(XRT_OS_WINDOWS) + if (sess->xcn != NULL && sess->is_d3d11_native_compositor) { + return comp_d3d11_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; + } +#endif + (void)sess; return false; } 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 7249456266..37e39ddba0 100644 --- a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c +++ b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c @@ -3149,6 +3149,14 @@ oxr_session_frame_end(struct oxr_logger *log, struct oxr_session *sess, const Xr comp_vk_native_compositor_set_view_routing(&sess->xcn->base, &route); } #endif +#if defined(XRT_HAVE_D3D11_NATIVE_COMPOSITOR) && defined(XRT_OS_WINDOWS) + // Multi-screen M3 on Windows: same hand-off to the in-process D3D11 compositor. + if (sess->is_d3d11_native_compositor && sess->xcn != NULL) { + struct xrt_segment_view_routing route; + oxr_session_take_segment_view_routing(sess, &route); + comp_d3d11_compositor_set_view_routing(&sess->xcn->base, &route); + } +#endif 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 7024480796..39f556aec1 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -171,9 +171,10 @@ 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, with at least two registry screens that have a DP factory. A - * single-screen box, a service session and every other platform keep the - * pre-M3 count, so existing multiview apps and the CTS see no change. + * Linux or the in-process D3D11 compositor on Windows, with at least two + * registry screens that have a DP factory. A single-screen box, a service + * session and every other platform keep the pre-M3 count, so existing + * multiview apps and the CTS see no change. */ { uint32_t with_factory = 0; @@ -192,6 +193,24 @@ oxr_system_fill_in( } } } +#elif defined(XRT_OS_WINDOWS) && defined(XRT_HAVE_D3D11_NATIVE_COMPOSITOR) + // Multi-screen M3 on Windows: the in-process D3D11 compositor + // segments a window the same way (comp_d3d11_segments); the + // registry's D3D11 factory is what says a screen has a DP. The + // 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. + 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) { + 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++) { + if (reg->entries[e].dp_factory_d3d11 != NULL) { + with_factory++; + } + } + } #endif sys->multiview_set_capacity = oxr_segment_views_set_capacity(with_factory, can_segment); if (sys->multiview_set_capacity > 1) { diff --git a/test_apps/handle/cube_handle_d3d11_win/main.cpp b/test_apps/handle/cube_handle_d3d11_win/main.cpp index 01d8a40a4e..aaf85efe10 100644 --- a/test_apps/handle/cube_handle_d3d11_win/main.cpp +++ b/test_apps/handle/cube_handle_d3d11_win/main.cpp @@ -1136,6 +1136,19 @@ static void RenderOneFrame(RenderState& rs) { viewState.next = &rawProbe; } + // Multi-screen M3 (ADR-041 + XR_DXR_display_info v22): how + // many located views are live this frame, and which of them + // belong to which display when the window spans displays — + // chained in front of whatever the rig probe put there. + XrViewActivityStateDXR viewActivity = {XR_TYPE_VIEW_ACTIVITY_STATE_DXR}; + XrViewDisplayBindingDXR viewBindingStorage[4] = {}; + XrViewDisplayBindingsDXR viewBindings = {XR_TYPE_VIEW_DISPLAY_BINDINGS_DXR}; + viewBindings.bindingCapacityInput = 4; + viewBindings.bindings = viewBindingStorage; + viewBindings.next = viewState.next; + viewActivity.next = &viewBindings; + viewState.next = &viewActivity; + // XR_DXR_view_rig (#396 W7): drive the runtime rig matching // the app's current mode (C selects the rig) with the app's // tunables — the runtime owns the window/canvas resolve and @@ -1185,6 +1198,46 @@ static void RenderOneFrame(RenderState& rs) { xrLocateViews(xr.session, &locateInfo, &viewState, 8, &viewCount, rawViews); + // Multi-screen M3: with the window spanning displays the + // runtime hands out one view set per display; render ALL + // of them (the sum is activeViewCount), each set's local + // view j into tile j at that display's segment rect (placed + // below once the tile size is known). Tiled layout only; + // the array layout keeps its two slices. + const bool routedViews = + !ArrayLayoutEnabled() && !monoMode && + xr.viewConfigType == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR && + viewBindings.bindingCountOutput > 0 && viewBindings.bindingCountOutput <= 4; + if (routedViews) { + int routedEyes = 0; + for (uint32_t b = 0; b < viewBindings.bindingCountOutput; b++) { + const int end = (int)(viewBindingStorage[b].firstView + viewBindingStorage[b].viewCount); + if (end > routedEyes) routedEyes = end; + } + if (routedEyes > (int)locatedCount) routedEyes = (int)locatedCount; + if (routedEyes > 0) eyeCount = routedEyes; + } + { + // Log the bindings when they change (one line, never per frame). + static std::string lastBindings; + std::string now = routedViews ? "routed" : "single"; + for (uint32_t b = 0; routedViews && b < viewBindings.bindingCountOutput; b++) { + const XrViewDisplayBindingDXR& vb = viewBindingStorage[b]; + char buf[160]; + snprintf(buf, sizeof(buf), " [display 0x%llx views %u..%u at %d,%d %dx%d]", + (unsigned long long)vb.displayId, vb.firstView, + vb.firstView + vb.viewCount - 1, vb.segmentRect.offset.x, + vb.segmentRect.offset.y, vb.segmentRect.extent.width, + vb.segmentRect.extent.height); + now += buf; + } + if (now != lastBindings) { + LOG_INFO("view sets: %s (activeViewCount=%u, rendering %d)", now.c_str(), + viewActivity.activeViewCount, eyeCount); + lastBindings = now; + } + } + // #1370: the grip / joint markers are located in LOCAL. On the // rig path the runtime returns render-ready views IN LOCAL, so a // LOCAL pose draws as-is. On the RAW path (no rig chained) @@ -1468,9 +1521,47 @@ static void RenderOneFrame(RenderState& rs) { if (renderH > capH) renderH = capH; } + // Multi-screen M3: per-view placement under routing — the + // display's segment rect (window px) scaled into the tile, + // edges rounded independently so two segments sharing a + // seam share the tile column (cube_handle_vk_linux does + // exactly this). + struct ViewPlace { + uint32_t tile; + int32_t x, y; + uint32_t w, h; + bool set; + }; + ViewPlace viewPlace[8] = {}; + if (routedViews && g_windowWidth > 0 && g_windowHeight > 0) { + const float fx = (float)renderW / (float)g_windowWidth; + const float fy = (float)renderH / (float)g_windowHeight; + for (uint32_t b = 0; b < viewBindings.bindingCountOutput; b++) { + const XrViewDisplayBindingDXR& vb = viewBindingStorage[b]; + const int32_t x0 = (int32_t)lroundf((float)vb.segmentRect.offset.x * fx); + const int32_t y0 = (int32_t)lroundf((float)vb.segmentRect.offset.y * fy); + const int32_t x1 = (int32_t)lroundf( + (float)(vb.segmentRect.offset.x + vb.segmentRect.extent.width) * fx); + const int32_t y1 = (int32_t)lroundf( + (float)(vb.segmentRect.offset.y + vb.segmentRect.extent.height) * fy); + const uint32_t sw = x1 > x0 ? (uint32_t)(x1 - x0) : 1; + const uint32_t sh = y1 > y0 ? (uint32_t)(y1 - y0) : 1; + for (uint32_t j = 0; j < vb.viewCount && j < tileColumns * tileRows; j++) { + const uint32_t i = vb.firstView + j; + if (i >= 8) break; + viewPlace[i] = {j, x0, y0, sw, sh, true}; + } + } + } + for (int eye = 0; eye < eyeCount; eye++) { + const bool placed = routedViews && eye < 8 && viewPlace[eye].set; uint32_t tileX = monoMode ? 0 : (eye % tileColumns); uint32_t tileY = monoMode ? 0 : (eye / tileColumns); + if (placed) { + tileX = viewPlace[eye].tile % tileColumns; + tileY = viewPlace[eye].tile / tileColumns; + } // ARRAY: render into array slice `eye` via a per-slice // TEXTURE2DARRAY RTV at a full (0,0) viewport. TILED: @@ -1507,6 +1598,13 @@ static void RenderOneFrame(RenderState& rs) { vp.TopLeftY = arrayLayout ? 0.0f : (FLOAT)(tileY * renderH); vp.Width = (FLOAT)renderW; vp.Height = (FLOAT)renderH; + if (placed) { + // Multi-screen M3: this view's display segment inside its tile. + vp.TopLeftX = (FLOAT)((int32_t)(tileX * renderW) + viewPlace[eye].x); + vp.TopLeftY = (FLOAT)((int32_t)(tileY * renderH) + viewPlace[eye].y); + vp.Width = (FLOAT)viewPlace[eye].w; + vp.Height = (FLOAT)viewPlace[eye].h; + } vp.MaxDepth = 1.0f; renderer.context->RSSetViewports(1, &vp); @@ -1603,6 +1701,14 @@ static void RenderOneFrame(RenderState& rs) { (int32_t)renderW, (int32_t)renderH }; + if (placed) { + // Multi-screen M3: the sub-rect this view was rendered into. + projectionViews[eye].subImage.imageRect.offset = { + (int32_t)(tileX * renderW) + viewPlace[eye].x, + (int32_t)(tileY * renderH) + viewPlace[eye].y}; + projectionViews[eye].subImage.imageRect.extent = {(int32_t)viewPlace[eye].w, + (int32_t)viewPlace[eye].h}; + } projectionViews[eye].subImage.imageArrayIndex = arrayLayout ? (uint32_t)eye : 0; int safeIdx = (eye < (int)viewCount) ? eye : 0; diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 8e2ebb577e..3f8d0a9cc4 100644 --- a/tests/tests_oxr_segment_views.cpp +++ b/tests/tests_oxr_segment_views.cpp @@ -72,7 +72,7 @@ two_screen_table() TEST_CASE("view-set capacity: only where a window can ever be split", "[oxr][segment_views]") { - // No segmenting compositor (Windows/macOS/Android, a service session): 1. + // No segmenting compositor (macOS/Android, a non-D3D11 Windows session, a service session): 1. CHECK(oxr_segment_views_set_capacity(0, false) == 1); CHECK(oxr_segment_views_set_capacity(3, false) == 1); // One DP-backed screen (or none): 1 — the pre-M3 count.