Skip to main content

alpha2_render/cpu/
image.rs

1//! Pictures the CPU renderer keeps: what a graph node draws, and the
2//! pictures sources lend it.
3
4use super::half;
5use crate::{RenderError, RenderSize};
6
7/// A rectangle of pixels: `x..x + width` by `y..y + height`.
8#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
9pub(crate) struct PixelBox {
10    pub(crate) x: u32,
11    pub(crate) y: u32,
12    pub(crate) width: u32,
13    pub(crate) height: u32,
14}
15
16impl PixelBox {
17    pub(crate) const EMPTY: Self = Self {
18        x: 0,
19        y: 0,
20        width: 0,
21        height: 0,
22    };
23
24    pub(crate) const fn whole(size: RenderSize) -> Self {
25        Self {
26            x: 0,
27            y: 0,
28            width: size.width,
29            height: size.height,
30        }
31    }
32
33    pub(crate) const fn is_empty(self) -> bool {
34        self.width == 0 || self.height == 0
35    }
36
37    pub(crate) const fn right(self) -> u32 {
38        self.x + self.width
39    }
40
41    pub(crate) const fn bottom(self) -> u32 {
42        self.y + self.height
43    }
44
45    pub(crate) fn pixels(self) -> usize {
46        self.width as usize * self.height as usize
47    }
48
49    pub(crate) const fn contains(self, x: u32, y: u32) -> bool {
50        x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
51    }
52
53    /// The pixels both hold; empty when they do not meet.
54    pub(crate) fn intersect(self, other: Self) -> Self {
55        let x = self.x.max(other.x);
56        let y = self.y.max(other.y);
57        let right = self.right().min(other.right());
58        let bottom = self.bottom().min(other.bottom());
59        if right <= x || bottom <= y {
60            return Self::EMPTY;
61        }
62        Self {
63            x,
64            y,
65            width: right - x,
66            height: bottom - y,
67        }
68    }
69
70    /// The smallest box holding both; an empty box adds nothing.
71    pub(crate) fn union(self, other: Self) -> Self {
72        if self.is_empty() {
73            return other;
74        }
75        if other.is_empty() {
76            return self;
77        }
78        let x = self.x.min(other.x);
79        let y = self.y.min(other.y);
80        Self {
81            x,
82            y,
83            width: self.right().max(other.right()) - x,
84            height: self.bottom().max(other.bottom()) - y,
85        }
86    }
87
88    /// Grown by `margin` pixels on every side, kept inside `size`.
89    pub(crate) fn expand(self, margin: u32, size: RenderSize) -> Self {
90        if self.is_empty() {
91            return self;
92        }
93        let x = self.x.saturating_sub(margin);
94        let y = self.y.saturating_sub(margin);
95        let right = self.right().saturating_add(margin).min(size.width);
96        let bottom = self.bottom().saturating_add(margin).min(size.height);
97        Self {
98            x,
99            y,
100            width: right - x,
101            height: bottom - y,
102        }
103    }
104}
105
106/// How a picture stores its premultiplied, sRGB-encoded RGBA.
107#[derive(Clone, Debug)]
108pub(crate) enum Pixels {
109    /// Half-float bits, as the `Rgba16Float` intermediate holds them.
110    Half(Vec<[u16; 4]>),
111    /// 8-bit codes, as an `Rgba8Unorm` texture holds them.
112    Unorm8(Vec<[u8; 4]>),
113}
114
115/// A picture the CPU renderer drew or was lent: premultiplied, sRGB-encoded
116/// RGBA, as every GPU intermediate texture holds it.
117///
118/// Only its content rectangle is stored; every pixel outside it is
119/// transparent. A label on a 1080p canvas keeps the label's pixels, not the
120/// canvas's.
121#[derive(Clone, Debug)]
122pub struct CpuImage {
123    size: RenderSize,
124    bounds: PixelBox,
125    pixels: Pixels,
126}
127
128impl CpuImage {
129    pub(crate) fn new(size: RenderSize, bounds: PixelBox, pixels: Pixels) -> Self {
130        let bounds = bounds.intersect(PixelBox::whole(size));
131        debug_assert_eq!(
132            match &pixels {
133                Pixels::Half(pixels) => pixels.len(),
134                Pixels::Unorm8(pixels) => pixels.len(),
135            },
136            bounds.pixels()
137        );
138        Self {
139            size,
140            bounds,
141            pixels,
142        }
143    }
144
145    /// A transparent picture of `size`, which stores nothing.
146    pub(crate) fn transparent(size: RenderSize) -> Self {
147        Self {
148            size,
149            bounds: PixelBox::EMPTY,
150            pixels: Pixels::Half(Vec::new()),
151        }
152    }
153
154    /// Premultiplied, sRGB-encoded RGBA8 pixels, rows packed, as
155    /// [`crate::Renderer::upload_rgba8`] takes them: a picture sampled as
156    /// that texture is.
157    ///
158    /// # Errors
159    /// Returns [`RenderError::InvalidImageData`] when `rgba8` does not hold
160    /// four bytes per pixel of `size`.
161    pub fn from_premultiplied_rgba8(size: RenderSize, rgba8: &[u8]) -> Result<Self, RenderError> {
162        let expected = size.width as usize * size.height as usize * 4;
163        if rgba8.len() != expected {
164            return Err(RenderError::InvalidImageData);
165        }
166        let pixels = rgba8.as_chunks::<4>().0.to_vec();
167        Ok(Self::new(
168            size,
169            PixelBox::whole(size),
170            Pixels::Unorm8(pixels),
171        ))
172    }
173
174    /// The picture's dimensions.
175    #[must_use]
176    pub const fn size(&self) -> RenderSize {
177        self.size
178    }
179
180    pub(crate) const fn bounds(&self) -> PixelBox {
181        self.bounds
182    }
183
184    pub(crate) const fn pixels(&self) -> &Pixels {
185        &self.pixels
186    }
187
188    /// Bytes the picture holds.
189    #[must_use]
190    pub fn retained_bytes(&self) -> u64 {
191        let pixels = self.bounds.pixels() as u64;
192        match self.pixels {
193            Pixels::Half(_) => pixels * 8,
194            Pixels::Unorm8(_) => pixels * 4,
195        }
196    }
197
198    /// The picture as tightly packed RGBA8, as [`crate::Renderer::read_rgba8`]
199    /// reads a texture back: each component rounded to the nearest code.
200    #[must_use]
201    pub fn read_rgba8(&self) -> Vec<u8> {
202        let width = self.size.width as usize;
203        let mut bytes = vec![0_u8; width * self.size.height as usize * 4];
204        let bounds = self.bounds;
205        for row in 0..bounds.height as usize {
206            let start = ((bounds.y as usize + row) * width + bounds.x as usize) * 4;
207            let out = &mut bytes[start..start + bounds.width as usize * 4];
208            match &self.pixels {
209                Pixels::Half(pixels) => {
210                    let line = &pixels[row * bounds.width as usize..][..bounds.width as usize];
211                    for (pixel, out) in line.iter().zip(out.as_chunks_mut::<4>().0) {
212                        *out = half::load4(*pixel).map(half::unorm8);
213                    }
214                }
215                Pixels::Unorm8(pixels) => {
216                    let line = &pixels[row * bounds.width as usize..][..bounds.width as usize];
217                    out.copy_from_slice(line.as_flattened());
218                }
219            }
220        }
221        bytes
222    }
223
224    /// The pixel at `(x, y)`, which must lie inside the picture: transparent
225    /// outside the content rectangle.
226    #[inline]
227    pub(crate) fn texel(&self, x: u32, y: u32) -> [f32; 4] {
228        let bounds = self.bounds;
229        if !bounds.contains(x, y) {
230            return [0.0; 4];
231        }
232        let index = (y - bounds.y) as usize * bounds.width as usize + (x - bounds.x) as usize;
233        match &self.pixels {
234            Pixels::Half(pixels) => half::load4(pixels[index]),
235            Pixels::Unorm8(pixels) => pixels[index].map(half::unorm8_value),
236        }
237    }
238
239    /// Pixels `x0..x0 + out.len()` of row `y`, which must lie inside the
240    /// picture, into `out`: transparent outside the content rectangle.
241    pub(crate) fn decode_row(&self, y: u32, x0: u32, out: &mut [[f32; 4]]) {
242        let bounds = self.bounds;
243        if y < bounds.y || y >= bounds.bottom() {
244            out.fill([0.0; 4]);
245            return;
246        }
247        let row = (y - bounds.y) as usize * bounds.width as usize;
248        // The part of the request inside the content, decoded; zeros around it.
249        let start = x0.max(bounds.x);
250        #[allow(clippy::cast_possible_truncation)]
251        let end = (x0 + out.len() as u32).min(bounds.right());
252        out.fill([0.0; 4]);
253        if start >= end {
254            return;
255        }
256        let target = &mut out[(start - x0) as usize..(end - x0) as usize];
257        let source = row + (start - bounds.x) as usize..row + (end - bounds.x) as usize;
258        match &self.pixels {
259            Pixels::Half(pixels) => {
260                let table = half::table();
261                for (pixel, bits) in target.iter_mut().zip(&pixels[source]) {
262                    *pixel = bits.map(|bits| table[usize::from(bits)]);
263                }
264            }
265            Pixels::Unorm8(pixels) => {
266                for (pixel, codes) in target.iter_mut().zip(&pixels[source]) {
267                    *pixel = codes.map(half::unorm8_value);
268                }
269            }
270        }
271    }
272
273    /// The pixel at `(x, y)` with the edge repeated past it, as a sampler
274    /// with clamp-to-edge addressing reads it.
275    #[inline]
276    pub(crate) fn texel_clamped(&self, x: i64, y: i64) -> [f32; 4] {
277        let last_x = i64::from(self.size.width) - 1;
278        let last_y = i64::from(self.size.height) - 1;
279        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
280        self.texel(x.clamp(0, last_x) as u32, y.clamp(0, last_y) as u32)
281    }
282
283    /// A bilinear sample at normalized position `(u, v)`, the edge repeated:
284    /// what `textureSampleLevel` with a linear, clamp-to-edge sampler reads
285    /// at level 0.
286    #[inline]
287    pub(crate) fn sample(&self, u: f32, v: f32) -> [f32; 4] {
288        #[allow(clippy::cast_precision_loss)]
289        let x = u * self.size.width as f32 - 0.5;
290        #[allow(clippy::cast_precision_loss)]
291        let y = v * self.size.height as f32 - 0.5;
292        let x0 = x.floor();
293        let y0 = y.floor();
294        let fx = x - x0;
295        let fy = y - y0;
296        #[allow(clippy::cast_possible_truncation)]
297        let (ix, iy) = (x0 as i64, y0 as i64);
298        let t00 = self.texel_clamped(ix, iy);
299        let t10 = self.texel_clamped(ix + 1, iy);
300        let t01 = self.texel_clamped(ix, iy + 1);
301        let t11 = self.texel_clamped(ix + 1, iy + 1);
302        let mut out = [0.0; 4];
303        for channel in 0..4 {
304            let top = t00[channel] + (t10[channel] - t00[channel]) * fx;
305            let bottom = t01[channel] + (t11[channel] - t01[channel]) * fx;
306            out[channel] = top + (bottom - top) * fy;
307        }
308        out
309    }
310}