Skip to content

perf(d3d11): mask-only post-weave alpha gate - no strip copy, no back-buffer read (2D-under-the-lens P1) - #305

Merged
dfattal merged 1 commit into
mainfrom
perf/alpha-gate-skip
Oct 6, 2026
Merged

dfattal merged 1 commit into
mainfrom
perf/alpha-gate-skip

Conversation

@dfattal

@dfattal dfattal commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

P1 of the "2D under the lens" perf work. The P0 timing (#302) measured the post-weave alpha_gate at 0.3-1.1 ms/frame, against sr_weave at 0.6-0.9 ms. That was on the browser present-owner weave (has_overlay=2): RTX 3080 laptop, 3840x2040, whole-page overlay, static gallery. The gate cost more than SR itself on many frames.

What the alpha gate computes and why it exists

The SR weaver produces opaque RGB. Its alpha is either 1.0 (compose order 2) or the 2D layer's filtered coverage (order 3, 2D_OVER_COVERAGE), but never the 3D content's transparency. Two consumers need real transparency:

  • Client-present (#551). The browser imports the woven output and draws it back whole-window over its own page. The gaps between 3D elements must have alpha 0 (page shows through), and 3D content must have alpha 1. Inside the 3D tiles, the overlay's holes give coverage ~0, so without the gate the 3D content would vanish.
  • Compose-under-bg (WGC). The shaped/transparent window presented through DWM must punch (0,0,0,0) holes where the app's atlas is transparent.

The gate decides per screen pixel whether all views' atlas alpha are 0 at that tile-local position. Transparent pixels become the under-layer (live desktop, the 2D-under backdrop, the flattened bg, and/or the weaver's filtered 2D layer). Opaque pixels become (woven.rgb, 1).

It is not skippable on this path. The browser's composite needs alpha 1 on the 3D content and the weaver doesn't write it. So this PR makes the gate cheaper instead of skipping it.

What was expensive

Every 3D weave did all of the following:

  • CopyResource of the whole woven target into ck_strip_tex: 3840x2040 RGBA8 is 31 MB read + 31 MB written.
  • A full-canvas pass that re-read that copy and the atlas.
  • A rewrite of every pixel, including all the transparent page area where the result equals the input.

The change: mask-only gate

In the common configuration, the full gate's result depends on the woven pixel only in ways the output-merger can apply in place. That configuration is: no 2D-under backdrop, no opaque-present flatten, a fixed-point (8/10-bit UNORM) target, and a 2D layer that is absent (has_overlay 0) or coverage-alpha (has_overlay 2).

pixel full gate output mask-only gate
all views transparent, has_overlay 2 (w.rgb + 0, a + 0) = woven unchanged discard (no write)
all views transparent, has_overlay 0 (0,0,0,0) src (0,0,0,0) with blend RGB = dst*src.a, A = src.a
any view opaque (woven.rgb, 1) alpha-only write of 1 (has_overlay 2) / src (0,0,0,1) with the same blend (has_overlay 0)

So alpha_gate_mask_ps reads only the atlas. It uses the full gate's transparency loop verbatim, including the exact-texel Load rule from the 1-px-border fix. One of two blend states does the rest. There is no CopyResource, no back-buffer read, and with has_overlay 2 no writes at all outside the 3D content.

has_overlay 1 (it needs max(layer.a, woven.rgb)), backdrop, flatten and float targets keep the full gate, unchanged. DXR_LEIA_ALPHA_GATE_FULL=1 forces the full gate everywhere, for A/B and as a no-rebuild rollback. If the mask pipeline fails to build, it logs once and falls back to the full gate.

Equivalence. The result is identical by construction for the configurations it takes, with one deliberate difference. The full gate reads the woven RGB back with a bilinear Sample at the texel centre. The shader comment in this same file documents that this can pick up 1/256 of the neighbouring column. The mask-only gate leaves the RGB untouched instead, so opaque-pixel RGB can differ from before by at most 1 LSB at hard vertical edges, toward the exact weaver output. Alpha is identical. The gate leaves the same default blend state bound on exit.

Timing (P0 line kept)

Leia D3D11 DP timing keeps every field (alpha_gate a/b (n=, with 2D layer=) unchanged) and appends gate_mask_only=<n>: how many of the read-back gates took the mask-only pass.

Expected savings

  • On the browser path, alpha_gate should fall from 0.3-1.1 ms to a fraction of that. The 62 MB copy and the whole-canvas back-buffer read and write are gone. What remains is two atlas loads per pixel (the atlas is already hot from the weave) plus alpha writes on the 3D content only.
  • sr_weave is unchanged.
  • Compose-under-bg sessions with no backdrop also take the mask path (has_overlay 0), so they get the same saving.

Test plan

Build: scripts\build-windows.bat build in a worktree against the matching runtime worktree (done; compiles clean).

Deploy: rename-swap the dev DisplayXR-LeiaSR.dll into C:\Program Files\DisplayXR\Plugins\LeiaSR\ (keep the original as .orig-p1). Relaunch the service from an explorer.exe .bat with DXR_WEAVE_GPU_TIMING=1 and SR_SIMULATED_VIEWER=0,0,600,63 in its env. Restore the DLL afterwards.

  1. bperf A/B on the same DLL. DisplayXR Browser whole-page overlay, static gallery, 100% overlay reuse, ~60 s per arm:

    • A: service env + DXR_LEIA_ALPHA_GATE_FULL=1.
    • B: without it.

    Compare alpha_gate, sr_weave (should be flat), runtime weave and total. Expect gate_mask_only == n in B and 0 in A.

  2. Visual, feat(gl,#613): GL DP display-zones vtable + #68 remap-skip gate #73 woven dump. touch %TEMP%\dxr_weave_dump_trigger in each arm on the same page state, then diff dxr73_weave_output. Alpha should match exactly, and RGB to within 1 LSB (see Equivalence). Check the 1-px canvas border case (an element edge on an odd column): there must be no opaque black line outside the canvas.

  3. Transparent-window app. A shaped/transparent window on the compose-under-bg or client-present path (e.g. cube_handle_d3d11_win with transparency, or a transparent demo):

    • The desktop shows through the transparent regions and the silhouettes look the same in A and B.
    • Check the post-weave workspace_screenshot.png alpha with DXR_ATLAS_CAPTURE_RAW_ALPHA=1 where applicable.
    • With a 2D-under backdrop or DXR_PRESENT_OPAQUE=1, the full gate must still run (gate_mask_only=0).
  4. 2D (V-toggle) on the transparent app: the gate also runs after the 2D blit with has_overlay 0. Holes must still punch.

Risks

  • A future weaver that writes alpha outside [0,1] on a float target is excluded by the UNORM-only guard.
  • The mask shader duplicates the full gate's transparency loop. The two must stay in sync, and both carry a comment saying so.
  • Rollback without a rebuild: DXR_LEIA_ALPHA_GATE_FULL=1.

🤖 Generated with Claude Code

…-buffer read

The post-weave alpha gate (client-present and compose-under-bg transparency)
copied the whole woven target into ck_strip_tex every 3D weave, then re-read
and rewrote every canvas pixel. In the common configuration - no 2D-under
backdrop, no opaque-present flatten, and a 2D layer that is either absent
(has_overlay 0) or coverage-alpha (SR order 3, has_overlay 2) - its result
depends on the woven pixel only through the output-merger:

  all views transparent, has_overlay 2 : woven pixel unchanged
  all views transparent, has_overlay 0 : (0,0,0,0)
  any view opaque                       : (woven.rgb, 1)

So a mask-only shader reads just the atlas and a blend state applies the
result in place: has_overlay 2 = alpha-only write + discard on transparent
pixels; has_overlay 0 = RGB*src.a, A=src.a. No CopyResource, no back-buffer
read, and on the browser's whole-page-overlay path no writes outside the 3D
content at all. has_overlay 1, backdrop, flatten and float targets keep the
full gate unchanged; DXR_LEIA_ALPHA_GATE_FULL=1 forces it everywhere (A/B,
rollback).

The DXR_WEAVE_GPU_TIMING line keeps its fields and appends
gate_mask_only=<n> so the A/B is read off the same knob.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal marked this pull request as ready for review October 6, 2026 01:06
@dfattal
dfattal merged commit 80dc605 into main Oct 6, 2026
16 checks passed
@dfattal
dfattal deleted the perf/alpha-gate-skip branch October 6, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant