Skip to main content

telar_renderer_core/
image.rs

1use wide::u32x4;
2use xxhash_rust::xxh3::xxh3_64_with_seed;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
5pub enum ImageFilter {
6    #[default]
7    Nearest,
8    Linear,
9}
10
11#[derive(Debug, Clone)]
12pub struct ImageData {
13    /// Content address: equal pixels at equal dimensions give equal ids, whoever built them and whenever.
14    ///
15    /// The renderers key their texture caches on this and nothing else, so the id has to identify the *image*. A per-construction counter identified the allocation instead: a caller that rebuilt the same image each frame — which is what building an `ImageData` inside a widget body does — minted a fresh key every time, and every entry behind it became unreachable weight the cache could only shed by hitting its byte budget.
16    pub id: u64,
17    /// RGBA8 pixels with premultiplied alpha. Premultiplication is applied automatically in `new()`.
18    pub pixels: Vec<u8>,
19    pub width: u32,
20    pub height: u32,
21}
22
23impl ImageData {
24    pub fn new(pixels: Vec<u8>, width: u32, height: u32) -> Self {
25        assert_eq!(
26            pixels.len(),
27            (width * height * 4) as usize,
28            "pixels must be RGBA8: width * height * 4 bytes"
29        );
30        let mut pixels = pixels;
31        premultiply_rgba(&mut pixels);
32        Self::addressed(pixels, width, height)
33    }
34
35    /// Builds from bytes that are ALREADY premultiplied (e.g. a resvg `Pixmap`), skipping the premultiply step `new()` performs.
36    pub fn from_premultiplied(pixels: Vec<u8>, width: u32, height: u32) -> Self {
37        assert_eq!(
38            pixels.len(),
39            (width * height * 4) as usize,
40            "pixels must be RGBA8: width * height * 4 bytes"
41        );
42        Self::addressed(pixels, width, height)
43    }
44
45    // Hashed after premultiplication so both constructors address the same finished image alike. The dimensions ride in as the seed rather than as leading bytes: one buffer can be several images (a 4x1 and a 2x2 share their bytes), and seeding keeps that distinction while leaving the pixels a single one-shot pass.
46    fn addressed(pixels: Vec<u8>, width: u32, height: u32) -> Self {
47        let seed = ((width as u64) << 32) | height as u64;
48        Self {
49            id: xxh3_64_with_seed(&pixels, seed),
50            pixels,
51            width,
52            height,
53        }
54    }
55}
56
57#[inline]
58pub fn premultiply_rgba(pixels: &mut [u8]) {
59    let mut iter = pixels.chunks_exact_mut(16);
60    for chunk in iter.by_ref() {
61        let r = u32x4::new([
62            chunk[0] as u32,
63            chunk[4] as u32,
64            chunk[8] as u32,
65            chunk[12] as u32,
66        ]);
67        let g = u32x4::new([
68            chunk[1] as u32,
69            chunk[5] as u32,
70            chunk[9] as u32,
71            chunk[13] as u32,
72        ]);
73        let b = u32x4::new([
74            chunk[2] as u32,
75            chunk[6] as u32,
76            chunk[10] as u32,
77            chunk[14] as u32,
78        ]);
79        let a = u32x4::new([
80            chunk[3] as u32,
81            chunk[7] as u32,
82            chunk[11] as u32,
83            chunk[15] as u32,
84        ]);
85        let bias = u32x4::splat(128);
86        let shift = u32x4::splat(8);
87        let r_new = ((r * a) + bias) >> shift;
88        let g_new = ((g * a) + bias) >> shift;
89        let b_new = ((b * a) + bias) >> shift;
90        let ra = r_new.to_array();
91        let ga = g_new.to_array();
92        let ba = b_new.to_array();
93        chunk[0] = ra[0] as u8;
94        chunk[4] = ra[1] as u8;
95        chunk[8] = ra[2] as u8;
96        chunk[12] = ra[3] as u8;
97        chunk[1] = ga[0] as u8;
98        chunk[5] = ga[1] as u8;
99        chunk[9] = ga[2] as u8;
100        chunk[13] = ga[3] as u8;
101        chunk[2] = ba[0] as u8;
102        chunk[6] = ba[1] as u8;
103        chunk[10] = ba[2] as u8;
104        chunk[14] = ba[3] as u8;
105    }
106    for chunk in iter.into_remainder().chunks_exact_mut(4) {
107        let a = chunk[3] as u32;
108        chunk[0] = ((chunk[0] as u32 * a + 128) >> 8) as u8;
109        chunk[1] = ((chunk[1] as u32 * a + 128) >> 8) as u8;
110        chunk[2] = ((chunk[2] as u32 * a + 128) >> 8) as u8;
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn opaque(pixels: &[[u8; 3]]) -> Vec<u8> {
119        pixels
120            .iter()
121            .flat_map(|[r, g, b]| [*r, *g, *b, 255])
122            .collect()
123    }
124
125    // The property the texture caches depend on: a widget body that rebuilds its image every frame must land on the entry it filled last frame, not mint a new one.
126    #[test]
127    fn the_same_image_built_twice_gets_the_same_id() {
128        let once = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 60]]), 2, 1);
129        let again = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 60]]), 2, 1);
130        assert_eq!(once.id, again.id);
131    }
132
133    #[test]
134    fn different_pixels_get_different_ids() {
135        let a = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 60]]), 2, 1);
136        let b = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 61]]), 2, 1);
137        assert_ne!(a.id, b.id);
138    }
139
140    // Same bytes, different shape: the dimensions have to be part of the address or a 4x1 would be served the 2x2's texture.
141    #[test]
142    fn the_same_bytes_at_different_dimensions_get_different_ids() {
143        let wide = ImageData::new(
144            opaque(&[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]),
145            4,
146            1,
147        );
148        let square = ImageData::new(
149            opaque(&[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]),
150            2,
151            2,
152        );
153        assert_ne!(wide.id, square.id);
154    }
155
156    // `new` premultiplies and `from_premultiplied` does not, so addressing has to happen after that step or the two would disagree about an image they both finished identically.
157    #[test]
158    fn both_constructors_address_the_same_finished_image_alike() {
159        let half_alpha = vec![200, 100, 50, 128];
160        let mut premultiplied = half_alpha.clone();
161        premultiply_rgba(&mut premultiplied);
162        assert_eq!(
163            ImageData::new(half_alpha, 1, 1).id,
164            ImageData::from_premultiplied(premultiplied, 1, 1).id
165        );
166    }
167}