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/// A texture an application rendered itself, handed to Telar to place in the frame.
12///
13/// Deliberately opaque here: this crate is the vocabulary the CPU rasterizer shares with the GPU one, and
14/// naming a `wgpu` type would drag the whole GPU stack into builds that never touch one. The backend that
15/// can use the handle downcasts it; the one that cannot ignores the command.
16///
17/// Which means the trait is public for the backends, not for applications: a handle only draws through the
18/// backend that made it, so an implementation written outside one is recognised by nobody and its image
19/// comes out empty (with a warning). Applications build these with `telar::gpu::image`.
20pub trait ExternalTexture: std::fmt::Debug + Send + Sync {
21    fn as_any(&self) -> &dyn std::any::Any;
22}
23
24#[derive(Debug, Clone)]
25enum ImageSource {
26    Pixels(Vec<u8>),
27    External(std::sync::Arc<dyn ExternalTexture>),
28}
29
30#[derive(Debug, Clone)]
31pub struct ImageData {
32    /// Content address: equal pixels at equal dimensions give equal ids, whoever built them and whenever.
33    ///
34    /// 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.
35    ///
36    /// An external texture cannot be hashed, so its owner supplies the id and carries the same duty: keep it
37    /// stable while the texture is, and change it when the texture object is replaced.
38    pub id: u64,
39    source: ImageSource,
40    pub width: u32,
41    pub height: u32,
42}
43
44impl ImageData {
45    pub fn new(pixels: Vec<u8>, width: u32, height: u32) -> Self {
46        assert_eq!(
47            pixels.len(),
48            (width * height * 4) as usize,
49            "pixels must be RGBA8: width * height * 4 bytes"
50        );
51        let mut pixels = pixels;
52        premultiply_rgba(&mut pixels);
53        Self::addressed(pixels, width, height)
54    }
55
56    /// Builds from bytes that are ALREADY premultiplied (e.g. a resvg `Pixmap`), skipping the premultiply step `new()` performs.
57    pub fn from_premultiplied(pixels: Vec<u8>, width: u32, height: u32) -> Self {
58        assert_eq!(
59            pixels.len(),
60            (width * height * 4) as usize,
61            "pixels must be RGBA8: width * height * 4 bytes"
62        );
63        Self::addressed(pixels, width, height)
64    }
65
66    /// Refers to a texture the application owns and keeps filling, rather than pixels Telar uploads.
67    ///
68    /// `id` addresses the texture *object*, not its contents: the whole point is that the contents change
69    /// every frame without Telar being told. Bump it only when the texture itself is replaced — a resize,
70    /// a format change — so the bind group built against the old one is dropped.
71    pub fn external(
72        texture: std::sync::Arc<dyn ExternalTexture>,
73        id: u64,
74        width: u32,
75        height: u32,
76    ) -> Self {
77        Self {
78            id,
79            source: ImageSource::External(texture),
80            width,
81            height,
82        }
83    }
84
85    /// The premultiplied RGBA8 bytes; empty when the picture lives in a texture Telar does not own.
86    ///
87    /// Empty rather than `Option` so a backend that cannot use an external texture needs no special case:
88    /// every path that turns these bytes into a raster already has to reject a buffer too short for the
89    /// dimensions, and an empty one takes that branch.
90    pub fn pixels(&self) -> &[u8] {
91        match &self.source {
92            ImageSource::Pixels(p) => p,
93            ImageSource::External(_) => &[],
94        }
95    }
96
97    pub fn external_texture(&self) -> Option<&std::sync::Arc<dyn ExternalTexture>> {
98        match &self.source {
99            ImageSource::External(t) => Some(t),
100            ImageSource::Pixels(_) => None,
101        }
102    }
103
104    // 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.
105    fn addressed(pixels: Vec<u8>, width: u32, height: u32) -> Self {
106        let seed = ((width as u64) << 32) | height as u64;
107        Self {
108            id: xxh3_64_with_seed(&pixels, seed),
109            source: ImageSource::Pixels(pixels),
110            width,
111            height,
112        }
113    }
114}
115
116#[inline]
117pub fn premultiply_rgba(pixels: &mut [u8]) {
118    let mut iter = pixels.chunks_exact_mut(16);
119    for chunk in iter.by_ref() {
120        let r = u32x4::new([
121            chunk[0] as u32,
122            chunk[4] as u32,
123            chunk[8] as u32,
124            chunk[12] as u32,
125        ]);
126        let g = u32x4::new([
127            chunk[1] as u32,
128            chunk[5] as u32,
129            chunk[9] as u32,
130            chunk[13] as u32,
131        ]);
132        let b = u32x4::new([
133            chunk[2] as u32,
134            chunk[6] as u32,
135            chunk[10] as u32,
136            chunk[14] as u32,
137        ]);
138        let a = u32x4::new([
139            chunk[3] as u32,
140            chunk[7] as u32,
141            chunk[11] as u32,
142            chunk[15] as u32,
143        ]);
144        let bias = u32x4::splat(128);
145        let shift = u32x4::splat(8);
146        let r_new = ((r * a) + bias) >> shift;
147        let g_new = ((g * a) + bias) >> shift;
148        let b_new = ((b * a) + bias) >> shift;
149        let ra = r_new.to_array();
150        let ga = g_new.to_array();
151        let ba = b_new.to_array();
152        chunk[0] = ra[0] as u8;
153        chunk[4] = ra[1] as u8;
154        chunk[8] = ra[2] as u8;
155        chunk[12] = ra[3] as u8;
156        chunk[1] = ga[0] as u8;
157        chunk[5] = ga[1] as u8;
158        chunk[9] = ga[2] as u8;
159        chunk[13] = ga[3] as u8;
160        chunk[2] = ba[0] as u8;
161        chunk[6] = ba[1] as u8;
162        chunk[10] = ba[2] as u8;
163        chunk[14] = ba[3] as u8;
164    }
165    for chunk in iter.into_remainder().chunks_exact_mut(4) {
166        let a = chunk[3] as u32;
167        chunk[0] = ((chunk[0] as u32 * a + 128) >> 8) as u8;
168        chunk[1] = ((chunk[1] as u32 * a + 128) >> 8) as u8;
169        chunk[2] = ((chunk[2] as u32 * a + 128) >> 8) as u8;
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn opaque(pixels: &[[u8; 3]]) -> Vec<u8> {
178        pixels
179            .iter()
180            .flat_map(|[r, g, b]| [*r, *g, *b, 255])
181            .collect()
182    }
183
184    // 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.
185    #[test]
186    fn the_same_image_built_twice_gets_the_same_id() {
187        let once = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 60]]), 2, 1);
188        let again = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 60]]), 2, 1);
189        assert_eq!(once.id, again.id);
190    }
191
192    #[test]
193    fn different_pixels_get_different_ids() {
194        let a = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 60]]), 2, 1);
195        let b = ImageData::new(opaque(&[[10, 20, 30], [40, 50, 61]]), 2, 1);
196        assert_ne!(a.id, b.id);
197    }
198
199    // Same bytes, different shape: the dimensions have to be part of the address or a 4x1 would be served the 2x2's texture.
200    #[test]
201    fn the_same_bytes_at_different_dimensions_get_different_ids() {
202        let wide = ImageData::new(
203            opaque(&[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]),
204            4,
205            1,
206        );
207        let square = ImageData::new(
208            opaque(&[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]),
209            2,
210            2,
211        );
212        assert_ne!(wide.id, square.id);
213    }
214
215    // `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.
216    #[test]
217    fn both_constructors_address_the_same_finished_image_alike() {
218        let half_alpha = vec![200, 100, 50, 128];
219        let mut premultiplied = half_alpha.clone();
220        premultiply_rgba(&mut premultiplied);
221        assert_eq!(
222            ImageData::new(half_alpha, 1, 1).id,
223            ImageData::from_premultiplied(premultiplied, 1, 1).id
224        );
225    }
226}