diff options
| author | srdusr <[email protected]> | 2024-10-30 23:52:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-10-30 23:52:00 +0200 |
| commit | 9748bca006cd2498c4c0f2730d8789c667bda52b (patch) | |
| tree | f917af35cd32f9745c940253f2400ab0b92f0b0a /crates/wayland/src/blur.rs | |
| parent | 3509cf6127185d9fed5a63ceca18a003213a20d7 (diff) | |
| download | srdwm-9748bca006cd2498c4c0f2730d8789c667bda52b.tar.gz srdwm-9748bca006cd2498c4c0f2730d8789c667bda52b.zip | |
Add srdwm's own native session-lock screen (blur, PAM auth)
A real ext-session-lock-v1 implementation drawn by the compositor
itself, not a hand-off to an external locker process: a live-blurred
background, PAM authentication on a background thread, and its own
config surface (lock_config.rs) rather than hardcoded appearance.
Diffstat (limited to 'crates/wayland/src/blur.rs')
| -rw-r--r-- | crates/wayland/src/blur.rs | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/crates/wayland/src/blur.rs b/crates/wayland/src/blur.rs new file mode 100644 index 0000000..7560fa2 --- /dev/null +++ b/crates/wayland/src/blur.rs @@ -0,0 +1,190 @@ +//! CPU box blur for the native lock screen's captured background +//! (`native_lock.rs`). Not a true Gaussian blur - no blur primitive is +//! available without a GPU shader (the udev backend's `PixmanRenderer` is +//! software-only), the same "approximate falloff over true blur" tradeoff +//! `decoration::shadow_bitmap` already accepts for drop shadows. Three +//! box-blur passes approximate a Gaussian closely enough to read as a +//! real blur rather than an obviously-boxy one, a standard trick (Adobe's +//! own CSS `filter: blur()` polyfills use the same three-pass +//! approximation). +//! +//! Runs once, at lock time, on the just-captured screen content - not +//! per frame - so a straightforward `O(pixels)` sliding-window +//! implementation (not `O(pixels * radius)`, which would make a large +//! radius on a real screen resolution noticeably slow even as a one-time +//! cost) is what actually matters here, not raw simplicity. + +/// Blurs `buf` (a BGRA8/XRGB8888 pixel buffer, 4 bytes per pixel, alpha +/// byte untouched either way) in place. `radius` of `0` is a deliberate +/// no-op, not clamped up to some minimum - see `LockConfig::blur_radius`'s +/// own doc comment. +pub(crate) fn box_blur(buf: &mut [u8], width: usize, height: usize, radius: u32) { + if radius == 0 || width == 0 || height == 0 { + return; + } + let radius = radius as usize; + // Three passes, alternating axis, approximates a Gaussian kernel. + for _ in 0..3 { + blur_horizontal(buf, width, height, radius); + blur_vertical(buf, width, height, radius); + } +} + +/// Sliding-window box blur along each row: the window's running sum is +/// updated by removing the pixel that just left it and adding the one +/// that just entered, rather than re-summing `2 * radius + 1` pixels at +/// every single output pixel. +fn blur_horizontal(buf: &mut [u8], width: usize, height: usize, radius: usize) { + let mut row = vec![0u8; width * 4]; + for y in 0..height { + let row_start = y * width * 4; + row.copy_from_slice(&buf[row_start..row_start + width * 4]); + let mut sum = [0i64; 3]; + let mut count = 0i64; + for x in 0..=radius.min(width.saturating_sub(1)) { + add_pixel(&row, x, &mut sum, &mut count); + } + for x in 0..width { + write_average(buf, row_start + x * 4, &sum, count); + let leaving = x as isize - radius as isize; + if leaving >= 0 { + remove_pixel(&row, leaving as usize, &mut sum, &mut count); + } + let entering = x + radius + 1; + if entering < width { + add_pixel(&row, entering, &mut sum, &mut count); + } + } + } +} + +/// Same sliding-window technique as [`blur_horizontal`], transposed -- +/// copies each column into a contiguous scratch buffer first so the +/// window updates are still sequential memory access, not a +/// `width * 4`-strided walk on every add/remove. +fn blur_vertical(buf: &mut [u8], width: usize, height: usize, radius: usize) { + let stride = width * 4; + let mut col = vec![0u8; height * 4]; + for x in 0..width { + for y in 0..height { + let idx = y * stride + x * 4; + col[y * 4..y * 4 + 4].copy_from_slice(&buf[idx..idx + 4]); + } + let mut sum = [0i64; 3]; + let mut count = 0i64; + for y in 0..=radius.min(height.saturating_sub(1)) { + add_pixel(&col, y, &mut sum, &mut count); + } + for y in 0..height { + write_average(buf, y * stride + x * 4, &sum, count); + let leaving = y as isize - radius as isize; + if leaving >= 0 { + remove_pixel(&col, leaving as usize, &mut sum, &mut count); + } + let entering = y + radius + 1; + if entering < height { + add_pixel(&col, entering, &mut sum, &mut count); + } + } + } +} + +fn add_pixel(buf: &[u8], index: usize, sum: &mut [i64; 3], count: &mut i64) { + let i = index * 4; + sum[0] += buf[i] as i64; + sum[1] += buf[i + 1] as i64; + sum[2] += buf[i + 2] as i64; + *count += 1; +} + +fn remove_pixel(buf: &[u8], index: usize, sum: &mut [i64; 3], count: &mut i64) { + let i = index * 4; + sum[0] -= buf[i] as i64; + sum[1] -= buf[i + 1] as i64; + sum[2] -= buf[i + 2] as i64; + *count -= 1; +} + +/// Writes the window's current average into `buf` at byte offset `at`, +/// leaving the alpha byte (`at + 3`) untouched - the captured background +/// is always fully opaque, so there is nothing meaningful to blur there. +fn write_average(buf: &mut [u8], at: usize, sum: &[i64; 3], count: i64) { + buf[at] = (sum[0] / count) as u8; + buf[at + 1] = (sum[1] / count) as u8; + buf[at + 2] = (sum[2] / count) as u8; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_radius_is_a_no_op() { + let mut buf = vec![10, 20, 30, 255, 200, 100, 50, 255]; + let original = buf.clone(); + box_blur(&mut buf, 2, 1, 0); + assert_eq!(buf, original); + } + + #[test] + fn a_uniform_image_is_unchanged_by_blurring() { + // Blurring a flat color must not shift it - the clearest possible + // regression check for an off-by-one in the sliding window (a + // wrong window size would still average to *something*, but a + // biased one, not the same flat color back). + let (w, h) = (10, 10); + let mut buf = vec![0u8; w * h * 4]; + for px in buf.chunks_exact_mut(4) { + px.copy_from_slice(&[40, 80, 120, 255]); + } + box_blur(&mut buf, w, h, 3); + for px in buf.chunks_exact(4) { + assert_eq!(px, [40, 80, 120, 255]); + } + } + + #[test] + fn alpha_is_never_touched() { + let (w, h) = (4, 4); + let mut buf = vec![0u8; w * h * 4]; + for (i, px) in buf.chunks_exact_mut(4).enumerate() { + px.copy_from_slice(&[i as u8, i as u8, i as u8, (i * 17) as u8]); + } + box_blur(&mut buf, w, h, 2); + for (i, px) in buf.chunks_exact(4).enumerate() { + assert_eq!(px[3], (i * 17) as u8, "alpha byte at pixel {i} must survive unchanged"); + } + } + + #[test] + fn a_bright_spot_spreads_into_its_dark_neighbours_with_falloff() { + // Large enough that a genuinely-far corner exists even after three + // cascaded passes (each pass spreads influence roughly `radius` + // further, so three passes of radius 2 reach noticeably past 2 + // pixels out - a smaller canvas made this assert something the + // algorithm was never expected to guarantee). + let (w, h) = (41, 41); + let mut buf = vec![0u8; w * h * 4]; + for px in buf.chunks_exact_mut(4) { + px.copy_from_slice(&[0, 0, 0, 255]); + } + let (cx, cy) = (20, 20); + let center = (cy * w + cx) * 4; + buf[center..center + 3].copy_from_slice(&[255, 255, 255]); + + box_blur(&mut buf, w, h, 2); + + let near = ((cy * w) + cx + 1) * 4; // immediately right of center + let far = ((cy * w) + cx + 10) * 4; // well outside the blur's reach + let corner = 0; // top-left, far from the bright spot on both axes + assert!(buf[near] > 0, "a pixel next to the bright spot must pick up some of its brightness after blurring"); + assert!(buf[near] > buf[far], "brightness must fall off with distance, not spread flatly to the whole image"); + assert_eq!(buf[corner], 0, "a pixel far outside the blur's reach must stay untouched"); + } + + #[test] + fn zero_sized_buffer_does_not_panic() { + let mut buf: Vec<u8> = Vec::new(); + box_blur(&mut buf, 0, 0, 5); + } +} |