Skip to main content

fidget_raster/
lib.rs

1//! 2D and 3D rendering
2//!
3//! To render something, build a configuration object then call its `run`
4//! function, e.g. [`pixel::RenderConfig::run`] and
5//! [`voxel::RenderConfig::run`].
6#![warn(missing_docs)]
7use fidget_core::{
8    eval::Function,
9    render::{ImageSize, RenderHandle, ThreadPool, TileSizes},
10    shape::{Shape, ShapeVars},
11};
12use nalgebra::{Const, OPoint, Point2, Vector2};
13use rayon::prelude::*;
14use zerocopy::{Immutable, IntoBytes};
15
16pub mod effects;
17pub mod pixel;
18pub mod voxel;
19
20#[derive(Copy, Clone, Debug)]
21pub(crate) struct Tile<const N: usize> {
22    /// Corner of this tile, in global screen (pixel) coordinates
23    pub corner: OPoint<usize, Const<N>>,
24}
25
26impl<const N: usize> Tile<N> {
27    /// Build a new tile from its global coordinates
28    #[inline]
29    pub(crate) fn new(corner: OPoint<usize, Const<N>>) -> Tile<N> {
30        Tile { corner }
31    }
32
33    /// Converts a relative position within the tile into a global position
34    ///
35    /// This function operates in pixel space, using the `.xy` coordinates
36    pub(crate) fn add(&self, pos: Vector2<usize>) -> Point2<usize> {
37        let corner = Point2::new(self.corner[0], self.corner[1]);
38        corner + pos
39    }
40}
41
42/// Helper struct to borrow from [`TileSizes`]
43///
44/// This object has the same guarantees as `TileSizes`, but trims items off the
45/// front of the `Vec<usize>` based on the image size.
46#[derive(Copy, Clone)]
47pub(crate) struct TileSizesRef<'a>(&'a [usize]);
48
49impl<'a> std::ops::Index<usize> for TileSizesRef<'a> {
50    type Output = usize;
51
52    fn index(&self, i: usize) -> &Self::Output {
53        &self.0[i]
54    }
55}
56
57impl TileSizesRef<'_> {
58    /// Builds a new `TileSizesRef` based on the maximum tile size
59    fn new(tiles: &TileSizes, max_size: usize) -> TileSizesRef<'_> {
60        let i = tiles
61            .iter()
62            .position(|t| *t < max_size)
63            .unwrap_or(tiles.len())
64            .saturating_sub(1);
65        TileSizesRef(&tiles[i..])
66    }
67
68    /// Returns the last (smallest) tile size
69    pub fn last(&self) -> usize {
70        *self.0.last().unwrap()
71    }
72
73    /// Gets a tile size by index
74    pub fn get(&self, i: usize) -> Option<usize> {
75        self.0.get(i).copied()
76    }
77
78    /// Returns the data offset of a global pixel position within a root tile
79    ///
80    /// The root tile is implicit: it's set by the largest tile size and aligned
81    /// to multiples of that size.
82    #[inline]
83    pub(crate) fn pixel_offset(&self, pos: Point2<usize>) -> usize {
84        // Find the relative position within the root tile
85        let x = pos.x % self.0[0];
86        let y = pos.y % self.0[0];
87
88        // Apply the relative offset and find the data index
89        x + y * self.0[0]
90    }
91}
92
93/// Grand unified render function
94///
95/// This handles tile generation and building + calling render workers in
96/// parallel (using [`rayon`] for parallelism at the tile level).
97///
98/// It returns a set of output tiles, or `None` if rendering has been cancelled
99pub(crate) fn render_tiles<'a, F: Function, W: RenderWorker<'a, F>>(
100    shape: Shape<F>,
101    vars: &'a ShapeVars<f32>,
102    config: &'a W::Config,
103    tile_sizes: TileSizesRef<'a>,
104) -> Option<Vec<(Tile<2>, W::Output)>>
105where
106    W::Config: Send + Sync,
107{
108    use rayon::prelude::*;
109
110    let mut tiles = vec![];
111    let t = tile_sizes[0];
112    let width = config.width() as usize;
113    let height = config.height() as usize;
114    for i in 0..width.div_ceil(t) {
115        for j in 0..height.div_ceil(t) {
116            tiles.push(Tile::new(Point2::new(
117                i * tile_sizes[0],
118                j * tile_sizes[0],
119            )));
120        }
121    }
122
123    let mut rh = RenderHandle::new(shape);
124
125    let _ = rh.i_tape(&mut vec![]); // populate i_tape before cloning
126    let ts = tile_sizes;
127    let init = || {
128        let rh = rh.clone();
129        let worker = W::new(config, ts, vars);
130        (worker, rh)
131    };
132
133    match config.threads() {
134        None => {
135            let mut worker = W::new(config, tile_sizes, vars);
136            tiles
137                .into_iter()
138                .map(|tile| {
139                    if config.is_cancelled() {
140                        Err(())
141                    } else {
142                        let pixels = worker.render_tile(&mut rh, tile);
143                        Ok((tile, pixels))
144                    }
145                })
146                .collect::<Result<Vec<_>, ()>>()
147                .ok()
148        }
149
150        Some(p) => p.run(|| {
151            tiles
152                .into_par_iter()
153                .map_init(init, |(w, rh), tile| {
154                    if config.is_cancelled() {
155                        Err(())
156                    } else {
157                        let pixels = w.render_tile(rh, tile);
158                        Ok((tile, pixels))
159                    }
160                })
161                .collect::<Result<Vec<_>, ()>>()
162                .ok()
163        }),
164    }
165}
166
167/// Helper trait for tiled rendering configuration
168pub(crate) trait RenderConfig: RenderSize {
169    fn threads(&self) -> Option<&ThreadPool>;
170    fn is_cancelled(&self) -> bool;
171}
172
173/// Trait for things that have a width and height in pixels
174pub trait RenderSize {
175    /// Width of the render, in voxels or pixels
176    fn width(&self) -> u32;
177    /// Height of the render, in voxels or pixels
178    fn height(&self) -> u32;
179}
180
181/// Helper trait for a tiled renderer worker
182pub(crate) trait RenderWorker<'a, F: Function> {
183    type Config: RenderConfig;
184    type Output: Send;
185
186    /// Build a new worker
187    ///
188    /// Workers are typically built on a per-thread basis
189    fn new(
190        cfg: &'a Self::Config,
191        tile_sizes: TileSizesRef<'a>,
192        vars: &'a ShapeVars<f32>,
193    ) -> Self;
194
195    /// Render a single tile, returning a worker-dependent output
196    fn render_tile(
197        &mut self,
198        shape: &mut RenderHandle<F>,
199        tile: Tile<2>,
200    ) -> Self::Output;
201}
202
203/// Generic image type
204///
205/// The image is laid out in row-major order, and can be indexed either by a
206/// `usize` index or a `(row, column)` tuple.
207///
208/// ```text
209///        0 ------------> width (columns)
210///        |             |
211///        |             |
212///        |             |
213///        V--------------
214///   height (rows)
215///
216/// Users will likely be using one of the image typedefs ([`voxel::Image`] and
217/// [`pixel::Image`]).
218/// ```
219#[derive(Clone)]
220pub struct Image<P, S = ImageSize> {
221    data: Vec<P>,
222    size: S,
223}
224
225impl RenderSize for pixel::RenderSize {
226    fn width(&self) -> u32 {
227        self.width()
228    }
229    fn height(&self) -> u32 {
230        self.height()
231    }
232}
233
234impl RenderSize for voxel::RenderSize {
235    fn width(&self) -> u32 {
236        self.width()
237    }
238    fn height(&self) -> u32 {
239        self.height()
240    }
241}
242
243impl<P: Send, S: RenderSize + Sync> Image<P, S> {
244    /// Generates an image by computing a per-pixel function
245    ///
246    /// This should be called on the _output_ image; the closure takes `(x, y)`
247    /// tuples and is expected to capture one or more source images.
248    pub fn apply_effect<F: Fn(usize, usize) -> P + Send + Sync>(
249        &mut self,
250        f: F,
251        threads: Option<&ThreadPool>,
252    ) {
253        let r = |(y, row): (usize, &mut [P])| {
254            for (x, v) in row.iter_mut().enumerate() {
255                *v = f(x, y);
256            }
257        };
258
259        if let Some(threads) = threads {
260            threads.run(|| {
261                self.data
262                    .par_chunks_mut(self.size.width() as usize)
263                    .enumerate()
264                    .for_each(r)
265            })
266        } else {
267            self.data
268                .chunks_mut(self.size.width() as usize)
269                .enumerate()
270                .for_each(r)
271        }
272    }
273}
274
275impl<P: IntoBytes + Immutable, S: RenderSize> Image<P, S> {
276    /// Returns the raw bytes of the image
277    pub fn as_bytes(&self) -> &[u8] {
278        self.data.as_bytes()
279    }
280}
281
282impl<P, S: Default> Default for Image<P, S> {
283    fn default() -> Self {
284        Image {
285            data: vec![],
286            size: S::default(),
287        }
288    }
289}
290
291impl<P: Default + Clone, S: RenderSize> Image<P, S> {
292    /// Builds a new image filled with `P::default()`
293    pub fn new(size: S) -> Self {
294        Self {
295            data: vec![
296                P::default();
297                size.width() as usize * size.height() as usize
298            ],
299            size,
300        }
301    }
302}
303
304impl<P, S: Clone> Image<P, S> {
305    /// Returns the image size
306    pub fn size(&self) -> S {
307        self.size.clone()
308    }
309
310    /// Generates an image by mapping a simple function over each pixel
311    pub fn map<T, F: Fn(&P) -> T>(&self, f: F) -> Image<T, S> {
312        let data = self.data.iter().map(f).collect();
313        Image {
314            data,
315            size: self.size.clone(),
316        }
317    }
318
319    /// Returns the pixel data as a slice
320    pub fn as_slice(&self) -> &[P] {
321        &self.data
322    }
323
324    /// Decomposes the image into its components
325    pub fn take(self) -> (Vec<P>, S) {
326        (self.data, self.size)
327    }
328}
329
330impl<P, S: RenderSize> Image<P, S> {
331    /// Returns the image width
332    pub fn width(&self) -> usize {
333        self.size.width() as usize
334    }
335
336    /// Returns the image height
337    pub fn height(&self) -> usize {
338        self.size.height() as usize
339    }
340
341    /// Checks a `(row, column)` position
342    ///
343    /// Returns the input position in the 1D array if valid; panics otherwise
344    fn decode_position(&self, pos: (usize, usize)) -> usize {
345        let (row, col) = pos;
346        assert!(
347            row < self.height(),
348            "row ({row}) must be less than image height ({})",
349            self.height()
350        );
351        assert!(
352            col < self.width(),
353            "column ({col}) must be less than image width ({})",
354            self.width()
355        );
356        row * self.width() + col
357    }
358
359    /// Builds an image from its components
360    ///
361    /// Returns an error if `data` does not match the number of pixels in `size`
362    pub fn build(data: Vec<P>, size: S) -> Result<Self, BadPixelCount> {
363        let expected = u64::from(size.width()) * u64::from(size.height());
364        let actual = data.len();
365        if expected != actual as u64 {
366            return Err(BadPixelCount {
367                expected,
368                actual,
369                width: size.width(),
370                height: size.height(),
371            });
372        }
373        Ok(Self { data, size })
374    }
375}
376
377impl<P, S> Image<P, S> {
378    /// Iterates over pixel values
379    pub fn iter(&self) -> impl Iterator<Item = &P> + '_ {
380        self.data.iter()
381    }
382
383    /// Returns the number of pixels in the image
384    pub fn len(&self) -> usize {
385        self.data.len()
386    }
387
388    /// Checks whether the image is empty
389    pub fn is_empty(&self) -> bool {
390        self.data.is_empty()
391    }
392}
393
394impl<'a, P: 'a, S> IntoIterator for &'a Image<P, S> {
395    type Item = &'a P;
396    type IntoIter = std::slice::Iter<'a, P>;
397    fn into_iter(self) -> Self::IntoIter {
398        self.data.iter()
399    }
400}
401
402impl<P, S> IntoIterator for Image<P, S> {
403    type Item = P;
404    type IntoIter = std::vec::IntoIter<P>;
405    fn into_iter(self) -> Self::IntoIter {
406        self.data.into_iter()
407    }
408}
409
410impl<P, S> std::ops::Index<usize> for Image<P, S> {
411    type Output = P;
412    fn index(&self, index: usize) -> &Self::Output {
413        &self.data[index]
414    }
415}
416
417impl<P, S> std::ops::IndexMut<usize> for Image<P, S> {
418    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
419        &mut self.data[index]
420    }
421}
422
423macro_rules! define_image_index {
424    ($ty:ty) => {
425        impl<P, S> std::ops::Index<$ty> for Image<P, S> {
426            type Output = [P];
427            fn index(&self, index: $ty) -> &Self::Output {
428                &self.data[index]
429            }
430        }
431
432        impl<P, S> std::ops::IndexMut<$ty> for Image<P, S> {
433            fn index_mut(&mut self, index: $ty) -> &mut Self::Output {
434                &mut self.data[index]
435            }
436        }
437    };
438}
439
440define_image_index!(std::ops::Range<usize>);
441define_image_index!(std::ops::RangeTo<usize>);
442define_image_index!(std::ops::RangeFrom<usize>);
443define_image_index!(std::ops::RangeInclusive<usize>);
444define_image_index!(std::ops::RangeToInclusive<usize>);
445define_image_index!(std::ops::RangeFull);
446
447/// Indexes an image with `(row, col)`
448impl<P, S: RenderSize> std::ops::Index<(usize, usize)> for Image<P, S> {
449    type Output = P;
450    fn index(&self, pos: (usize, usize)) -> &Self::Output {
451        let index = self.decode_position(pos);
452        &self.data[index]
453    }
454}
455
456impl<P, S: RenderSize> std::ops::IndexMut<(usize, usize)> for Image<P, S> {
457    fn index_mut(&mut self, pos: (usize, usize)) -> &mut Self::Output {
458        let index = self.decode_position(pos);
459        &mut self.data[index]
460    }
461}
462
463impl<P: Default + Copy + Clone> Image<P, voxel::RenderSize> {
464    /// Returns the image depth in voxels
465    pub fn depth(&self) -> usize {
466        self.size.depth() as usize
467    }
468}
469
470/// Three-channel color image
471pub type ColorImage = Image<[u8; 3]>;
472
473/// Error type for image builder
474#[derive(thiserror::Error, Debug, PartialEq)]
475#[error(
476    "bad pixel count: expected {expected} ({width} × {height}), got {actual}"
477)]
478pub struct BadPixelCount {
479    /// Expected pixel count from size
480    pub expected: u64,
481    /// Actual pixel count in data
482    pub actual: usize,
483    /// Expected width
484    pub width: u32,
485    /// Expected height
486    pub height: u32,
487}
488
489#[cfg(test)]
490mod test {
491    use super::*;
492
493    #[test]
494    fn image_construction() {
495        let i = Image::build(vec![1, 2, 3, 4, 5, 6], ImageSize::new(2, 3));
496        assert!(i.is_ok());
497
498        let i = Image::build(vec![1, 2, 3, 4, 5, 6], ImageSize::new(3, 2));
499        assert!(i.is_ok());
500
501        let i = Image::build(vec![1, 2, 3, 4, 5], ImageSize::new(2, 3));
502        let Err(e) = i else {
503            panic!("expected error, got valid image");
504        };
505        assert_eq!(
506            e,
507            BadPixelCount {
508                expected: 6,
509                actual: 5,
510                width: 2,
511                height: 3,
512            }
513        );
514    }
515}