Skip to main content

alpha2_render/cpu/
image.rs

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