From 01ae60959921f32633d16c2428391c9938b05403 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:25:29 -0700 Subject: [PATCH 1/4] feat(plugin): append create_dp_d3d12_for_screen + D3D12 set_present_origin (multi-screen M6) The D3D12 twins of the D3D11 per-screen slots, appended per ADR-020 with no XRT_PLUGIN_API_VERSION_CURRENT bump (ABI stays 5): - xrt_plugin_iface::create_dp_d3d12_for_screen, after create_dp_d3d11_for_screen; announced by XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D12_FOR_SCREEN, probed with xrt_plugin_iface_has_create_dp_d3d12_for_screen(). - xrt_display_processor_d3d12::set_present_origin, after get_background_preview; announced by XRT_DP_D3D12_HAS_PRESENT_ORIGIN, called through a struct_size-gated helper. Plus a D3D12 tolerates_resample() helper mirroring the D3D11 one. The D3D12 contract differs in one place: every segment records onto one command list, which carries no viewport/scissor a DP could inherit, so a screen-bound DP sets both from the canvas itself. sim_display implements both on D3D12 (screen-bound instance answers its own screen's metrics and confines viewport + scissor to the canvas) so the CI self-test keeps a complete sim-display. tests_stereo_camera pins the new iface tail; xrt_plugin_iface.md documents the Windows slots. Co-Authored-By: Claude Fable 5.1 --- docs/reference/xrt_plugin_iface.md | 33 +++- .../sim_display/sim_display_interface.h | 16 ++ .../drivers/sim_display/sim_display_plugin.c | 7 + .../sim_display_processor_d3d12.cpp | 183 ++++++++++++++++-- .../include/xrt/xrt_display_processor_d3d12.h | 74 ++++++- src/xrt/include/xrt/xrt_plugin.h | 63 ++++++ tests/tests_stereo_camera.cpp | 10 +- 7 files changed, 362 insertions(+), 24 deletions(-) diff --git a/docs/reference/xrt_plugin_iface.md b/docs/reference/xrt_plugin_iface.md index 0d979c7519..9aad03f41c 100644 --- a/docs/reference/xrt_plugin_iface.md +++ b/docs/reference/xrt_plugin_iface.md @@ -344,6 +344,35 @@ Appended after `get_display_info_for_monitor` per ADR-020 (`struct_size`-gated, belong to the ACTIVE plug-in; mixed vendors are M4. Reference implementation: `sim_display_dp_factory_vk_for_screen` (`drivers/sim_display/sim_display_processor.c`). +### `create_dp_d3d11_for_screen` / `create_dp_d3d12_for_screen` (multi-screen M6, Windows) + +```c +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); +xrt_result_t (*create_dp_d3d12_for_screen)(struct xrt_plugin_instance *inst, void *d3d12_device, + void *d3d12_command_queue, void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d12 **out_xdp); +``` + +The Windows twins of `create_dp_vk_for_screen`, for the in-process D3D11 and D3D12 compositors. +Same `binding` rules; `window_handle` is the session's real HWND only for the DP that keeps the +vendor's drag phase-snap and NULL for every other screen's DP, whose phase comes from the +appended per-API `set_present_origin` slot (`xrt_display_processor_d3d11::set_present_origin`, +`xrt_display_processor_d3d12::set_present_origin`, both `struct_size`-gated, announced by +`XRT_DP_D3D11_HAS_PRESENT_ORIGIN` / `XRT_DP_D3D12_HAS_PRESENT_ORIGIN`), called before every weave, +plus the canvas offset. `process_atlas` gets `canvas = the segment` and a pre-cropped atlas +holding exactly that segment's views; confine viewport AND scissor to the canvas and do not +assume you are the first writer. On D3D12 every segment records onto ONE command list, which +carries no viewport/scissor a DP could inherit — the DP sets both itself from the canvas; the +atlas arrives as a plain 2D texture in `COMMON` with its own SRV descriptor. Appended after +`create_dp_vk_for_screen` (D3D11) and `create_dp_metal_for_screen` (D3D12) per ADR-020 (ABI stays +5); guard with `#ifdef XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D11_FOR_SCREEN` / +`XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D12_FOR_SCREEN`. Reference implementations: +`sim_display_dp_factory_d3d11_for_screen` / `sim_display_dp_factory_d3d12_for_screen`. + **The 1:1 capability.** Report through the existing `get_scanout_caps` slot whether your output survives a display-server resample: `XRT_DP_SCANOUT_FLAG_TOLERATES_RESAMPLE` in `xrt_dp_scanout_caps::flags` (carved from the former `reserved[0]`, so an older plug-in reads @@ -839,8 +868,8 @@ Flags (`XRT_PLUGIN_STEREO_CAMERA_*`) mirror the XR bits: `SHARED_WITH_EYE_TRACKI everything that was on `main` before them: `… vk_bundle_fn_table_offset` → `create_dp_d3d11_lift` (ADR-042) → `get_platform_state` (ADR-045) → `stereo_camera_enumerate … stereo_camera_close` → `get_display_info_for_monitor` (multi-screen M1) → `create_dp_vk_for_screen` (multi-screen M2) -→ `create_dp_d3d11_for_screen` (M6, Windows) → `create_dp_metal_for_screen` (macOS, now the last -member). +→ `create_dp_d3d11_for_screen` (M6, D3D11) → `create_dp_metal_for_screen` (macOS) → +`create_dp_d3d12_for_screen` (M6, D3D12, now the last member). `tests_stereo_camera` pins that order (lift right after the vk fingerprint, platform-state right after lift, the camera block right after platform-state and ending the struct), so a reorder or a slot squeezed in between fails on the host, not on a vendor box. diff --git a/src/xrt/drivers/sim_display/sim_display_interface.h b/src/xrt/drivers/sim_display/sim_display_interface.h index ed269b500b..b8bbe5233d 100644 --- a/src/xrt/drivers/sim_display/sim_display_interface.h +++ b/src/xrt/drivers/sim_display/sim_display_interface.h @@ -338,6 +338,22 @@ sim_display_dp_factory_d3d12(void *d3d12_device, void *window_handle, struct xrt_display_processor_d3d12 **out_xdp); +/*! + * Multi-screen M6: per-screen D3D12 factory, the D3D12 twin of + * `sim_display_dp_factory_d3d11_for_screen` — matches + * `xrt_plugin_iface::create_dp_d3d12_for_screen`. The DP describes the bound + * screen and confines its draw (viewport + scissor) to the canvas it is handed. + * + * @ingroup drv_sim_display + */ +xrt_result_t +sim_display_dp_factory_d3d12_for_screen(struct xrt_plugin_instance *inst, + void *d3d12_device, + void *d3d12_command_queue, + void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d12 **out_xdp); + /*! * Set an external device as the pose source for a sim_display HMD. * diff --git a/src/xrt/drivers/sim_display/sim_display_plugin.c b/src/xrt/drivers/sim_display/sim_display_plugin.c index fb083873f8..d27abf50de 100644 --- a/src/xrt/drivers/sim_display/sim_display_plugin.c +++ b/src/xrt/drivers/sim_display/sim_display_plugin.c @@ -385,6 +385,13 @@ static struct xrt_plugin_iface g_sim_display_iface = { #else .create_dp_metal_for_screen = NULL, #endif + +/* Multi-screen M6 (D3D12): one D3D12 DP per screen a spanning window covers. */ +#if defined(_WIN32) + .create_dp_d3d12_for_screen = sim_display_dp_factory_d3d12_for_screen, +#else + .create_dp_d3d12_for_screen = NULL, +#endif }; diff --git a/src/xrt/drivers/sim_display/sim_display_processor_d3d12.cpp b/src/xrt/drivers/sim_display/sim_display_processor_d3d12.cpp index ff05c14522..af7626a4d1 100644 --- a/src/xrt/drivers/sim_display/sim_display_processor_d3d12.cpp +++ b/src/xrt/drivers/sim_display/sim_display_processor_d3d12.cpp @@ -21,6 +21,7 @@ #include "xrt/xrt_display_processor_d3d12.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" @@ -199,6 +200,22 @@ struct sim_display_processor_d3d12_impl uint32_t zone_last_mask_w, zone_last_mask_h; uint64_t zone_last_seq; bool zone_active; //!< A client mask is currently published (not cleared). + + //! Multi-screen M6 (D3D12): this instance was created for ONE screen + //! (`create_dp_d3d12_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 / D3D11 DPs; + //! sim's D3D12 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_d3d12_impl * @@ -233,13 +250,6 @@ sim_dp_d3d12_process_atlas(struct xrt_display_processor_d3d12 *xdp, uint32_t canvas_width, uint32_t canvas_height) { - // TODO(#85): Pass canvas_offset_x/y to vendor weaver for interlacing - // phase correction once Leia SR SDK supports sub-rect offset. - (void)canvas_offset_x; - (void)canvas_offset_y; - (void)canvas_width; - (void)canvas_height; - (void)format; // colorspace handled by SRV/format selection, not this arg (void)target_resource; // sim_display uses target_rtv_cpu_handle via render pass @@ -292,7 +302,10 @@ sim_dp_d3d12_process_atlas(struct xrt_display_processor_d3d12 *xdp, rtv_handle.ptr = static_cast(target_rtv_cpu_handle); cmd_list->OMSetRenderTargets(1, &rtv_handle, FALSE, nullptr); - // Set viewport + // Set viewport + scissor. A screen-bound DP (multi-screen M6) is handed a + // canvas that is ITS segment of the target and must draw only there (a + // command list carries no viewport/scissor to inherit, so the DP sets + // both); the unbound DP keeps the pre-M6 full-target draw. D3D12_VIEWPORT viewport = {}; viewport.TopLeftX = 0.0f; viewport.TopLeftY = 0.0f; @@ -300,14 +313,22 @@ sim_dp_d3d12_process_atlas(struct xrt_display_processor_d3d12 *xdp, viewport.Height = static_cast(target_height); viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; - cmd_list->RSSetViewports(1, &viewport); - - // Set scissor rect D3D12_RECT scissor = {}; scissor.left = 0; scissor.top = 0; scissor.right = static_cast(target_width); scissor.bottom = static_cast(target_height); + 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); + scissor.left = canvas_offset_x; + scissor.top = canvas_offset_y; + scissor.right = canvas_offset_x + static_cast(canvas_width); + scissor.bottom = canvas_offset_y + static_cast(canvas_height); + } + cmd_list->RSSetViewports(1, &viewport); cmd_list->RSSetScissorRects(1, &scissor); // Set SRV via root descriptor table @@ -334,8 +355,101 @@ sim_dp_d3d12_process_atlas(struct xrt_display_processor_d3d12 *xdp, } -// #856: panel geometry -> compositor computes window-scoped Kooima. -SIM_ZONE_DEFINE_PANEL_METRIC_FNS(sim_dp_d3d12, xrt_display_processor_d3d12) +// #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_d3d12_get_display_dimensions(struct xrt_display_processor_d3d12 *xdp, float *out_width_m, float *out_height_m) +{ + struct sim_display_processor_d3d12_impl *sdp = sim_dp_d3d12(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_d3d12_get_display_pixel_info(struct xrt_display_processor_d3d12 *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_d3d12_impl *sdp = sim_dp_d3d12(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 parity with the Vulkan / D3D11 DPs; no D3D12 sim mode reads it (#817). + */ +static void +sim_dp_d3d12_set_present_origin(struct xrt_display_processor_d3d12 *xdp, int32_t panel_x, int32_t panel_y) +{ + struct sim_display_processor_d3d12_impl *sdp = sim_dp_d3d12(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). Same rules as + * the D3D11 DP's sim_dp_d3d11_bind_screen: 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. + */ +static void +sim_dp_d3d12_bind_screen(struct sim_display_processor_d3d12_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_d3d12_get_predicted_eye_positions(struct xrt_display_processor_d3d12 *xdp, @@ -618,10 +732,11 @@ sim_dp_d3d12_get_scanout_caps(struct xrt_display_processor_d3d12 *xdp, struct xr * */ -extern "C" xrt_result_t -sim_display_processor_d3d12_create(enum sim_display_output_mode mode, - void *d3d12_device, - struct xrt_display_processor_d3d12 **out_xdp) +static xrt_result_t +sim_display_processor_d3d12_create_bound(enum sim_display_output_mode mode, + void *d3d12_device, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d12 **out_xdp) { if (out_xdp == nullptr) { return XRT_ERROR_DEVICE_CREATION_FAILED; @@ -651,6 +766,8 @@ sim_display_processor_d3d12_create(enum sim_display_output_mode mode, sdp->base.publish_local_zone_mask = sim_dp_d3d12_publish_local_zone_mask; // #224 / ADR-027 sdp->base.clear_local_zone_mask = sim_dp_d3d12_clear_local_zone_mask; // #224 / ADR-027 sdp->base.get_scanout_caps = sim_dp_d3d12_get_scanout_caps; + sdp->base.set_present_origin = sim_dp_d3d12_set_present_origin; // multi-screen M6 + sim_dp_d3d12_bind_screen(sdp, binding); // #224 / ADR-027 zone test double config (shared parser). sim_zone_config_from_env(&sdp->zone_cfg, "D3D12"); @@ -751,6 +868,15 @@ sim_display_processor_d3d12_create(enum sim_display_output_mode mode, } +extern "C" xrt_result_t +sim_display_processor_d3d12_create(enum sim_display_output_mode mode, + void *d3d12_device, + struct xrt_display_processor_d3d12 **out_xdp) +{ + return sim_display_processor_d3d12_create_bound(mode, d3d12_device, nullptr, out_xdp); +} + + /* * * Factory function — matches xrt_dp_factory_d3d12_fn_t signature. @@ -770,3 +896,26 @@ sim_display_dp_factory_d3d12(void *d3d12_device, return sim_display_processor_d3d12_create(mode, d3d12_device, out_xdp); } + + +/* + * Multi-screen M6 (D3D12): one D3D12 DP per screen a spanning window covers. + * The window handle is ignored like the plain factory's (sim weaves against no + * window); the binding seeds the instance's screen. + */ +extern "C" xrt_result_t +sim_display_dp_factory_d3d12_for_screen(struct xrt_plugin_instance *inst, + void *d3d12_device, + void *d3d12_command_queue, + void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d12 **out_xdp) +{ + (void)inst; + (void)d3d12_command_queue; + (void)window_handle; + if (binding == nullptr) { + return XRT_ERROR_DEVICE_CREATION_FAILED; + } + return sim_display_processor_d3d12_create_bound(sim_display_get_output_mode(), d3d12_device, binding, out_xdp); +} diff --git a/src/xrt/include/xrt/xrt_display_processor_d3d12.h b/src/xrt/include/xrt/xrt_display_processor_d3d12.h index 745316982e..3116c0bcab 100644 --- a/src/xrt/include/xrt/xrt_display_processor_d3d12.h +++ b/src/xrt/include/xrt/xrt_display_processor_d3d12.h @@ -509,6 +509,32 @@ struct xrt_display_processor_d3d12 */ bool (*get_background_preview)(struct xrt_display_processor_d3d12 *xdp, struct xrt_dp_background_preview *out_preview); + + /*! + * Multi-screen M6 (ADR-047 D2 on Windows, the D3D12 twin of + * xrt_display_processor_d3d11::set_present_origin): where the window's + * client area sits on the DP's screen, so a WINDOWLESS DP (created + * through `create_dp_d3d12_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, before process_atlas + * (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 get_background_preview per ADR-020 (append-only within a + * major; no version bump — gated by @ref XRT_DP_HAS_SLOT), announced by + * XRT_DP_D3D12_HAS_PRESENT_ORIGIN. + * + * @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_d3d12 *xdp, int32_t panel_x, int32_t panel_y); }; @@ -580,7 +606,8 @@ XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d12, set_predicted_sca */ #define XRT_DP_D3D12_HAS_PREDICTED_SCANOUT 1 XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d12, get_background_preview) == XRT_DP_D3D12_BASE_OFF + 23 * sizeof(void *), XRT_DP_ABI_MSG); -XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_d3d12) == XRT_DP_D3D12_BASE_OFF + 24 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(offsetof(struct xrt_display_processor_d3d12, set_present_origin) == XRT_DP_D3D12_BASE_OFF + 24 * sizeof(void *), XRT_DP_ABI_MSG); +XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_d3d12) == XRT_DP_D3D12_BASE_OFF + 25 * sizeof(void *), XRT_DP_ABI_MSG); /*! * Defined when this header carries the get_background_preview slot, so a @@ -603,6 +630,13 @@ XRT_DP_ABI_ASSERT(sizeof(struct xrt_display_processor_d3d12) */ #define XRT_DP_D3D12_HAS_BACKEND_STATE 1 +/*! + * Defined when this header carries the set_present_origin slot (multi-screen + * M6, D3D12), so a plug-in built against an older runtime can #ifdef-guard its + * implementation. Purely additive (ADR-020). + */ +#define XRT_DP_D3D12_HAS_PRESENT_ORIGIN 1 + // clang-format on /*! @@ -1053,6 +1087,44 @@ xrt_display_processor_d3d12_get_background_preview(struct xrt_display_processor_ return xdp->get_background_preview(xdp, out_preview); } +/*! + * @copydoc xrt_display_processor_d3d12::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_d3d12 + */ +static inline void +xrt_display_processor_d3d12_set_present_origin(struct xrt_display_processor_d3d12 *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. The + * D3D12 twin of xrt_display_processor_d3d11_tolerates_resample. + * + * @public @memberof xrt_display_processor_d3d12 + */ +static inline bool +xrt_display_processor_d3d12_tolerates_resample(struct xrt_display_processor_d3d12 *xdp) +{ + struct xrt_dp_scanout_caps caps; + xrt_dp_scanout_caps_init(&caps); + if (!xrt_display_processor_d3d12_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 02acf9349f..500c6ec254 100644 --- a/src/xrt/include/xrt/xrt_plugin.h +++ b/src/xrt/include/xrt/xrt_plugin.h @@ -1370,6 +1370,47 @@ struct xrt_plugin_iface void *window_handle, const struct xrt_screen_binding *binding, struct xrt_display_processor_metal **out_xdp); + + /*! + * Create a D3D12 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 D3D12 compositor weaves a window that spans several + * monitors per segment, each by its own screen's DP, into the one + * presented back buffer. The D3D12 twin of + * @ref create_dp_d3d11_for_screen. + * + * Same contract as @ref create_dp_d3d12 (`xrt_dp_factory_d3d12_fn_t`), + * plus everything @ref create_dp_d3d11_for_screen promises, in D3D12 + * terms: + * - @p binding names the screen; never NULL. + * - @p window_handle: the session's real HWND for the majority + * screen's DP, NULL for every other screen's DP, which is windowless + * and gets its phase from + * @ref xrt_display_processor_d3d12::set_present_origin plus the + * canvas offset. A DP must weave with a NULL window. + * - The compositor records every segment onto ONE command list: + * @ref xrt_display_processor_d3d12::process_atlas gets + * `canvas = the segment`, a pre-cropped plain 2D atlas holding + * exactly that segment's views (COMMON state, its own SRV + * descriptor), and the shared back buffer. A command list carries no + * state a DP could inherit, so the DP itself 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. 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_metal_for_screen. Announced by + * @ref XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D12_FOR_SCREEN. + */ + xrt_result_t (*create_dp_d3d12_for_screen)(struct xrt_plugin_instance *inst, + void *d3d12_device, + void *d3d12_command_queue, + void *window_handle, + const struct xrt_screen_binding *binding, + struct xrt_display_processor_d3d12 **out_xdp); }; /*! @@ -1482,6 +1523,28 @@ xrt_plugin_iface_has_create_dp_metal_for_screen(const struct xrt_plugin_iface *i iface->create_dp_metal_for_screen != NULL; } +/*! + * Defined when @ref xrt_plugin_iface carries @ref + * xrt_plugin_iface::create_dp_d3d12_for_screen (multi-screen M6, Windows + * D3D12), so a plug-in built against an older runtime header can + * #ifdef-guard implementing it. + */ +#define XRT_PLUGIN_IFACE_HAS_CREATE_DP_D3D12_FOR_SCREEN 1 + +/*! + * True when @p iface implements @ref + * xrt_plugin_iface::create_dp_d3d12_for_screen (and its struct_size covers the + * slot). + */ +static inline bool +xrt_plugin_iface_has_create_dp_d3d12_for_screen(const struct xrt_plugin_iface *iface) +{ + return iface != NULL && + iface->struct_size >= offsetof(struct xrt_plugin_iface, create_dp_d3d12_for_screen) + + sizeof(iface->create_dp_d3d12_for_screen) && + iface->create_dp_d3d12_for_screen != NULL; +} + /*! * True when @p iface fills the whole stereo camera slot set (and its * struct_size covers it). diff --git a/tests/tests_stereo_camera.cpp b/tests/tests_stereo_camera.cpp index 7a215f9b56..d5b1f28941 100644 --- a/tests/tests_stereo_camera.cpp +++ b/tests/tests_stereo_camera.cpp @@ -314,9 +314,9 @@ TEST_CASE("plug-in iface: camera slots are appended after lift and platform-stat 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, M2 create_dp_vk_for_screen right after that, and M6 - // (Windows) create_dp_d3d11_for_screen right after that, and the macOS - // segments create_dp_metal_for_screen right after that; it now ends the - // struct. + // (Windows) create_dp_d3d11_for_screen right after that, the macOS + // segments create_dp_metal_for_screen right after that, then M6 D3D12 + // create_dp_d3d12_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) == @@ -325,7 +325,9 @@ TEST_CASE("plug-in iface: camera slots are appended after lift and platform-stat offsetof(xrt_plugin_iface, create_dp_vk_for_screen) + sizeof(void *)); CHECK(offsetof(xrt_plugin_iface, create_dp_metal_for_screen) == offsetof(xrt_plugin_iface, create_dp_d3d11_for_screen) + sizeof(void *)); - CHECK(offsetof(xrt_plugin_iface, create_dp_metal_for_screen) + sizeof(void *) == sizeof(xrt_plugin_iface)); + CHECK(offsetof(xrt_plugin_iface, create_dp_d3d12_for_screen) == + offsetof(xrt_plugin_iface, create_dp_metal_for_screen) + sizeof(void *)); + CHECK(offsetof(xrt_plugin_iface, create_dp_d3d12_for_screen) + sizeof(void *) == sizeof(xrt_plugin_iface)); xrt_plugin_iface iface{}; iface.struct_size = (uint32_t)offsetof(xrt_plugin_iface, stereo_camera_enumerate); From ee086f234167b9caf1187b1de007cd8bfac51bde Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:25:30 -0700 Subject: [PATCH 2/4] feat(d3d12): weave a window spanning monitors per screen, with per-segment views (multi-screen M6 + M3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-process D3D12 compositor (the Unity display provider's path) gets the pre-split D3D11 segment model: - comp_d3d12_segments.{h,cpp}: the twin of comp_d3d11_segments — screen table join, DP lifecycle with hysteresis, decide loop, primary-first weave order, flat-2D fill. GPU work in D3D12 terms: per-screen crop textures (COMMON -> COPY_DEST -> COMMON), each with its own slot in a module-owned shader-visible SRV heap (descriptors are read at execute time, so one slot cannot serve two DPs on one list), explicit source barriers, viewport + scissor = segment before every DP. - comp_d3d12_compositor_set_screens + d3d12_segments_tick at the weave site: off zero-copy, the #918 split, the #1264 reroute, a zones / Local2D canvas, and a back buffer that does not match the client area; the window rect comes from the HWND in device px. Repaints take the same path. - M3: comp_d3d12_compositor_get_segment_metrics / set_view_routing, the renderer's mosaic routing (projection layers per segment rect with the tile's blend mode decided once; quads + equirect2 once per segment with that segment's camera), comp_d3d12_renderer_blit_rect for the flat fill, and a routed frame is never zero-copy. - oxr: session create / xrLocateViews / xrEndFrame hand-offs for D3D12; the Windows view-set capacity counts a D3D11 OR D3D12 factory (oxr_segment_views_win_entry_has_dp, host-tested). Co-Authored-By: Claude Fable 5.1 --- src/xrt/compositor/d3d12/CMakeLists.txt | 2 + .../d3d12/comp_d3d12_compositor.cpp | 338 ++++++- .../compositor/d3d12/comp_d3d12_compositor.h | 40 + .../compositor/d3d12/comp_d3d12_renderer.cpp | 284 ++++-- .../compositor/d3d12/comp_d3d12_renderer.h | 58 ++ .../compositor/d3d12/comp_d3d12_segments.cpp | 841 ++++++++++++++++++ .../compositor/d3d12/comp_d3d12_segments.h | 182 ++++ .../state_trackers/oxr/oxr_segment_views.h | 14 + src/xrt/state_trackers/oxr/oxr_session.c | 33 +- .../oxr/oxr_session_frame_end.c | 8 + src/xrt/state_trackers/oxr/oxr_system.c | 25 +- tests/tests_oxr_segment_views.cpp | 25 +- 12 files changed, 1770 insertions(+), 80 deletions(-) create mode 100644 src/xrt/compositor/d3d12/comp_d3d12_segments.cpp create mode 100644 src/xrt/compositor/d3d12/comp_d3d12_segments.h diff --git a/src/xrt/compositor/d3d12/CMakeLists.txt b/src/xrt/compositor/d3d12/CMakeLists.txt index f29a4e0c0f..81ffad6c9c 100644 --- a/src/xrt/compositor/d3d12/CMakeLists.txt +++ b/src/xrt/compositor/d3d12/CMakeLists.txt @@ -18,6 +18,8 @@ if(WIN32 AND XRT_HAVE_D3D12) comp_d3d12_outcomp.h comp_d3d12_renderer.cpp comp_d3d12_renderer.h + comp_d3d12_segments.cpp + comp_d3d12_segments.h comp_d3d12_swapchain.cpp comp_d3d12_swapchain.h comp_d3d12_target.cpp diff --git a/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp b/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp index 1b670f9986..49f9d616eb 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp @@ -14,6 +14,8 @@ #include "util/comp_display_refresh_win.h" #include "comp_d3d12_renderer.h" #include "comp_d3d12_outcomp.h" +#include "comp_d3d12_segments.h" // multi-screen M6 (D3D12) +#include "xrt/xrt_screen.h" // #918 D12-3 — the output-device split: the activation decision and the // cross-adapter transport, both shared with the D3D11 legs. @@ -433,6 +435,30 @@ struct comp_d3d12_compositor //! D3D12 display processor. struct xrt_display_processor_d3d12 *display_processor; + /*! + * Multi-screen M6 (ADR-047 D2 on Windows, D3D12): a window spanning + * monitors is woven per segment, each by its own screen's DP, into the + * one back buffer — the twin of the D3D11 compositor's fields of the + * same names. The screen list + DP registry arrive at session creation + * (comp_d3d12_compositor_set_screens, under c->mutex); the manager is + * (re)built lazily on the weave thread, which also holds c->mutex. + */ + struct comp_d3d12_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; + /*! + * 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_d3d12_eff_layout seg_route_logged; //!< change-only logging of the routed layout + /*! * XR_DXR_depth_budget (rear depth budget). * @@ -3784,6 +3810,174 @@ d3d12_publish_rear_budget_zones(struct comp_d3d12_compositor *c) comp_rear_budget_set_zone_rects(&c->rear_budget, zones, count, os_monotonic_get_ns()); } +/*! + * Multi-screen M3: turn the routing the last xrLocateViews handed out into + * per-segment tile rects on this frame's effective layout. The D3D12 twin of + * d3d11_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 +d3d12_route_effective_layout(struct comp_d3d12_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_d3d12_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: D3D12 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 +d3d12_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; +} + +/*! + * Multi-screen M6 (ADR-047 D2, D3D12): does this window span monitors? The + * twin of the D3D11 compositor's pre-split gate — only off the zero-copy path + * (the atlas must be ours to crop: a repaint is never armed on it), off the + * #918 split and the #1264 reroute (another device owns the weave there), for + * a whole-window canvas (a zones / Local2D frame keeps the single-DP path) and + * while the back buffer matches the client area (the segment rects are window + * px). A window on the primary screen only stays single-DP, byte for byte. + * Publishes the table it took for the next xrLocateViews (M3). Weave thread, + * under c->mutex. + * + * @param[out] out_window The window's client area in desktop device px. + * @return true = this weave takes the split path. + */ +static bool +d3d12_segments_tick(struct comp_d3d12_compositor *c, + bool atlas_is_ours, + const struct u_canvas_rect *eff_canvas, + uint32_t tgt_width, + uint32_t tgt_height, + struct comp_seg_rect *out_window) +{ + bool seg_split = false; + struct comp_seg_rect win = {0, 0, 0, 0}; + bool have_win = false; + if (atlas_is_ours && !c->split_active && !c->reroute.active && !eff_canvas->valid && c->hwnd != nullptr && + c->seg_screens != nullptr) { + // Device px — every DisplayXR executable is per-monitor DPI aware + // (#1201), the same space the screen registry's monitor rects are in. + RECT rc; + POINT origin = {0, 0}; + if (GetClientRect(c->hwnd, &rc) && rc.right > 0 && rc.bottom > 0 && ClientToScreen(c->hwnd, &origin)) { + win.x = origin.x; + win.y = origin.y; + win.w = (uint32_t)rc.right; + win.h = (uint32_t)rc.bottom; + have_win = win.w == tgt_width && win.h == tgt_height; + } + } + if (have_win) { + 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_d3d12_segments_destroy(&c->segments); + c->seg_rebuild = false; + } + if (c->segments == nullptr) { + c->segments = comp_d3d12_segments_create(d3d12_out_device(c), d3d12_out_queue(c)); + if (c->segments != nullptr) { + comp_d3d12_segments_set_screens(c->segments, c->seg_screens, c->seg_sys_info, + c->seg_pinned_display_id); + } + } + } + if (comp_d3d12_segments_enabled(c->segments)) { + const struct comp_seg_rect canvas = {0, 0, win.w, win.h}; + const uint32_t mode_index = (c->xdev != nullptr && c->xdev->hmd != nullptr) + ? c->xdev->hmd->active_rendering_mode_index + : 0; + seg_split = comp_d3d12_segments_update(c->segments, &win, &canvas, mode_index); + // Every segment DP follows the session's 2D/3D mode, like the primary. + comp_d3d12_segments_set_display_mode(c->segments, c->hardware_display_3d); + } + } + + // Multi-screen M3: publish the table this weave took. 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, win.w, win.h}; + (void)comp_d3d12_segments_get_metrics(c->segments, &win, &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; + } + *out_window = win; + return seg_split; +} + static xrt_result_t d3d12_dp_weave_and_present(struct comp_d3d12_compositor *c, bool is_repaint, ID3D12Resource **out_back_buffer) { @@ -4059,16 +4253,48 @@ d3d12_dp_weave_and_present(struct comp_d3d12_compositor *c, bool is_repaint, ID3 xrt_display_processor_d3d12_set_predicted_scanout( c->display_processor, comp_d3d12_target_predict_weave_to_scanout_ns(c->target)); - // Pass actual backbuffer dimensions to the DP. Canvas offset and size are - // passed separately — the DP uses them to set a viewport sub-rect for - // correct interlacing phase. - xrt_display_processor_d3d12_process_atlas( - c->display_processor, wl, dp_resource, - d3d12_bind_dp_atlas_srv(c, dp_resource), // #854: real SRV — sim DP binds it; SR weaver ignores it - rtv_handle.ptr, back_buffer, weave_view_w, weave_view_h, weave_cols, weave_rows, - static_cast(DXGI_FORMAT_R8G8B8A8_UNORM), tgt_width, tgt_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): one DP per screen a spanning window covers. + // Only on the runtime's own atlas — never the app's swapchain image + // (zero-copy), which this path must not crop. + struct comp_seg_rect seg_window = {0, 0, 0, 0}; + const bool atlas_is_ours = + dp_resource != nullptr && c->renderer != nullptr && + c->repaint.atlas == static_cast(comp_d3d12_renderer_get_atlas_resource(c->renderer)); + const bool seg_split = d3d12_segments_tick(c, atlas_is_ours, &eff_canvas, tgt_width, tgt_height, &seg_window); + + if (seg_split) { + struct comp_d3d12_segments_frame sf = {}; + sf.cmd_list = wl; + sf.src_resource = dp_resource; + sf.view_width = weave_view_w; + sf.view_height = weave_view_h; + sf.tile_columns = weave_cols; + sf.tile_rows = weave_rows; + sf.src_format = static_cast(DXGI_FORMAT_R8G8B8A8_UNORM); + sf.target_resource = back_buffer; + sf.target_rtv = rtv_handle.ptr; + sf.target_width = tgt_width; + sf.target_height = tgt_height; + sf.canvas.x = 0; + sf.canvas.y = 0; + sf.canvas.w = seg_window.w; + sf.canvas.h = seg_window.h; + sf.transparent_background = c->transparent_background; + sf.primary_dp = c->display_processor; + sf.renderer = c->renderer; + comp_d3d12_segments_record(c->segments, &sf); + } else { + // Pass actual backbuffer dimensions to the DP. Canvas offset and size are + // passed separately — the DP uses them to set a viewport sub-rect for + // correct interlacing phase. + xrt_display_processor_d3d12_process_atlas( + c->display_processor, wl, dp_resource, + d3d12_bind_dp_atlas_srv(c, dp_resource), // #854: real SRV — sim DP binds it; SR weaver ignores it + rtv_handle.ptr, back_buffer, weave_view_w, weave_view_h, weave_cols, weave_rows, + static_cast(DXGI_FORMAT_R8G8B8A8_UNORM), tgt_width, tgt_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 at @@ -4709,6 +4935,8 @@ d3d12_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_d3d12_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). + d3d12_route_effective_layout(c, d3d12_projection_layer_view_count(&c->layer_accum)); /* * Zero-copy check: can we pass the app's swapchain directly to the DP? @@ -4827,6 +5055,13 @@ d3d12_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_resource = nullptr; + } + // Reset command allocator and command list c->cmd_allocator->Reset(); c->cmd_list->Reset(c->cmd_allocator, nullptr); @@ -5782,6 +6017,14 @@ d3d12_compositor_destroy(struct xrt_compositor *xc) comp_xbridge_destroy(&c->xbridge); } + // Multi-screen M6: segment DPs + crop textures, before the primary DP (the + // repaint thread is joined and the GPU idle, so no weave is in flight). + comp_d3d12_segments_destroy(&c->segments); + free(c->seg_screens); + c->seg_screens = nullptr; + free(c->seg_sys_info); + c->seg_sys_info = nullptr; + // Destroy DP input crop resource if (c->dp_input_resource != nullptr) { c->dp_input_resource->Release(); @@ -6622,6 +6865,81 @@ comp_d3d12_compositor_request_display_mode(struct xrt_compositor *xc, bool enabl return xrt_display_processor_d3d12_request_display_mode(c->display_processor, enable_3d); } +extern "C" void +comp_d3d12_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_d3d12_compositor *c = d3d12_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 (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; + // ...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_d3d12_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_d3d12_compositor *c = d3d12_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_d3d12_get_predicted_eye_positions(c->display_processor, &s->eyes) && + s->eyes.valid; + } else { + s->have_eyes = comp_d3d12_segments_get_eyes(c->segments, s->screen_id, &s->eyes); + } + } + return out->count > 0; +} + +extern "C" void +comp_d3d12_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing) +{ + if (xc == nullptr || routing == nullptr) { + return; + } + struct comp_d3d12_compositor *c = d3d12_comp(xc); + std::lock_guard seg_lock(c->seg_pub_mutex); + c->seg_route = *routing; +} + extern "C" void comp_d3d12_compositor_set_eye_tracking_mode(struct xrt_compositor *xc, uint32_t mode) { diff --git a/src/xrt/compositor/d3d12/comp_d3d12_compositor.h b/src/xrt/compositor/d3d12/comp_d3d12_compositor.h index 2cab6f31b2..7339e5d9b9 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_compositor.h +++ b/src/xrt/compositor/d3d12/comp_d3d12_compositor.h @@ -181,6 +181,46 @@ comp_d3d12_compositor_set_content_mask(struct xrt_compositor *xc, bool comp_d3d12_compositor_request_display_mode(struct xrt_compositor *xc, bool enable_3d); +/*! + * Multi-screen M6 (ADR-047 D2 on Windows, D3D12): 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. The D3D12 twin of comp_d3d11_compositor_set_screens. + * + * @ingroup comp_d3d12 + */ +struct xrt_screen_list; +void +comp_d3d12_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); + +/*! + * Multi-screen M3 (Windows D3D12): 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_d3d12 + */ +bool +comp_d3d12_compositor_get_segment_metrics(struct xrt_compositor *xc, struct xrt_segment_metrics *out); + +/*! + * Multi-screen M3 (Windows D3D12): 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_d3d12 + */ +void +comp_d3d12_compositor_set_view_routing(struct xrt_compositor *xc, const struct xrt_segment_view_routing *routing); + /*! * Select the eye-tracking control mode (MANAGED=0 / MANUAL=1) on the display * processor — the policy counterpart to @ref diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp index 729bfa3f94..f04bd06670 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp @@ -1347,6 +1347,88 @@ render_window_space_layer(struct comp_d3d12_renderer *r, } +/*! + * Multi-screen M3: the end of the last routed view range (0 = unrouted). + */ +static uint32_t +routed_view_end(const struct comp_d3d12_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; +} + +/*! + * Multi-screen M3: how many draws tile @p vi takes — one per routed segment + * under routing, else one. + */ +static uint32_t +tile_draw_count(const struct comp_d3d12_eff_layout *layout) +{ + return layout->route_count > 0 ? layout->route_count : 1u; +} + +/*! + * Multi-screen M3: draw @p k of tile @p vi — which layer view it shows and + * where, in target px. Unrouted: the tile's own view at the tile box + * (@p vp_w × @p vp_h, the mono clamp applied). Routed: segment k's local view + * @p vi at the segment's rect inside the tile. False = nothing to draw (the + * segment has fewer views, or an empty rect). + */ +static bool +tile_draw(const struct comp_d3d12_eff_layout *layout, + uint32_t vi, + uint32_t k, + uint32_t vp_w, + uint32_t vp_h, + uint32_t *out_view, + D3D12_VIEWPORT *out_vp, + D3D12_RECT *out_scissor) +{ + const uint32_t tile_x = (vi % layout->cols) * layout->tile_w; + const uint32_t tile_y = (vi / layout->cols) * layout->tile_h; + D3D12_VIEWPORT vp = {}; + vp.TopLeftX = static_cast(tile_x); + vp.TopLeftY = static_cast(tile_y); + vp.Width = static_cast(vp_w); + vp.Height = static_cast(vp_h); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + D3D12_RECT sc = {}; + sc.left = tile_x; + sc.top = tile_y; + sc.right = tile_x + vp_w; + sc.bottom = tile_y + vp_h; + uint32_t view = vi; + if (layout->route_count > 0) { + if (k >= layout->route_count || k >= XRT_MAX_SEGMENTS || vi >= layout->route[k].view_count || + layout->route[k].w == 0 || layout->route[k].h == 0) { + return false; + } + view = layout->route[k].first_view + vi; + vp.TopLeftX += static_cast(layout->route[k].x); + vp.TopLeftY += static_cast(layout->route[k].y); + vp.Width = static_cast(layout->route[k].w); + vp.Height = static_cast(layout->route[k].h); + sc.left = static_cast(tile_x) + layout->route[k].x; + sc.top = static_cast(tile_y) + layout->route[k].y; + sc.right = sc.left + static_cast(layout->route[k].w); + sc.bottom = sc.top + static_cast(layout->route[k].h); + } + if (view >= XRT_MAX_VIEWS) { + return false; + } + *out_view = view; + *out_vp = vp; + *out_scissor = sc; + return true; +} + /*! * Render one `XrCompositionLayerQuad` into every view tile it is visible in * (#1581). Ported from the D3D11 reference render_quad_layer(), and sharing @@ -1394,10 +1476,14 @@ render_quad_layer(struct comp_d3d12_renderer *r, * Resolved up front so a quad no view can see costs no barrier and no * descriptor slot. */ + // Multi-screen M3: under routing every routed view (camera) is a + // candidate, framed against the whole routed view count. + const uint32_t routed_end = routed_view_end(layout); + const uint32_t vis_count = routed_end > view_count ? routed_end : view_count; bool visible[XRT_MAX_VIEWS] = {}; bool any_visible = false; - for (uint32_t vi = 0; vi < view_count && vi < XRT_MAX_VIEWS; vi++) { - visible[vi] = is_layer_view_visible_n(data, vi, view_count) && + for (uint32_t vi = 0; vi < vis_count && vi < XRT_MAX_VIEWS; vi++) { + visible[vi] = is_layer_view_visible_n(data, vi, vis_count) && comp_layer_quad_is_front_facing(&q->pose, &cameras[vi].pose.position); any_visible = any_visible || visible[vi]; } @@ -1494,8 +1580,15 @@ render_quad_layer(struct comp_d3d12_renderer *r, } } - for (uint32_t vi = 0; vi < view_count && vi < XRT_MAX_VIEWS; vi++) { - if (!visible[vi]) { + // Multi-screen M3: one draw per tile, or one per routed segment per tile + // (draw d = tile ti, segment d % n — view vi at its rect). + const uint32_t draws_per_tile = tile_draw_count(layout); + for (uint32_t d = 0; d < view_count * draws_per_tile && d / draws_per_tile < XRT_MAX_VIEWS; d++) { + const uint32_t ti = d / draws_per_tile; + uint32_t vi = 0; + D3D12_VIEWPORT vp = {}; + D3D12_RECT scissor = {}; + if (!tile_draw(layout, ti, d % draws_per_tile, vp_w, vp_h, &vi, &vp, &scissor) || !visible[vi]) { continue; } @@ -1532,7 +1625,7 @@ render_quad_layer(struct comp_d3d12_renderer *r, * SHADER emits: the fold below is half of that mode, the * blending-off pipeline state the other. */ - const enum comp_layer_blend_mode mode = comp_layer_subrect_blend_mode(&tiles[vi], data->flags); + const enum comp_layer_blend_mode mode = comp_layer_subrect_blend_mode(&tiles[ti], data->flags); comp_layer_blend_fold_opaque_cover(mode, color_scale, color_bias); const float array_params[4] = {layered ? static_cast(q->sub.array_index) : 0.0f, 0.0f, 0.0f, @@ -1545,23 +1638,7 @@ render_quad_layer(struct comp_d3d12_renderer *r, cmd_list->SetGraphicsRoot32BitConstants(1, 4, color_bias, 24); cmd_list->SetGraphicsRoot32BitConstants(1, 4, array_params, 28); - const uint32_t tile_x = (vi % layout->cols) * layout->tile_w; - const uint32_t tile_y = (vi / layout->cols) * layout->tile_h; - - D3D12_VIEWPORT vp = {}; - vp.TopLeftX = static_cast(tile_x); - vp.TopLeftY = static_cast(tile_y); - vp.Width = static_cast(vp_w); - vp.Height = static_cast(vp_h); - vp.MinDepth = 0.0f; - vp.MaxDepth = 1.0f; cmd_list->RSSetViewports(1, &vp); - - D3D12_RECT scissor = {}; - scissor.left = tile_x; - scissor.top = tile_y; - scissor.right = tile_x + vp_w; - scissor.bottom = tile_y + vp_h; cmd_list->RSSetScissorRects(1, &scissor); cmd_list->DrawInstanced(4, 1, 0, 0); @@ -1636,10 +1713,14 @@ render_equirect2_layer(struct comp_d3d12_renderer *r, * and applying it here would drop a viewer standing INSIDE the sphere, * which is the ordinary case for a 360 background. */ + // Multi-screen M3: under routing every routed view (camera) is a + // candidate, framed against the whole routed view count. + const uint32_t routed_end = routed_view_end(layout); + const uint32_t vis_count = routed_end > view_count ? routed_end : view_count; bool visible[XRT_MAX_VIEWS] = {}; bool any_visible = false; - for (uint32_t vi = 0; vi < view_count && vi < XRT_MAX_VIEWS; vi++) { - visible[vi] = is_layer_view_visible_n(data, vi, view_count); + for (uint32_t vi = 0; vi < vis_count && vi < XRT_MAX_VIEWS; vi++) { + visible[vi] = is_layer_view_visible_n(data, vi, vis_count); any_visible = any_visible || visible[vi]; } if (!any_visible) { @@ -1732,8 +1813,15 @@ render_equirect2_layer(struct comp_d3d12_renderer *r, } } - for (uint32_t vi = 0; vi < view_count && vi < XRT_MAX_VIEWS; vi++) { - if (!visible[vi]) { + // Multi-screen M3: one draw per tile, or one per routed segment per tile + // (draw d = tile ti, segment d % n — view vi at its rect). + const uint32_t draws_per_tile = tile_draw_count(layout); + for (uint32_t d = 0; d < view_count * draws_per_tile && d / draws_per_tile < XRT_MAX_VIEWS; d++) { + const uint32_t ti = d / draws_per_tile; + uint32_t vi = 0; + D3D12_VIEWPORT vp = {}; + D3D12_RECT scissor = {}; + if (!tile_draw(layout, ti, d % draws_per_tile, vp_w, vp_h, &vi, &vp, &scissor) || !visible[vi]) { continue; } @@ -1783,7 +1871,7 @@ render_equirect2_layer(struct comp_d3d12_renderer *r, * covers and nowhere else, which is what "the layer's alpha is one" * means. */ - const enum comp_layer_blend_mode mode = comp_layer_subrect_blend_mode(&tiles[vi], data->flags); + const enum comp_layer_blend_mode mode = comp_layer_subrect_blend_mode(&tiles[ti], data->flags); comp_layer_blend_fold_opaque_cover(mode, constants.color_scale, constants.color_bias); // The spec says +INFINITY for "as far away as possible"; the shader @@ -1801,23 +1889,7 @@ render_equirect2_layer(struct comp_d3d12_renderer *r, // there is no per-field offset table to keep in step with the HLSL. cmd_list->SetGraphicsRoot32BitConstants(1, kEquirect2RootConstants, &constants, 0); - const uint32_t tile_x = (vi % layout->cols) * layout->tile_w; - const uint32_t tile_y = (vi / layout->cols) * layout->tile_h; - - D3D12_VIEWPORT vp = {}; - vp.TopLeftX = static_cast(tile_x); - vp.TopLeftY = static_cast(tile_y); - vp.Width = static_cast(vp_w); - vp.Height = static_cast(vp_h); - vp.MinDepth = 0.0f; - vp.MaxDepth = 1.0f; cmd_list->RSSetViewports(1, &vp); - - D3D12_RECT scissor = {}; - scissor.left = tile_x; - scissor.top = tile_y; - scissor.right = tile_x + vp_w; - scissor.bottom = tile_y + vp_h; cmd_list->RSSetScissorRects(1, &scissor); // Fullscreen strip, 4 vertices — the viewport restricts it to the tile. @@ -2982,8 +3054,13 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_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 > view_count ? routed_end : view_count; struct comp_layer_view_camera cameras[XRT_MAX_VIEWS] = {}; - for (uint32_t view = 0; view < view_count && 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, view_count, have_wm ? &canvas_center : nullptr, have_wm ? canvas->window_width_m : 0.0f, have_wm ? canvas->window_height_m : 0.0f, &cameras[view]); @@ -3072,7 +3149,39 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_renderer *renderer, uint32_t layer_view_count = layer->data.view_count; - for (uint32_t vi = 0; vi < layer_view_count && vi < view_count; vi++) { + // Multi-screen M3: a routed frame paints each tile as a mosaic — one + // draw per segment, layer view first_view[k] + ti at the segment's + // rect (zones are never routed). Unrouted: one draw, view ti. + const bool routed = layout->route_count > 0 && !is_zone; + const uint32_t draws_per_tile = routed ? layout->route_count : 1u; + const uint32_t tile_count = + routed ? view_count : (layer_view_count < view_count ? layer_view_count : view_count); + struct comp_layer_tile_state *tile = nullptr; + enum comp_layer_blend_mode proj_mode = COMP_LAYER_BLEND_REPLACE; + for (uint32_t d = 0; d < tile_count * draws_per_tile; d++) { + const uint32_t ti = d / draws_per_tile; + const uint32_t dk = d % draws_per_tile; + if (dk == 0) { + // #1598 for the tile, decided ONCE before its draws (see + // below): every segment of a tile's first projection layer + // is that tile's base. + tile = ti < XRT_MAX_VIEWS ? &tiles[ti] : nullptr; + proj_mode = COMP_LAYER_BLEND_REPLACE; + if (!is_zone) { + proj_mode = comp_layer_tile_blend_mode(tile, layer->data.flags); + } + } + uint32_t vi = ti; + if (routed) { + if (dk >= XRT_MAX_SEGMENTS || ti >= layout->route[dk].view_count || + layout->route[dk].w == 0 || layout->route[dk].h == 0) { + continue; + } + vi = layout->route[dk].first_view + ti; + if (vi >= layer_view_count || vi >= XRT_MAX_VIEWS) { + continue; + } + } /* * #1598: resolved BEFORE the swapchain lookups below, so * the mark is EAGER — a projection layer that fails to @@ -3096,12 +3205,6 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_renderer *renderer, * unflagged zone must stay PREMULTIPLIED so its texture * alpha survives). */ - struct comp_layer_tile_state *tile = vi < XRT_MAX_VIEWS ? &tiles[vi] : nullptr; - enum comp_layer_blend_mode proj_mode = COMP_LAYER_BLEND_REPLACE; - if (!is_zone) { - proj_mode = comp_layer_tile_blend_mode(tile, layer->data.flags); - } - struct xrt_swapchain *xsc = layer->sc_array[vi]; if (xsc == nullptr) { continue; @@ -3224,7 +3327,7 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_renderer *renderer, gpu_srv.ptr += renderer->srv_descriptor_size * srv_slot; // Tile position in the effective atlas grid (#542) - uint32_t tile_idx = vi; + uint32_t tile_idx = ti; uint32_t tile_x = (tile_idx % layout->cols) * layout->tile_w; uint32_t tile_y = (tile_idx / layout->cols) * layout->tile_h; @@ -3316,13 +3419,24 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_renderer *renderer, cmd_list->SetPipelineState(proj_pso); } - cmd_list->RSSetViewports(1, &vp); - D3D12_RECT scissor = {}; scissor.left = tile_x; scissor.top = tile_y; scissor.right = tile_x + vp_w; scissor.bottom = tile_y + vp_h; + if (routed) { + // Multi-screen M3: this segment's rect inside the tile. + vp.TopLeftX += static_cast(layout->route[dk].x); + vp.TopLeftY += static_cast(layout->route[dk].y); + vp.Width = static_cast(layout->route[dk].w); + vp.Height = static_cast(layout->route[dk].h); + scissor.left = static_cast(tile_x) + layout->route[dk].x; + scissor.top = static_cast(tile_y) + layout->route[dk].y; + scissor.right = scissor.left + static_cast(layout->route[dk].w); + scissor.bottom = scissor.top + static_cast(layout->route[dk].h); + } + + cmd_list->RSSetViewports(1, &vp); cmd_list->RSSetScissorRects(1, &scissor); if (draw_log && is_zone) { @@ -3500,6 +3614,9 @@ comp_d3d12_renderer_compute_effective_layout(struct comp_d3d12_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) { out_layout->cols = 1; out_layout->rows = 1; @@ -3930,3 +4047,64 @@ comp_d3d12_renderer_flatten_local_2d(struct comp_d3d12_renderer *renderer, return XRT_SUCCESS; } + + +extern "C" xrt_result_t +comp_d3d12_renderer_blit_rect(struct comp_d3d12_renderer *renderer, + void *cmd_list_ptr, + void *srv_heap, + uint64_t src_srv, + uint32_t src_tex_w, + uint32_t src_tex_h, + int32_t src_x, + int32_t src_y, + uint32_t src_w, + uint32_t src_h, + uint64_t rtv, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h) +{ + auto *cmd_list = static_cast(cmd_list_ptr); + auto *heap = static_cast(srv_heap); + if (renderer == nullptr || cmd_list == nullptr || heap == nullptr || src_srv == 0 || rtv == 0 || + src_tex_w == 0 || src_tex_h == 0 || src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 || + renderer->blit_pso[0] == nullptr) { + return XRT_ERROR_D3D12; + } + + ID3D12DescriptorHeap *heaps[] = {heap}; + cmd_list->SetDescriptorHeaps(1, heaps); + cmd_list->SetGraphicsRootSignature(renderer->root_signature); + // Index 0 = the R8G8B8A8_UNORM pipeline, the DXGI back buffer's format. + cmd_list->SetPipelineState(renderer->blit_pso[0]); + D3D12_CPU_DESCRIPTOR_HANDLE rtv_handle; + rtv_handle.ptr = static_cast(rtv); + cmd_list->OMSetRenderTargets(1, &rtv_handle, FALSE, nullptr); + cmd_list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + D3D12_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.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + cmd_list->RSSetViewports(1, &vp); + D3D12_RECT sc = {dst_x, dst_y, dst_x + static_cast(dst_w), dst_y + static_cast(dst_h)}; + cmd_list->RSSetScissorRects(1, &sc); + + D3D12_GPU_DESCRIPTOR_HANDLE gpu; + gpu.ptr = src_srv; + cmd_list->SetGraphicsRootDescriptorTable(0, gpu); + const float src_rect[4] = { + static_cast(src_x) / static_cast(src_tex_w), + static_cast(src_y) / static_cast(src_tex_h), + static_cast(src_w) / static_cast(src_tex_w), + static_cast(src_h) / static_cast(src_tex_h), + }; + cmd_list->SetGraphicsRoot32BitConstants(1, 4, src_rect, 0); + cmd_list->DrawInstanced(4, 1, 0, 0); + return XRT_SUCCESS; +} diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.h b/src/xrt/compositor/d3d12/comp_d3d12_renderer.h index dcaa577ac4..9a3d6a41a2 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.h +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.h @@ -41,6 +41,27 @@ struct comp_d3d12_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 D3D12, the twin of comp_d3d11_eff_layout's): + * 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_d3d12_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_d3d12_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]; }; /*! @@ -164,6 +185,43 @@ comp_d3d12_renderer_draw_window_space_pass(struct comp_d3d12_renderer *renderer, uint32_t target_height, const struct comp_d3d12_eff_layout *layout); +/*! + * Multi-screen M6: linearly scale one rect of a texture into one rect of a + * render target — the flat-2D fill of a segment no DP can weave (and of the + * canvas no screen covers), the D3D12 twin of comp_d3d11_renderer_blit_rect. + * Opaque, no colour transform, through the renderer's UNORM blit pipeline + * (the DXGI back buffer's format). Records onto @p cmd_list: binds + * @p srv_heap, the root signature + pipeline, @p rtv, viewport + scissor = + * @p dst; the caller re-binds whatever it needs afterwards. The source must + * already be in PIXEL_SHADER_RESOURCE. + * + * @param cmd_list ID3D12GraphicsCommandList*. + * @param srv_heap ID3D12DescriptorHeap* holding @p src_srv (shader-visible). + * @param src_srv The source's SRV, D3D12_GPU_DESCRIPTOR_HANDLE::ptr. + * @param src_tex_w,src_tex_h The source texture's size. + * @param src_x,src_y,src_w,src_h Source rect in source texels. + * @param rtv D3D12_CPU_DESCRIPTOR_HANDLE::ptr of the destination. + * @param dst_x,dst_y,dst_w,dst_h Destination rect in target pixels. + * + * @ingroup comp_d3d12 + */ +xrt_result_t +comp_d3d12_renderer_blit_rect(struct comp_d3d12_renderer *renderer, + void *cmd_list, + void *srv_heap, + uint64_t src_srv, + uint32_t src_tex_w, + uint32_t src_tex_h, + int32_t src_x, + int32_t src_y, + uint32_t src_w, + uint32_t src_h, + uint64_t rtv, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h); + /*! * Get the atlas texture SRV GPU descriptor handle for weaving. * diff --git a/src/xrt/compositor/d3d12/comp_d3d12_segments.cpp b/src/xrt/compositor/d3d12/comp_d3d12_segments.cpp new file mode 100644 index 0000000000..78f8246c2a --- /dev/null +++ b/src/xrt/compositor/d3d12/comp_d3d12_segments.cpp @@ -0,0 +1,841 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen segment display processors for the in-process D3D12 + * compositor (multi-screen M6, ADR-047 D2 on Windows). + * @ingroup comp_d3d12 + * + * Mirrors d3d11/comp_d3d11_segments.cpp: 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 is D3D12. What D3D12 changes: + * + * - Everything is RECORDED onto the compositor's one weave command list, so a + * descriptor cannot be rewritten between two DPs that read it: each screen + * owns its own slot in this module's shader-visible SRV heap (plus one slot + * for the flat-2D source). + * - A command list carries no viewport/scissor a DP could inherit — every DP + * sets both from the canvas it is handed (the screen-bound contract on + * `create_dp_d3d12_for_screen`). The module still sets them per segment so a + * DP that only sets one of the two stays confined. + * - Resource states are explicit: the crops and the source go through + * COMMON → COPY_* → COMMON, the DP reads its crop in COMMON exactly like the + * primary path hands it the atlas (#747: never leave a promoted state behind + * a barrier the caller declares from COMMON). + * - No deferred-release list: every weave ends in a full queue wait, so the + * next weave's update may release whatever the last one used. + */ + +#include "comp_d3d12_segments.h" + +#include "comp_d3d12_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. + * + */ + +//! Descriptor slots: one per screen's crop, then one for the flat-2D source. +#define SEG_SRV_SLOT_FLAT COMP_SEGMENTS_MAX_SCREENS +#define SEG_SRV_SLOTS (COMP_SEGMENTS_MAX_SCREENS + 1) + +//! One per-screen crop texture: the atlas tiles cut down to one segment. +struct seg_crop +{ + ID3D12Resource *texture; //!< plain 2D, COMMON between frames + uint32_t w, h; + DXGI_FORMAT format; +}; + +//! Per-screen runtime state (indexed like comp_d3d12_segments::screens). +struct seg_screen_state +{ + struct xrt_display_processor_d3d12 *dp; //!< NULL until the lifecycle creates it. + bool configured; //!< set_background_2d(NULL) sent once. + 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_d3d12_segments +{ + ID3D12Device *device; + ID3D12CommandQueue *queue; + + //! Shader-visible SRV heap, @ref SEG_SRV_SLOTS slots. + ID3D12DescriptorHeap *srv_heap; + UINT srv_inc; + + bool enabled; + 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]; + + 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_d3d12_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->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, in COMMON. +static bool +crop_ensure(ID3D12Device *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); + D3D12_RESOURCE_DESC desc = {}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = w; + desc.Height = h; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = format; + desc.SampleDesc.Count = 1; + desc.Flags = D3D12_RESOURCE_FLAG_NONE; + D3D12_HEAP_PROPERTIES heap = {}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + HRESULT hr = dev->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, + nullptr, IID_PPV_ARGS(&c->texture)); + if (FAILED(hr)) { + U_LOG_E("segments: D3D12 crop texture %ux%u failed: 0x%lx", w, h, hr); + c->texture = nullptr; + return false; + } + c->texture->SetName(L"DXR.segment_crop"); // #747: debug-layer attribution + c->w = w; + c->h = h; + c->format = format; + return true; +} + +//! Write an SRV for @p res into heap slot @p slot; returns its GPU handle. +static uint64_t +write_srv(struct comp_d3d12_segments *segs, uint32_t slot, ID3D12Resource *res) +{ + const D3D12_RESOURCE_DESC rd = res->GetDesc(); + D3D12_SHADER_RESOURCE_VIEW_DESC sd = {}; + // The in-process DP samples raw bytes (ADR-021 Model A), the same choice + // the compositor's own DP-input SRV makes; a view cannot be typeless. + sd.Format = d3d_dxgi_format_to_unorm_sample(rd.Format); + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MipLevels = 1; + sd.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + D3D12_CPU_DESCRIPTOR_HANDLE cpu = segs->srv_heap->GetCPUDescriptorHandleForHeapStart(); + cpu.ptr += (SIZE_T)segs->srv_inc * slot; + segs->device->CreateShaderResourceView(res, &sd, cpu); + D3D12_GPU_DESCRIPTOR_HANDLE gpu = segs->srv_heap->GetGPUDescriptorHandleForHeapStart(); + gpu.ptr += (UINT64)segs->srv_inc * slot; + return gpu.ptr; +} + +static void +transition(ID3D12GraphicsCommandList *cl, ID3D12Resource *res, D3D12_RESOURCE_STATES from, D3D12_RESOURCE_STATES to) +{ + D3D12_RESOURCE_BARRIER b = {}; + b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + b.Transition.pResource = res; + b.Transition.StateBefore = from; + b.Transition.StateAfter = to; + b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + cl->ResourceBarrier(1, &b); +} + +static void +dp_release(struct comp_d3d12_segments *segs, uint32_t i) +{ + struct seg_screen_state *st = &segs->st[i]; + if (st->dp != nullptr) { + U_LOG_I("segments: retiring the D3D12 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_d3d12 *dp = st->dp; + st->dp = nullptr; + os_mutex_unlock(&segs->dp_mutex); + xrt_display_processor_d3d12_destroy(&dp); + } + st->configured = false; + 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_d3d12`, for the reason given on + * xrt_plugin_iface::create_dp_vk_for_screen. + */ +static bool +dp_create(struct comp_d3d12_segments *segs, uint32_t i) +{ + 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_d3d12 *dp = nullptr; + if (segs->screens[i].has_dp_factory && xrt_plugin_iface_has_create_dp_d3d12_for_screen(iface)) { + xret = iface->create_dp_d3d12_for_screen(segs->inst[i], segs->device, segs->queue, nullptr, + &segs->bindings[i], &dp); + } + if (xret != XRT_SUCCESS || dp == nullptr || dp->process_atlas == nullptr) { + if (dp != nullptr) { + xrt_display_processor_d3d12_destroy(&dp); + } + U_LOG_W( + "segments: could not create a DP for screen 0x%016llx ('%s', plug-in '%s') via " + "create_dp_d3d12_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; + } + // Same post-create handshake the compositor gives its primary DP + // (d3d12_make_dp): the output format (the DXGI back buffer's) and + // "the runtime presents the full target". + xrt_display_processor_d3d12_set_output_format(dp, (uint32_t)DXGI_FORMAT_R8G8B8A8_UNORM); + xrt_display_processor_d3d12_set_shared_texture_present(dp, false); + + os_mutex_lock(&segs->dp_mutex); + st->dp = dp; + os_mutex_unlock(&segs->dp_mutex); + st->tolerates_resample = xrt_display_processor_d3d12_tolerates_resample(dp); + st->configured = false; + 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_d3d12_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(ID3D12GraphicsCommandList *cl, const struct comp_seg_rect *r) +{ + D3D12_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; + cl->RSSetViewports(1, &vp); + D3D12_RECT sc = {r->x, r->y, r->x + (LONG)r->w, r->y + (LONG)r->h}; + cl->RSSetScissorRects(1, &sc); +} + + +/* + * + * 'Exported' functions. + * + */ + +extern "C" struct comp_d3d12_segments * +comp_d3d12_segments_create(void *d3d12_device, void *d3d12_queue) +{ + if (d3d12_device == nullptr || d3d12_queue == nullptr) { + return nullptr; + } + struct comp_d3d12_segments *segs = U_TYPED_CALLOC(struct comp_d3d12_segments); + if (segs == nullptr) { + return nullptr; + } + segs->device = static_cast(d3d12_device); + segs->queue = static_cast(d3d12_queue); + + D3D12_DESCRIPTOR_HEAP_DESC hd = {}; + hd.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + hd.NumDescriptors = SEG_SRV_SLOTS; + hd.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + HRESULT hr = segs->device->CreateDescriptorHeap(&hd, IID_PPV_ARGS(&segs->srv_heap)); + if (FAILED(hr)) { + U_LOG_E("segments: D3D12 SRV heap failed: 0x%lx", hr); + free(segs); + return nullptr; + } + segs->srv_inc = segs->device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + + 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].transparent_latched = -1; + segs->st[i].mode_sent = -1; + } + return segs; +} + +extern "C" void +comp_d3d12_segments_destroy(struct comp_d3d12_segments **segs_ptr) +{ + if (segs_ptr == nullptr || *segs_ptr == nullptr) { + return; + } + struct comp_d3d12_segments *segs = *segs_ptr; + for (uint32_t i = 0; i < COMP_SEGMENTS_MAX_SCREENS; i++) { + dp_release(segs, i); + } + if (segs->srv_heap != nullptr) { + segs->srv_heap->Release(); + segs->srv_heap = nullptr; + } + os_mutex_destroy(&segs->dp_mutex); + free(segs); + *segs_ptr = nullptr; +} + +extern "C" void +comp_d3d12_segments_set_screens(struct comp_d3d12_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 + // D3D12 factory being set says the claim covers D3D12. + cs->has_dp_factory = e->dp_factory_d3d12 != nullptr && + xrt_plugin_iface_has_create_dp_d3d12_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); + } + segs->info[n] = s->info; + 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, D3D12)", + n, primary_entry->plugin_id); + } +} + +extern "C" bool +comp_d3d12_segments_enabled(const struct comp_d3d12_segments *segs) +{ + return segs != nullptr && segs->enabled; +} + +extern "C" bool +comp_d3d12_segments_update(struct comp_d3d12_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + uint32_t mode_index) +{ + if (segs == nullptr || !segs->enabled || window_desktop == nullptr) { + return false; + } + + comp_segments_compute(window_desktop, canvas, segs->screens, segs->screen_count, &segs->table); + + struct comp_segments_actions act; + comp_segments_lifecycle_update(&segs->lc, &segs->table, &act); + for (uint32_t a = 0; a < act.count; a++) { + const int i = screen_index_of(segs, act.items[a].screen_id); + if (i < 0) { + continue; + } + if (act.items[a].action == COMP_SEG_ACTION_CREATE) { + const bool ok = dp_create(segs, (uint32_t)i); + 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. 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_d3d12_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: D3D12 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: D3D12 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_d3d12_segments_record(struct comp_d3d12_segments *segs, const struct comp_d3d12_segments_frame *f) +{ + if (segs == nullptr || f == nullptr || f->cmd_list == nullptr || f->src_resource == nullptr || + f->target_rtv == 0 || f->tile_columns == 0 || f->tile_rows == 0 || f->view_width == 0 || + f->view_height == 0 || segs->srv_heap == nullptr) { + return false; + } + auto *cl = static_cast(f->cmd_list); + auto *src = static_cast(f->src_resource); + D3D12_CPU_DESCRIPTOR_HANDLE rtv; + rtv.ptr = (SIZE_T)f->target_rtv; + const struct comp_segment_table *t = &segs->table; + const D3D12_RESOURCE_DESC src_desc = src->GetDesc(); + + /* + * 1. Decide each segment: which DP (if any) and whether it may weave. + */ + struct xrt_display_processor_d3d12 *dp_of[COMP_SEGMENTS_MAX] = {}; + struct seg_screen_state *st_of[COMP_SEGMENTS_MAX] = {}; + int screen_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; + screen_of[k] = i; + 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}; + cl->ClearRenderTargetView(rtv, clear, 0, nullptr); + } + + /* + * 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. The + * source goes COMMON → COPY_SOURCE → COMMON around all of them. + */ + bool any_crop = false; + 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; + } + if (!any_crop) { + transition(cl, src, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE); + any_crop = true; + } + transition(cl, crop->texture, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST); + D3D12_TEXTURE_COPY_LOCATION dst_loc = {}; + dst_loc.pResource = crop->texture; + dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + D3D12_TEXTURE_COPY_LOCATION src_loc = {}; + src_loc.pResource = src; + src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + for (uint32_t row = 0; row < f->tile_rows; row++) { + for (uint32_t col = 0; col < f->tile_columns; col++) { + D3D12_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; + cl->CopyTextureRegion(&dst_loc, col * tile[k].w, row * tile[k].h, 0, &src_loc, &box); + } + } + transition(cl, crop->texture, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON); + } + if (any_crop) { + transition(cl, src, D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON); + } + + /* + * 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, its own crop + descriptor, the back + * buffer re-bound and viewport + scissor = the segment. + */ + ID3D12DescriptorHeap *heaps[] = {segs->srv_heap}; + 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_d3d12 *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_d3d12_set_background_2d(dp, nullptr, 0, 0); + } + if (st->transparent_latched != (int)f->transparent_background) { + st->transparent_latched = (int)f->transparent_background; + xrt_display_processor_d3d12_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_d3d12_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_d3d12_set_present_origin(dp, g->present_origin_x, + g->present_origin_y); + } + } + const uint64_t srv = write_srv(segs, (uint32_t)screen_of[k], st->crop.texture); + cl->SetDescriptorHeaps(1, heaps); + cl->OMSetRenderTargets(1, &rtv, FALSE, nullptr); + set_viewport_scissor(cl, &g->window_rect); + xrt_display_processor_d3d12_process_atlas( + dp, cl, st->crop.texture, srv, f->target_rtv, f->target_resource, 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: one view (the middle one — the left eye of a + * stereo pair), linearly scaled from the view tile to the window rect. + */ + 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); + const uint64_t srv = write_srv(segs, SEG_SRV_SLOT_FLAT, src); + transition(cl, src, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + 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_d3d12_renderer_blit_rect(f->renderer, cl, segs->srv_heap, srv, (uint32_t)src_desc.Width, + src_desc.Height, base_x + s.x, base_y + s.y, s.w, s.h, + f->target_rtv, d->x, d->y, d->w, d->h); + } + transition(cl, src, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COMMON); + } + + // Leave the back buffer bound, viewport + scissor = the whole target, the + // state the single-DP path's caller set before process_atlas. + cl->OMSetRenderTargets(1, &rtv, FALSE, nullptr); + { + const struct comp_seg_rect whole = {0, 0, f->target_width, f->target_height}; + set_viewport_scissor(cl, &whole); + } + return any_woven; +} + +extern "C" void +comp_d3d12_segments_set_display_mode(struct comp_d3d12_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_d3d12_request_display_mode(st->dp, enable_3d); + } + } +} + +extern "C" bool +comp_d3d12_segments_get_metrics(const struct comp_d3d12_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_d3d12_segments_get_eyes(struct comp_d3d12_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_d3d12_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/d3d12/comp_d3d12_segments.h b/src/xrt/compositor/d3d12/comp_d3d12_segments.h new file mode 100644 index 0000000000..038ddafe10 --- /dev/null +++ b/src/xrt/compositor/d3d12/comp_d3d12_segments.h @@ -0,0 +1,182 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen segment display processors for the in-process D3D12 + * compositor (multi-screen M6, ADR-047 D2 on Windows). + * @ingroup comp_d3d12 + * + * The D3D12 twin of d3d11/comp_d3d11_segments.h (itself the 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 plain 2D atlas holding exactly that segment's views, + * `canvas = segment rect`, and its own present origin; the results land in the + * one DXGI back buffer, all recorded onto the compositor's one weave command + * list. A segment with no DP for its screen (a plug-in without + * `create_dp_d3d12_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_d3d12_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. + * + * GPU lifetime: every weave of the in-process D3D12 compositor ends in a full + * wait on its queue, so by the time the next weave runs this module's update + * nothing it recorded is still in flight — retired DPs, crop textures and the + * descriptors are released or rewritten there without a deferred-release list. + * + * Design note: docs/architecture/comp-segments.md (§ Windows / D3D12). + */ +#pragma once + +#include "xrt/xrt_compiler.h" +#include "xrt/xrt_compositor.h" +#include "xrt/xrt_display_metrics.h" +#include "xrt/xrt_display_processor_d3d12.h" +#include "xrt/xrt_screen.h" +#include "util/comp_segments.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct comp_d3d12_segments; +struct comp_d3d12_renderer; + +/*! + * Create the segment manager. Only the small SRV descriptor heap is allocated + * up front; nothing else until a segment needs it. + * + * @param d3d12_device The device segment DPs and crop textures are created on + * (ID3D12Device*, borrowed). + * @param d3d12_queue The queue handed to new segment DPs + * (ID3D12CommandQueue*, borrowed). + */ +struct comp_d3d12_segments * +comp_d3d12_segments_create(void *d3d12_device, void *d3d12_queue); + +/*! + * Tear down every segment DP, crop texture and the descriptor heap. The caller + * guarantees no weave is in flight (the repaint thread is stopped and the GPU + * is idle). + */ +void +comp_d3d12_segments_destroy(struct comp_d3d12_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_d3d12_segments_set_screens(struct comp_d3d12_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_d3d12_segments_enabled(const struct comp_d3d12_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 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_d3d12_segments_record); false = the single-DP path. + */ +bool +comp_d3d12_segments_update(struct comp_d3d12_segments *segs, + const struct comp_seg_rect *window_desktop, + const struct comp_seg_rect *canvas, + uint32_t mode_index); + +//! Everything the split path needs about this frame. +struct comp_d3d12_segments_frame +{ + //! The open weave list (ID3D12GraphicsCommandList*). + void *cmd_list; + //! The DP's input this frame: the atlas cropped to content + //! (ID3D12Resource*, plain 2D, in COMMON on entry and on return). + void *src_resource; + 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 (ID3D12Resource*, in RENDER_TARGET), its RTV + //! (D3D12_CPU_DESCRIPTOR_HANDLE::ptr) and its size. + void *target_resource; + uint64_t 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 session's primary DP (weaves the primary screen's segment). The + //! caller has already fed it this frame's background and timing. + struct xrt_display_processor_d3d12 *primary_dp; + //! For the flat-2D fill (comp_d3d12_renderer_blit_rect). + struct comp_d3d12_renderer *renderer; +}; + +/*! + * Record the split frame onto @ref comp_d3d12_segments_frame::cmd_list: 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), and blit flat 2D + * into the segments that cannot be woven. Leaves the back buffer bound as the + * render target, viewport + scissor = the whole target. + * + * @return true if at least one DP wove. + */ +bool +comp_d3d12_segments_record(struct comp_d3d12_segments *segs, const struct comp_d3d12_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_d3d12_segments_set_display_mode(struct comp_d3d12_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_d3d12_segments_get_eyes(struct comp_d3d12_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_d3d12_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_d3d12_segments_get_metrics(const struct comp_d3d12_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/state_trackers/oxr/oxr_segment_views.h b/src/xrt/state_trackers/oxr/oxr_segment_views.h index e7e9b80794..a12a0872ff 100644 --- a/src/xrt/state_trackers/oxr/oxr_segment_views.h +++ b/src/xrt/state_trackers/oxr/oxr_segment_views.h @@ -51,6 +51,20 @@ oxr_segment_views_set_capacity(uint32_t screens_with_dp_factory, bool can_segmen return screens_with_dp_factory < XRT_MAX_SEGMENTS ? screens_with_dp_factory : XRT_MAX_SEGMENTS; } +/*! + * Does one DP-registry entry give its screen a display processor a Windows + * in-process compositor can segment with? Both the D3D11 and the D3D12 + * compositor segment windows (multi-screen M6), and the view-set capacity is + * per system, so a screen counts toward @ref oxr_segment_views_set_capacity + * when EITHER factory is set (the entry's `dp_factory_d3d11` / + * `dp_factory_d3d12`). + */ +static inline bool +oxr_segment_views_win_entry_has_dp(const void *dp_factory_d3d11, const void *dp_factory_d3d12) +{ + return dp_factory_d3d11 != NULL || dp_factory_d3d12 != NULL; +} + /*! * The view count `PRIMARY_MULTIVIEW_DXR` reports: the device max per view set, * times the system's set capacity (@ref oxr_segment_views_set_capacity), capped diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index b8161a668f..c36c76c18d 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -4172,11 +4172,11 @@ locate_views_one(struct oxr_logger *log, */ //! The compositor's segment table, when this session's compositor segments its -//! window (in-process Vulkan on desktop Linux, in-process D3D11 on Windows, the -//! D3D11 service for a Windows IPC session that enabled it — one round trip, -//! only on a box with two DP-backed screens). Everything else — the other -//! native compositors, every other service session — answers false and keeps -//! one view set. +//! window (in-process Vulkan on desktop Linux, in-process D3D11 / D3D12 on +//! Windows, the D3D11 service for a Windows IPC session that enabled it — one +//! round trip, only on a box with two DP-backed screens). Everything else — +//! the other native compositors, every other service session — answers false +//! and keeps one view set. static bool locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics *out) { @@ -4198,6 +4198,11 @@ locate_get_segment_metrics(struct oxr_session *sess, struct xrt_segment_metrics if (sess->xcn != NULL && sess->is_metal_native_compositor) { return comp_metal_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; } +#endif +#if defined(XRT_HAVE_D3D12_NATIVE_COMPOSITOR) && defined(XRT_OS_WINDOWS) + if (sess->xcn != NULL && sess->is_d3d12_native_compositor) { + return comp_d3d12_compositor_get_segment_metrics(&sess->xcn->base, out) && out->count > 0; + } #endif (void)sess; return false; @@ -6329,6 +6334,24 @@ oxr_session_create(struct oxr_logger *log, } #endif +#if defined(XRT_HAVE_D3D12_NATIVE_COMPOSITOR) && defined(XRT_OS_WINDOWS) + // Multi-screen M6: the same hand-off to the in-process D3D12 compositor + // (the Unity display provider's path) — per-segment DPs, windowless, phase + // from set_present_origin; the primary screen's DP keeps the real HWND. + if (sess->is_d3d12_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_d3d12_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. 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 6f02abda67..1e22466c39 100644 --- a/src/xrt/state_trackers/oxr/oxr_session_frame_end.c +++ b/src/xrt/state_trackers/oxr/oxr_session_frame_end.c @@ -3186,6 +3186,14 @@ oxr_session_frame_end(struct oxr_logger *log, struct oxr_session *sess, const Xr sess->seg_route_sent = route; } } +#if defined(XRT_HAVE_D3D12_NATIVE_COMPOSITOR) && defined(XRT_OS_WINDOWS) + // Multi-screen M3 on Windows: same hand-off to the in-process D3D12 compositor. + if (sess->is_d3d12_native_compositor && sess->xcn != NULL) { + struct xrt_segment_view_routing route; + oxr_session_take_segment_view_routing(sess, &route); + comp_d3d12_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 8b0fa06366..ee85f641ac 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -171,9 +171,9 @@ oxr_system_fill_in( /* * Multi-screen M3: advertise one view set per segment only where a * window can ever be split — the in-process Vulkan compositor on desktop - * Linux, the in-process D3D11 compositor or the D3D11 service on Windows, - * or the in-process Metal compositor on macOS, with at least two registry - * screens that have a DP factory. A single-screen box, a Linux service + * Linux, the in-process D3D11 / D3D12 compositors or the D3D11 service on + * Windows, or the in-process Metal compositor on macOS, with at least two + * registry screens that have a DP factory. A single-screen box, a Linux service * session and every other platform keep the pre-M3 count, so existing * multiview apps and the CTS see no change. */ @@ -194,13 +194,15 @@ 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. +#elif defined(XRT_OS_WINDOWS) && \ + (defined(XRT_HAVE_D3D11_NATIVE_COMPOSITOR) || defined(XRT_HAVE_D3D12_NATIVE_COMPOSITOR)) + // Multi-screen M3 on Windows: the in-process D3D11 and D3D12 + // compositors segment a window the same way (comp_d3d11_segments, + // comp_d3d12_segments); a registry D3D11 or D3D12 factory is what + // says a screen has a DP. The capacity is per system, so a GL / + // Vulkan session on the same box advertises the doubled count too + // and never splits — the tail views then repeat view 0, which the + // contract allows. // // Service sessions too (ADR-047 Amendment 3): the D3D11 service // segments a direct IPC client's window the same way. The registry @@ -215,7 +217,8 @@ oxr_system_fill_in( 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) { + if (oxr_segment_views_win_entry_has_dp(reg->entries[e].dp_factory_d3d11, + reg->entries[e].dp_factory_d3d12)) { with_factory++; } } diff --git a/tests/tests_oxr_segment_views.cpp b/tests/tests_oxr_segment_views.cpp index 3f8d0a9cc4..05c8d61a31 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 (macOS/Android, a non-D3D11 Windows session, a service session): 1. + // No segmenting compositor (macOS/Android, a GL / Vulkan 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. @@ -83,6 +83,29 @@ TEST_CASE("view-set capacity: only where a window can ever be split", "[oxr][seg CHECK(oxr_segment_views_set_capacity(5, true) == XRT_MAX_SEGMENTS); } +TEST_CASE("Windows: a screen counts toward the capacity with a D3D11 OR a D3D12 factory", "[oxr][segment_views]") +{ + int d3d11_factory = 0; + int d3d12_factory = 0; + // Multi-screen M6: both in-process Windows compositors segment, so either + // factory makes the screen DP-backed. + CHECK(oxr_segment_views_win_entry_has_dp(&d3d11_factory, &d3d12_factory)); + CHECK(oxr_segment_views_win_entry_has_dp(&d3d11_factory, nullptr)); + CHECK(oxr_segment_views_win_entry_has_dp(nullptr, &d3d12_factory)); + // A D3D12-only plug-in (no D3D11 factory) used to count zero screens. + CHECK_FALSE(oxr_segment_views_win_entry_has_dp(nullptr, nullptr)); + + // Two D3D12-only screens: two view sets, as two D3D11 screens give. + uint32_t with_factory = 0; + const void *entries[2][2] = {{nullptr, &d3d12_factory}, {nullptr, &d3d12_factory}}; + for (const auto &e : entries) { + if (oxr_segment_views_win_entry_has_dp(e[0], e[1])) { + with_factory++; + } + } + CHECK(oxr_segment_views_set_capacity(with_factory, true) == 2); +} + TEST_CASE("PRIMARY_MULTIVIEW_DXR reports one view set per possible segment", "[oxr][segment_views]") { // Capacity 1: exactly the pre-M3 count (the device max). From 8edce1e25079813a4f23a60696844d89e97cad25 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:25:30 -0700 Subject: [PATCH 3/4] feat(test-apps): cube_handle_d3d12_win renders per-segment views (multi-screen M3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of the cube_handle_d3d11_win M3 change: chain XrViewActivityStateDXR + XrViewDisplayBindingsDXR, render every view set the runtime hands out, each at its display's segment rect inside its tile, and log the bindings once per change ("view sets: ...") — the rig check's app-side proof. Co-Authored-By: Claude Fable 5.1 --- .../handle/cube_handle_d3d12_win/main.cpp | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/test_apps/handle/cube_handle_d3d12_win/main.cpp b/test_apps/handle/cube_handle_d3d12_win/main.cpp index 81353db15d..1ff2656d95 100644 --- a/test_apps/handle/cube_handle_d3d12_win/main.cpp +++ b/test_apps/handle/cube_handle_d3d12_win/main.cpp @@ -709,6 +709,19 @@ static void RenderThreadFunc( 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 display // rig with the app's tunables — the runtime owns the // window resolve and the Kooima math, and returns @@ -736,6 +749,45 @@ static void RenderThreadFunc( } 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). The D3D11 cube's + // logic, unchanged. + const bool routedViews = + !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; + } + } + // XR_DXR_view_rig raw-channel verification (#396 W7): // one-shot proof the raw channel reports the DP's full // per-view set. @@ -836,6 +888,39 @@ static void RenderThreadFunc( } } + // 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 (the D3D11 cube does exactly + // this). + struct ViewPlace { + uint32_t tile; + int32_t x, y; + uint32_t w, h; + bool set; + }; + ViewPlace viewPlace[8] = {}; + if (routedViews && windowW > 0 && windowH > 0) { + const float fx = (float)renderW / (float)windowW; + const float fy = (float)renderH / (float)windowH; + 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}; + } + } + } + uint32_t imageIndex; if (AcquireSwapchainImage(*xr, imageIndex)) { ID3D12Resource* swapchainTexture = (*swapchainImages)[imageIndex].texture; @@ -843,10 +928,24 @@ static void RenderThreadFunc( for (int eye = 0; eye < eyeCount; eye++) { // Tile-aware viewport positioning + 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; + } uint32_t vpX = tileX * renderW; uint32_t vpY = tileY * renderH; + uint32_t vpW = renderW; + uint32_t vpH = renderH; + if (placed) { + // Multi-screen M3: this view's display segment inside its tile. + vpX = (uint32_t)((int32_t)vpX + viewPlace[eye].x); + vpY = (uint32_t)((int32_t)vpY + viewPlace[eye].y); + vpW = viewPlace[eye].w; + vpH = viewPlace[eye].h; + } XMMATRIX viewMatrix, projMatrix; if (useAppProjection) { @@ -864,7 +963,7 @@ static void RenderThreadFunc( RenderScene(*renderer, swapchainTexture, rtvIdx, vpX, vpY, - renderW, renderH, + vpW, vpH, viewMatrix, projMatrix, useAppProjection ? 1.0f : inputSnapshot.viewParams.scaleFactor, eye == 0); // clear only on first eye @@ -874,7 +973,7 @@ static void RenderThreadFunc( projectionViews[eye].subImage.imageRect.offset = { (int32_t)vpX, (int32_t)vpY}; projectionViews[eye].subImage.imageRect.extent = { - (int32_t)renderW, (int32_t)renderH}; + (int32_t)vpW, (int32_t)vpH}; projectionViews[eye].subImage.imageArrayIndex = 0; int rawIdx = (eye < (int)viewCount) ? eye : 0; projectionViews[eye].pose = monoMode ? monoPose : rawViews[rawIdx].pose; From 9e0f41220feb1e8f7252f411273f2118f72c63f4 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 9 Oct 2026 00:25:30 -0700 Subject: [PATCH 4/4] docs(multi-screen): D3D12 segments status (comp-segments, roadmap M6) Co-Authored-By: Claude Fable 5.1 --- docs/architecture/comp-segments.md | 13 +++++++++++++ docs/roadmap/multi-screen.md | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/architecture/comp-segments.md b/docs/architecture/comp-segments.md index f47c42bd5d..73cff9ae8f 100644 --- a/docs/architecture/comp-segments.md +++ b/docs/architecture/comp-segments.md @@ -261,6 +261,19 @@ buffer, in order). What differs on macOS: - Test knob: `cube_handle_metal_macos` honours `DXR_TEST_WINDOW_RECT=x,y,w,h` (content rect, top-down points) and, once at frame `DXR_TEST_WINDOW_MOVE_FRAME` (default 300), `DXR_TEST_WINDOW_RECT2`. +## Windows / D3D12 (M6) + +Status: the in-process D3D12 compositor (the Unity display provider's path) carries the same +model in `d3d12/comp_d3d12_segments.{h,cpp}`, behaving like the pre-split D3D11 path: two more +appended slots, no ABI bump (`xrt_plugin_iface::create_dp_d3d12_for_screen`, +`xrt_display_processor_d3d12::set_present_origin`), every segment recorded onto the one weave +command list (crop per screen, each crop with its own SRV descriptor, the DP sets viewport + +scissor from the canvas itself), the same non-segmented cases (plus the #1264 reroute and a back +buffer that does not match the client area), M3 per-segment views through +`comp_d3d12_compositor_get_segment_metrics` / `set_view_routing` and a mosaic in the D3D12 +renderer, and `oxr_system` counting a D3D11 **or** D3D12 factory toward the view-set capacity. +sim_display implements both D3D12 slots. Not yet run on hardware. + ## 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 52c2a81c0e..195e379498 100644 --- a/docs/roadmap/multi-screen.md +++ b/docs/roadmap/multi-screen.md @@ -208,7 +208,7 @@ multiview + display_info v22. An app may also pin itself to one display | **M3** Per-segment views | Same window: `xrLocateViews` returns 4 views, each half rendered from its own frustum | oxr: per-segment Kooima + eyes from the segment DP; `XrViewDisplayBindingsDXR`; `activeViewCount`; tile routing by segment; legacy stereo = majority segment + 2D blit | none | cube renders the correct perspective on each half; `check_displayxr_app.py` rule for v22 | | **M4 = T1** Mixed vendor on `ds1-linux` | **Leia weave + DS1 tracking on the DS1 half, sim anaglyph on the laptop half** | loader: two plug-ins resident, DPs created from each screen's factory; plug-in display info per screen | leia-linux: `window = 0` always (one line, `leia_sr_linux_sdk.c:1470`); per-DP display state instead of `g_ctx.display` / statics; `srLensDisable` right after `srCreateLens` if the lens must start off. Confirmed by the SR side: a windowless Linux Vulkan weaver fed the DS1 segment + present origin weaves exactly that viewport, votes the lens on once, then the DP owns it. Works on today's SDK for **one** Leia panel. With SR-P0 (verified on the DS1 2026-10-07, branch `ST-5481-weaver-external-routing`): check `SrWeaverRoutingCapabilities` (tag 24) chained on `SrRuntimeCapabilities.pNext`, then chain `SrWeaverRoutingInfo{mode=SR_WEAVER_ROUTING_EXTERNAL}` (tag 22) on `SrWeaverCreateInfoVulkan.pNext`, `window = 0`, present origin per frame, lens via `srLensEnable/Disable` | David eyeballs it; screenshot both halves | | **M5 = T2** Two Leia panels | Each panel tracked by its own camera | per-screen tracker plumbing already in place from M3 | **SR-P1** enumeration + identity, **SR-P2** bind display/weaver/lens/tracker by `displayId`, **SR-P3** N trackers + lens per display (SRService refactor, ST-3160 camera pairing). Plug-in: per-DP SR display + tracker, claims from `srEnumerateDisplays` | needs a second panel on a dev box | -| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). the HWND follows the majority screen (ADR-047 Amendment 2: 20 % margin held 0.5 s, windowless replacement first, rollback on failure; rig-unverified); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed. Service half: a direct (non-workspace) IPC client's window is segmented by the service's direct pipeline, with per-segment views over IPC (ADR-047 Amendment 3); compose/shell mode and hosted windows stay single-screen.** | shell with a window straddling two panels | +| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). the HWND follows the majority screen (ADR-047 Amendment 2: 20 % margin held 0.5 s, windowless replacement first, rollback on failure; rig-unverified); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed. In-process D3D12 (the Unity provider's path): the same, pre-split — `comp_d3d12_segments`, appended `create_dp_d3d12_for_screen` + D3D12 `set_present_origin`, sim_display D3D12, D3D12 mosaic routing; rig-checked with the split off; single-DP under the #918 split for now. Service half: a direct (non-workspace) IPC client's window is segmented by the service's direct pipeline, with per-segment views over IPC (ADR-047 Amendment 3); compose/shell mode and hosted windows stay single-screen.** | shell with a window straddling two panels | Ordering rationale: M0–M3 are hardware-free beyond "two monitors" and exercise everything with sim_display, so the model is proven before any SR dependency. M4 is David's stated