GPU background rendering (EGL/GLES2) with software chrome, plus expanded tests
The Ken Burns wallpaper pan and dim veil move to the GPU: the lock surface is wrapped in a wl_egl_window, the wallpaper is a textured quad whose shader applies the pan transform and veil (GPU bilinear makes sub-pixel panning free — the ~19ms/frame software bilinear is gone, idle CPU drops from 40% to ~0%), and the clock/pill/status chrome is still composed with tiny-skia and composited over. EGL init failure falls back to the fully-software path unchanged. Notable fixes along the way: - Text blending is now destination-aware: the old blend forced output alpha to 255, which is correct over the opaque background but made every glyph opaque in the chrome-only pixmap the GPU path composites. - wayland-client switches to its C backend for raw surface pointers, and khronos-egl/glow replace a hand-rolled EGL loader. Tests grew from 72 to 88: GPU pan_region parity with the software path (and edge/bounds/corner invariants), shader-uniform/EGL-attrib sanity, chrome bounding-rect tracking, and a pixel-level parity test proving the GPU split stays within double-rounding ULP range of the single-pass compose.
This commit is contained in:
parent
f0b66cd791
commit
3d2e35e003
11 changed files with 1774 additions and 89 deletions
|
|
@ -162,32 +162,60 @@ impl TextRenderer {
|
|||
if a == 0 {
|
||||
return;
|
||||
}
|
||||
blend_over_opaque(pixmap, px as u32, py as u32, r, g, b, a);
|
||||
blend_over(pixmap, px as u32, py as u32, r, g, b, a);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Alpha-blends a straight-alpha `(r, g, b, a)` source pixel over an
|
||||
/// **opaque** destination pixel (always true here — the lock screen
|
||||
/// background is painted fully opaque before any text or UI chrome).
|
||||
/// Because the destination alpha is always 255, the blended result is also
|
||||
/// opaque, so the `PremultipliedColorU8` invariant (`rgb <= a`) always holds.
|
||||
fn blend_over_opaque(pixmap: &mut Pixmap, x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
|
||||
/// Alpha-blends a straight-alpha `(r, g, b, a)` source pixel over a
|
||||
/// destination of *any* alpha. Two paths use this:
|
||||
///
|
||||
/// - **Full compose**: the background is painted fully opaque before any
|
||||
/// text, so the destination alpha is always 255 and the result is opaque
|
||||
/// (the exact formula below, kept byte-identical to the historic one).
|
||||
/// - **GPU chrome** (`compose_chrome`): text is drawn into a *transparent*
|
||||
/// pixmap that is later composited over the GPU background, so glyph
|
||||
/// edges must keep real alpha — a forced-255 blend here would make every
|
||||
/// glyph opaque and, composited over the background, visibly wrong.
|
||||
///
|
||||
/// Premultiplied source-over: `out = src_pm + dst_pm * (1 - src_a)`, which
|
||||
/// preserves the `PremultipliedColorU8` invariant (`rgb <= a`).
|
||||
fn blend_over(pixmap: &mut Pixmap, x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
|
||||
let idx = (y * pixmap.width() + x) as usize;
|
||||
let pixels = pixmap.pixels_mut();
|
||||
let Some(dst) = pixels.get(idx).copied() else {
|
||||
return;
|
||||
};
|
||||
let a32 = a as u32;
|
||||
let mix = |s: u8, d: u8| -> u8 { ((s as u32 * a32 + d as u32 * (255 - a32)) / 255) as u8 };
|
||||
let blended = PremultipliedColorU8::from_rgba(
|
||||
mix(r, dst.red()),
|
||||
mix(g, dst.green()),
|
||||
mix(b, dst.blue()),
|
||||
255,
|
||||
);
|
||||
if let Some(blended) = blended {
|
||||
let sa = a as u32;
|
||||
if dst.alpha() == 255 {
|
||||
// Opaque destination: the classic exact blend. RGB mixes toward the
|
||||
// source, alpha stays 255 — identical to the pre-split behavior so
|
||||
// the single-pass software path doesn't move a single pixel.
|
||||
let mix = |s: u8, d: u8| -> u8 { ((s as u32 * sa + d as u32 * (255 - sa)) / 255) as u8 };
|
||||
if let Some(blended) = PremultipliedColorU8::from_rgba(
|
||||
mix(r, dst.red()),
|
||||
mix(g, dst.green()),
|
||||
mix(b, dst.blue()),
|
||||
255,
|
||||
) {
|
||||
pixels[idx] = blended;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// General (possibly transparent) destination: premultiplied source-over.
|
||||
// out_a = sa + da*(255-sa)/255; out_rgb = src_rgb*sa/255 + dst_rgb*(1-sa).
|
||||
let da = dst.alpha() as u32;
|
||||
let out_a = (sa + da * (255 - sa) / 255) as u8;
|
||||
let out_c = |c: u8, dc: u8| -> u8 {
|
||||
(c as u32 * sa / 255 + dc as u32 * (255 - sa) / 255) as u8
|
||||
};
|
||||
if let Some(blended) = PremultipliedColorU8::from_rgba(
|
||||
out_c(r, dst.red()),
|
||||
out_c(g, dst.green()),
|
||||
out_c(b, dst.blue()),
|
||||
out_a,
|
||||
) {
|
||||
pixels[idx] = blended;
|
||||
}
|
||||
}
|
||||
|
|
@ -260,4 +288,45 @@ mod tests {
|
|||
"10%-alpha text must not render near-white, got {low_max}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_line_onto_transparent_keeps_real_alpha() {
|
||||
// Regression: the GPU path (compose_chrome) draws text into a
|
||||
// transparent pixmap that is later composited over the GPU background.
|
||||
// The old blend forced output alpha to 255, so every glyph became
|
||||
// opaque and, once composited, rendered visibly wrong (dark, covering
|
||||
// the background instead of blending). Glyph cores must carry real
|
||||
// alpha here so the final source-over composite is correct.
|
||||
let mut renderer = TextRenderer::new();
|
||||
|
||||
let mut t = Pixmap::new(200, 40).unwrap(); // starts transparent
|
||||
renderer.draw_line(
|
||||
&mut t,
|
||||
"12:34",
|
||||
"sans-serif",
|
||||
24.0,
|
||||
tiny_skia::Color::WHITE,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
// Full-coverage glyph cores are legitimately opaque, but the AA
|
||||
// edges must carry real intermediate alphas — the old forced-255
|
||||
// blend made *every* drawn pixel (edges included) fully opaque.
|
||||
let has_edge = t
|
||||
.pixels()
|
||||
.iter()
|
||||
.any(|p| p.alpha() > 0 && p.alpha() < 255);
|
||||
assert!(
|
||||
has_edge,
|
||||
"glyph AA edges must keep intermediate alphas onto a transparent pixmap"
|
||||
);
|
||||
// And a 50%-alpha draw must not produce fully-opaque pixels.
|
||||
let mut t2 = Pixmap::new(200, 40).unwrap();
|
||||
let half = tiny_skia::Color::from_rgba(1.0, 1.0, 1.0, 0.5).unwrap();
|
||||
renderer.draw_line(&mut t2, "12:34", "sans-serif", 24.0, half, 0.0, 0.0);
|
||||
assert!(
|
||||
t2.pixels().iter().all(|p| p.alpha() <= 128 + 3),
|
||||
"50%-alpha text onto transparent must stay ~half alpha"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue