srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/blur.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-10-30 23:52:00 +0200
committersrdusr <[email protected]>2024-10-30 23:52:00 +0200
commit9748bca006cd2498c4c0f2730d8789c667bda52b (patch)
treef917af35cd32f9745c940253f2400ab0b92f0b0a /crates/wayland/src/blur.rs
parent3509cf6127185d9fed5a63ceca18a003213a20d7 (diff)
downloadsrdwm-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.rs190
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);
+ }
+}