fix: window.opacity transparency on Windows (wgpu + cpu backends)#1583
Open
shiena wants to merge 2 commits into
Open
fix: window.opacity transparency on Windows (wgpu + cpu backends)#1583shiena wants to merge 2 commits into
shiena wants to merge 2 commits into
Conversation
b1b1be0 to
e8cc7a9
Compare
On Windows the wgpu surface picks `PreMultiplied` alpha mode (the only non-Opaque mode the DX12 / Vulkan WSI paths offer), so DWM interprets framebuffer RGB as already multiplied by alpha. The grid bg pass emits `(0,0,0,0)` for default-bg cells and lets the cleared color show through, but `LoadOp::Clear` was writing the raw `(bg, opacity)` value instead of premultiplied `(bg*opacity, opacity)`. The compositor then treated the dark background as if it were premultiplied, which made `window.opacity < 1` render almost fully transparent. Premultiply the clear color in `Sugarloaf::render_wgpu` for any alpha mode that isn't `PostMultiplied` (only mode that wants straight RGBA). Promote `WgpuContext::alpha_mode` to `pub` so the renderer can branch on it. Also fix the cfg gating in `screen/mod.rs` that selected the CPU backend on default Windows builds. The `wgpu` crate feature in rioterm isn't on by default — only `--features wgpu` enables it — but the sugarloaf + rio-backend deps are already forced to `features = ["wgpu"]` on Windows via the target dependency override. Without this OS-level fallback the `Backend::Webgpu => SugarloafBackend::Cpu` arm fired and the user fell into the CPU rasterizer (which doesn't write per-pixel alpha), so `with_transparent(true)` + `DwmEnableBlurBehindWindow` showed a fully-transparent window regardless of the configured opacity.
The CPU rasterizer wrote 0x00RRGGBB pixels (alpha = 0) and every blend function masked the alpha byte back out (`& 0x00ff_ffff`, or `pack_opaque` that omits the upper byte). That was fine on platforms where the OS ignores swap-chain alpha, but with `with_transparent(true)` plus `DwmEnableBlurBehindWindow` (engaged when `window.opacity < 1`), DWM treats the framebuffer alpha as the per-pixel transparency. The whole window therefore came out 100% see-through on the CPU backend whenever opacity was set, regardless of the configured value. Switch the CPU framebuffer to premultiplied RGBA throughout: - `pack_opaque` now writes `alpha = 0xff` (no-op for OSes that ignore it). - bg fill in `render_cpu` uses `pack_premul((R,G,B)·a, a)` so the cleared color carries `window.opacity` into the alpha byte. - All four blend variants in `renderer/cpu.rs` (scalar SWAR + 3 SIMD flavours) now compute the A channel via the same `(x*inv) / 255` SWAR trick used for G, and stop stripping alpha from the source/result. The premultiplied invariant (RGB ≤ A) keeps the byte-wise add safe from carry across channels. - `grid/cpu.rs::blend_over` and `text.rs::blend_premul_over` (separate scalar paths used by the grid bg/text and UI text overlays) get the same A-channel computation. New `pack_premul` helper added next to each `pack_opaque`. - Caller sites that splatted `(src_premul & 0x00ff_ffff)` and post-masked the SIMD result with `& mask_rgb_*` are updated to splat the full premultiplied src and drop the post-mask. Default-bg cells emit `(0,0,0,0)`; with `sa == 0` the early-out branch returns `dst` unchanged, so the cleared bg's alpha bleeds through and the window renders translucent at the configured opacity.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On Windows,
[window] opacity < 1rendered the window almost entirely see-through instead of partially translucent.Reproducible with the default
cargo build(wgpu backend) and with[renderer] use-cpu = true(cpu backend).Root cause
with_transparent(opacity < 1.)triggersDwmEnableBlurBehindWindow, after which DWM uses the framebuffer alpha for per-pixel compositing.Both backends were producing the wrong alpha:
wgpu backend had two independent issues:
frontends/rioterm/src/screen/mod.rsgates the wgpu code path on rioterm's ownwgpufeature, which is off in defaultcargo build.Default Windows builds therefore fell into the CPU rasterizer, even though
sugarloafandrio-backendalready getfeatures = ["wgpu"]via the[target.'cfg(windows)']dep override, so the wgpu code is in the binary.Sugarloaf::render_wgpuwroteLoadOp::Clear((bg_r, bg_g, bg_b, opacity))raw.wgpu picks
PreMultipliedalpha mode on Windows (the only non-Opaque option DX12 / Vulkan WSI offer), so DWM read the dark bg as if it were already-multiplied — the implied "straight" RGB came out near-fully transparent.cpu backend wrote
0x00RRGGBBpixels (α = 0) and every blend function masked the alpha byte out (pack_opaqueomits it, SIMD results post-masked with& 0x00ff_ffff).Harmless on platforms that ignore swap-chain alpha; with DWM reading the alpha byte, every pixel came out fully transparent.
Fix
wgpu:
cfg(any(feature = "wgpu", target_os = "windows")).LoadOp::Clearcolor inrender_wgpufor any alpha mode that isn'tPostMultiplied.Promote
WgpuContext::alpha_modetopubso the renderer can branch on it.cpu — switch the softbuffer framebuffer to premultiplied RGBA end-to-end:
pack_opaquewritesα = 0xff(no-op on OSes that ignore the byte).render_cpuusespack_premul((R,G,B)·a, a).renderer/cpu.rs(scalar SWAR + x4/x8 SIMD const-src + var-src) gain an A-channel computation via the same(x · inv) / 255SWAR trick as G, and stop stripping alpha from the source / result.grid/cpu.rsandtext.rs(grid cells and UI text overlays) get the same A-channel fix, with a newpack_premulhelper next to eachpack_opaque.(src_premul & 0x00ff_ffff)and post-masked SIMD results are updated to splat the full premultiplied src and drop the post-mask.The premultiplied invariant (RGB ≤ A) keeps byte-wise channel adds safe from inter-channel carry.
Default-bg cells continue to emit
(0, 0, 0, 0); thesa == 0early-out returnsdstunchanged, so the cleared bg's alpha bleeds through.Tested
Windows 11 / NVIDIA RTX 5070,
opacity = 0.6:Bgra8Unorm,PreMultipliedsurface) — renders as ~60% bg + ~40% desktop.use-cpu = true) — renders translucent; tabs, prompt text, glyph AA edges, command palette overlay all composite correctly.opacity = 1.0(default) unaffected on both paths.