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