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
13 changes: 13 additions & 0 deletions docs/adr/ADR-047-multi-screen-segments-and-per-screen-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,16 @@ detail, not the model.
service. The first three shipped on the LeiaSR Linux line on 2026-10-07; the fourth
needs two panels.
- Milestones, gates and the SR work items: `docs/roadmap/multi-screen.md`.

## Amendment 1 (2026-10-09): segments under the weave-on-scanout split

The first Windows implementation refused to segment a window whenever the D3D11 #918
output-device split was engaged, which ADR-039 makes the default on every hybrid box and
keeps even on one adapter, so the shipping configuration never segmented. Decision: **a
segment DP lives on the device that presents.** Under the split that is the output
(scanout) device, where the session's own DP already is: the segment DPs are created there
through the unchanged `create_dp_d3d11_for_screen` slot (it is handed the output device and
context), they weave the output-side copy of the composed atlas (the egress slot), and each
segment keeps its own viewport, scissor and present origin exactly as off the split. The
split is not a separate segment path; the primary screen's DP keeps the real HWND. Detail:
`docs/architecture/comp-segments.md` § *Windows / D3D11*.
15 changes: 13 additions & 2 deletions docs/architecture/comp-segments.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,19 @@ deferred-release list because the immediate context executes in order). What dif
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`.
- **Under the #918 weave-on-scanout split** (ADR-039 — the default on hybrid boxes, and engaged even
on one adapter) the window is segmented the same way, on the device that presents: the segment
manager, its DPs and its crop textures live on the OUTPUT device (`d3d11_out_device()` /
`d3d11_out_context()`, which off the split are the app's), the DPs' input is the egress slot being
woven (the output-side copy of the composed atlas, already cropped to the box that slot was painted
at), and the flat-2D fill draws through the output composite unit (`comp_d3d11_outcomp_blit_rect`,
the renderer's blit re-made on that device). Same function, same table, same per-segment viewport /
scissor / present origin, same #1863 routing (the mosaic is painted on the app device and crosses the
bridge like any atlas). A slot one frame behind a resizing window is cropped at its own view size
against the live canvas — the single-DP split weave's one-frame lag, no more. A split session logs
one WARN when its segment manager is made (`segments: under the #918 output-device split …`).
- **Not segmented on Windows:** zero-copy frames, a zones / Local2D frame (any non-whole-window
canvas), the shared-texture path, a pinned session, `DXR_SEGMENTS=0`.
The service / hosted path is not segmented yet (ADR-035 amendment, M6 second half).
- **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).
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`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). HWND stays with the primary screen's DP (majority hand-off is a follow-up); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; service/hosted half not started.** | shell with a window straddling two panels |
| **M6 = T3** Service / hosted / Windows | Shell and hosted apps on N screens; Windows parity | ADR-035 amendment: one presenter per 3D screen in the service, clients segmented across presenters; hosted window per screen (#715 fix folds in); Windows: HWND to majority segment DP, windowless secondaries | SR-P0 **already ships on q3**, so no SDK gate: plug-in Windows DP chains `SrWeaverRoutingInfo{EXTERNAL, KEEP_DRAG_SNAP}` on the HWND DP and `{EXTERNAL}` + NULL HWND on secondaries, forwards `set_present_origin` → `srWeaverSetPresentOrigin` in **device px relative to that SR display, rotation applied** (a DPI-virtualised origin weaves on the wrong lattice and looks plausible — see `docs/reference/dpi-awareness.md`). **In-process D3D11 half: runtime `comp_d3d11_segments` + two appended slots (`create_dp_d3d11_for_screen`, D3D11 `set_present_origin`, no ABI bump) + sim_display D3D11; plug-in: screen-bound D3D11 DP chains EXTERNAL (+KEEP_DRAG_SNAP with the HWND) + `SrDisplayBindingInfo`, per-frame `srWeaverSetPresentOrigin` when windowless, scissor = canvas, and pins `srWeaverSetSimulatedViewer` on a non-active display (lens left to the active panel until SR D3). HWND stays with the primary screen's DP (majority hand-off is a follow-up); M3 per-segment views ported too (D3D11 mosaic routing, `get_segment_metrics` / `set_view_routing`), so a straddling window's second panel renders from its own viewer; works under the #918 weave-on-scanout split (ADR-039, the hybrid-box default — segment DPs on the output device, fed from the egress slot; ADR-047 Amendment 1), so no `DXR_D3D_FORCE_GPU` / `DXR_SPLIT_SAME_ADAPTER` knobs are needed; service/hosted half not started.** | shell with a window straddling two panels |

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
33 changes: 27 additions & 6 deletions src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2672,13 +2672,23 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint)

/*
* 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.
* the zero-copy path (the atlas must be ours to crop) 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 stays single-DP,
* byte for byte.
*
* The #918 weave-on-scanout split (ADR-039, the default on hybrid boxes)
* takes the same path: everything here is already on the device that
* presents. The primary DP was made on it, the segment manager is created
* on d3d11_out_device() and hands its DPs d3d11_out_context(), and
* atlas_srv is the egress slot being woven — the output-side copy of the
* composed atlas, already cropped to the box that slot was painted at. A
* slot one frame behind the window (the R2 resize lag) is cropped per
* segment at its own view size against the live canvas, the same
* one-frame lag the single-DP split weave has.
*/
bool seg_split = false;
if (!zero_copy && !c->split_active && !eff_canvas.valid && c->have_seg_window && c->seg_screens != nullptr &&
if (!zero_copy && !eff_canvas.valid && c->have_seg_window && c->seg_screens != nullptr &&
atlas_srv != nullptr) {
if (c->seg_rebuild || c->segments == nullptr) {
// The app thread reads c->segments for eyes (M3): swap under the lock.
Expand All @@ -2693,6 +2703,17 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint)
comp_d3d11_segments_set_screens(c->segments, c->seg_screens, c->seg_sys_info,
c->seg_pinned_display_id);
}
// Lifecycle (once per manager), split sessions only so the
// split-off WARN set is unchanged: names the device the
// segment DPs are made on, the rig's proof they present.
if (c->segments != nullptr && c->split_active &&
comp_d3d11_segments_enabled(c->segments)) {
U_LOG_W(
"segments: under the #918 output-device split — segment DPs are created on "
"the OUTPUT device (LUID=%08lx:%08lx, device %p) and weave the egress slot",
(unsigned long)c->out_luid.HighPart, (unsigned long)c->out_luid.LowPart,
(void *)c->out_dev);
}
}
}
if (comp_d3d11_segments_enabled(c->segments)) {
Expand Down Expand Up @@ -2748,7 +2769,7 @@ d3d11_dp_weave(struct comp_d3d11_compositor *c, bool is_repaint)
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;
sf.outcomp = c->outcomp; // on the output device, like the target
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
Expand Down
170 changes: 170 additions & 0 deletions src/xrt/compositor/d3d11/comp_d3d11_outcomp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "comp_d3d11_outcomp.h"
#include "d3d_shared/comp_masked_composite_shaders.h"
#include "d3d11_layer_shaders.h" // multi-screen M6: the rect blit reuses the renderer's shaders

#include "util/u_logging.h"
#include "util/u_misc.h"
Expand Down Expand Up @@ -169,6 +170,18 @@ struct comp_d3d11_outcomp
uint32_t weave_scratch_w, weave_scratch_h;
//! Grow events, reported once at destroy — the number #918 F10 is about.
uint64_t weave_scratch_reallocs;

/*!
* Multi-screen M6: the scaled rect blit (@ref comp_d3d11_outcomp_blit_rect),
* the renderer's projection shaders + linear sampler re-made on this
* device. Created on first use; @ref blit_failed latches a failed create
* so it is not retried every frame.
*/
ID3D11VertexShader *blit_vs;
ID3D11PixelShader *blit_ps;
ID3D11Buffer *blit_cb;
ID3D11SamplerState *sampler_linear;
bool blit_failed;
};

#define SAFE_RELEASE(p) \
Expand Down Expand Up @@ -400,6 +413,10 @@ comp_d3d11_outcomp_destroy(struct comp_d3d11_outcomp **outcomp_ptr)
}
}

SAFE_RELEASE(oc->sampler_linear);
SAFE_RELEASE(oc->blit_cb);
SAFE_RELEASE(oc->blit_ps);
SAFE_RELEASE(oc->blit_vs);
SAFE_RELEASE(oc->weave_scratch_srv);
SAFE_RELEASE(oc->weave_scratch);
SAFE_RELEASE(oc->depth_stencil_state);
Expand Down Expand Up @@ -620,3 +637,156 @@ comp_d3d11_outcomp_composite_2d_masked(struct comp_d3d11_outcomp *outcomp,
rtv->Release();
return XRT_SUCCESS;
}

/*!
* Create the blit's shaders, constant buffer and linear sampler on first use.
* The same HLSL and sampler desc as comp_d3d11_renderer's, so a flat-2D fill
* on the output device matches the one the renderer draws off the split.
*/
static bool
outcomp_ensure_blit(struct comp_d3d11_outcomp *oc)
{
if (oc->blit_vs != nullptr && oc->blit_ps != nullptr && oc->blit_cb != nullptr &&
oc->sampler_linear != nullptr) {
return true;
}
if (oc->blit_failed) {
return false;
}
oc->blit_failed = true; // cleared on success below

ID3DBlob *blob = nullptr;
if (outcomp_compile_shader(projection_vs_source, "VSMain", "vs_5_0", &blob) != XRT_SUCCESS) {
U_LOG_E("D3D11 outcomp: blit vertex shader compile failed");
return false;
}
HRESULT hr =
oc->dev->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &oc->blit_vs);
blob->Release();
if (FAILED(hr)) {
U_LOG_E("D3D11 outcomp: blit vertex shader create failed: 0x%08x", hr);
return false;
}
if (outcomp_compile_shader(projection_ps_source, "PSMain", "ps_5_0", &blob) != XRT_SUCCESS) {
U_LOG_E("D3D11 outcomp: blit pixel shader compile failed");
return false;
}
hr = oc->dev->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &oc->blit_ps);
blob->Release();
if (FAILED(hr)) {
U_LOG_E("D3D11 outcomp: blit pixel shader create failed: 0x%08x", hr);
return false;
}

D3D11_BUFFER_DESC cb = {};
cb.ByteWidth = sizeof(LayerConstants);
cb.Usage = D3D11_USAGE_DYNAMIC;
cb.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cb.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
hr = oc->dev->CreateBuffer(&cb, nullptr, &oc->blit_cb);
if (FAILED(hr)) {
U_LOG_E("D3D11 outcomp: blit constant buffer create failed: 0x%08x", hr);
return false;
}

D3D11_SAMPLER_DESC sd = {};
sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
hr = oc->dev->CreateSamplerState(&sd, &oc->sampler_linear);
if (FAILED(hr)) {
U_LOG_E("D3D11 outcomp: linear sampler create failed: 0x%08x", hr);
return false;
}
oc->blit_failed = false;
return true;
}

extern "C" xrt_result_t
comp_d3d11_outcomp_blit_rect(struct comp_d3d11_outcomp *outcomp,
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 (outcomp == nullptr || src_srv == nullptr || rtv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 ||
dst_h == 0) {
return XRT_ERROR_D3D;
}
if (!outcomp_ensure_blit(outcomp)) {
return XRT_ERROR_D3D;
}
ID3D11DeviceContext *ctx = outcomp->ctx;
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);
// Scissor is off in this rasterizer state; the viewport alone confines a
// fullscreen strip to the destination rect.
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
ctx->IASetInputLayout(nullptr);
ctx->VSSetShader(outcomp->blit_vs, nullptr, 0);
ctx->PSSetShader(outcomp->blit_ps, nullptr, 0);
ctx->RSSetState(outcomp->rasterizer_state);
ctx->OMSetDepthStencilState(outcomp->depth_stencil_state, 0);
ctx->OMSetBlendState(outcomp->blend_opaque, nullptr, 0xFFFFFFFF);
ctx->PSSetSamplers(0, 1, &outcomp->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(outcomp->blit_cb, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
if (FAILED(hr)) {
return XRT_ERROR_D3D;
}
memcpy(mapped.pData, &constants, sizeof(constants));
ctx->Unmap(outcomp->blit_cb, 0);
ctx->VSSetConstantBuffers(0, 1, &outcomp->blit_cb);
ctx->PSSetConstantBuffers(0, 1, &outcomp->blit_cb);

ctx->Draw(4, 0);

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