Skip to content

Fix alpha blend factor so AA edges don't leak through on transparent surfaces - #22

Open
mxaddict wants to merge 1 commit into
lapce:floemfrom
mxaddict:alpha-blend-fix
Open

Fix alpha blend factor so AA edges don't leak through on transparent surfaces#22
mxaddict wants to merge 1 commit into
lapce:floemfrom
mxaddict:alpha-blend-fix

Conversation

@mxaddict

Copy link
Copy Markdown

Summary

The render pipeline uses SrcAlpha + OneMinusSrcAlpha for both the color
and alpha components of the blend state (src/lib.rs blend pipeline
setup). The color formula is correct source-over for a straight-alpha
fragment output:

out.rgb = src.rgb · src.a + dst.rgb · (1 − src.a)

…but applying the same factors to the alpha channel produces:

out.a = src.a² + dst.a · (1 − src.a)

— the source alpha gets squared. For a fragment that is conceptually
fully opaque but partially covered (AA edge of an SDF shape, antialiased
glyph), src.a = coverage, and the framebuffer ends up holding
coverage² instead of coverage.

On opaque surfaces (CompositeAlphaMode::Opaque) this is invisible — the
alpha channel is discarded. But on transparent surfaces (most Wayland
compositors negotiate PreMultiplied, smoked-glass overlays, layer-shell
panels, popovers) the compositor blends every glyph and rounded-corner
edge against whatever is behind the window. Result: visible desktop
shine-through at borders and text.

This PR uses the standard "source-over with straight-alpha source" alpha
formula:

out.a = src.a + dst.a · (1 − src.a)

i.e. One + OneMinusSrcAlpha for the alpha component. The color blend
is unchanged.

Test plan

  • Run existing vger examples on macOS / Linux X11 / Linux Wayland —
    confirm no visible regression on opaque output (alpha is discarded
    there, so should be byte-identical).
  • Render the existing demos against a checkerboard backdrop on a
    transparent surface — before: alpha-squared edges visibly translucent;
    after: clean source-over compositing.

I came at this from the consumer side: the floem-based pikr launcher
(layer-shell Wayland surface) was bleeding the desktop through every
glyph edge and rounded badge corner. With this blend fix, the framebuffer
alpha matches the conceptual coverage.

Notes

The change also adjusts gpu_vec::bind_group_entry to spell the
elided lifetime explicitly (wgpu::BindGroupEntry<'_>), silencing a
clippy mismatched_lifetime_syntaxes warning on recent rustc. Drop-in
clean-up only.

Branch is currently based on cfdec48 (the rev floem_vger_renderer
pins); happy to rebase onto main if you'd like before merging.

@panekj

panekj commented May 16, 2026

Copy link
Copy Markdown

Branch is currently based on cfdec48 (the rev floem_vger_renderer
pins); happy to rebase onto main if you'd like before merging.

You might want to check if you have all repos pulled and up to date.
Floem uses vger-rs pinned to 54ab813 (which is on floem branch in this repo and we don't use main)
https://github.com/lapce/floem/blob/bcafaaad3fbf47091e28a64934295194d8e14463/vger/Cargo.toml#L11

@mxaddict

mxaddict commented May 16, 2026 via email

Copy link
Copy Markdown
Author

The pipeline used the same `SrcAlpha + OneMinusSrcAlpha` blend for both
color AND alpha. For alpha that produces `out_a = src_a² + dst_a·(1 −
src_a)` — the source alpha is squared. Antialiased SDF edges and text
glyphs (where the conceptual fragment has full opacity but coverage <
1) end up writing alpha < their coverage to the framebuffer.

On a transparent (PreMultiplied) Wayland surface that means the
compositor blends every glyph / rounded-corner edge against the
desktop, leaving visible "shine through" at borders and text. Color
blend is unchanged. Alpha blend now uses `One + OneMinusSrcAlpha` so
`out_a = src_a + dst_a·(1 − src_a)` — correct source-over alpha
compositing.

Also fixes a hidden-lifetime warning in `gpu_vec::bind_group_entry`
(`wgpu::BindGroupEntry` → `wgpu::BindGroupEntry<'_>`).

Caught by pikr (floem launcher) on a layer-shell Wayland surface.
@mxaddict
mxaddict changed the base branch from main to floem May 16, 2026 13:22
@mxaddict

Copy link
Copy Markdown
Author

Thanks @panekj — rebased onto floem (now sitting one commit above fd91283, the current branch tip) and retargeted the PR.

One unrelated note from running cargo test on the rebased branch: tests/common.rs:174,177 declares multiview_mask: None, twice inside the same RenderPassDescriptor, which fails to compile with error[E0062]: field 'multiview_mask' specified more than once. That's already on floem HEAD — not introduced here — but worth flagging since cargo test won't pass against the branch until it's fixed. The library itself builds clean.

@mxaddict

Copy link
Copy Markdown
Author

Quick follow-up @panekj — I have an upcoming PR against lapce/floem too (threads a transparent: bool flag from WindowConfig down into the vger/vello surface alpha-mode pick, so non-transparent windows opt into CompositeAlphaMode::Opaque and AA edges stop leaking through). Does that repo use the same convention — i.e. is there a non-main branch I should target there as well — or is main the right base for floem PRs? Want to make sure I don't open another one against the wrong branch.

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.

2 participants