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
8use denise::Color;
9
10/// Mask selecting the two 8-bit lanes at bits 0..8 and 16..24.
11const LANES: u32 = 0x00FF_00FF;
12
13/// Multiplies two packed 8-bit lanes by `a` (`0..=255`) and divides by 255 with
14/// correct rounding.
15///
16/// The `+ 0x80` bias and the fold-back of the high bits make this exact, not the
17/// usual `>> 8` approximation: `mul_lanes(x, 255) == x` and `mul_lanes(x, 0) == 0`.
18/// Cheap approximations get those endpoints wrong, and an opaque fill that lands on
19/// 254 instead of 255 is visible as banding the moment anything is drawn twice.
20#[inline(always)]
21const fn mul_lanes(x: u32, a: u32) -> u32 {
22    let t = x * a + 0x0080_0080;
23    ((t + ((t >> 8) & LANES)) >> 8) & LANES
24}
25
26/// A colour prepared for drawing.
27///
28/// Premultiplication happens once here rather than once per pixel. Constructing a
29/// `Paint` is the only division-by-255 in a fill.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Paint {
32    /// `0xAARRGGBB`, colour channels premultiplied by alpha.
33    premul: u32,
34    /// Straight alpha, `0..=255`.
35    alpha: u32,
36}
37
38impl Paint {
39    /// Prepares a colour for drawing.
40    #[inline]
41    pub const fn new(color: Color) -> Self {
42        let a = color.a as u32;
43        let rb = mul_lanes(((color.r as u32) << 16) | color.b as u32, a);
44        let g = mul_lanes(color.g as u32, a);
45        Self {
46            premul: (a << 24) | rb | (g << 8),
47            alpha: a,
48        }
49    }
50
51    /// The premultiplied `0xAARRGGBB` word.
52    #[inline]
53    pub const fn premultiplied(self) -> u32 {
54        self.premul
55    }
56
57    /// Straight alpha, `0..=255`.
58    #[inline]
59    pub const fn alpha(self) -> u32 {
60        self.alpha
61    }
62
63    /// Returns `true` if drawing can skip the read-modify-write and just store.
64    #[inline]
65    pub const fn is_opaque(self) -> bool {
66        self.alpha == 255
67    }
68
69    /// Returns `true` if drawing would change nothing.
70    #[inline]
71    pub const fn is_invisible(self) -> bool {
72        self.alpha == 0
73    }
74
75    /// This paint scaled by an anti-aliasing coverage of `0..=255`.
76    #[inline]
77    pub const fn scaled(self, coverage: u32) -> Self {
78        let premul = scale_premul(self.premul, coverage);
79        Self {
80            premul,
81            alpha: premul >> 24,
82        }
83    }
84}
85
86impl From<Color> for Paint {
87    #[inline]
88    fn from(color: Color) -> Self {
89        Paint::new(color)
90    }
91}
92
93/// Scales a premultiplied `0xAARRGGBB` word — alpha lane included — by a
94/// coverage of `0..=255`.
95#[inline(always)]
96pub(crate) const fn scale_premul(px: u32, coverage: u32) -> u32 {
97    let rb = mul_lanes(px & LANES, coverage);
98    let ag = mul_lanes((px >> 8) & LANES, coverage);
99    rb | (ag << 8)
100}
101
102/// Premultiplies straight-alpha `0xAARRGGBB` words in place.
103///
104/// This is [`Paint::new`]'s arithmetic applied to a buffer: exact at both
105/// endpoints, done once at load time so a blit never divides by 255 per frame.
106/// Decoders hand out straight alpha; [`Canvas::blit`](crate::Canvas::blit)
107/// consumes premultiplied — this is the bridge between them.
108pub fn premultiply(pixels: &mut [u32]) {
109    for px in pixels {
110        match *px >> 24 {
111            255 => {}
112            0 => *px = 0,
113            a => {
114                let rb = mul_lanes(*px & LANES, a);
115                let g = mul_lanes((*px >> 8) & 0xFF, a);
116                *px = (a << 24) | rb | (g << 8);
117            }
118        }
119    }
120}
121
122/// Composites a premultiplied source over a destination pixel.
123///
124/// Both words are `0xAARRGGBB`. The alpha lane is composited too, so this is
125/// correct for an `Argb8888` target as well as an opaque `Xrgb8888` one.
126#[inline(always)]
127pub const fn source_over(dst: u32, src_premul: u32, alpha: u32) -> u32 {
128    let inv = 255 - alpha;
129    let rb = mul_lanes(dst & LANES, inv);
130    let ag = mul_lanes((dst >> 8) & LANES, inv);
131    // No lane can carry: s*a/255 + d*(255-a)/255 <= 255, and neither term can land
132    // exactly on .5, so the two roundings cannot both push up.
133    src_premul + (rb | (ag << 8))
134}
135
136/// Overwrites a span with an opaque word.
137#[inline]
138pub fn fill_span(span: &mut [u32], word: u32) {
139    span.fill(word);
140}
141
142/// Composites a constant paint over a span.
143#[inline]
144pub fn blend_span(span: &mut [u32], paint: Paint) {
145    if paint.is_invisible() {
146        return;
147    }
148    if paint.is_opaque() {
149        span.fill(paint.premultiplied());
150        return;
151    }
152    let src = paint.premultiplied();
153    let alpha = paint.alpha();
154    for px in span {
155        *px = source_over(*px, src, alpha);
156    }
157}
158
159/// Composites a paint over a single pixel at `coverage` (`0..=255`).
160#[inline]
161pub fn blend_pixel(dst: &mut u32, paint: Paint, coverage: u32) {
162    if coverage == 0 {
163        return;
164    }
165    let paint = if coverage == 255 {
166        paint
167    } else {
168        paint.scaled(coverage)
169    };
170    *dst = source_over(*dst, paint.premultiplied(), paint.alpha());
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn opaque_paint_replaces_destination() {
179        let p = Paint::new(Color::rgb(10, 20, 30));
180        assert!(p.is_opaque());
181        assert_eq!(
182            source_over(0xFFFF_FFFF, p.premultiplied(), p.alpha()),
183            0xFF0A_141E
184        );
185    }
186
187    #[test]
188    fn transparent_paint_preserves_destination() {
189        let p = Paint::new(Color::rgba(10, 20, 30, 0));
190        assert_eq!(
191            source_over(0xFF12_3456, p.premultiplied(), p.alpha()),
192            0xFF12_3456
193        );
194    }
195
196    #[test]
197    fn premultiply_is_exact_at_the_endpoints() {
198        // The whole point of the rounding correction: full alpha must round-trip.
199        let c = Color::rgba(0xAB, 0xCD, 0xEF, 255);
200        assert_eq!(Paint::new(c).premultiplied(), 0xFFAB_CDEF);
201        assert_eq!(
202            Paint::new(Color::rgba(0xAB, 0xCD, 0xEF, 0)).premultiplied(),
203            0
204        );
205    }
206
207    #[test]
208    fn half_alpha_over_black_is_half_the_colour() {
209        let p = Paint::new(Color::rgba(200, 100, 50, 128));
210        let out = source_over(0xFF00_0000, p.premultiplied(), p.alpha());
211        // 200 * 128/255 = 100.4, 100 * 128/255 = 50.2, 50 * 128/255 = 25.1
212        assert_eq!(out & 0x00FF_FFFF, 0x0064_3219);
213    }
214
215    #[test]
216    fn no_lane_ever_carries_into_its_neighbour() {
217        // Exhaustive over alpha for the worst-case saturated channels: a carry here
218        // would corrupt the neighbouring channel rather than merely round badly.
219        for a in 0..=255u32 {
220            let p = Paint::new(Color::rgba(255, 255, 255, a as u8));
221            let out = source_over(0xFFFF_FFFF, p.premultiplied(), p.alpha());
222            assert_eq!(out, 0xFFFF_FFFF, "alpha {a} carried");
223        }
224    }
225
226    #[test]
227    fn blending_is_monotonic_in_alpha() {
228        let mut previous = 0u32;
229        for a in 0..=255u32 {
230            let p = Paint::new(Color::rgba(255, 0, 0, a as u8));
231            let red = source_over(0xFF00_0000, p.premultiplied(), p.alpha()) >> 16 & 0xFF;
232            assert!(red >= previous, "alpha {a} went backwards");
233            previous = red;
234        }
235        assert_eq!(previous, 255);
236    }
237
238    #[test]
239    fn coverage_scaling_matches_direct_alpha() {
240        // Painting at alpha 255 with coverage c must equal painting at alpha c.
241        for c in [0u32, 1, 64, 127, 128, 200, 254, 255] {
242            let scaled = Paint::new(Color::rgb(200, 100, 50)).scaled(c);
243            let direct = Paint::new(Color::rgba(200, 100, 50, c as u8));
244            assert_eq!(scaled.alpha(), direct.alpha(), "coverage {c}");
245            assert_eq!(
246                scaled.premultiplied(),
247                direct.premultiplied(),
248                "coverage {c}"
249            );
250        }
251    }
252}