Skip to main content

denise_render/
blend.rs

1//! Pixel arithmetic.
2//!
3//! Everything here is integer. There is no `f32` in the rasteriser at all, which
4//! buys three things: no `libm` dependency in a `no_std` build, no FPU traffic on
5//! targets where that is expensive, and — most usefully — output that is
6//! bit-identical on x86 and ARM, so a golden-image test is a meaningful test.
7
8pub use denise::Paint;
9pub use denise::paint::scale_premul;
10use denise::paint::{LANES, mul_lanes};
11
12/// Premultiplies straight-alpha `0xAARRGGBB` words in place.
13///
14/// This is [`Paint::new`]'s arithmetic applied to a buffer: exact at both
15/// endpoints, done once at load time so a blit never divides by 255 per frame.
16/// Decoders hand out straight alpha; [`Canvas::blit`](crate::Canvas::blit)
17/// consumes premultiplied — this is the bridge between them.
18pub fn premultiply(pixels: &mut [u32]) {
19    for px in pixels {
20        match *px >> 24 {
21            255 => {}
22            0 => *px = 0,
23            a => {
24                let rb = mul_lanes(*px & LANES, a);
25                let g = mul_lanes((*px >> 8) & 0xFF, a);
26                *px = (a << 24) | rb | (g << 8);
27            }
28        }
29    }
30}
31
32/// Composites a premultiplied source over a destination pixel.
33///
34/// Both words are `0xAARRGGBB`. The alpha lane is composited too, so this is
35/// correct for an `Argb8888` target as well as an opaque `Xrgb8888` one.
36#[inline(always)]
37pub const fn source_over(dst: u32, src_premul: u32, alpha: u32) -> u32 {
38    let inv = 255 - alpha;
39    let rb = mul_lanes(dst & LANES, inv);
40    let ag = mul_lanes((dst >> 8) & LANES, inv);
41    // No lane can carry: s*a/255 + d*(255-a)/255 <= 255, and neither term can land
42    // exactly on .5, so the two roundings cannot both push up.
43    src_premul + (rb | (ag << 8))
44}
45
46/// Overwrites a span with an opaque word.
47#[inline]
48pub fn fill_span(span: &mut [u32], word: u32) {
49    span.fill(word);
50}
51
52/// Composites a constant paint over a span.
53#[inline]
54pub fn blend_span(span: &mut [u32], paint: Paint) {
55    if paint.is_invisible() {
56        return;
57    }
58    if paint.is_opaque() {
59        span.fill(paint.premultiplied());
60        return;
61    }
62    let src = paint.premultiplied();
63    let alpha = paint.alpha();
64    for px in span {
65        *px = source_over(*px, src, alpha);
66    }
67}
68
69/// Composites a paint over a single pixel at `coverage` (`0..=255`).
70#[inline]
71pub fn blend_pixel(dst: &mut u32, paint: Paint, coverage: u32) {
72    if coverage == 0 {
73        return;
74    }
75    let paint = if coverage == 255 {
76        paint
77    } else {
78        paint.scaled(coverage)
79    };
80    *dst = source_over(*dst, paint.premultiplied(), paint.alpha());
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use denise::Color;
87
88    #[test]
89    fn opaque_paint_replaces_destination() {
90        let p = Paint::new(Color::rgb(10, 20, 30));
91        assert!(p.is_opaque());
92        assert_eq!(
93            source_over(0xFFFF_FFFF, p.premultiplied(), p.alpha()),
94            0xFF0A_141E
95        );
96    }
97
98    #[test]
99    fn transparent_paint_preserves_destination() {
100        let p = Paint::new(Color::rgba(10, 20, 30, 0));
101        assert_eq!(
102            source_over(0xFF12_3456, p.premultiplied(), p.alpha()),
103            0xFF12_3456
104        );
105    }
106
107    #[test]
108    fn premultiply_is_exact_at_the_endpoints() {
109        // The whole point of the rounding correction: full alpha must round-trip.
110        let c = Color::rgba(0xAB, 0xCD, 0xEF, 255);
111        assert_eq!(Paint::new(c).premultiplied(), 0xFFAB_CDEF);
112        assert_eq!(
113            Paint::new(Color::rgba(0xAB, 0xCD, 0xEF, 0)).premultiplied(),
114            0
115        );
116    }
117
118    #[test]
119    fn half_alpha_over_black_is_half_the_colour() {
120        let p = Paint::new(Color::rgba(200, 100, 50, 128));
121        let out = source_over(0xFF00_0000, p.premultiplied(), p.alpha());
122        // 200 * 128/255 = 100.4, 100 * 128/255 = 50.2, 50 * 128/255 = 25.1
123        assert_eq!(out & 0x00FF_FFFF, 0x0064_3219);
124    }
125
126    #[test]
127    fn no_lane_ever_carries_into_its_neighbour() {
128        // Exhaustive over alpha for the worst-case saturated channels: a carry here
129        // would corrupt the neighbouring channel rather than merely round badly.
130        for a in 0..=255u32 {
131            let p = Paint::new(Color::rgba(255, 255, 255, a as u8));
132            let out = source_over(0xFFFF_FFFF, p.premultiplied(), p.alpha());
133            assert_eq!(out, 0xFFFF_FFFF, "alpha {a} carried");
134        }
135    }
136
137    #[test]
138    fn blending_is_monotonic_in_alpha() {
139        let mut previous = 0u32;
140        for a in 0..=255u32 {
141            let p = Paint::new(Color::rgba(255, 0, 0, a as u8));
142            let red = source_over(0xFF00_0000, p.premultiplied(), p.alpha()) >> 16 & 0xFF;
143            assert!(red >= previous, "alpha {a} went backwards");
144            previous = red;
145        }
146        assert_eq!(previous, 255);
147    }
148
149    #[test]
150    fn coverage_scaling_matches_direct_alpha() {
151        // Painting at alpha 255 with coverage c must equal painting at alpha c.
152        for c in [0u32, 1, 64, 127, 128, 200, 254, 255] {
153            let scaled = Paint::new(Color::rgb(200, 100, 50)).scaled(c);
154            let direct = Paint::new(Color::rgba(200, 100, 50, c as u8));
155            assert_eq!(scaled.alpha(), direct.alpha(), "coverage {c}");
156            assert_eq!(
157                scaled.premultiplied(),
158                direct.premultiplied(),
159                "coverage {c}"
160            );
161        }
162    }
163}