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 rb = mul_lanes(self.premul & LANES, coverage);
79        let ag = mul_lanes((self.premul >> 8) & LANES, coverage);
80        let premul = rb | (ag << 8);
81        Self {
82            premul,
83            alpha: premul >> 24,
84        }
85    }
86}
87
88impl From<Color> for Paint {
89    #[inline]
90    fn from(color: Color) -> Self {
91        Paint::new(color)
92    }
93}
94
95/// Composites a premultiplied source over a destination pixel.
96///
97/// Both words are `0xAARRGGBB`. The alpha lane is composited too, so this is
98/// correct for an `Argb8888` target as well as an opaque `Xrgb8888` one.
99#[inline(always)]
100pub const fn source_over(dst: u32, src_premul: u32, alpha: u32) -> u32 {
101    let inv = 255 - alpha;
102    let rb = mul_lanes(dst & LANES, inv);
103    let ag = mul_lanes((dst >> 8) & LANES, inv);
104    // No lane can carry: s*a/255 + d*(255-a)/255 <= 255, and neither term can land
105    // exactly on .5, so the two roundings cannot both push up.
106    src_premul + (rb | (ag << 8))
107}
108
109/// Overwrites a span with an opaque word.
110#[inline]
111pub fn fill_span(span: &mut [u32], word: u32) {
112    span.fill(word);
113}
114
115/// Composites a constant paint over a span.
116#[inline]
117pub fn blend_span(span: &mut [u32], paint: Paint) {
118    if paint.is_invisible() {
119        return;
120    }
121    if paint.is_opaque() {
122        span.fill(paint.premultiplied());
123        return;
124    }
125    let src = paint.premultiplied();
126    let alpha = paint.alpha();
127    for px in span {
128        *px = source_over(*px, src, alpha);
129    }
130}
131
132/// Composites a paint over a single pixel at `coverage` (`0..=255`).
133#[inline]
134pub fn blend_pixel(dst: &mut u32, paint: Paint, coverage: u32) {
135    if coverage == 0 {
136        return;
137    }
138    let paint = if coverage == 255 {
139        paint
140    } else {
141        paint.scaled(coverage)
142    };
143    *dst = source_over(*dst, paint.premultiplied(), paint.alpha());
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn opaque_paint_replaces_destination() {
152        let p = Paint::new(Color::rgb(10, 20, 30));
153        assert!(p.is_opaque());
154        assert_eq!(
155            source_over(0xFFFF_FFFF, p.premultiplied(), p.alpha()),
156            0xFF0A_141E
157        );
158    }
159
160    #[test]
161    fn transparent_paint_preserves_destination() {
162        let p = Paint::new(Color::rgba(10, 20, 30, 0));
163        assert_eq!(
164            source_over(0xFF12_3456, p.premultiplied(), p.alpha()),
165            0xFF12_3456
166        );
167    }
168
169    #[test]
170    fn premultiply_is_exact_at_the_endpoints() {
171        // The whole point of the rounding correction: full alpha must round-trip.
172        let c = Color::rgba(0xAB, 0xCD, 0xEF, 255);
173        assert_eq!(Paint::new(c).premultiplied(), 0xFFAB_CDEF);
174        assert_eq!(
175            Paint::new(Color::rgba(0xAB, 0xCD, 0xEF, 0)).premultiplied(),
176            0
177        );
178    }
179
180    #[test]
181    fn half_alpha_over_black_is_half_the_colour() {
182        let p = Paint::new(Color::rgba(200, 100, 50, 128));
183        let out = source_over(0xFF00_0000, p.premultiplied(), p.alpha());
184        // 200 * 128/255 = 100.4, 100 * 128/255 = 50.2, 50 * 128/255 = 25.1
185        assert_eq!(out & 0x00FF_FFFF, 0x0064_3219);
186    }
187
188    #[test]
189    fn no_lane_ever_carries_into_its_neighbour() {
190        // Exhaustive over alpha for the worst-case saturated channels: a carry here
191        // would corrupt the neighbouring channel rather than merely round badly.
192        for a in 0..=255u32 {
193            let p = Paint::new(Color::rgba(255, 255, 255, a as u8));
194            let out = source_over(0xFFFF_FFFF, p.premultiplied(), p.alpha());
195            assert_eq!(out, 0xFFFF_FFFF, "alpha {a} carried");
196        }
197    }
198
199    #[test]
200    fn blending_is_monotonic_in_alpha() {
201        let mut previous = 0u32;
202        for a in 0..=255u32 {
203            let p = Paint::new(Color::rgba(255, 0, 0, a as u8));
204            let red = source_over(0xFF00_0000, p.premultiplied(), p.alpha()) >> 16 & 0xFF;
205            assert!(red >= previous, "alpha {a} went backwards");
206            previous = red;
207        }
208        assert_eq!(previous, 255);
209    }
210
211    #[test]
212    fn coverage_scaling_matches_direct_alpha() {
213        // Painting at alpha 255 with coverage c must equal painting at alpha c.
214        for c in [0u32, 1, 64, 127, 128, 200, 254, 255] {
215            let scaled = Paint::new(Color::rgb(200, 100, 50)).scaled(c);
216            let direct = Paint::new(Color::rgba(200, 100, 50, c as u8));
217            assert_eq!(scaled.alpha(), direct.alpha(), "coverage {c}");
218            assert_eq!(
219                scaled.premultiplied(),
220                direct.premultiplied(),
221                "coverage {c}"
222            );
223        }
224    }
225}