SIGN IN SIGN UP

fix: window.opacity transparency on Windows (#1905)

* fix: window.opacity rendering through wgpu PreMultiplied surfaces

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.

* fix(cpu): preserve framebuffer alpha so window.opacity works

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.

* opacity: alpha-capable adapter, paired swar, alpha tests

* libsugarloaf: default new renderer field

* test var_src kernel at extreme alphas too

* review fixes: premul gating, adapter fallback, shared premul module

---------

Co-authored-by: KOGA Mitsuhiro <shiena.jp@gmail.com>
R
Raphael Amorim committed
357281638216876a2406c46e50033e7143256175
Parent: 482414f
Committed by GitHub <noreply@github.com> on 8/28/2026, 9:52:25 PM