Skip to main content

agg_rust/
blur.rs

1//! Stack blur and recursive blur implementations.
2//!
3//! Port of `agg_blur.h`.
4//! Provides fast approximate Gaussian blur using the stack blur algorithm
5//! and an IIR recursive blur alternative.
6
7use crate::color::Rgba8;
8use crate::rendering_buffer::RowAccessor;
9
10// ============================================================================
11// Lookup tables for stack blur (fast division replacement)
12// ============================================================================
13
14/// Multiplication factors for fast division in stack blur.
15/// Indexed by radius (0..254). Replaces division with multiply+shift.
16#[rustfmt::skip]
17const STACK_BLUR8_MUL: [u32; 255] = [
18    512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
19    454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
20    482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
21    437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
22    497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
23    320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
24    446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
25    329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,
26    505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,
27    399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,
28    324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,
29    268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,
30    451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,
31    385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,
32    332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,
33    289,287,285,282,280,278,275,273,271,269,267,265,263,261,259,
34];
35
36/// Right-shift amounts for fast division in stack blur.
37/// Indexed by radius (0..254).
38#[rustfmt::skip]
39const STACK_BLUR8_SHR: [u32; 255] = [
40     9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
41    17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
42    19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
43    20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
44    21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
45    21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
46    22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
47    22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
48    23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
49    23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
50    23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
51    23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
52    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
53    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
54    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
55    24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
56];
57
58// ============================================================================
59// Stack blur for RGBA32
60// ============================================================================
61
62/// Apply stack blur to an RGBA32 rendering buffer.
63///
64/// Port of C++ `stack_blur_rgba32<Img>`.
65/// Operates in-place on the buffer with independent horizontal and vertical
66/// radii. Radius is clamped to 254.
67///
68/// Component order: R=0, G=1, B=2, A=3.
69pub fn stack_blur_rgba32(rbuf: &mut RowAccessor, mut rx: u32, mut ry: u32) {
70    let w = rbuf.width() as usize;
71    let h = rbuf.height() as usize;
72    if w == 0 || h == 0 {
73        return;
74    }
75    let wm = w - 1;
76    let hm = h - 1;
77
78    // Horizontal pass
79    if rx > 0 {
80        if rx > 254 {
81            rx = 254;
82        }
83        let rx = rx as usize;
84        let div = rx * 2 + 1;
85        let mul_sum = STACK_BLUR8_MUL[rx] as u64;
86        let shr_sum = STACK_BLUR8_SHR[rx];
87
88        let mut stack = vec![[0u8; 4]; div];
89
90        for y in 0..h {
91            let row = unsafe {
92                let ptr = rbuf.row_ptr(y as i32);
93                std::slice::from_raw_parts_mut(ptr, w * 4)
94            };
95
96            let mut sum_r: u64 = 0;
97            let mut sum_g: u64 = 0;
98            let mut sum_b: u64 = 0;
99            let mut sum_a: u64 = 0;
100            let mut sum_in_r: u64 = 0;
101            let mut sum_in_g: u64 = 0;
102            let mut sum_in_b: u64 = 0;
103            let mut sum_in_a: u64 = 0;
104            let mut sum_out_r: u64 = 0;
105            let mut sum_out_g: u64 = 0;
106            let mut sum_out_b: u64 = 0;
107            let mut sum_out_a: u64 = 0;
108
109            // Initialize with first pixel (edge extension)
110            let src_r = row[0] as u64;
111            let src_g = row[1] as u64;
112            let src_b = row[2] as u64;
113            let src_a = row[3] as u64;
114
115            for i in 0..=rx {
116                stack[i] = [row[0], row[1], row[2], row[3]];
117                let w = (i + 1) as u64;
118                sum_r += src_r * w;
119                sum_g += src_g * w;
120                sum_b += src_b * w;
121                sum_a += src_a * w;
122                sum_out_r += src_r;
123                sum_out_g += src_g;
124                sum_out_b += src_b;
125                sum_out_a += src_a;
126            }
127
128            let mut src_off = 0usize; // offset into row for source pixel
129            for i in 1..=rx {
130                if i <= wm {
131                    src_off = i * 4;
132                }
133                stack[i + rx] = [row[src_off], row[src_off + 1], row[src_off + 2], row[src_off + 3]];
134                let w = (rx + 1 - i) as u64;
135                sum_r += row[src_off] as u64 * w;
136                sum_g += row[src_off + 1] as u64 * w;
137                sum_b += row[src_off + 2] as u64 * w;
138                sum_a += row[src_off + 3] as u64 * w;
139                sum_in_r += row[src_off] as u64;
140                sum_in_g += row[src_off + 1] as u64;
141                sum_in_b += row[src_off + 2] as u64;
142                sum_in_a += row[src_off + 3] as u64;
143            }
144
145            let mut stack_ptr = rx;
146            let mut xp = rx;
147            if xp > wm {
148                xp = wm;
149            }
150            src_off = xp * 4;
151
152            for x in 0..w {
153                let dst_off = x * 4;
154                row[dst_off] = ((sum_r * mul_sum) >> shr_sum) as u8;
155                row[dst_off + 1] = ((sum_g * mul_sum) >> shr_sum) as u8;
156                row[dst_off + 2] = ((sum_b * mul_sum) >> shr_sum) as u8;
157                row[dst_off + 3] = ((sum_a * mul_sum) >> shr_sum) as u8;
158
159                sum_r -= sum_out_r;
160                sum_g -= sum_out_g;
161                sum_b -= sum_out_b;
162                sum_a -= sum_out_a;
163
164                let mut stack_start = stack_ptr + div - rx;
165                if stack_start >= div {
166                    stack_start -= div;
167                }
168
169                let sp = &stack[stack_start];
170                sum_out_r -= sp[0] as u64;
171                sum_out_g -= sp[1] as u64;
172                sum_out_b -= sp[2] as u64;
173                sum_out_a -= sp[3] as u64;
174
175                if xp < wm {
176                    src_off += 4;
177                    xp += 1;
178                }
179
180                stack[stack_start] = [row[src_off], row[src_off + 1], row[src_off + 2], row[src_off + 3]];
181
182                sum_in_r += row[src_off] as u64;
183                sum_in_g += row[src_off + 1] as u64;
184                sum_in_b += row[src_off + 2] as u64;
185                sum_in_a += row[src_off + 3] as u64;
186                sum_r += sum_in_r;
187                sum_g += sum_in_g;
188                sum_b += sum_in_b;
189                sum_a += sum_in_a;
190
191                stack_ptr += 1;
192                if stack_ptr >= div {
193                    stack_ptr = 0;
194                }
195
196                let sp = &stack[stack_ptr];
197                sum_out_r += sp[0] as u64;
198                sum_out_g += sp[1] as u64;
199                sum_out_b += sp[2] as u64;
200                sum_out_a += sp[3] as u64;
201                sum_in_r -= sp[0] as u64;
202                sum_in_g -= sp[1] as u64;
203                sum_in_b -= sp[2] as u64;
204                sum_in_a -= sp[3] as u64;
205            }
206        }
207    }
208
209    // Vertical pass
210    if ry > 0 {
211        if ry > 254 {
212            ry = 254;
213        }
214        let ry = ry as usize;
215        let div = ry * 2 + 1;
216        let mul_sum = STACK_BLUR8_MUL[ry] as u64;
217        let shr_sum = STACK_BLUR8_SHR[ry];
218
219        let mut stack = vec![[0u8; 4]; div];
220        let stride = rbuf.stride() as isize;
221
222        for x in 0..w {
223            let base_ptr = unsafe { rbuf.row_ptr(0).add(x * 4) };
224
225            let mut sum_r: u64 = 0;
226            let mut sum_g: u64 = 0;
227            let mut sum_b: u64 = 0;
228            let mut sum_a: u64 = 0;
229            let mut sum_in_r: u64 = 0;
230            let mut sum_in_g: u64 = 0;
231            let mut sum_in_b: u64 = 0;
232            let mut sum_in_a: u64 = 0;
233            let mut sum_out_r: u64 = 0;
234            let mut sum_out_g: u64 = 0;
235            let mut sum_out_b: u64 = 0;
236            let mut sum_out_a: u64 = 0;
237
238            let src_pix = unsafe { std::slice::from_raw_parts(base_ptr, 4) };
239            for i in 0..=ry {
240                stack[i] = [src_pix[0], src_pix[1], src_pix[2], src_pix[3]];
241                let w = (i + 1) as u64;
242                sum_r += src_pix[0] as u64 * w;
243                sum_g += src_pix[1] as u64 * w;
244                sum_b += src_pix[2] as u64 * w;
245                sum_a += src_pix[3] as u64 * w;
246                sum_out_r += src_pix[0] as u64;
247                sum_out_g += src_pix[1] as u64;
248                sum_out_b += src_pix[2] as u64;
249                sum_out_a += src_pix[3] as u64;
250            }
251
252            let mut src_ptr = base_ptr;
253            for i in 1..=ry {
254                if i <= hm {
255                    src_ptr = unsafe { src_ptr.offset(stride) };
256                }
257                let p = unsafe { std::slice::from_raw_parts(src_ptr, 4) };
258                stack[i + ry] = [p[0], p[1], p[2], p[3]];
259                let w = (ry + 1 - i) as u64;
260                sum_r += p[0] as u64 * w;
261                sum_g += p[1] as u64 * w;
262                sum_b += p[2] as u64 * w;
263                sum_a += p[3] as u64 * w;
264                sum_in_r += p[0] as u64;
265                sum_in_g += p[1] as u64;
266                sum_in_b += p[2] as u64;
267                sum_in_a += p[3] as u64;
268            }
269
270            let mut stack_ptr = ry;
271            let mut yp = ry;
272            if yp > hm {
273                yp = hm;
274            }
275            src_ptr = unsafe { base_ptr.offset(yp as isize * stride) };
276            let mut dst_ptr = base_ptr;
277
278            for _y in 0..h {
279                let dst = unsafe { std::slice::from_raw_parts_mut(dst_ptr, 4) };
280                dst[0] = ((sum_r * mul_sum) >> shr_sum) as u8;
281                dst[1] = ((sum_g * mul_sum) >> shr_sum) as u8;
282                dst[2] = ((sum_b * mul_sum) >> shr_sum) as u8;
283                dst[3] = ((sum_a * mul_sum) >> shr_sum) as u8;
284                dst_ptr = unsafe { dst_ptr.offset(stride) };
285
286                sum_r -= sum_out_r;
287                sum_g -= sum_out_g;
288                sum_b -= sum_out_b;
289                sum_a -= sum_out_a;
290
291                let mut stack_start = stack_ptr + div - ry;
292                if stack_start >= div {
293                    stack_start -= div;
294                }
295
296                let sp = &stack[stack_start];
297                sum_out_r -= sp[0] as u64;
298                sum_out_g -= sp[1] as u64;
299                sum_out_b -= sp[2] as u64;
300                sum_out_a -= sp[3] as u64;
301
302                if yp < hm {
303                    src_ptr = unsafe { src_ptr.offset(stride) };
304                    yp += 1;
305                }
306
307                let p = unsafe { std::slice::from_raw_parts(src_ptr, 4) };
308                stack[stack_start] = [p[0], p[1], p[2], p[3]];
309
310                sum_in_r += p[0] as u64;
311                sum_in_g += p[1] as u64;
312                sum_in_b += p[2] as u64;
313                sum_in_a += p[3] as u64;
314                sum_r += sum_in_r;
315                sum_g += sum_in_g;
316                sum_b += sum_in_b;
317                sum_a += sum_in_a;
318
319                stack_ptr += 1;
320                if stack_ptr >= div {
321                    stack_ptr = 0;
322                }
323
324                let sp = &stack[stack_ptr];
325                sum_out_r += sp[0] as u64;
326                sum_out_g += sp[1] as u64;
327                sum_out_b += sp[2] as u64;
328                sum_out_a += sp[3] as u64;
329                sum_in_r -= sp[0] as u64;
330                sum_in_g -= sp[1] as u64;
331                sum_in_b -= sp[2] as u64;
332                sum_in_a -= sp[3] as u64;
333            }
334        }
335    }
336}
337
338// ============================================================================
339// Recursive blur (IIR Gaussian approximation)
340// ============================================================================
341
342/// Recursive blur calculator for RGBA channels.
343///
344/// Port of C++ `recursive_blur_calc_rgba<double>`.
345#[derive(Clone, Copy, Default)]
346struct RecursiveBlurCalcRgba {
347    r: f64,
348    g: f64,
349    b: f64,
350    a: f64,
351}
352
353impl RecursiveBlurCalcRgba {
354    fn from_pix(c: &Rgba8) -> Self {
355        Self {
356            r: c.r as f64,
357            g: c.g as f64,
358            b: c.b as f64,
359            a: c.a as f64,
360        }
361    }
362
363    fn calc(
364        b_coeff: f64,
365        b1: f64,
366        b2: f64,
367        b3: f64,
368        c1: &Self,
369        c2: &Self,
370        c3: &Self,
371        c4: &Self,
372    ) -> Self {
373        Self {
374            r: b_coeff * c1.r + b1 * c2.r + b2 * c3.r + b3 * c4.r,
375            g: b_coeff * c1.g + b1 * c2.g + b2 * c3.g + b3 * c4.g,
376            b: b_coeff * c1.b + b1 * c2.b + b2 * c3.b + b3 * c4.b,
377            a: b_coeff * c1.a + b1 * c2.a + b2 * c3.a + b3 * c4.a,
378        }
379    }
380
381    fn to_pix(&self) -> Rgba8 {
382        Rgba8::new(
383            self.r as u32,
384            self.g as u32,
385            self.b as u32,
386            self.a as u32,
387        )
388    }
389}
390
391/// Apply recursive (IIR) Gaussian blur to an RGBA32 rendering buffer.
392///
393/// Port of C++ `recursive_blur<rgba8, recursive_blur_calc_rgba<>>`.
394/// Uses Young-van Vliet recursive Gaussian approximation.
395/// Operates in-place on the buffer.
396pub fn recursive_blur_rgba32(rbuf: &mut RowAccessor, radius: f64) {
397    recursive_blur_rgba32_x(rbuf, radius);
398    recursive_blur_rgba32_y(rbuf, radius);
399}
400
401/// Horizontal recursive blur pass.
402pub fn recursive_blur_rgba32_x(rbuf: &mut RowAccessor, radius: f64) {
403    if radius < 0.62 {
404        return;
405    }
406    let w = rbuf.width() as usize;
407    let h = rbuf.height() as usize;
408    if w < 3 {
409        return;
410    }
411
412    let s = radius * 0.5;
413    let q = if s < 2.5 {
414        3.97156 - 4.14554 * (1.0 - 0.26891 * s).sqrt()
415    } else {
416        0.98711 * s - 0.96330
417    };
418
419    let q2 = q * q;
420    let q3 = q2 * q;
421
422    let b0 = 1.0 / (1.578250 + 2.444130 * q + 1.428100 * q2 + 0.422205 * q3);
423    let mut b1 = 2.44413 * q + 2.85619 * q2 + 1.26661 * q3;
424    let mut b2 = -1.42810 * q2 - 1.26661 * q3;
425    let mut b3 = 0.422205 * q3;
426    let b = 1.0 - (b1 + b2 + b3) * b0;
427
428    b1 *= b0;
429    b2 *= b0;
430    b3 *= b0;
431
432    let wm = w as i32 - 1;
433
434    let mut sum1 = vec![RecursiveBlurCalcRgba::default(); w];
435    let mut sum2 = vec![RecursiveBlurCalcRgba::default(); w];
436    let mut buf = vec![Rgba8::new(0, 0, 0, 0); w];
437
438    for y in 0..h {
439        // Read pixels from row
440        let row = unsafe {
441            let ptr = rbuf.row_ptr(y as i32);
442            std::slice::from_raw_parts(ptr, w * 4)
443        };
444
445        let pix = |x: usize| -> Rgba8 {
446            let off = x * 4;
447            Rgba8::new(
448                row[off] as u32,
449                row[off + 1] as u32,
450                row[off + 2] as u32,
451                row[off + 3] as u32,
452            )
453        };
454
455        // Forward pass
456        let c = RecursiveBlurCalcRgba::from_pix(&pix(0));
457        sum1[0] = RecursiveBlurCalcRgba::calc(b, b1, b2, b3, &c, &c, &c, &c);
458        let c = RecursiveBlurCalcRgba::from_pix(&pix(1));
459        sum1[1] = RecursiveBlurCalcRgba::calc(b, b1, b2, b3, &c, &sum1[0], &sum1[0], &sum1[0]);
460        let c = RecursiveBlurCalcRgba::from_pix(&pix(2));
461        sum1[2] = RecursiveBlurCalcRgba::calc(b, b1, b2, b3, &c, &sum1[1], &sum1[0], &sum1[0]);
462
463        for x in 3..w {
464            let c = RecursiveBlurCalcRgba::from_pix(&pix(x));
465            sum1[x] = RecursiveBlurCalcRgba::calc(
466                b, b1, b2, b3, &c, &sum1[x - 1], &sum1[x - 2], &sum1[x - 3],
467            );
468        }
469
470        // Backward pass
471        let wmi = wm as usize;
472        sum2[wmi] = RecursiveBlurCalcRgba::calc(
473            b, b1, b2, b3, &sum1[wmi], &sum1[wmi], &sum1[wmi], &sum1[wmi],
474        );
475        sum2[wmi - 1] = RecursiveBlurCalcRgba::calc(
476            b, b1, b2, b3, &sum1[wmi - 1], &sum2[wmi], &sum2[wmi], &sum2[wmi],
477        );
478        sum2[wmi - 2] = RecursiveBlurCalcRgba::calc(
479            b, b1, b2, b3, &sum1[wmi - 2], &sum2[wmi - 1], &sum2[wmi], &sum2[wmi],
480        );
481        buf[wmi] = sum2[wmi].to_pix();
482        buf[wmi - 1] = sum2[wmi - 1].to_pix();
483        buf[wmi - 2] = sum2[wmi - 2].to_pix();
484
485        for x in (0..=(wmi as i32 - 3)).rev() {
486            let x = x as usize;
487            sum2[x] = RecursiveBlurCalcRgba::calc(
488                b, b1, b2, b3, &sum1[x], &sum2[x + 1], &sum2[x + 2], &sum2[x + 3],
489            );
490            buf[x] = sum2[x].to_pix();
491        }
492
493        // Write back to row
494        let row = unsafe {
495            let ptr = rbuf.row_ptr(y as i32);
496            std::slice::from_raw_parts_mut(ptr, w * 4)
497        };
498        for x in 0..w {
499            let off = x * 4;
500            row[off] = buf[x].r;
501            row[off + 1] = buf[x].g;
502            row[off + 2] = buf[x].b;
503            row[off + 3] = buf[x].a;
504        }
505    }
506}
507
508/// Vertical recursive blur pass.
509pub fn recursive_blur_rgba32_y(rbuf: &mut RowAccessor, radius: f64) {
510    if radius < 0.62 {
511        return;
512    }
513    let w = rbuf.width() as usize;
514    let h = rbuf.height() as usize;
515    if h < 3 {
516        return;
517    }
518
519    let s = radius * 0.5;
520    let q = if s < 2.5 {
521        3.97156 - 4.14554 * (1.0 - 0.26891 * s).sqrt()
522    } else {
523        0.98711 * s - 0.96330
524    };
525
526    let q2 = q * q;
527    let q3 = q2 * q;
528
529    let b0 = 1.0 / (1.578250 + 2.444130 * q + 1.428100 * q2 + 0.422205 * q3);
530    let mut b1 = 2.44413 * q + 2.85619 * q2 + 1.26661 * q3;
531    let mut b2 = -1.42810 * q2 - 1.26661 * q3;
532    let mut b3 = 0.422205 * q3;
533    let b = 1.0 - (b1 + b2 + b3) * b0;
534
535    b1 *= b0;
536    b2 *= b0;
537    b3 *= b0;
538
539    let hm = h as i32 - 1;
540    let stride = rbuf.stride() as isize;
541
542    let mut sum1 = vec![RecursiveBlurCalcRgba::default(); h];
543    let mut sum2 = vec![RecursiveBlurCalcRgba::default(); h];
544    let mut buf = vec![Rgba8::new(0, 0, 0, 0); h];
545
546    for x in 0..w {
547        let base_ptr = unsafe { rbuf.row_ptr(0).add(x * 4) };
548
549        let pix = |yi: usize| -> Rgba8 {
550            let p = unsafe { std::slice::from_raw_parts(base_ptr.offset(yi as isize * stride), 4) };
551            Rgba8::new(p[0] as u32, p[1] as u32, p[2] as u32, p[3] as u32)
552        };
553
554        // Forward pass
555        let c = RecursiveBlurCalcRgba::from_pix(&pix(0));
556        sum1[0] = RecursiveBlurCalcRgba::calc(b, b1, b2, b3, &c, &c, &c, &c);
557        let c = RecursiveBlurCalcRgba::from_pix(&pix(1));
558        sum1[1] = RecursiveBlurCalcRgba::calc(b, b1, b2, b3, &c, &sum1[0], &sum1[0], &sum1[0]);
559        let c = RecursiveBlurCalcRgba::from_pix(&pix(2));
560        sum1[2] = RecursiveBlurCalcRgba::calc(b, b1, b2, b3, &c, &sum1[1], &sum1[0], &sum1[0]);
561
562        for yi in 3..h {
563            let c = RecursiveBlurCalcRgba::from_pix(&pix(yi));
564            sum1[yi] = RecursiveBlurCalcRgba::calc(
565                b, b1, b2, b3, &c, &sum1[yi - 1], &sum1[yi - 2], &sum1[yi - 3],
566            );
567        }
568
569        // Backward pass
570        let hmi = hm as usize;
571        sum2[hmi] = RecursiveBlurCalcRgba::calc(
572            b, b1, b2, b3, &sum1[hmi], &sum1[hmi], &sum1[hmi], &sum1[hmi],
573        );
574        sum2[hmi - 1] = RecursiveBlurCalcRgba::calc(
575            b, b1, b2, b3, &sum1[hmi - 1], &sum2[hmi], &sum2[hmi], &sum2[hmi],
576        );
577        sum2[hmi - 2] = RecursiveBlurCalcRgba::calc(
578            b, b1, b2, b3, &sum1[hmi - 2], &sum2[hmi - 1], &sum2[hmi], &sum2[hmi],
579        );
580        buf[hmi] = sum2[hmi].to_pix();
581        buf[hmi - 1] = sum2[hmi - 1].to_pix();
582        buf[hmi - 2] = sum2[hmi - 2].to_pix();
583
584        for yi in (0..=(hmi as i32 - 3)).rev() {
585            let yi = yi as usize;
586            sum2[yi] = RecursiveBlurCalcRgba::calc(
587                b, b1, b2, b3, &sum1[yi], &sum2[yi + 1], &sum2[yi + 2], &sum2[yi + 3],
588            );
589            buf[yi] = sum2[yi].to_pix();
590        }
591
592        // Write back column
593        for yi in 0..h {
594            let p = unsafe {
595                std::slice::from_raw_parts_mut(base_ptr.offset(yi as isize * stride), 4)
596            };
597            p[0] = buf[yi].r;
598            p[1] = buf[yi].g;
599            p[2] = buf[yi].b;
600            p[3] = buf[yi].a;
601        }
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use crate::rendering_buffer::RowAccessor;
609
610    fn make_buffer(w: u32, h: u32) -> (Vec<u8>, RowAccessor) {
611        let stride = (w * 4) as i32;
612        let buf = vec![0u8; (h * w * 4) as usize];
613        let mut ra = RowAccessor::new();
614        unsafe {
615            ra.attach(buf.as_ptr() as *mut u8, w, h, stride);
616        }
617        (buf, ra)
618    }
619
620    fn set_pixel(ra: &mut RowAccessor, x: usize, y: usize, r: u8, g: u8, b: u8, a: u8) {
621        let row = unsafe {
622            let ptr = ra.row_ptr(y as i32);
623            std::slice::from_raw_parts_mut(ptr, (ra.width() as usize) * 4)
624        };
625        let off = x * 4;
626        row[off] = r;
627        row[off + 1] = g;
628        row[off + 2] = b;
629        row[off + 3] = a;
630    }
631
632    fn get_pixel(ra: &RowAccessor, x: usize, y: usize) -> [u8; 4] {
633        let row = unsafe {
634            let ptr = ra.row_ptr(y as i32);
635            std::slice::from_raw_parts(ptr, (ra.width() as usize) * 4)
636        };
637        let off = x * 4;
638        [row[off], row[off + 1], row[off + 2], row[off + 3]]
639    }
640
641    #[test]
642    fn test_stack_blur_zero_radius() {
643        let (_buf, mut ra) = make_buffer(10, 10);
644        set_pixel(&mut ra, 5, 5, 255, 0, 0, 255);
645
646        let before = get_pixel(&ra, 5, 5);
647        stack_blur_rgba32(&mut ra, 0, 0);
648        let after = get_pixel(&ra, 5, 5);
649        assert_eq!(before, after);
650    }
651
652    #[test]
653    fn test_stack_blur_spreads_pixel() {
654        let (_buf, mut ra) = make_buffer(20, 20);
655        // Set center pixel to white
656        set_pixel(&mut ra, 10, 10, 255, 255, 255, 255);
657
658        stack_blur_rgba32(&mut ra, 3, 3);
659
660        // Center should still have some value
661        let center = get_pixel(&ra, 10, 10);
662        assert!(center[0] > 0, "center should have some red after blur");
663
664        // Neighboring pixel should have received some blur
665        let neighbor = get_pixel(&ra, 11, 10);
666        assert!(neighbor[0] > 0, "neighbor should have some red after blur");
667    }
668
669    #[test]
670    fn test_stack_blur_uniform_stays_uniform() {
671        let (_buf, mut ra) = make_buffer(10, 10);
672        // Fill with uniform gray
673        for y in 0..10 {
674            for x in 0..10 {
675                set_pixel(&mut ra, x, y, 128, 128, 128, 255);
676            }
677        }
678
679        stack_blur_rgba32(&mut ra, 2, 2);
680
681        // All pixels should remain approximately 128
682        for y in 0..10 {
683            for x in 0..10 {
684                let p = get_pixel(&ra, x, y);
685                assert!(
686                    (p[0] as i32 - 128).abs() <= 1,
687                    "pixel ({x},{y}) r={} should be ~128",
688                    p[0]
689                );
690            }
691        }
692    }
693
694    #[test]
695    fn test_recursive_blur_zero_radius() {
696        let (_buf, mut ra) = make_buffer(10, 10);
697        set_pixel(&mut ra, 5, 5, 255, 0, 0, 255);
698
699        let before = get_pixel(&ra, 5, 5);
700        recursive_blur_rgba32(&mut ra, 0.0); // radius < 0.62, no-op
701        let after = get_pixel(&ra, 5, 5);
702        assert_eq!(before, after);
703    }
704
705    #[test]
706    fn test_recursive_blur_spreads_pixel() {
707        let (_buf, mut ra) = make_buffer(20, 20);
708        set_pixel(&mut ra, 10, 10, 255, 255, 255, 255);
709
710        recursive_blur_rgba32(&mut ra, 3.0);
711
712        let center = get_pixel(&ra, 10, 10);
713        assert!(center[0] > 0);
714
715        let neighbor = get_pixel(&ra, 11, 10);
716        assert!(neighbor[0] > 0, "neighbor should have some value after blur");
717    }
718
719    #[test]
720    fn test_recursive_blur_uniform_stays_uniform() {
721        let (_buf, mut ra) = make_buffer(10, 10);
722        for y in 0..10 {
723            for x in 0..10 {
724                set_pixel(&mut ra, x, y, 100, 100, 100, 255);
725            }
726        }
727
728        recursive_blur_rgba32(&mut ra, 2.0);
729
730        for y in 0..10 {
731            for x in 0..10 {
732                let p = get_pixel(&ra, x, y);
733                assert!(
734                    (p[0] as i32 - 100).abs() <= 2,
735                    "pixel ({x},{y}) r={} should be ~100",
736                    p[0]
737                );
738            }
739        }
740    }
741}