Skip to main content

alpha2_render/cpu/
image.rs

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