Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/architecture/comp-segments.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,35 @@ or destroy is one WARN.
- Capture: with the window split, the post-compose atlas capture also writes each
segment's DP input as `<stem>.seg<i>.png`.

## Windows / D3D11 (M6)

The in-process D3D11 compositor carries the same model in `d3d11/comp_d3d11_segments.{h,cpp}`,
a line-for-line twin of the Vulkan manager with the GPU work in D3D11 terms
(`CopySubresourceRegion` crops, `ClearRenderTargetView`, a renderer rect blit for flat 2D, no
deferred-release list because the immediate context executes in order). What differs on Windows:

- **Two appended ABI slots, no version bump** (ADR-020): `xrt_plugin_iface::create_dp_d3d11_for_screen`
(the D3D11 twin of the Vulkan per-screen factory) and `xrt_display_processor_d3d11::set_present_origin`.
A plug-in without them gets flat 2D on its other screens, exactly like Vulkan.
- **The window.** The session's primary DP keeps the real HWND and so the vendor's drag phase-snap;
every other screen's DP is created with a NULL window and phases from `set_present_origin` + the
canvas offset. The vendor weaver reads the current D3D11 viewport **and scissor** and writes only
inside them, so the segment path sets both to the segment before every `process_atlas`, and the
back buffer is re-bound before every DP. (ADR-047's "HWND follows the majority segment" is a
follow-up: today the HWND stays with the primary screen's DP.)
- **Units.** The window rect comes from `ClientToScreen` in a per-monitor-DPI-aware process, i.e.
device px — the same space as the registry's monitor rects. A DPI-virtualised origin would weave on
the wrong lattice and look plausible (`docs/reference/dpi-awareness.md`).
- **The untracked screen.** The SR tracker follows the runtime's active display only, so a weaver
bound to any other display sees no face and would fall back to 2D. The Leia plug-in pins that
weaver to its nominal viewer and leaves the lens to the active panel (David, 2026-10-07; SR D3/D4
move both per device later). The runtime does nothing special: the DP owns it.
- **Not segmented on Windows:** zero-copy frames, the #918 output-adapter split, a zones / Local2D
frame (any non-whole-window canvas), the shared-texture path, a pinned session, `DXR_SEGMENTS=0`.
The service / hosted path is not segmented yet (ADR-035 amendment, M6 second half).
- **sim_display** implements both slots on D3D11 too, so a two-monitor Windows box with no vendor
hardware exercises the split path (anaglyph on both halves).

## Per-segment views (M3)

Under `PRIMARY_MULTIVIEW_DXR` each segment gets its **own** views instead of a crop of
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap/multi-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ multiview + display_info v22. An app may also pin itself to one display
| **M3** Per-segment views | Same window: `xrLocateViews` returns 4 views, each half rendered from its own frustum | oxr: per-segment Kooima + eyes from the segment DP; `XrViewDisplayBindingsDXR`; `activeViewCount`; tile routing by segment; legacy stereo = majority segment + 2D blit | none | cube renders the correct perspective on each half; `check_displayxr_app.py` rule for v22 |
| **M4 = T1** Mixed vendor on `ds1-linux` | **Leia weave + DS1 tracking on the DS1 half, sim anaglyph on the laptop half** | loader: two plug-ins resident, DPs created from each screen's factory; plug-in display info per screen | leia-linux: `window = 0` always (one line, `leia_sr_linux_sdk.c:1470`); per-DP display state instead of `g_ctx.display` / statics; `srLensDisable` right after `srCreateLens` if the lens must start off. Confirmed by the SR side: a windowless Linux Vulkan weaver fed the DS1 segment + present origin weaves exactly that viewport, votes the lens on once, then the DP owns it. Works on today's SDK for **one** Leia panel. With SR-P0 (verified on the DS1 2026-10-07, branch `ST-5481-weaver-external-routing`): check `SrWeaverRoutingCapabilities` (tag 24) chained on `SrRuntimeCapabilities.pNext`, then chain `SrWeaverRoutingInfo{mode=SR_WEAVER_ROUTING_EXTERNAL}` (tag 22) on `SrWeaverCreateInfoVulkan.pNext`, `window = 0`, present origin per frame, lens via `srLensEnable/Disable` | David eyeballs it; screenshot both halves |
| **M5 = T2** Two Leia panels | Each panel tracked by its own camera | per-screen tracker plumbing already in place from M3 | **SR-P1** enumeration + identity, **SR-P2** bind display/weaver/lens/tracker by `displayId`, **SR-P3** N trackers + lens per display (SRService refactor, ST-3160 camera pairing). Plug-in: per-DP SR display + tracker, claims from `srEnumerateDisplays` | needs a second panel on a dev box |
| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`) | shell with a window straddling two panels |
| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). HWND stays with the primary screen's DP (majority hand-off is a follow-up); service/hosted half not started.** | shell with a window straddling two panels |

Ordering rationale: M0–M3 are hardware-free beyond "two monitors" and exercise everything
with sim_display, so the model is proven before any SR dependency. M4 is David's stated
Expand Down
2 changes: 2 additions & 0 deletions src/xrt/compositor/d3d11/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ if(WIN32 AND XRT_HAVE_D3D11)
comp_d3d11_outcomp.h
comp_d3d11_renderer.cpp
comp_d3d11_renderer.h
comp_d3d11_segments.cpp
comp_d3d11_segments.h
comp_d3d11_state_guard.h
comp_d3d11_swapchain.cpp
comp_d3d11_swapchain.h
Expand Down
138 changes: 131 additions & 7 deletions src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "util/comp_display_refresh_win.h"
#include "comp_d3d11_renderer.h"
#include "comp_d3d11_outcomp.h"
#include "comp_d3d11_segments.h" // multi-screen M6
#include "xrt/xrt_screen.h"
#include "comp_d3d11_window.h"
#include "comp_d3d11_state_guard.h"
#include "comp_xbridge.h"
Expand Down Expand Up @@ -488,6 +490,21 @@ struct comp_d3d11_compositor
//! Generic D3D11 display processor (vendor-agnostic weaving).
struct xrt_display_processor_d3d11 *display_processor;

/*!
* Multi-screen M6 (ADR-047 D2 on Windows): a window spanning monitors is
* woven per segment, each by its own screen's DP, into the one back
* buffer. The screen list + DP registry arrive at session creation
* (comp_d3d11_compositor_set_screens, under c->mutex); the manager is
* (re)built lazily on the weave thread, which also holds c->mutex.
*/
struct comp_d3d11_segments *segments;
struct xrt_screen_list *seg_screens; //!< heap copy, NULL = not handed over
struct xrt_system_compositor_info *seg_sys_info; //!< heap copy (the DP registry)
bool seg_rebuild; //!< a new list arrived: rebuild on the next weave
uint64_t seg_pinned_display_id;
struct comp_seg_rect seg_window; //!< the window's client area in desktop device px
bool have_seg_window;

//! System devices (for qwerty driver keyboard input and display mode toggle).
struct xrt_system_devices *xsysd;

Expand Down Expand Up @@ -2564,13 +2581,72 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint)
xrt_display_processor_d3d11_set_predicted_scanout(
c->display_processor, comp_d3d11_target_predict_weave_to_scanout_ns(c->target));

xrt_display_processor_d3d11_process_atlas(
c->display_processor, d3d11_out_context(c), atlas_srv, view_width, view_height,
tile_columns, tile_rows, DXGI_FORMAT_R8G8B8A8_UNORM, target_width, target_height,
eff_canvas.valid ? eff_canvas.x : 0,
eff_canvas.valid ? eff_canvas.y : 0,
eff_canvas.valid ? eff_canvas.w : 0,
eff_canvas.valid ? eff_canvas.h : 0);
/*
* Multi-screen M6 (ADR-047 D2): does this window span monitors? Only off
* the zero-copy path (the atlas must be ours to crop), off the #918 split
* (the output adapter owns that pipeline), and for a whole-window canvas
* (a zones / Local2D frame keeps the single-DP path — out of scope here).
* A window on the primary screen only also stays single-DP, byte for byte.
*/
bool seg_split = false;
if (!zero_copy && !c->split_active && !eff_canvas.valid && c->have_seg_window && c->seg_screens != nullptr &&
atlas_srv != nullptr) {
if (c->seg_rebuild) {
comp_d3d11_segments_destroy(&c->segments);
c->seg_rebuild = false;
}
if (c->segments == nullptr) {
c->segments = comp_d3d11_segments_create(d3d11_out_device(c));
if (c->segments != nullptr) {
comp_d3d11_segments_set_screens(c->segments, c->seg_screens, c->seg_sys_info,
c->seg_pinned_display_id);
}
}
if (comp_d3d11_segments_enabled(c->segments)) {
const struct comp_seg_rect canvas = {0, 0, c->seg_window.w, c->seg_window.h};
const uint32_t mode_index =
(c->xdev != nullptr && c->xdev->hmd != nullptr) ? c->xdev->hmd->active_rendering_mode_index : 0;
seg_split = comp_d3d11_segments_update(c->segments, &c->seg_window, &canvas, d3d11_out_context(c),
mode_index);
// Every segment DP follows the session's 2D/3D mode, like the primary.
comp_d3d11_segments_set_display_mode(c->segments, c->hardware_display_3d);
}
}

if (seg_split) {
struct comp_d3d11_segments_frame sf = {};
sf.context = d3d11_out_context(c);
sf.src_srv = atlas_srv;
sf.view_width = view_width;
sf.view_height = view_height;
sf.tile_columns = tile_columns;
sf.tile_rows = tile_rows;
sf.src_format = (uint32_t)DXGI_FORMAT_R8G8B8A8_UNORM;
sf.target_rtv = comp_d3d11_target_get_rtv(c->target);
sf.target_width = target_width;
sf.target_height = target_height;
sf.canvas.x = 0;
sf.canvas.y = 0;
sf.canvas.w = c->seg_window.w;
sf.canvas.h = c->seg_window.h;
sf.transparent_background = c->transparent_background;
sf.atlas_encoding = -1; // the in-process D3D11 path declares none to the primary either
sf.primary_dp = c->display_processor;
sf.renderer = c->renderer;
comp_d3d11_segments_record(c->segments, &sf);
// The segment path leaves the back buffer bound at full viewport;
// re-bind through the target so everything after sees exactly the
// state the single-DP path leaves.
comp_d3d11_target_bind(c->target);
} else {
xrt_display_processor_d3d11_process_atlas(
c->display_processor, d3d11_out_context(c), atlas_srv, view_width, view_height,
tile_columns, tile_rows, DXGI_FORMAT_R8G8B8A8_UNORM, target_width, target_height,
eff_canvas.valid ? eff_canvas.x : 0,
eff_canvas.valid ? eff_canvas.y : 0,
eff_canvas.valid ? eff_canvas.w : 0,
eff_canvas.valid ? eff_canvas.h : 0);
}

// XR_DXR_depth_budget: evaluate the rear budget from the background the
// DP just composited under. AFTER process_atlas, because that is the point
Expand Down Expand Up @@ -4017,6 +4093,14 @@ d3d11_compositor_destroy(struct xrt_compositor *xc)
comp_d3d11_window_set_snap_provider(c->own_window, NULL, NULL);
}

// Multi-screen M6: segment DPs + crop textures, before the primary DP
// (the repaint thread is already stopped, so no weave is in flight).
comp_d3d11_segments_destroy(&c->segments);
free(c->seg_screens);
c->seg_screens = nullptr;
free(c->seg_sys_info);
c->seg_sys_info = nullptr;

// Destroy display processor (handles all vendor cleanup internally)
xrt_display_processor_d3d11_destroy(&c->display_processor);

Expand Down Expand Up @@ -8207,6 +8291,15 @@ comp_d3d11_compositor_get_window_metrics(struct xrt_compositor *xc, struct xrt_w
POINT client_origin = {0, 0};
ClientToScreen(metrics_hwnd, &client_origin);

// Multi-screen M6: the window's desktop rect, for the segment table.
// Device px — every DisplayXR executable is per-monitor DPI aware
// (#1201), the same space the screen registry's monitor rects are in.
c->seg_window.x = client_origin.x;
c->seg_window.y = client_origin.y;
c->seg_window.w = win_px_w;
c->seg_window.h = win_px_h;
c->have_seg_window = true;

// Compute pixel size (meters per pixel)
float pixel_size_x = disp_w_m / (float)disp_px_w;
float pixel_size_y = disp_h_m / (float)disp_px_h;
Expand Down Expand Up @@ -8268,6 +8361,37 @@ comp_d3d11_compositor_request_display_mode(struct xrt_compositor *xc, bool enabl
return false;
}

extern "C" void
comp_d3d11_compositor_set_screens(struct xrt_compositor *xc,
const struct xrt_screen_list *list,
const struct xrt_system_compositor_info *info,
uint64_t pinned_display_id)
{
if (xc == nullptr || list == nullptr || info == nullptr) {
return;
}
struct comp_d3d11_compositor *c = d3d11_comp(xc);
struct xrt_screen_list *copy = U_TYPED_CALLOC(struct xrt_screen_list);
struct xrt_system_compositor_info *info_copy = U_TYPED_CALLOC(struct xrt_system_compositor_info);
if (copy == nullptr || info_copy == nullptr) {
free(copy);
free(info_copy);
return;
}
*copy = *list;
*info_copy = *info;

// The weave (app thread's layer_commit and the repaint thread) runs under
// c->mutex; the manager is rebuilt there on the next weave, never here.
std::lock_guard<std::mutex> lock(c->mutex);
c->seg_rebuild = true;
free(c->seg_screens);
c->seg_screens = copy;
free(c->seg_sys_info);
c->seg_sys_info = info_copy;
c->seg_pinned_display_id = pinned_display_id;
}

extern "C" void
comp_d3d11_compositor_set_eye_tracking_mode(struct xrt_compositor *xc, uint32_t mode)
{
Expand Down
18 changes: 18 additions & 0 deletions src/xrt/compositor/d3d11/comp_d3d11_compositor.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,24 @@ bool
comp_d3d11_compositor_get_window_metrics(struct xrt_compositor *xc,
struct xrt_window_metrics *out_metrics);

/*!
* Multi-screen M6 (ADR-047 D2 on Windows): hand the compositor the system's
* screen list, so a window that spans several monitors is woven per segment,
* each by its own screen's display processor, into the one back buffer. Call
* once at session creation. @p pinned_display_id is the session's
* `XrSessionDisplayBindingDXR` (0 = none); a pinned session is never
* segmented. A window on the primary screen only keeps the single-DP path
* byte for byte.
*
* @ingroup comp_d3d11
*/
struct xrt_screen_list;
void
comp_d3d11_compositor_set_screens(struct xrt_compositor *xc,
const struct xrt_screen_list *list,
const struct xrt_system_compositor_info *info,
uint64_t pinned_display_id);

/*!
* XR_DXR_depth_budget: whether this session asked for the rear depth budget.
*
Expand Down
86 changes: 86 additions & 0 deletions src/xrt/compositor/d3d11/comp_d3d11_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2509,3 +2509,89 @@ comp_d3d11_renderer_blit_stretch(struct comp_d3d11_renderer *renderer,
return XRT_SUCCESS;
}

xrt_result_t
comp_d3d11_renderer_blit_rect(struct comp_d3d11_renderer *renderer,
void *src_srv,
int32_t src_x,
int32_t src_y,
uint32_t src_w,
uint32_t src_h,
void *rtv,
int32_t dst_x,
int32_t dst_y,
uint32_t dst_w,
uint32_t dst_h)
{
if (renderer == nullptr || src_srv == nullptr || rtv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 ||
dst_h == 0) {
return XRT_ERROR_D3D;
}
auto internals = get_internals(renderer->c);
ID3D11DeviceContext *ctx = internals.context;
auto *srv = static_cast<ID3D11ShaderResourceView *>(src_srv);
auto *target_rtv = static_cast<ID3D11RenderTargetView *>(rtv);

// The source rect in normalised texture space needs the texture's size.
ID3D11Resource *res = nullptr;
srv->GetResource(&res);
if (res == nullptr) {
return XRT_ERROR_D3D;
}
D3D11_TEXTURE2D_DESC desc = {};
static_cast<ID3D11Texture2D *>(res)->GetDesc(&desc);
res->Release();
if (desc.Width == 0 || desc.Height == 0) {
return XRT_ERROR_D3D;
}

ctx->OMSetRenderTargets(1, &target_rtv, nullptr);
D3D11_VIEWPORT vp = {};
vp.TopLeftX = static_cast<float>(dst_x);
vp.TopLeftY = static_cast<float>(dst_y);
vp.Width = static_cast<float>(dst_w);
vp.Height = static_cast<float>(dst_h);
vp.MaxDepth = 1.0f;
ctx->RSSetViewports(1, &vp);
// The renderer's rasterizer state has scissor off; the viewport alone
// confines a fullscreen strip to the destination rect.
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
ctx->IASetInputLayout(nullptr);
ctx->VSSetShader(renderer->projection_vs, nullptr, 0);
ctx->PSSetShader(renderer->projection_ps, nullptr, 0);
ctx->RSSetState(renderer->rasterizer_state);
ctx->OMSetDepthStencilState(renderer->depth_stencil_state, 0);
ctx->OMSetBlendState(renderer->blend_opaque, nullptr, 0xFFFFFFFF);
ctx->PSSetSamplers(0, 1, &renderer->sampler_linear);
ctx->PSSetShaderResources(0, 1, &srv);

LayerConstants constants = {};
constants.mvp[0] = 1.0f;
constants.mvp[5] = 1.0f;
constants.mvp[10] = 1.0f;
constants.mvp[15] = 1.0f;
constants.post_transform[0] = static_cast<float>(src_x) / static_cast<float>(desc.Width);
constants.post_transform[1] = static_cast<float>(src_y) / static_cast<float>(desc.Height);
constants.post_transform[2] = static_cast<float>(src_w) / static_cast<float>(desc.Width);
constants.post_transform[3] = static_cast<float>(src_h) / static_cast<float>(desc.Height);
constants.color_scale[0] = 1.0f;
constants.color_scale[1] = 1.0f;
constants.color_scale[2] = 1.0f;
constants.color_scale[3] = 1.0f;

D3D11_MAPPED_SUBRESOURCE mapped;
HRESULT hr = ctx->Map(renderer->constant_buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
if (FAILED(hr)) {
return XRT_ERROR_D3D;
}
memcpy(mapped.pData, &constants, sizeof(constants));
ctx->Unmap(renderer->constant_buffer, 0);
ctx->VSSetConstantBuffers(0, 1, &renderer->constant_buffer);
ctx->PSSetConstantBuffers(0, 1, &renderer->constant_buffer);

ctx->Draw(4, 0);

ID3D11ShaderResourceView *null_srv = nullptr;
ctx->PSSetShaderResources(0, 1, &null_srv);
return XRT_SUCCESS;
}

Loading
Loading