From 7e6eb2d35731da7c725eac6ea3dfa077521f07de Mon Sep 17 00:00:00 2001 From: dfattal Date: Wed, 7 Oct 2026 23:31:09 -0700 Subject: [PATCH 1/4] feat(plugin-abi): D3D11 per-screen factory + set_present_origin slots; sim_display implements both (multi-screen M6) Two appended slots, no XRT_PLUGIN_API_VERSION bump (ADR-020): xrt_plugin_iface::create_dp_d3d11_for_screen (the D3D11 twin of the Vulkan per-screen factory, takes the xrt_screen_binding) and xrt_display_processor_d3d11::set_present_origin (slot 32; the size assert moves to 33), each announced by a HAS macro so a plug-in built against an older header can guard. sim_display's D3D11 DP implements both: a screen-bound instance describes its screen and confines its draw to the canvas it is handed; the unbound DP is unchanged. Co-Authored-By: Claude Fable 5.1 --- .../sim_display/sim_display_interface.h | 16 ++ .../drivers/sim_display/sim_display_plugin.c | 7 + .../sim_display_processor_d3d11.cpp | 166 +++++++++++++++++- .../include/xrt/xrt_display_processor_d3d11.h | 73 +++++++- src/xrt/include/xrt/xrt_plugin.h | 64 +++++++ 5 files changed, 318 insertions(+), 8 deletions(-) diff --git a/src/xrt/drivers/sim_display/sim_display_interface.h b/src/xrt/drivers/sim_display/sim_display_interface.h index da7003b578..eeab51ce07 100644 --- a/src/xrt/drivers/sim_display/sim_display_interface.h +++ b/src/xrt/drivers/sim_display/sim_display_interface.h @@ -283,6 +283,22 @@ sim_display_dp_factory_d3d11(void *d3d11_device, void *window_handle, struct xrt_display_processor_d3d11 **out_xdp); +/*! + * Multi-screen M6: per-screen D3D11 factory, the Windows twin of + * `sim_display_dp_factory_vk_for_screen` — matches + * `xrt_plugin_iface::create_dp_d3d11_for_screen`. The DP describes the bound + * screen and confines its draw to the canvas it is handed. + * + * @ingroup drv_sim_display + */ +xrt_result_t +sim_display_dp_factory_d3d11_for_screen(struct xrt_plugin_instance *inst, + void *d3d11_device, + void *d3d11_context, + void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d11 **out_xdp); + /*! * Create a simulation D3D12 display processor. * diff --git a/src/xrt/drivers/sim_display/sim_display_plugin.c b/src/xrt/drivers/sim_display/sim_display_plugin.c index 464595d421..48ed73b690 100644 --- a/src/xrt/drivers/sim_display/sim_display_plugin.c +++ b/src/xrt/drivers/sim_display/sim_display_plugin.c @@ -371,6 +371,13 @@ static struct xrt_plugin_iface g_sim_display_iface = { #else .create_dp_vk_for_screen = NULL, #endif + + /* Multi-screen M6: one D3D11 DP per screen a spanning window covers. */ +#if defined(_WIN32) + .create_dp_d3d11_for_screen = sim_display_dp_factory_d3d11_for_screen, +#else + .create_dp_d3d11_for_screen = NULL, +#endif }; diff --git a/src/xrt/drivers/sim_display/sim_display_processor_d3d11.cpp b/src/xrt/drivers/sim_display/sim_display_processor_d3d11.cpp index 8380670064..f6999130c0 100644 --- a/src/xrt/drivers/sim_display/sim_display_processor_d3d11.cpp +++ b/src/xrt/drivers/sim_display/sim_display_processor_d3d11.cpp @@ -19,6 +19,7 @@ #include "xrt/xrt_display_processor_d3d11.h" #include "xrt/xrt_display_metrics.h" +#include "xrt/xrt_plugin.h" // xrt_screen_binding (multi-screen M6) #include "util/u_debug.h" #include "util/u_logging.h" @@ -249,6 +250,21 @@ struct sim_display_processor_d3d11_impl //! ADR-042: the FAKE lift module, only when SIM_DISPLAY_FAKE_LIFT=1 (else //! NULL and the five lift slots stay NULL — sim_display ships no module). struct sim_fake_lift *fake_lift; + + //! Multi-screen M6: this instance was created for ONE screen + //! (`create_dp_d3d11_for_screen`); then it describes THAT screen and + //! confines its draw to the canvas it is handed. The unbound DP — every + //! pre-M6 caller — keeps the process-wide panel and the full-target draw. + bool screen_bound; + uint64_t screen_monitor_id; + int32_t screen_left, screen_top; + uint32_t screen_px_w, screen_px_h; + float screen_w_m, screen_h_m; + //! set_present_origin: the window's client-area origin on the bound + //! screen (device px). Stored for parity with the Vulkan DP; sim's D3D11 + //! modes have no phase-sensitive interlace (#817), so it affects no pixel. + bool have_present_origin; + int32_t present_origin_x, present_origin_y; }; static inline struct sim_display_processor_d3d11_impl * @@ -363,7 +379,10 @@ sim_dp_d3d11_process_atlas(struct xrt_display_processor_d3d11 *xdp, } ctx->UpdateSubresource(sdp->tile_cb, 0, nullptr, &tile_data, 0, 0); - // Set viewport + // Set viewport. A screen-bound DP (multi-screen M6) is handed a canvas + // that is ITS segment of the target and must draw only there; the + // unbound DP keeps the pre-M6 full-target draw (its rasterizer state has + // scissor off, so the viewport alone confines the fullscreen strip). D3D11_VIEWPORT viewport = {}; viewport.TopLeftX = 0.0f; viewport.TopLeftY = 0.0f; @@ -371,6 +390,12 @@ sim_dp_d3d11_process_atlas(struct xrt_display_processor_d3d11 *xdp, viewport.Height = static_cast(target_height); viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; + if (sdp->screen_bound && canvas_width > 0 && canvas_height > 0) { + viewport.TopLeftX = static_cast(canvas_offset_x); + viewport.TopLeftY = static_cast(canvas_offset_y); + viewport.Width = static_cast(canvas_width); + viewport.Height = static_cast(canvas_height); + } ctx->RSSetViewports(1, &viewport); // Bind shaders, sampler, SRV, and constant buffer @@ -396,8 +421,102 @@ sim_dp_d3d11_process_atlas(struct xrt_display_processor_d3d11 *xdp, } -// #856: panel geometry -> compositor computes window-scoped Kooima. -SIM_ZONE_DEFINE_PANEL_METRIC_FNS(sim_dp_d3d11, xrt_display_processor_d3d11) +// #856: panel geometry -> compositor computes window-scoped Kooima. A +// screen-bound instance (multi-screen M6) answers for ITS screen; the unbound +// one for the process-wide panel at the desktop origin, as before. +static bool +sim_dp_d3d11_get_display_dimensions(struct xrt_display_processor_d3d11 *xdp, float *out_width_m, float *out_height_m) +{ + struct sim_display_processor_d3d11_impl *sdp = sim_dp_d3d11(xdp); + if (out_width_m == nullptr || out_height_m == nullptr) { + return false; + } + if (sdp->screen_bound) { + *out_width_m = sdp->screen_w_m; + *out_height_m = sdp->screen_h_m; + } else { + sim_display_get_panel_metrics(out_width_m, out_height_m, nullptr, nullptr); + } + return (*out_width_m > 0.0f && *out_height_m > 0.0f); +} + +static bool +sim_dp_d3d11_get_display_pixel_info(struct xrt_display_processor_d3d11 *xdp, + uint32_t *out_pixel_width, + uint32_t *out_pixel_height, + int32_t *out_screen_left, + int32_t *out_screen_top) +{ + struct sim_display_processor_d3d11_impl *sdp = sim_dp_d3d11(xdp); + if (out_pixel_width == nullptr || out_pixel_height == nullptr) { + return false; + } + int32_t left = 0, top = 0; + if (sdp->screen_bound) { + *out_pixel_width = sdp->screen_px_w; + *out_pixel_height = sdp->screen_px_h; + left = sdp->screen_left; + top = sdp->screen_top; + } else { + sim_display_get_panel_metrics(nullptr, nullptr, out_pixel_width, out_pixel_height); + } + if (out_screen_left != nullptr) { + *out_screen_left = left; + } + if (out_screen_top != nullptr) { + *out_screen_top = top; + } + return (*out_pixel_width > 0 && *out_pixel_height > 0); +} + +/*! + * Multi-screen M6: the window's client-area origin on the bound screen. Kept + * for the Vulkan DP's parity (and for a future phase-sensitive D3D11 mode); + * no D3D11 sim mode reads it today (#817). + */ +static void +sim_dp_d3d11_set_present_origin(struct xrt_display_processor_d3d11 *xdp, int32_t panel_x, int32_t panel_y) +{ + struct sim_display_processor_d3d11_impl *sdp = sim_dp_d3d11(xdp); + sdp->have_present_origin = true; + sdp->present_origin_x = panel_x; + sdp->present_origin_y = panel_y; +} + +/*! + * Seed the per-instance screen from a binding (multi-screen M6). Physical size + * from the EDID mm, else the process-wide panel's; pixels from the device + * mode, else the desktop size; origin = the screen's desktop origin. Same + * rules as the Vulkan DP's sim_dp_bind_screen. + */ +static void +sim_dp_d3d11_bind_screen(struct sim_display_processor_d3d11_impl *sdp, const struct xrt_screen_binding *b) +{ + if (b == nullptr || b->struct_size < offsetof(struct xrt_screen_binding, desktop_scale)) { + return; + } + float def_w_m = 0.0f, def_h_m = 0.0f; + uint32_t def_px_w = 0, def_px_h = 0; + sim_display_get_panel_metrics(&def_w_m, &def_h_m, &def_px_w, &def_px_h); + + sdp->screen_bound = true; + sdp->screen_monitor_id = b->monitor_id; + sdp->screen_left = b->desktop_left; + sdp->screen_top = b->desktop_top; + sdp->screen_px_w = b->native_pixel_width != 0 ? b->native_pixel_width : b->desktop_width; + sdp->screen_px_h = b->native_pixel_height != 0 ? b->native_pixel_height : b->desktop_height; + if (sdp->screen_px_w == 0 || sdp->screen_px_h == 0) { + sdp->screen_px_w = def_px_w; + sdp->screen_px_h = def_px_h; + } + if (b->physical_width_mm != 0 && b->physical_height_mm != 0) { + sdp->screen_w_m = (float)b->physical_width_mm / 1000.0f; + sdp->screen_h_m = (float)b->physical_height_mm / 1000.0f; + } else { + sdp->screen_w_m = def_w_m; + sdp->screen_h_m = def_h_m; + } +} static bool sim_dp_d3d11_get_predicted_eye_positions(struct xrt_display_processor_d3d11 *xdp, @@ -838,10 +957,11 @@ sim_dp_d3d11_lift_convert_blob(struct xrt_display_processor_d3d11 *xdp, out_format, out_bytes, out_size); } -extern "C" xrt_result_t -sim_display_processor_d3d11_create(enum sim_display_output_mode mode, - void *d3d11_device, - struct xrt_display_processor_d3d11 **out_xdp) +static xrt_result_t +sim_display_processor_d3d11_create_bound(enum sim_display_output_mode mode, + void *d3d11_device, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d11 **out_xdp) { if (out_xdp == nullptr) { return XRT_ERROR_DEVICE_CREATION_FAILED; @@ -874,6 +994,8 @@ sim_display_processor_d3d11_create(enum sim_display_output_mode mode, sdp->base.get_scanout_caps = sim_dp_d3d11_get_scanout_caps; sdp->base.set_background_2d = sim_dp_d3d11_set_background_2d; // #491 part 3 sdp->base.set_window = sim_dp_d3d11_set_window; // #1008 + sdp->base.set_present_origin = sim_dp_d3d11_set_present_origin; // multi-screen M6 + sim_dp_d3d11_bind_screen(sdp, binding); // #224 / ADR-027 local-zone test double config (shared parser — see // sim_display_zone_common.h for the SIM_DISPLAY_ZONE_GRID / @@ -997,6 +1119,14 @@ sim_display_processor_d3d11_create(enum sim_display_output_mode mode, * */ +extern "C" xrt_result_t +sim_display_processor_d3d11_create(enum sim_display_output_mode mode, + void *d3d11_device, + struct xrt_display_processor_d3d11 **out_xdp) +{ + return sim_display_processor_d3d11_create_bound(mode, d3d11_device, nullptr, out_xdp); +} + extern "C" xrt_result_t sim_display_dp_factory_d3d11(void *d3d11_device, void *d3d11_context, @@ -1010,3 +1140,25 @@ sim_display_dp_factory_d3d11(void *d3d11_device, return sim_display_processor_d3d11_create(mode, d3d11_device, out_xdp); } + +/* + * Multi-screen M6: one D3D11 DP per screen a spanning window covers. The + * window handle is ignored like the plain factory's (sim weaves against no + * window, #1008); the binding seeds the instance's screen. + */ +extern "C" xrt_result_t +sim_display_dp_factory_d3d11_for_screen(struct xrt_plugin_instance *inst, + void *d3d11_device, + void *d3d11_context, + void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d11 **out_xdp) +{ + (void)inst; + (void)d3d11_context; + (void)window_handle; + if (binding == nullptr) { + return XRT_ERROR_DEVICE_CREATION_FAILED; + } + return sim_display_processor_d3d11_create_bound(sim_display_get_output_mode(), d3d11_device, binding, out_xdp); +} diff --git a/src/xrt/include/xrt/xrt_display_processor_d3d11.h b/src/xrt/include/xrt/xrt_display_processor_d3d11.h index ec6b95b69c..4c14b8a4db 100644 --- a/src/xrt/include/xrt/xrt_display_processor_d3d11.h +++ b/src/xrt/include/xrt/xrt_display_processor_d3d11.h @@ -756,6 +756,30 @@ struct xrt_display_processor_d3d11 * @param strength [0,1], or negative for the DP's default. */ void (*set_overlay_2d_filter_strength)(struct xrt_display_processor_d3d11 *xdp, float strength); + + /*! + * Multi-screen M6 (ADR-047 D2 on Windows, the D3D11 twin of + * @ref xrt_display_processor_vk::set_present_origin): where the window's + * client area sits on the DP's screen, so a WINDOWLESS DP (created + * through `create_dp_d3d11_for_screen` with a NULL window) can phase its + * interlace to the panel. Phase = this origin + the canvas offset handed + * to @ref process_atlas, in that screen's device pixels relative to its + * top-left; either may be negative for a window that starts off-screen. + * The compositor calls it every frame it weaves (cheap; a drag moves it). + * + * A DP that has the real HWND derives its phase from the window itself + * and may ignore this call. + * + * Optional — an absent slot (older plug-in `struct_size`) or NULL ⟹ the + * DP has no windowed-phase support and weaves display-scoped. Appended + * after @ref set_overlay_2d_filter_strength per ADR-020 (append-only + * within a major; no version bump — gated by @ref XRT_DP_HAS_SLOT). + * + * @param xdp Pointer to self. + * @param panel_x Window client-area left edge, screen-relative device px. + * @param panel_y Window client-area top edge, screen-relative device px. + */ + void (*set_present_origin)(struct xrt_display_processor_d3d11 *xdp, int32_t panel_x, int32_t panel_y); }; @@ -838,7 +862,8 @@ XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d11, lift_convert) XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d11, lift_convert_blob) == XRT_DP_D3D11_BASE_OFF + 29 * sizeof(void *), XRT_DP_ABI_MSG); XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d11, set_overlay_2d) == XRT_DP_D3D11_BASE_OFF + 30 * sizeof(void *), XRT_DP_ABI_MSG); XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d11, set_overlay_2d_filter_strength) == XRT_DP_D3D11_BASE_OFF + 31 * sizeof(void *), XRT_DP_ABI_MSG); -XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_d3d11) == XRT_DP_D3D11_BASE_OFF + 32 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d11, set_present_origin) == XRT_DP_D3D11_BASE_OFF + 32 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_d3d11) == XRT_DP_D3D11_BASE_OFF + 33 * sizeof(void *), XRT_DP_ABI_MSG); /*! * Defined when this header carries the five lift slots (lift_get_caps, @@ -892,6 +917,13 @@ XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_d3d11) */ #define XRT_DP_D3D11_HAS_OVERLAY_2D_FILTER_STRENGTH 1 +/*! + * Defined when this header carries the set_present_origin slot (multi-screen + * M6), so a plug-in built against an older runtime can #ifdef-guard its + * implementation. Purely additive (ADR-020). + */ +#define XRT_DP_D3D11_HAS_PRESENT_ORIGIN 1 + // clang-format on /*! @@ -1449,6 +1481,45 @@ xrt_display_processor_d3d11_has_lift_blob(struct xrt_display_processor_d3d11 *xd return XRT_DP_HAS_SLOT(xdp, lift_convert_blob) && xdp->lift_convert_blob != NULL; } +/*! + * @copydoc xrt_display_processor_d3d11::set_present_origin + * + * Helper for calling through the function pointer; a no-op when the slot is + * absent (older plug-in `struct_size`) or NULL. + * + * @public @memberof xrt_display_processor_d3d11 + */ +static inline void +xrt_display_processor_d3d11_set_present_origin(struct xrt_display_processor_d3d11 *xdp, + int32_t panel_x, + int32_t panel_y) +{ + if (!XRT_DP_HAS_SLOT(xdp, set_present_origin) || xdp->set_present_origin == NULL) { + return; + } + xdp->set_present_origin(xdp, panel_x, panel_y); +} + +/*! + * Does the DP's output survive a display-server resample + * (@ref XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE)? Every "didn't answer" case — + * absent slot, NULL, false return — is false: the conservative reading, under + * which the runtime's 1:1 gates behave exactly as they did before the flag. + * The D3D11 twin of @ref xrt_display_processor_vk_tolerates_resample. + * + * @public @memberof xrt_display_processor_d3d11 + */ +static inline bool +xrt_display_processor_d3d11_tolerates_resample(struct xrt_display_processor_d3d11 *xdp) +{ + struct xrt_dp_scanout_caps caps; + xrt_dp_scanout_caps_init(&caps); + if (!xrt_display_processor_d3d11_get_scanout_caps(xdp, &caps)) { + return false; + } + return (caps.flags & XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE) != 0; +} + #ifdef __cplusplus } #endif diff --git a/src/xrt/include/xrt/xrt_plugin.h b/src/xrt/include/xrt/xrt_plugin.h index d3e1034305..69e384879c 100644 --- a/src/xrt/include/xrt/xrt_plugin.h +++ b/src/xrt/include/xrt/xrt_plugin.h @@ -1275,6 +1275,48 @@ struct xrt_plugin_iface int32_t target_format, const struct xrt_screen_binding *binding, struct xrt_display_processor **out_xdp); + + /*! + * Create a D3D11 display processor for ONE screen this plug-in won in + * the per-monitor registry (multi-screen M6, ADR-047 D2 on Windows): the + * in-process D3D11 compositor weaves a window that spans several + * monitors per segment, each by its own screen's DP, into the one + * presented back buffer. + * + * Same contract as @ref create_dp_d3d11 (`xrt_dp_factory_d3d11_fn_t`), + * plus everything @ref create_dp_vk_for_screen promises: + * - @p binding names the screen; never NULL. Vendor DPs resolve it to + * their own display identity (`binding->display_id`, else their + * per-monitor claim) and bind their weaver to THAT display. + * - @p window_handle: the session's real HWND for the DP of the screen + * that holds the MAJORITY of the window (it keeps the vendor's drag + * phase-snap), NULL for every other screen's DP, which is windowless + * and gets its phase from + * @ref xrt_display_processor_d3d11::set_present_origin plus the + * canvas offset. A DP must weave with a NULL window. + * - The compositor binds the back buffer, then calls + * @ref xrt_display_processor_d3d11::process_atlas with + * `canvas = the segment` and a pre-cropped atlas holding exactly that + * segment's views; the DP MUST confine its output — viewport AND + * scissor — to the canvas, and must not assume it is the first + * writer to the back buffer this frame. + * - Several instances coexist in one process, one per screen. + * + * Optional. NULL (or a plug-in whose `struct_size` predates this field) + * ⟹ the plug-in's screens other than the session's primary one get a + * flat 2D view (never a @ref create_dp_d3d11 DP, for the reason given on + * @ref create_dp_vk_for_screen). Appended per ADR-020 (append-only + * within a major; gated by @ref struct_size; no + * XRT_PLUGIN_API_VERSION_CURRENT bump) after + * @ref create_dp_vk_for_screen. Announced by + * @ref XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D11_FOR_SCREEN. + */ + xrt_result_t (*create_dp_d3d11_for_screen)(struct xrt_plugin_instance *inst, + void *d3d11_device, + void *d3d11_context, + void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d11 **out_xdp); }; /*! @@ -1343,6 +1385,28 @@ xrt_plugin_iface_has_create_dp_vk_for_screen(const struct xrt_plugin_iface *ifac iface->create_dp_vk_for_screen != NULL; } +/*! + * Defined when @ref xrt_plugin_iface carries @ref + * xrt_plugin_iface::create_dp_d3d11_for_screen (multi-screen M6, Windows), so + * a plug-in built against an older runtime header can #ifdef-guard + * implementing it. + */ +#define XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D11_FOR_SCREEN 1 + +/*! + * True when @p iface implements @ref + * xrt_plugin_iface::create_dp_d3d11_for_screen (and its struct_size covers the + * slot). + */ +static inline bool +xrt_plugin_iface_has_create_dp_d3d11_for_screen(const struct xrt_plugin_iface *iface) +{ + return iface != NULL && + iface->struct_size >= offsetof(struct xrt_plugin_iface, create_dp_d3d11_for_screen) + + sizeof(iface->create_dp_d3d11_for_screen) && + iface->create_dp_d3d11_for_screen != NULL; +} + /*! * True when @p iface fills the whole stereo camera slot set (and its * struct_size covers it). From c5c6012b55ace3b52f9fc2038627fe4fdcb6e915 Mon Sep 17 00:00:00 2001 From: dfattal Date: Wed, 7 Oct 2026 23:31:10 -0700 Subject: [PATCH 2/4] feat(d3d11): weave a window spanning monitors per segment (multi-screen M6, ADR-047 D2 on Windows) comp_d3d11_segments mirrors the Vulkan segment manager: screen-table join from the registry, lifecycle with hysteresis, two-pass weave (primary first, then left to right), CopySubresourceRegion crops, viewport AND scissor = segment before every process_atlas, back buffer re-bound between DPs, flat-2D fill of unwoven segments and uncovered canvas via a new renderer rect blit. The compositor takes the screen list at session creation (oxr_session hands it over on Windows like it does for vk_native on Linux), caches the window's desktop rect from the metrics read that feeds Kooima, and decides the split at the one weave site shared by the app frame and the repaint thread. A window on the primary monitor only keeps the single-DP path byte for byte. Not segmented: the #918 output-adapter split, zones/Local2D frames, zero-copy, shared-texture sessions, pinned sessions, DXR_SEGMENTS=0. Co-Authored-By: Claude Fable 5.1 --- src/xrt/compositor/d3d11/CMakeLists.txt | 2 + .../d3d11/comp_d3d11_compositor.cpp | 138 +++- .../compositor/d3d11/comp_d3d11_compositor.h | 18 + .../compositor/d3d11/comp_d3d11_renderer.cpp | 86 +++ .../compositor/d3d11/comp_d3d11_renderer.h | 29 + .../compositor/d3d11/comp_d3d11_segments.cpp | 718 ++++++++++++++++++ .../compositor/d3d11/comp_d3d11_segments.h | 157 ++++ .../compositor/d3d11/comp_d3d11_target.cpp | 9 + src/xrt/compositor/d3d11/comp_d3d11_target.h | 10 + src/xrt/state_trackers/oxr/oxr_session.c | 19 + 10 files changed, 1179 insertions(+), 7 deletions(-) create mode 100644 src/xrt/compositor/d3d11/comp_d3d11_segments.cpp create mode 100644 src/xrt/compositor/d3d11/comp_d3d11_segments.h diff --git a/src/xrt/compositor/d3d11/CMakeLists.txt b/src/xrt/compositor/d3d11/CMakeLists.txt index 8e78fbfd63..6f2364ae9e 100644 --- a/src/xrt/compositor/d3d11/CMakeLists.txt +++ b/src/xrt/compositor/d3d11/CMakeLists.txt @@ -20,6 +20,8 @@ if(WIN32 AND XRT_HAVE_D3D11) 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 diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp index 6429954861..14b9ae0f63 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp @@ -14,6 +14,8 @@ #include "util/comp_display_refresh_win.h" #include "comp_d3d11_renderer.h" #include "comp_d3d11_outcomp.h" +#include "comp_d3d11_segments.h" // multi-screen M6 +#include "xrt/xrt_screen.h" #include "comp_d3d11_window.h" #include "comp_d3d11_state_guard.h" #include "comp_xbridge.h" @@ -488,6 +490,21 @@ struct comp_d3d11_compositor //! Generic D3D11 display processor (vendor-agnostic weaving). struct xrt_display_processor_d3d11 *display_processor; + /*! + * Multi-screen M6 (ADR-047 D2 on Windows): a window spanning monitors is + * woven per segment, each by its own screen's DP, into the one back + * buffer. The screen list + DP registry arrive at session creation + * (comp_d3d11_compositor_set_screens, under c->mutex); the manager is + * (re)built lazily on the weave thread, which also holds c->mutex. + */ + struct comp_d3d11_segments *segments; + struct xrt_screen_list *seg_screens; //!< heap copy, NULL = not handed over + struct xrt_system_compositor_info *seg_sys_info; //!< heap copy (the DP registry) + bool seg_rebuild; //!< a new list arrived: rebuild on the next weave + 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; + //! System devices (for qwerty driver keyboard input and display mode toggle). struct xrt_system_devices *xsysd; @@ -2564,13 +2581,72 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint) xrt_display_processor_d3d11_set_predicted_scanout( c->display_processor, comp_d3d11_target_predict_weave_to_scanout_ns(c->target)); - xrt_display_processor_d3d11_process_atlas( - c->display_processor, d3d11_out_context(c), atlas_srv, view_width, view_height, - tile_columns, tile_rows, DXGI_FORMAT_R8G8B8A8_UNORM, target_width, target_height, - eff_canvas.valid ? eff_canvas.x : 0, - eff_canvas.valid ? eff_canvas.y : 0, - eff_canvas.valid ? eff_canvas.w : 0, - eff_canvas.valid ? eff_canvas.h : 0); + /* + * Multi-screen M6 (ADR-047 D2): does this window span monitors? Only off + * the zero-copy path (the atlas must be ours to crop), off the #918 split + * (the output adapter owns that pipeline), and for a whole-window canvas + * (a zones / Local2D frame keeps the single-DP path — out of scope here). + * A window on the primary screen only also stays single-DP, byte for byte. + */ + 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 (comp_d3d11_segments_enabled(c->segments)) { + const struct comp_seg_rect canvas = {0, 0, c->seg_window.w, c->seg_window.h}; + const uint32_t mode_index = + (c->xdev != nullptr && c->xdev->hmd != nullptr) ? c->xdev->hmd->active_rendering_mode_index : 0; + seg_split = comp_d3d11_segments_update(c->segments, &c->seg_window, &canvas, d3d11_out_context(c), + mode_index); + // Every segment DP follows the session's 2D/3D mode, like the primary. + comp_d3d11_segments_set_display_mode(c->segments, c->hardware_display_3d); + } + } + + if (seg_split) { + struct comp_d3d11_segments_frame sf = {}; + sf.context = d3d11_out_context(c); + sf.src_srv = atlas_srv; + sf.view_width = view_width; + sf.view_height = view_height; + sf.tile_columns = tile_columns; + sf.tile_rows = tile_rows; + sf.src_format = (uint32_t)DXGI_FORMAT_R8G8B8A8_UNORM; + sf.target_rtv = comp_d3d11_target_get_rtv(c->target); + sf.target_width = target_width; + sf.target_height = target_height; + sf.canvas.x = 0; + sf.canvas.y = 0; + sf.canvas.w = c->seg_window.w; + sf.canvas.h = c->seg_window.h; + sf.transparent_background = c->transparent_background; + sf.atlas_encoding = -1; // the in-process D3D11 path declares none to the primary either + sf.primary_dp = c->display_processor; + sf.renderer = c->renderer; + comp_d3d11_segments_record(c->segments, &sf); + // The segment path leaves the back buffer bound at full viewport; + // re-bind through the target so everything after sees exactly the + // state the single-DP path leaves. + comp_d3d11_target_bind(c->target); + } else { + xrt_display_processor_d3d11_process_atlas( + c->display_processor, d3d11_out_context(c), atlas_srv, view_width, view_height, + tile_columns, tile_rows, DXGI_FORMAT_R8G8B8A8_UNORM, target_width, target_height, + eff_canvas.valid ? eff_canvas.x : 0, + eff_canvas.valid ? eff_canvas.y : 0, + eff_canvas.valid ? eff_canvas.w : 0, + eff_canvas.valid ? eff_canvas.h : 0); + } // XR_DXR_depth_budget: evaluate the rear budget from the background the // DP just composited under. AFTER process_atlas, because that is the point @@ -4017,6 +4093,14 @@ d3d11_compositor_destroy(struct xrt_compositor *xc) comp_d3d11_window_set_snap_provider(c->own_window, NULL, NULL); } + // Multi-screen M6: segment DPs + crop textures, before the primary DP + // (the repaint thread is already stopped, so no weave is in flight). + comp_d3d11_segments_destroy(&c->segments); + free(c->seg_screens); + c->seg_screens = nullptr; + free(c->seg_sys_info); + c->seg_sys_info = nullptr; + // Destroy display processor (handles all vendor cleanup internally) xrt_display_processor_d3d11_destroy(&c->display_processor); @@ -8207,6 +8291,15 @@ comp_d3d11_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_w POINT client_origin = {0, 0}; ClientToScreen(metrics_hwnd, &client_origin); + // Multi-screen M6: the window's desktop rect, for the segment table. + // Device px — every DisplayXR executable is per-monitor DPI aware + // (#1201), the same space the screen registry's monitor rects are in. + c->seg_window.x = client_origin.x; + c->seg_window.y = client_origin.y; + c->seg_window.w = win_px_w; + c->seg_window.h = win_px_h; + c->have_seg_window = true; + // Compute pixel size (meters per pixel) float pixel_size_x = disp_w_m / (float)disp_px_w; float pixel_size_y = disp_h_m / (float)disp_px_h; @@ -8268,6 +8361,37 @@ comp_d3d11_compositor_request_display_mode(struct xrt_compositor *xc, bool enabl return false; } +extern "C" void +comp_d3d11_compositor_set_screens(struct xrt_compositor *xc, + const struct xrt_screen_list *list, + const struct xrt_system_compositor_info *info, + uint64_t pinned_display_id) +{ + if (xc == nullptr || list == nullptr || info == nullptr) { + return; + } + struct comp_d3d11_compositor *c = d3d11_comp(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; + *info_copy = *info; + + // The weave (app thread's layer_commit and the repaint thread) runs under + // c->mutex; the manager is rebuilt there on the next weave, never here. + std::lock_guard lock(c->mutex); + c->seg_rebuild = true; + 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; +} + extern "C" void comp_d3d11_compositor_set_eye_tracking_mode(struct xrt_compositor *xc, uint32_t mode) { diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.h b/src/xrt/compositor/d3d11/comp_d3d11_compositor.h index e9feb42adf..f9fc9e3e92 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.h @@ -226,6 +226,24 @@ bool comp_d3d11_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_window_metrics *out_metrics); +/*! + * Multi-screen M6 (ADR-047 D2 on Windows): hand the compositor the system's + * screen list, so a window that spans several monitors is woven per segment, + * each by its own screen's display processor, into the one back buffer. Call + * once at session creation. @p pinned_display_id is the session's + * `XrSessionDisplayBindingDXR` (0 = none); a pinned session is never + * segmented. A window on the primary screen only keeps the single-DP path + * byte for byte. + * + * @ingroup comp_d3d11 + */ +struct xrt_screen_list; +void +comp_d3d11_compositor_set_screens(struct xrt_compositor *xc, + const struct xrt_screen_list *list, + const struct xrt_system_compositor_info *info, + uint64_t pinned_display_id); + /*! * 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 66c3e25c34..b3b9731d72 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp @@ -2509,3 +2509,89 @@ comp_d3d11_renderer_blit_stretch(struct comp_d3d11_renderer *renderer, return XRT_SUCCESS; } +xrt_result_t +comp_d3d11_renderer_blit_rect(struct comp_d3d11_renderer *renderer, + void *src_srv, + int32_t src_x, + int32_t src_y, + uint32_t src_w, + uint32_t src_h, + void *rtv, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h) +{ + if (renderer == nullptr || src_srv == nullptr || rtv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 || + dst_h == 0) { + return XRT_ERROR_D3D; + } + auto internals = get_internals(renderer->c); + ID3D11DeviceContext *ctx = internals.context; + auto *srv = static_cast(src_srv); + auto *target_rtv = static_cast(rtv); + + // The source rect in normalised texture space needs the texture's size. + ID3D11Resource *res = nullptr; + srv->GetResource(&res); + if (res == nullptr) { + return XRT_ERROR_D3D; + } + D3D11_TEXTURE2D_DESC desc = {}; + static_cast(res)->GetDesc(&desc); + res->Release(); + if (desc.Width == 0 || desc.Height == 0) { + return XRT_ERROR_D3D; + } + + ctx->OMSetRenderTargets(1, &target_rtv, nullptr); + D3D11_VIEWPORT vp = {}; + vp.TopLeftX = static_cast(dst_x); + vp.TopLeftY = static_cast(dst_y); + vp.Width = static_cast(dst_w); + vp.Height = static_cast(dst_h); + vp.MaxDepth = 1.0f; + ctx->RSSetViewports(1, &vp); + // The renderer's rasterizer state has scissor off; the viewport alone + // confines a fullscreen strip to the destination rect. + ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + ctx->IASetInputLayout(nullptr); + ctx->VSSetShader(renderer->projection_vs, nullptr, 0); + ctx->PSSetShader(renderer->projection_ps, nullptr, 0); + ctx->RSSetState(renderer->rasterizer_state); + ctx->OMSetDepthStencilState(renderer->depth_stencil_state, 0); + ctx->OMSetBlendState(renderer->blend_opaque, nullptr, 0xFFFFFFFF); + ctx->PSSetSamplers(0, 1, &renderer->sampler_linear); + ctx->PSSetShaderResources(0, 1, &srv); + + LayerConstants constants = {}; + constants.mvp[0] = 1.0f; + constants.mvp[5] = 1.0f; + constants.mvp[10] = 1.0f; + constants.mvp[15] = 1.0f; + constants.post_transform[0] = static_cast(src_x) / static_cast(desc.Width); + constants.post_transform[1] = static_cast(src_y) / static_cast(desc.Height); + constants.post_transform[2] = static_cast(src_w) / static_cast(desc.Width); + constants.post_transform[3] = static_cast(src_h) / static_cast(desc.Height); + constants.color_scale[0] = 1.0f; + constants.color_scale[1] = 1.0f; + constants.color_scale[2] = 1.0f; + constants.color_scale[3] = 1.0f; + + D3D11_MAPPED_SUBRESOURCE mapped; + HRESULT hr = ctx->Map(renderer->constant_buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); + if (FAILED(hr)) { + return XRT_ERROR_D3D; + } + memcpy(mapped.pData, &constants, sizeof(constants)); + ctx->Unmap(renderer->constant_buffer, 0); + ctx->VSSetConstantBuffers(0, 1, &renderer->constant_buffer); + ctx->PSSetConstantBuffers(0, 1, &renderer->constant_buffer); + + ctx->Draw(4, 0); + + ID3D11ShaderResourceView *null_srv = nullptr; + ctx->PSSetShaderResources(0, 1, &null_srv); + return XRT_SUCCESS; +} + diff --git a/src/xrt/compositor/d3d11/comp_d3d11_renderer.h b/src/xrt/compositor/d3d11/comp_d3d11_renderer.h index ccf376b2a9..3c589a72d4 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_renderer.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_renderer.h @@ -352,6 +352,35 @@ comp_d3d11_renderer_blit_stretch(struct comp_d3d11_renderer *renderer, uint32_t target_width, uint32_t target_height); +/*! + * Multi-screen M6: linearly scale one rect of a texture into one rect of the + * currently relevant render target — the flat-2D fill of a segment no DP can + * weave (and of the canvas no screen covers), the D3D11 twin of the Vulkan + * segments' `vkCmdBlitImage`. Opaque, no colour transform. Binds @p rtv, a + * viewport = @p dst and the renderer's own quad pipeline; the caller re-binds + * whatever it needs afterwards. + * + * @param renderer The renderer. + * @param src_srv Source texture SRV (ID3D11ShaderResourceView*). + * @param src_x,src_y,src_w,src_h Source rect in source texels. + * @param rtv Destination render target view (ID3D11RenderTargetView*). + * @param dst_x,dst_y,dst_w,dst_h Destination rect in target pixels. + * + * @ingroup comp_d3d11 + */ +xrt_result_t +comp_d3d11_renderer_blit_rect(struct comp_d3d11_renderer *renderer, + void *src_srv, + int32_t src_x, + int32_t src_y, + uint32_t src_w, + uint32_t src_h, + void *rtv, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h); + #ifdef __cplusplus } diff --git a/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp b/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp new file mode 100644 index 0000000000..27451c53cc --- /dev/null +++ b/src/xrt/compositor/d3d11/comp_d3d11_segments.cpp @@ -0,0 +1,718 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen segment display processors for the in-process D3D11 + * compositor (multi-screen M6, ADR-047 D2 on Windows). + * @ingroup comp_d3d11 + * + * Mirrors vk_native/comp_vk_native_segments.c: the screen-table join, the + * lifecycle with hysteresis, the decide loop and the two-pass weave order are + * the same by construction; only the GPU work (crop copies, the clear, the + * flat-2D blit) is D3D11. There is no deferred-release list: the immediate + * context executes in order and D3D11 objects are refcounted, so a retired DP + * or texture can be released the moment the weave that used it has been + * recorded. + */ + +#include "comp_d3d11_segments.h" + +#include "comp_d3d11_renderer.h" +#include "d3d/d3d_dxgi_formats.h" +#include "os/os_threading.h" +#include "util/u_logging.h" +#include "util/u_misc.h" +#include "xrt/xrt_plugin.h" + +#include + +#include +#include +#include + +/* + * + * Internal types. + * + */ + +//! One per-screen crop texture: the atlas tiles cut down to one segment. +struct seg_crop +{ + ID3D11Texture2D *texture; + ID3D11ShaderResourceView *srv; + uint32_t w, h; + DXGI_FORMAT format; +}; + +//! Per-screen runtime state (indexed like comp_d3d11_segments::screens). +struct seg_screen_state +{ + struct xrt_display_processor_d3d11 *dp; //!< NULL until the lifecycle creates it. + bool configured; //!< set_background_2d(NULL) sent once. + int encoding_latched; //!< last atlas encoding declared, -1 = none. + int transparent_latched; //!< last transparency declared, -1 = none. + int mode_sent; //!< last 2D/3D mode sent, -1 = none. + bool tolerates_resample; + struct seg_crop crop; +}; + +struct comp_d3d11_segments +{ + ID3D11Device *device; + + bool enabled; + uint32_t screen_count; + struct comp_segments_screen screens[COMP_SEGMENTS_MAX_SCREENS]; + struct xrt_screen_binding bindings[COMP_SEGMENTS_MAX_SCREENS]; + const struct xrt_plugin_iface *iface[COMP_SEGMENTS_MAX_SCREENS]; + struct xrt_plugin_instance *inst[COMP_SEGMENTS_MAX_SCREENS]; + struct seg_screen_state st[COMP_SEGMENTS_MAX_SCREENS]; + + struct comp_segment_table table; + struct comp_segments_lifecycle lc; + + //! The app thread reads st[i].dp for eyes; the weave creates/destroys it. + struct os_mutex dp_mutex; + + bool mode_3d; + uint32_t mode_index; + + // Change-only logging. + bool have_logged_table; + bool logged_split; + struct comp_segment_table logged_table; +}; + + +/* + * + * Helpers. + * + */ + +static int +screen_index_of(const struct comp_d3d11_segments *segs, uint64_t screen_id) +{ + for (uint32_t i = 0; i < segs->screen_count; i++) { + if (segs->screens[i].id == screen_id) { + return (int)i; + } + } + return -1; +} + +static void +crop_destroy(struct seg_crop *c) +{ + if (c->srv != nullptr) { + c->srv->Release(); + c->srv = nullptr; + } + if (c->texture != nullptr) { + c->texture->Release(); + c->texture = nullptr; + } + c->w = c->h = 0; +} + +/*! + * (Re)create the crop texture at @p w × @p h in the atlas's format. The SRV is + * typed UNORM (the in-process DP samples raw bytes, ADR-021 Model A), which is + * the same choice the compositor's own DP-input crop makes. + */ +static bool +crop_ensure(ID3D11Device *dev, struct seg_crop *c, uint32_t w, uint32_t h, DXGI_FORMAT format) +{ + if (c->texture != nullptr && c->w == w && c->h == h && c->format == format) { + return true; + } + crop_destroy(c); + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = w; + desc.Height = h; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = format; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + HRESULT hr = dev->CreateTexture2D(&desc, nullptr, &c->texture); + if (FAILED(hr)) { + U_LOG_E("segments: crop texture %ux%u failed: 0x%lx", w, h, hr); + return false; + } + D3D11_SHADER_RESOURCE_VIEW_DESC sd = {}; + sd.Format = d3d_dxgi_format_to_unorm_sample(format); + sd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MostDetailedMip = 0; + sd.Texture2D.MipLevels = 1; + hr = dev->CreateShaderResourceView(c->texture, &sd, &c->srv); + if (FAILED(hr)) { + U_LOG_E("segments: crop SRV failed: 0x%lx", hr); + crop_destroy(c); + return false; + } + c->w = w; + c->h = h; + c->format = format; + return true; +} + +static void +dp_release(struct comp_d3d11_segments *segs, uint32_t i) +{ + struct seg_screen_state *st = &segs->st[i]; + if (st->dp != nullptr) { + U_LOG_I("segments: retiring the segment DP for screen 0x%016llx ('%s')", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name); + os_mutex_lock(&segs->dp_mutex); + struct xrt_display_processor_d3d11 *dp = st->dp; + st->dp = nullptr; + os_mutex_unlock(&segs->dp_mutex); + xrt_display_processor_d3d11_destroy(&dp); + } + st->configured = false; + st->encoding_latched = -1; + st->transparent_latched = -1; + st->mode_sent = -1; + st->tolerates_resample = false; + crop_destroy(&st->crop); +} + +/*! + * Create screen @p i's segment DP through the plug-in's per-screen factory. + * Windowless (NULL window) on purpose: a segment DP's phase comes from + * set_present_origin; the session's real HWND stays with the primary DP (the + * vendor's drag phase-snap follows it). + * + * No fallback to the plain `create_dp_d3d11`, for the reason given on + * xrt_plugin_iface::create_dp_vk_for_screen: that DP describes the plug-in's + * process-wide panel and is not bound by the canvas / not-first-writer + * contract, so it could wipe the segment woven before it. + */ +static bool +dp_create(struct comp_d3d11_segments *segs, uint32_t i, ID3D11DeviceContext *ctx) +{ + struct seg_screen_state *st = &segs->st[i]; + const struct xrt_plugin_iface *iface = segs->iface[i]; + xrt_result_t xret = XRT_ERROR_DEVICE_CREATION_FAILED; + struct xrt_display_processor_d3d11 *dp = nullptr; + if (segs->screens[i].has_dp_factory && xrt_plugin_iface_has_create_dp_d3d11_for_screen(iface)) { + xret = iface->create_dp_d3d11_for_screen(segs->inst[i], segs->device, ctx, nullptr, &segs->bindings[i], + &dp); + } + if (xret != XRT_SUCCESS || dp == nullptr || dp->process_atlas == nullptr) { + if (dp != nullptr) { + xrt_display_processor_d3d11_destroy(&dp); + } + U_LOG_W( + "segments: could not create a DP for screen 0x%016llx ('%s', plug-in '%s') via " + "create_dp_d3d11_for_screen (%d) — that segment stays flat 2D", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, segs->screens[i].plugin_id, + (int)xret); + return false; + } + os_mutex_lock(&segs->dp_mutex); + st->dp = dp; + os_mutex_unlock(&segs->dp_mutex); + st->tolerates_resample = xrt_display_processor_d3d11_tolerates_resample(dp); + st->configured = false; + st->encoding_latched = -1; + st->transparent_latched = -1; + st->mode_sent = -1; + // Lifecycle event (hysteresis-gated), not per frame. + U_LOG_W( + "segments: created a segment DP for screen 0x%016llx ('%s', plug-in '%s') via " + "create_dp_d3d11_for_screen — %s", + (unsigned long long)segs->screens[i].id, segs->bindings[i].device_name, segs->screens[i].plugin_id, + st->tolerates_resample ? "tolerates a resample" : "needs 1:1 pixels"); + return true; +} + +static void +set_viewport_scissor(ID3D11DeviceContext *ctx, const struct comp_seg_rect *r) +{ + D3D11_VIEWPORT vp = {}; + vp.TopLeftX = (float)r->x; + vp.TopLeftY = (float)r->y; + vp.Width = (float)r->w; + vp.Height = (float)r->h; + vp.MaxDepth = 1.0f; + ctx->RSSetViewports(1, &vp); + D3D11_RECT sc = {r->x, r->y, r->x + (LONG)r->w, r->y + (LONG)r->h}; + ctx->RSSetScissorRects(1, &sc); +} + + +/* + * + * 'Exported' functions. + * + */ + +extern "C" struct comp_d3d11_segments * +comp_d3d11_segments_create(void *d3d11_device) +{ + if (d3d11_device == nullptr) { + return nullptr; + } + struct comp_d3d11_segments *segs = U_TYPED_CALLOC(struct comp_d3d11_segments); + if (segs == nullptr) { + return nullptr; + } + segs->device = static_cast(d3d11_device); + os_mutex_init(&segs->dp_mutex); + comp_segments_lifecycle_init(&segs->lc, 0, 0); + segs->mode_index = UINT32_MAX; + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + segs->st[i].encoding_latched = -1; + segs->st[i].transparent_latched = -1; + segs->st[i].mode_sent = -1; + } + return segs; +} + +extern "C" void +comp_d3d11_segments_destroy(struct comp_d3d11_segments **segs_ptr) +{ + if (segs_ptr == nullptr || *segs_ptr == nullptr) { + return; + } + struct comp_d3d11_segments *segs = *segs_ptr; + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + dp_release(segs, i); + } + os_mutex_destroy(&segs->dp_mutex); + free(segs); + *segs_ptr = nullptr; +} + +extern "C" void +comp_d3d11_segments_set_screens(struct comp_d3d11_segments *segs, + const struct xrt_screen_list *list, + const struct xrt_system_compositor_info *info, + uint64_t pinned_display_id) +{ + if (segs == nullptr) { + return; + } + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + dp_release(segs, i); + } + comp_segments_lifecycle_init(&segs->lc, 0, 0); + segs->enabled = false; + segs->screen_count = 0; + segs->have_logged_table = false; + segs->logged_split = false; + memset(&segs->table, 0, sizeof(segs->table)); + + const char *env = getenv("DXR_SEGMENTS"); + if (env != nullptr && env[0] == '0') { + U_LOG_W("segments: DXR_SEGMENTS=0 — a window spanning screens keeps the single-DP path"); + return; + } + if (list == nullptr || info == nullptr || list->count < 2) { + return; + } + if (pinned_display_id != 0) { + U_LOG_I("segments: session pinned to display 0x%016llx (XrSessionDisplayBindingDXR) — no segmentation", + (unsigned long long)pinned_display_id); + return; + } + + // The primary: the system-default screen, woven by the session's own DP. + const struct xrt_dp_registry_entry *primary_entry = nullptr; + for (uint32_t i = 0; i < list->count && i < XRT_SCREEN_LIST_MAX; i++) { + if ((list->screens[i].flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) == 0) { + continue; + } + for (uint32_t e = 0; e < info->dp_registry.entry_count; e++) { + if (info->dp_registry.entries[e].monitor_id == list->screens[i].id) { + primary_entry = &info->dp_registry.entries[e]; + break; + } + } + } + if (primary_entry == nullptr) { + U_LOG_I("segments: the DP registry does not know the system-default screen — no segmentation"); + return; + } + + uint32_t n = 0; + bool mixed = false; + char vendors[256] = {0}; + for (uint32_t i = 0; i < list->count && i < XRT_SCREEN_LIST_MAX && n < COMP_SEGMENTS_MAX_SCREENS; i++) { + const struct xrt_screen *s = &list->screens[i]; + if (s->desktop_width == 0 || s->desktop_height == 0) { + continue; + } + const struct xrt_dp_registry_entry *e = nullptr; + for (uint32_t k = 0; k < info->dp_registry.entry_count; k++) { + if (info->dp_registry.entries[k].monitor_id == s->id) { + e = &info->dp_registry.entries[k]; + break; + } + } + + struct comp_segments_screen *cs = &segs->screens[n]; + memset(cs, 0, sizeof(*cs)); + cs->id = s->id; + cs->desktop.x = s->desktop_left; + cs->desktop.y = s->desktop_top; + cs->desktop.w = s->desktop_width; + cs->desktop.h = s->desktop_height; + cs->native_w = s->native_width; + cs->native_h = s->native_height; + cs->is_primary = (e == primary_entry); + snprintf(cs->plugin_id, sizeof(cs->plugin_id), "%s", s->plugin_id); + + segs->iface[n] = nullptr; + segs->inst[n] = nullptr; + if (e != nullptr && !cs->is_primary) { + // A segment's DP comes from ITS screen's registry entry, + // whichever plug-in that is (mixed-vendor rigs, M4). + if (strcmp(e->plugin_id, primary_entry->plugin_id) != 0) { + mixed = true; + if (strstr(vendors, e->plugin_id) == nullptr) { + size_t used = strlen(vendors); + snprintf(vendors + used, sizeof(vendors) - used, "%s%s", used > 0 ? ", " : "", + e->plugin_id); + } + } + segs->iface[n] = (const struct xrt_plugin_iface *)e->owning_iface; + segs->inst[n] = (struct xrt_plugin_instance *)e->owning_instance; + // The per-screen slot, and only it (see dp_create). The registry's + // D3D11 factory being set says the claim covers D3D11. + cs->has_dp_factory = e->dp_factory_d3d11 != nullptr && + xrt_plugin_iface_has_create_dp_d3d11_for_screen(segs->iface[n]); + } + + struct xrt_screen_binding *b = &segs->bindings[n]; + memset(b, 0, sizeof(*b)); + b->struct_size = (uint32_t)sizeof(*b); + b->monitor_id = s->id; + b->desktop_left = s->desktop_left; + b->desktop_top = s->desktop_top; + b->desktop_width = s->desktop_width; + b->desktop_height = s->desktop_height; + b->native_pixel_width = s->native_width; + b->native_pixel_height = s->native_height; + b->physical_width_mm = s->physical_width_mm; + b->physical_height_mm = s->physical_height_mm; + b->desktop_scale = s->desktop_scale; + snprintf(b->device_name, sizeof(b->device_name), "%s", s->device_name); + if (e != nullptr) { + snprintf(b->serial, sizeof(b->serial), "%s", e->serial); + } + n++; + } + segs->screen_count = n; + + if (mixed) { + U_LOG_I( + "segments: mixed-vendor screen table — primary '%s', other screens by '%s'; each segment DP " + "comes from its own screen's plug-in", + primary_entry->plugin_id, vendors); + } + segs->enabled = n >= 2; + if (segs->enabled) { + U_LOG_W( + "segments: %u screens, plug-in '%s' — a window spanning monitors is woven per monitor " + "(multi-screen M6, D3D11)", + n, primary_entry->plugin_id); + } +} + +extern "C" bool +comp_d3d11_segments_enabled(const struct comp_d3d11_segments *segs) +{ + return segs != nullptr && segs->enabled; +} + +extern "C" bool +comp_d3d11_segments_update(struct comp_d3d11_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + void *d3d11_context, + uint32_t mode_index) +{ + if (segs == nullptr || !segs->enabled || window_desktop == nullptr || d3d11_context == nullptr) { + return false; + } + auto *ctx = static_cast(d3d11_context); + + comp_segments_compute(window_desktop, canvas, segs->screens, segs->screen_count, &segs->table); + + struct comp_segments_actions act; + comp_segments_lifecycle_update(&segs->lc, &segs->table, &act); + for (uint32_t a = 0; a < act.count; a++) { + const int i = screen_index_of(segs, act.items[a].screen_id); + if (i < 0) { + continue; + } + if (act.items[a].action == COMP_SEG_ACTION_CREATE) { + const bool ok = dp_create(segs, (uint32_t)i, ctx); + comp_segments_lifecycle_set_created(&segs->lc, act.items[a].screen_id, ok); + } else if (act.items[a].action == COMP_SEG_ACTION_DESTROY) { + dp_release(segs, (uint32_t)i); + } + } + + const uint32_t cw = canvas != nullptr && canvas->w > 0 ? canvas->w : window_desktop->w; + const uint32_t ch = canvas != nullptr && canvas->h > 0 ? canvas->h : window_desktop->h; + const bool split = comp_segments_table_is_split(&segs->table, cw, ch); + + // A DP's resample tolerance can follow the rendering mode (sim_display: + // INTERLACED needs 1:1). Re-ask on a mode change or a table change — + // never per frame. + const bool table_changed = + split && (!segs->have_logged_table || !comp_segments_table_equal(&segs->table, &segs->logged_table)); + if (mode_index != segs->mode_index || table_changed) { + segs->mode_index = mode_index; + for (uint32_t i = 0; i < segs->screen_count; i++) { + if (segs->st[i].dp != nullptr) { + segs->st[i].tolerates_resample = + xrt_display_processor_d3d11_tolerates_resample(segs->st[i].dp); + } + } + } + + // One INFO line per table change while split, and one on leaving it. + if (table_changed) { + char buf[768]; + comp_segments_table_format(&segs->table, buf, sizeof(buf)); + U_LOG_I("segments: window %d,%d %ux%u -> %s", window_desktop->x, window_desktop->y, window_desktop->w, + window_desktop->h, buf); + segs->logged_table = segs->table; + segs->have_logged_table = true; + } else if (!split && segs->logged_split) { + U_LOG_I("segments: window %d,%d %ux%u is on the primary screen only — single-DP path", + window_desktop->x, window_desktop->y, window_desktop->w, window_desktop->h); + segs->have_logged_table = false; + } + segs->logged_split = split; + return split; +} + +extern "C" bool +comp_d3d11_segments_record(struct comp_d3d11_segments *segs, const struct comp_d3d11_segments_frame *f) +{ + if (segs == nullptr || f == nullptr || f->context == nullptr || f->src_srv == nullptr || + f->target_rtv == nullptr || f->tile_columns == 0 || f->tile_rows == 0 || f->view_width == 0 || + f->view_height == 0) { + return false; + } + auto *ctx = static_cast(f->context); + auto *src_srv = static_cast(f->src_srv); + auto *rtv = static_cast(f->target_rtv); + const struct comp_segment_table *t = &segs->table; + + // The atlas texture behind the SRV, for the crop copies. + ID3D11Resource *src_res = nullptr; + src_srv->GetResource(&src_res); + if (src_res == nullptr) { + return false; + } + auto *src_tex = static_cast(src_res); + D3D11_TEXTURE2D_DESC src_desc = {}; + src_tex->GetDesc(&src_desc); + + /* + * 1. Decide each segment: which DP (if any) and whether it may weave. + */ + struct xrt_display_processor_d3d11 *dp_of[COMP_SEGMENTS_MAX] = {}; + struct seg_screen_state *st_of[COMP_SEGMENTS_MAX] = {}; + bool weave[COMP_SEGMENTS_MAX] = {}; + struct comp_seg_rect tile[COMP_SEGMENTS_MAX] = {}; + bool have_tile[COMP_SEGMENTS_MAX] = {}; + + for (uint32_t k = 0; k < t->count; k++) { + const struct comp_segment *g = &t->seg[k]; + have_tile[k] = + comp_segments_tile_rect(&g->window_rect, &f->canvas, f->view_width, f->view_height, &tile[k]); + const int i = (int)g->screen_index; + st_of[k] = (i >= 0 && (uint32_t)i < segs->screen_count) ? &segs->st[i] : nullptr; + if (g->is_primary) { + dp_of[k] = f->primary_dp; + weave[k] = f->primary_dp != nullptr; + } else if (st_of[k] != nullptr) { + dp_of[k] = st_of[k]->dp; + weave[k] = comp_segments_decide(dp_of[k] != nullptr, st_of[k]->tolerates_resample, + g->screen_1to1) == COMP_SEG_RENDER_WEAVE; + } + if (!have_tile[k] || st_of[k] == nullptr) { + weave[k] = false; + } + } + + /* + * 2. Clear the target: a segment DP confines its draw to its canvas, so + * whatever no segment covers would otherwise be undefined. + */ + { + const float clear[4] = {0.0f, 0.0f, 0.0f, f->transparent_background ? 0.0f : 1.0f}; + ctx->ClearRenderTargetView(rtv, clear); + } + + /* + * 3. Crop each woven segment's views out of the atlas — crop before the + * DP is the law (ADR-030): a DP's atlas holds exactly its canvas. + */ + for (uint32_t k = 0; k < t->count; k++) { + if (!weave[k]) { + continue; + } + struct seg_crop *crop = &st_of[k]->crop; + const uint32_t cw = tile[k].w * f->tile_columns; + const uint32_t ch = tile[k].h * f->tile_rows; + if (!crop_ensure(segs->device, crop, cw, ch, src_desc.Format)) { + weave[k] = false; // no input image: flat 2D instead + continue; + } + for (uint32_t row = 0; row < f->tile_rows; row++) { + for (uint32_t col = 0; col < f->tile_columns; col++) { + D3D11_BOX box = {}; + box.left = col * f->view_width + (UINT)tile[k].x; + box.top = row * f->view_height + (UINT)tile[k].y; + box.right = box.left + tile[k].w; + box.bottom = box.top + tile[k].h; + box.front = 0; + box.back = 1; + ctx->CopySubresourceRegion(crop->texture, 0, col * tile[k].w, row * tile[k].h, 0, + src_tex, 0, &box); + } + } + } + + /* + * 4. Weave: the primary first (a DP built before M6 may still clear the + * whole target, so it must not run after a sibling), then left to right. + * Each gets canvas = its segment, with viewport AND scissor already set to + * it (the vendor weaver reads the current viewport/scissor and writes only + * inside them); the back buffer is re-bound before every DP because a DP + * may leave another target bound. + */ + bool any_woven = false; + for (uint32_t pass = 0; pass < 2; pass++) { + for (uint32_t k = 0; k < t->count; k++) { + const struct comp_segment *g = &t->seg[k]; + if (!weave[k] || g->is_primary != (pass == 0)) { + continue; + } + struct xrt_display_processor_d3d11 *dp = dp_of[k]; + struct seg_screen_state *st = st_of[k]; + if (!g->is_primary) { + if (!st->configured) { + st->configured = true; + xrt_display_processor_d3d11_set_background_2d(dp, nullptr, 0, 0); + } + if (f->atlas_encoding >= 0 && st->encoding_latched != f->atlas_encoding) { + st->encoding_latched = f->atlas_encoding; + xrt_display_processor_d3d11_set_atlas_encoding( + dp, (enum xrt_atlas_encoding)f->atlas_encoding); + } + if (st->transparent_latched != (int)f->transparent_background) { + st->transparent_latched = (int)f->transparent_background; + xrt_display_processor_d3d11_set_transparent_background( + dp, f->transparent_background, false); + } + if (st->mode_sent != (int)segs->mode_3d) { + st->mode_sent = (int)segs->mode_3d; + xrt_display_processor_d3d11_request_display_mode(dp, segs->mode_3d); + } + // Phase only where window px ARE device px; a resampled + // screen's origin is in the wrong units (and only a + // resample-tolerant DP weaves there, which has no phase). + if (g->screen_1to1 != COMP_SEG_1TO1_NO) { + xrt_display_processor_d3d11_set_present_origin(dp, g->present_origin_x, + g->present_origin_y); + } + } + ctx->OMSetRenderTargets(1, &rtv, nullptr); + set_viewport_scissor(ctx, &g->window_rect); + xrt_display_processor_d3d11_process_atlas(dp, ctx, st->crop.srv, tile[k].w, tile[k].h, + f->tile_columns, f->tile_rows, f->src_format, + f->target_width, f->target_height, g->window_rect.x, + g->window_rect.y, g->window_rect.w, g->window_rect.h); + any_woven = true; + } + } + + /* + * 5. Flat 2D for every segment that could not be woven, and for the + * canvas no segment covers (a monitor no plug-in claimed, or off every + * screen): one view (the middle one — the left eye of a stereo pair), + * linearly scaled from the view tile to the window rect (#1654 look). + */ + struct comp_seg_rect flat_dst[COMP_SEGMENTS_MAX + COMP_SEGMENTS_MAX_UNCOVERED]; + uint32_t flat_n = 0; + for (uint32_t k = 0; k < t->count; k++) { + if (!weave[k] && have_tile[k]) { + flat_dst[flat_n++] = t->seg[k].window_rect; + } + } + flat_n += comp_segments_uncovered(t, &f->canvas, &flat_dst[flat_n], COMP_SEGMENTS_MAX_UNCOVERED); + if (flat_n > 0 && f->renderer != nullptr) { + const uint32_t views = f->tile_columns * f->tile_rows; + const uint32_t vi = views > 0 ? (views - 1) / 2 : 0; + const int32_t base_x = (int32_t)((vi % f->tile_columns) * f->view_width); + const int32_t base_y = (int32_t)((vi / f->tile_columns) * f->view_height); + for (uint32_t k = 0; k < flat_n; k++) { + const struct comp_seg_rect *d = &flat_dst[k]; + struct comp_seg_rect s; + if (!comp_segments_tile_rect(d, &f->canvas, f->view_width, f->view_height, &s)) { + continue; // thinner than one source pixel + } + comp_d3d11_renderer_blit_rect(f->renderer, src_srv, base_x + s.x, base_y + s.y, s.w, s.h, rtv, + d->x, d->y, d->w, d->h); + } + } + + // Leave the back buffer bound, viewport = whole target, like the + // single-DP path does after process_atlas. + ctx->OMSetRenderTargets(1, &rtv, nullptr); + { + D3D11_VIEWPORT vp = {}; + vp.Width = (float)f->target_width; + vp.Height = (float)f->target_height; + vp.MaxDepth = 1.0f; + ctx->RSSetViewports(1, &vp); + D3D11_RECT sc = {0, 0, (LONG)f->target_width, (LONG)f->target_height}; + ctx->RSSetScissorRects(1, &sc); + } + src_res->Release(); + return any_woven; +} + +extern "C" void +comp_d3d11_segments_set_display_mode(struct comp_d3d11_segments *segs, bool enable_3d) +{ + if (segs == nullptr) { + return; + } + segs->mode_3d = enable_3d; + for (uint32_t i = 0; i < segs->screen_count; i++) { + struct seg_screen_state *st = &segs->st[i]; + if (st->dp != nullptr && st->mode_sent != (int)enable_3d) { + st->mode_sent = (int)enable_3d; + xrt_display_processor_d3d11_request_display_mode(st->dp, enable_3d); + } + } +} + +extern "C" bool +comp_d3d11_segments_get_eyes(struct comp_d3d11_segments *segs, uint64_t screen_id, struct xrt_eye_positions *out) +{ + if (segs == nullptr || out == nullptr) { + return false; + } + bool ok = false; + 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); + } + 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 new file mode 100644 index 0000000000..52b1bc1d32 --- /dev/null +++ b/src/xrt/compositor/d3d11/comp_d3d11_segments.h @@ -0,0 +1,157 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen segment display processors for the in-process D3D11 + * compositor (multi-screen M6, ADR-047 D2 on Windows). + * @ingroup comp_d3d11 + * + * The D3D11 twin of vk_native/comp_vk_native_segments.h. A window whose + * canvas covers several monitors is woven per segment: each segment (canvas ∩ + * screen, see util/comp_segments.h) gets the display processor of ITS screen, + * a pre-cropped atlas holding exactly that segment's views, `canvas = segment + * rect`, and its own present origin; the results land in the one DXGI back + * buffer. A segment with no DP for its screen (a plug-in without + * `create_dp_d3d11_for_screen`), or whose DP needs 1:1 pixels on a resampled + * screen, gets a flat 2D blit of one view instead. + * + * The session's primary DP (`comp_d3d11_compositor::display_processor`, the + * one holding the real HWND and so the vendor's drag phase-snap) keeps weaving + * the primary screen's segment and keeps owning everything view-related. A + * window entirely on the primary screen never enters this module's record + * path: that case stays byte-for-byte the single-DP path. + * + * Design note: docs/architecture/comp-segments.md (§ Windows / D3D11). + */ +#pragma once + +#include "xrt/xrt_compiler.h" +#include "xrt/xrt_compositor.h" +#include "xrt/xrt_display_processor_d3d11.h" +#include "xrt/xrt_screen.h" +#include "util/comp_segments.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct comp_d3d11_segments; +struct comp_d3d11_renderer; + +/*! + * Create the segment manager. Nothing is allocated on the GPU until a segment + * needs it. + * + * @param d3d11_device The device segment DPs and crop textures are created on + * (ID3D11Device*, borrowed; under the #918 split this is + * the OUTPUT device, the one the back buffer lives on). + */ +struct comp_d3d11_segments * +comp_d3d11_segments_create(void *d3d11_device); + +/*! + * Tear down every segment DP and crop texture. The caller guarantees no weave + * is in flight (the repaint thread is stopped). + */ +void +comp_d3d11_segments_destroy(struct comp_d3d11_segments **segs_ptr); + +/*! + * Hand over the system's screens and DP registry. Segmentation is enabled only + * when at least two screens are listed, the registry knows the system-default + * screen, and @p pinned_display_id is 0 (`XrSessionDisplayBindingDXR` pins a + * session to one display, which turns segmentation off). + */ +void +comp_d3d11_segments_set_screens(struct comp_d3d11_segments *segs, + const struct xrt_screen_list *list, + const struct xrt_system_compositor_info *info, + uint64_t pinned_display_id); + +//! Is segmentation possible at all for this session? +bool +comp_d3d11_segments_enabled(const struct comp_d3d11_segments *segs); + +/*! + * One metric update: recompute the segment table for the window, run the DP + * lifecycle (create/destroy secondary DPs with hysteresis), log a table change + * once at INFO. Call once per weave, before deciding which path records. + * + * @param window_desktop The window's client area in desktop device px. + * @param canvas The canvas in window px (NULL / empty = whole window). + * @param d3d11_context The context handed to new segment DPs. + * @param mode_index The head's active rendering-mode index; a change + * re-reads each segment DP's resample tolerance. + * @return true when this frame must take the split path + * (@ref comp_d3d11_segments_record); false = the single-DP path. + */ +bool +comp_d3d11_segments_update(struct comp_d3d11_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + void *d3d11_context, + uint32_t mode_index); + +//! Everything the split path needs about this frame. +struct comp_d3d11_segments_frame +{ + //! The context to record on (ID3D11DeviceContext*; the output context under the split). + void *context; + //! The DP's input this frame: the atlas cropped to content (ID3D11ShaderResourceView*). + void *src_srv; + uint32_t view_width; + uint32_t view_height; + uint32_t tile_columns; + uint32_t tile_rows; + //! The format declared to the DPs (DXGI_FORMAT of the atlas). + uint32_t src_format; + //! The back buffer (ID3D11RenderTargetView*) and its size. + void *target_rtv; + uint32_t target_width; + uint32_t target_height; + //! The canvas the atlas holds, window px (whole-canvas view dims above). + struct comp_seg_rect canvas; + //! Clear alpha for the target outside every segment; also re-declared to + //! segment DPs when it changes. + bool transparent_background; + //! The atlas encoding to declare to segment DPs (`enum + //! xrt_atlas_encoding`), or -1 to declare nothing. + int atlas_encoding; + //! The session's primary DP (weaves the primary screen's segment). The + //! caller has already fed it this frame's background, timing and encoding. + struct xrt_display_processor_d3d11 *primary_dp; + //! For the flat-2D fill (comp_d3d11_renderer_blit_rect). + struct comp_d3d11_renderer *renderer; +}; + +/*! + * Record the split frame: clear the target, crop each segment's views out of + * the atlas, run each segment's DP over its canvas (primary first, then left + * to right; viewport + scissor = segment), and blit flat 2D into the segments + * that cannot be woven. Leaves the back buffer bound as the render target. + * + * @return true if at least one DP wove. + */ +bool +comp_d3d11_segments_record(struct comp_d3d11_segments *segs, const struct comp_d3d11_segments_frame *f); + +/*! + * The session-wide hardware 2D/3D mode. Every segment DP follows it: sent to + * each live DP on a change and to every newly created one (the primary DP + * gets it from the compositor's own request path). Weave thread. + */ +void +comp_d3d11_segments_set_display_mode(struct comp_d3d11_segments *segs, bool enable_3d); + +/*! + * The predicted eyes of screen @p screen_id's segment DP, in that screen's + * display space. Thread-safe against the weave creating or destroying segment + * DPs. False when the screen has no segment DP (the primary's eyes come from + * the session's own DP). + */ +bool +comp_d3d11_segments_get_eyes(struct comp_d3d11_segments *segs, uint64_t screen_id, struct xrt_eye_positions *out); + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/compositor/d3d11/comp_d3d11_target.cpp b/src/xrt/compositor/d3d11/comp_d3d11_target.cpp index 3e5e7f8eac..88cb254b50 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_target.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_target.cpp @@ -1156,6 +1156,15 @@ comp_d3d11_target_get_back_buffer(struct comp_d3d11_target *target) return target->back_buffer; } +extern "C" void * +comp_d3d11_target_get_rtv(struct comp_d3d11_target *target) +{ + if (target == nullptr) { + return nullptr; + } + return target->rtv; +} + extern "C" xrt_result_t comp_d3d11_target_resize(struct comp_d3d11_target *target, uint32_t width, diff --git a/src/xrt/compositor/d3d11/comp_d3d11_target.h b/src/xrt/compositor/d3d11/comp_d3d11_target.h index 0b7c0bd262..ffebc23922 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_target.h +++ b/src/xrt/compositor/d3d11/comp_d3d11_target.h @@ -287,6 +287,16 @@ comp_d3d11_target_bind(struct comp_d3d11_target *target); void * comp_d3d11_target_get_back_buffer(struct comp_d3d11_target *target); +/*! + * Get the back buffer's render target view (ID3D11RenderTargetView*) — the + * view @ref comp_d3d11_target_bind binds. Multi-screen M6: the segment path + * clears it and re-binds it between segment DPs. + * + * @ingroup comp_d3d11 + */ +void * +comp_d3d11_target_get_rtv(struct comp_d3d11_target *target); + /*! * Resize the target swapchain. * diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index dd106cb3dd..083eb1c7e8 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -6200,6 +6200,25 @@ oxr_session_create(struct oxr_logger *log, } #endif +#if defined(XRT_HAVE_D3D11_NATIVE_COMPOSITOR) && defined(XRT_OS_WINDOWS) + // Multi-screen M6: the Windows twin — the in-process D3D11 compositor + // weaves a window that spans monitors per segment, each with its own + // screen's DP (windowless, phase from set_present_origin); the primary + // screen's DP keeps the real HWND. + if (sess->is_d3d11_native_compositor && sess->xcn != NULL) { + struct xrt_screen_list *screens = U_TYPED_CALLOC(struct xrt_screen_list); + if (screens != NULL) { + oxr_system_get_screens(sys, screens); + uint64_t pinned = 0; +#ifdef OXR_HAVE_DXR_display_info + pinned = bound_display_id; +#endif + comp_d3d11_compositor_set_screens(&sess->xcn->base, screens, &sys->xsysc->info, pinned); + free(screens); + } + } +#endif + #ifdef OXR_HAVE_DXR_depth_budget // XR_DXR_depth_budget: the create-info chain is gone by locate time, and // the rear-depth DEFAULT is a function of transparency, so remember it. From 29c0d118cec8a0b7bb4af05e6d6231945933bb21 Mon Sep 17 00:00:00 2001 From: dfattal Date: Wed, 7 Oct 2026 23:31:12 -0700 Subject: [PATCH 3/4] docs(multi-screen): the Windows / D3D11 segment path and the M6 status Co-Authored-By: Claude Fable 5.1 --- docs/architecture/comp-segments.md | 29 +++++++++++++++++++++++++++++ docs/roadmap/multi-screen.md | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index f151139585..6cd55bf44e 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -153,6 +153,35 @@ or destroy is one WARN. - Capture: with the window split, the post-compose atlas capture also writes each segment's DP input as `.seg.png`. +## Windows / D3D11 (M6) + +The in-process D3D11 compositor carries the same model in `d3d11/comp_d3d11_segments.{h,cpp}`, +a line-for-line twin of the Vulkan manager with the GPU work in D3D11 terms +(`CopySubresourceRegion` crops, `ClearRenderTargetView`, a renderer rect blit for flat 2D, no +deferred-release list because the immediate context executes in order). What differs on Windows: + +- **Two appended ABI slots, no version bump** (ADR-020): `xrt_plugin_iface::create_dp_d3d11_for_screen` + (the D3D11 twin of the Vulkan per-screen factory) and `xrt_display_processor_d3d11::set_present_origin`. + A plug-in without them gets flat 2D on its other screens, exactly like Vulkan. +- **The window.** The session's primary DP keeps the real HWND and so the vendor's drag phase-snap; + every other screen's DP is created with a NULL window and phases from `set_present_origin` + the + canvas offset. The vendor weaver reads the current D3D11 viewport **and scissor** and writes only + inside them, so the segment path sets both to the segment before every `process_atlas`, and the + back buffer is re-bound before every DP. (ADR-047's "HWND follows the majority segment" is a + follow-up: today the HWND stays with the primary screen's DP.) +- **Units.** The window rect comes from `ClientToScreen` in a per-monitor-DPI-aware process, i.e. + device px — the same space as the registry's monitor rects. A DPI-virtualised origin would weave on + the wrong lattice and look plausible (`docs/reference/dpi-awareness.md`). +- **The untracked screen.** The SR tracker follows the runtime's active display only, so a weaver + bound to any other display sees no face and would fall back to 2D. The Leia plug-in pins that + weaver to its nominal viewer and leaves the lens to the active panel (David, 2026-10-07; SR D3/D4 + move both per device later). The runtime does nothing special: the DP owns it. +- **Not segmented on Windows:** zero-copy frames, the #918 output-adapter split, 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). +- **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) Under `PRIMARY_MULTIVIEW_DXR` each segment gets its **own** views instead of a crop of diff --git a/docs/roadmap/multi-screen.md b/docs/roadmap/multi-screen.md index 5aaca692ef..f835aa8a3c 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`) | 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); 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 From 8a3a555c39443ccc08de3b47e88cdfae03141b14 Mon Sep 17 00:00:00 2001 From: dfattal Date: Thu, 8 Oct 2026 00:28:33 -0700 Subject: [PATCH 4/4] test(plugin-abi): the interface now ends with create_dp_d3d11_for_screen (multi-screen M6) The layout test pins the last slot of xrt_plugin_iface; the M6 append moved it. Co-Authored-By: Claude Fable 5.1 --- tests/tests_stereo_camera.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/tests_stereo_camera.cpp b/tests/tests_stereo_camera.cpp index d784f11071..51c1db4190 100644 --- a/tests/tests_stereo_camera.cpp +++ b/tests/tests_stereo_camera.cpp @@ -313,13 +313,16 @@ TEST_CASE("plug-in iface: camera slots are appended after lift and platform-stat CHECK(offsetof(xrt_plugin_iface, get_platform_state) == lift_end); CHECK(offsetof(xrt_plugin_iface, stereo_camera_enumerate) == platform_state_end); // Multi-screen M1 appended get_display_info_for_monitor right after the - // camera block, and M2 create_dp_vk_for_screen right after that; it now - // ends the struct. + // camera block, M2 create_dp_vk_for_screen right after that, and M6 + // (Windows) create_dp_d3d11_for_screen right after that; it now ends the + // struct. CHECK(offsetof(xrt_plugin_iface, get_display_info_for_monitor) == offsetof(xrt_plugin_iface, stereo_camera_close) + sizeof(void *)); CHECK(offsetof(xrt_plugin_iface, create_dp_vk_for_screen) == offsetof(xrt_plugin_iface, get_display_info_for_monitor) + sizeof(void *)); - CHECK(offsetof(xrt_plugin_iface, create_dp_vk_for_screen) + sizeof(void *) == sizeof(xrt_plugin_iface)); + CHECK(offsetof(xrt_plugin_iface, create_dp_d3d11_for_screen) == + offsetof(xrt_plugin_iface, create_dp_vk_for_screen) + sizeof(void *)); + CHECK(offsetof(xrt_plugin_iface, create_dp_d3d11_for_screen) + sizeof(void *) == sizeof(xrt_plugin_iface)); xrt_plugin_iface iface{}; iface.struct_size = (uint32_t)offsetof(xrt_plugin_iface, stereo_camera_enumerate);