Decision 0087
0087. A buffer with no alpha channel decides what the face can look like
Architecture decision record 0087: 0087. A buffer with no alpha channel decides what the face can look like
Status: superseded by
ADR-0088,
which checked this record’s three reasons for not switching renderer and found none of them
standing. What is still true here is the finding: softbuffer hands Wayland a buffer with no alpha
channel, and a surface drawn at an opacity that buffer cannot carry arrives darkened with black
corners. What is wrong is everything decided from it. Refines
ADR-0047, whose
renderer choice this record does not overturn, and bounds one sentence of
ADR-0021.
Context
The design tokens gained a [glass] block: an opacity, a blur, a shadow. tacid-design emits it
into the compositor’s decoration and layerrule blocks and into the notification daemon’s
colours, and tacid-shell drew every container at that opacity. The compositor blurs what is
behind a Tacid layer surface; the surface draws at less than full alpha so there is something to
blur. That is glass, and it was wrong in a way nothing in the tree could have caught.
checks.*.face photographs the shell over two terminals. Three measurements, each taken after
fixing what the last one showed:
A command box over a terminal was srgb(19,25,30). The surface colour at glass.opacity over
the terminal’s srgb(40,85,119) is (23,34,43); over the toolkit’s own background it is
(19,25,30), to the byte. Iced fills the window with the theme’s background before any container
draws, so every container’s alpha was blending against a colour iced chose. Fixed by giving each
surface a window that paints nothing.
Then it was srgb(17,22,27) at every point, whatever was behind it. 0.86 × (20,26,31): the
colour premultiplied by its own alpha and composited over black. And the rounded corners — pixels
deliberately left clear so the desktop would show — came back rgb(0,0,0). Four black notches,
unnoticeable at a glance because the desktop behind them is nearly black.
The cause is a constant in a dependency. tiny-skia reaches Wayland through softbuffer,
whose create_buffer call names wl_shm::Format::Xrgb8888 — no alpha channel, hardcoded, no
argument, no fallback. Every surface this shell hands a compositor is opaque by construction.
ADR-0047 chose the software renderer for three reasons that all still hold: a face must come up in a virtual machine, on a server’s remote display, and on the first boot after a driver change. None of them mentioned what the choice costs in appearance, because nobody knew.
Decision
The shell asks whether alpha reaches the compositor, and draws opaque when it does not.
ALPHA_SURVIVES in theme.rs is that question. Today the answer is false for every build, and
three things follow from it rather than being decided separately:
Skin::glassis1.0instead of the token’s opacity, so a surface is its own colour rather than that colour darkened by the fraction that could not show through.Skin::windowpaints the palette’sbackgroundinstead of nothing, because on a buffer with no alpha channel an unpainted pixel is not the desktop, it is black.- The command box fills its layer surface instead of hugging its content, because the rest of a
hugged surface is unpainted, and unpainted is a slab of
backgroundover somebody’s windows rather than the windows.
Glass outside the shell stays. The compositor rounds windows at the token radius, blurs behind
Tacid’s layer namespaces and mako’s, and draws shadows; the notification daemon takes the opacity
as #RRGGBBAA; the lock screen takes the control corner. Those are generated files held to their
own programs’ parsers by checks.*.session, and they are correct on hardware today. The shell is
the one surface that does not join in, and saying that plainly is better than a token file whose
value reaches four consumers and is quietly dropped by the fifth.
The check measures the shell’s opacity rather than asserting its translucency. The sheet’s own
colour must be the palette’s surface exactly, which fails on a surface darkened by an alpha it
could not carry; and a rounded corner must not be black while the desktop behind it is not. Both
assertions fail on the two states above, and both are statements about a shell that is honest
about having no glass rather than about glass.
Consequences
The face is rounded, shadowed by the compositor, and opaque in its own surfaces. On a machine with a GPU, windows are rounded and blurred and Tacid’s own strip is not — which is a visible inconsistency and the honest state of it.
ALPHA_SURVIVES is a compile-time answer to what will become a runtime question. Adding
iced/wgpu beside tiny-skia does not make it true: iced falls back to tiny-skia on a machine
with no working GPU, and there the buffer is Xrgb8888 again. Whoever adds the second renderer
owns asking which one the process actually got, and the shell’s Cargo file says so where they will
be reading.
The token file keeps a [glass] block one consumer ignores. That is the right shape: the value is
a property of the design, the ignoring is a property of one renderer, and moving the value out
would mean putting it back when the renderer changes.
Alternatives rejected
Draw the opacity anyway and call it glass without blur. It is not that. It is the same colour fourteen percent darker with black notches at its corners, and it looks deliberate, which is worse than looking wrong.
Switch to wgpu now. It answers the appearance question and reopens all three of ADR-0047’s: several hundred crates, a graphics stack that must work before a pixel is drawn, and no face on the one boot where a person most needs to see what their machine thinks is wrong. And it would not be verifiable here — this environment has no GPU, so a wgpu build falls back to the renderer whose limitation this record is about, and the glass would ship unmeasured.
Patch or fork softbuffer to ask for Argb8888. A fork of a rendering dependency, carried
forever, for a visual effect. The upstream constant may well change; this record does not need it
to.
Drop [glass] from the tokens. Four consumers use it correctly today. Removing a value
because a fifth cannot is deciding the design by the weakest renderer.
Leave it and let somebody notice. They would not have. Every photograph looked plausible; the first two states were found by arithmetic on pixels, not by looking.