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>, C>(
100    shape: Shape<F>,
101    vars: &'a ShapeVars<f32>,
102    render_config: &'a W::Config,
103    eval_config: &'a C,
104    tile_sizes: TileSizesRef<'a>,
105) -> Option<Vec<(Tile<2>, W::Output)>>
106where
107    W::Config: Send + Sync,
108    C: EvalConfig + Send + Sync,
109{
110    use rayon::prelude::*;
111
112    let mut tiles = vec![];
113    let t = tile_sizes[0];
114    let width = render_config.width() as usize;
115    let height = render_config.height() as usize;
116    for i in 0..width.div_ceil(t) {
117        for j in 0..height.div_ceil(t) {
118            tiles.push(Tile::new(Point2::new(
119                i * tile_sizes[0],
120                j * tile_sizes[0],
121            )));
122        }
123    }
124
125    let mut rh = RenderHandle::new(shape);
126
127    let _ = rh.i_tape(&mut vec![]); // populate i_tape before cloning
128    let ts = tile_sizes;
129    let init = || {
130        let rh = rh.clone();
131        let worker = W::new(render_config, ts, vars);
132        (worker, rh)
133    };
134
135    match eval_config.threads() {
136        None => {
137            let mut worker = W::new(render_config, tile_sizes, vars);
138            tiles
139                .into_iter()
140                .map(|tile| {
141                    if eval_config.is_cancelled() {
142                        Err(())
143                    } else {
144                        let pixels = worker.render_tile(&mut rh, tile);
145                        Ok((tile, pixels))
146                    }
147                })
148                .collect::<Result<Vec<_>, ()>>()
149                .ok()
150        }
151
152        Some(p) => p.run(|| {
153            tiles
154                .into_par_iter()
155                .map_init(init, |(w, rh), tile| {
156                    if eval_config.is_cancelled() {
157                        Err(())
158                    } else {
159                        let pixels = w.render_tile(rh, tile);
160                        Ok((tile, pixels))
161                    }
162                })
163                .collect::<Result<Vec<_>, ()>>()
164                .ok()
165        }),
166    }
167}
168
169/// Helper trait for tiled rendering configuration
170pub(crate) trait EvalConfig {
171    fn threads(&self) -> Option<&ThreadPool>;
172    fn is_cancelled(&self) -> bool;
173}
174
175/// Trait for things that have a width and height in pixels
176pub trait RenderSize {
177    /// Width of the render, in voxels or pixels
178    fn width(&self) -> u32;
179    /// Height of the render, in voxels or pixels
180    fn height(&self) -> u32;
181}
182
183/// Helper trait for a tiled renderer worker
184pub(crate) trait RenderWorker<'a, F: Function> {
185    type Config: RenderSize;
186    type Output: Send;
187
188    /// Build a new worker
189    ///
190    /// Workers are typically built on a per-thread basis
191    fn new(
192        cfg: &'a Self::Config,
193        tile_sizes: TileSizesRef<'a>,
194        vars: &'a ShapeVars<f32>,
195    ) -> Self;
196
197    /// Render a single tile, returning a worker-dependent output
198    fn render_tile(
199        &mut self,
200        shape: &mut RenderHandle<F>,
201        tile: Tile<2>,
202    ) -> Self::Output;
203}
204
205/// Generic image type
206///
207/// The image is laid out in row-major order, and can be indexed either by a
208/// `usize` index or a `(row, column)` tuple.
209///
210/// ```text
211///        0 ------------> width (columns)
212///        |             |
213///        |             |
214///        |             |
215///        V--------------
216///   height (rows)
217///
218/// Users will likely be using one of the image typedefs ([`voxel::Image`] and
219/// [`pixel::Image`]).
220/// ```
221#[derive(Clone)]
222pub struct Image<P, S = ImageSize> {
223    data: Vec<P>,
224    size: S,
225}
226
227impl RenderSize for pixel::RenderSize {
228    fn width(&self) -> u32 {
229        self.width()
230    }
231    fn height(&self) -> u32 {
232        self.height()
233    }
234}
235
236impl RenderSize for voxel::RenderSize {
237    fn width(&self) -> u32 {
238        self.width()
239    }
240    fn height(&self) -> u32 {
241        self.height()
242    }
243}
244
245impl<P: Send, S: RenderSize + Sync> Image<P, S> {
246    /// Generates an image by computing a per-pixel function
247    ///
248    /// This should be called on the _output_ image; the closure takes `(x, y)`
249    /// tuples and is expected to capture one or more source images.
250    pub fn apply_effect<F: Fn(usize, usize) -> P + Send + Sync>(
251        &mut self,
252        f: F,
253        threads: Option<&ThreadPool>,
254    ) {
255        let r = |(y, row): (usize, &mut [P])| {
256            for (x, v) in row.iter_mut().enumerate() {
257                *v = f(x, y);
258            }
259        };
260
261        if let Some(threads) = threads {
262            threads.run(|| {
263                self.data
264                    .par_chunks_mut(self.size.width() as usize)
265                    .enumerate()
266                    .for_each(r)
267            })
268        } else {
269            self.data
270                .chunks_mut(self.size.width() as usize)
271                .enumerate()
272                .for_each(r)
273        }
274    }
275}
276
277impl<P: IntoBytes + Immutable, S: RenderSize> Image<P, S> {
278    /// Returns the raw bytes of the image
279    pub fn as_bytes(&self) -> &[u8] {
280        self.data.as_bytes()
281    }
282}
283
284impl<P, S: Default> Default for Image<P, S> {
285    fn default() -> Self {
286        Image {
287            data: vec![],
288            size: S::default(),
289        }
290    }
291}
292
293impl<P: Default + Clone, S: RenderSize> Image<P, S> {
294    /// Builds a new image filled with `P::default()`
295    pub fn new(size: S) -> Self {
296        Self {
297            data: vec![
298                P::default();
299                size.width() as usize * size.height() as usize
300            ],
301            size,
302        }
303    }
304}
305
306impl<P, S: Clone> Image<P, S> {
307    /// Returns the image size
308    pub fn size(&self) -> S {
309        self.size.clone()
310    }
311
312    /// Generates an image by mapping a simple function over each pixel
313    pub fn map<T, F: Fn(&P) -> T>(&self, f: F) -> Image<T, S> {
314        let data = self.data.iter().map(f).collect();
315        Image {
316            data,
317            size: self.size.clone(),
318        }
319    }
320
321    /// Returns the pixel data as a slice
322    pub fn as_slice(&self) -> &[P] {
323        &self.data
324    }
325
326    /// Decomposes the image into its components
327    pub fn take(self) -> (Vec<P>, S) {
328        (self.data, self.size)
329    }
330}
331
332impl<P, S: RenderSize> Image<P, S> {
333    /// Returns the image width
334    pub fn width(&self) -> usize {
335        self.size.width() as usize
336    }
337
338    /// Returns the image height
339    pub fn height(&self) -> usize {
340        self.size.height() as usize
341    }
342
343    /// Checks a `(row, column)` position
344    ///
345    /// Returns the input position in the 1D array if valid; panics otherwise
346    fn decode_position(&self, pos: (usize, usize)) -> usize {
347        let (row, col) = pos;
348        assert!(
349            row < self.height(),
350            "row ({row}) must be less than image height ({})",
351            self.height()
352        );
353        assert!(
354            col < self.width(),
355            "column ({col}) must be less than image width ({})",
356            self.width()
357        );
358        row * self.width() + col
359    }
360
361    /// Builds an image from its components
362    ///
363    /// Returns an error if `data` does not match the number of pixels in `size`
364    pub fn build(data: Vec<P>, size: S) -> Result<Self, BadPixelCount> {
365        let expected = u64::from(size.width()) * u64::from(size.height());
366        let actual = data.len();
367        if expected != actual as u64 {
368            return Err(BadPixelCount {
369                expected,
370                actual,
371                width: size.width(),
372                height: size.height(),
373            });
374        }
375        Ok(Self { data, size })
376    }
377}
378
379impl<P, S> Image<P, S> {
380    /// Iterates over pixel values
381    pub fn iter(&self) -> impl Iterator<Item = &P> + '_ {
382        self.data.iter()
383    }
384
385    /// Returns the number of pixels in the image
386    pub fn len(&self) -> usize {
387        self.data.len()
388    }
389
390    /// Checks whether the image is empty
391    pub fn is_empty(&self) -> bool {
392        self.data.is_empty()
393    }
394}
395
396impl<'a, P: 'a, S> IntoIterator for &'a Image<P, S> {
397    type Item = &'a P;
398    type IntoIter = std::slice::Iter<'a, P>;
399    fn into_iter(self) -> Self::IntoIter {
400        self.data.iter()
401    }
402}
403
404impl<P, S> IntoIterator for Image<P, S> {
405    type Item = P;
406    type IntoIter = std::vec::IntoIter<P>;
407    fn into_iter(self) -> Self::IntoIter {
408        self.data.into_iter()
409    }
410}
411
412impl<P, S> std::ops::Index<usize> for Image<P, S> {
413    type Output = P;
414    fn index(&self, index: usize) -> &Self::Output {
415        &self.data[index]
416    }
417}
418
419impl<P, S> std::ops::IndexMut<usize> for Image<P, S> {
420    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
421        &mut self.data[index]
422    }
423}
424
425macro_rules! define_image_index {
426    ($ty:ty) => {
427        impl<P, S> std::ops::Index<$ty> for Image<P, S> {
428            type Output = [P];
429            fn index(&self, index: $ty) -> &Self::Output {
430                &self.data[index]
431            }
432        }
433
434        impl<P, S> std::ops::IndexMut<$ty> for Image<P, S> {
435            fn index_mut(&mut self, index: $ty) -> &mut Self::Output {
436                &mut self.data[index]
437            }
438        }
439    };
440}
441
442define_image_index!(std::ops::Range<usize>);
443define_image_index!(std::ops::RangeTo<usize>);
444define_image_index!(std::ops::RangeFrom<usize>);
445define_image_index!(std::ops::RangeInclusive<usize>);
446define_image_index!(std::ops::RangeToInclusive<usize>);
447define_image_index!(std::ops::RangeFull);
448
449/// Indexes an image with `(row, col)`
450impl<P, S: RenderSize> std::ops::Index<(usize, usize)> for Image<P, S> {
451    type Output = P;
452    fn index(&self, pos: (usize, usize)) -> &Self::Output {
453        let index = self.decode_position(pos);
454        &self.data[index]
455    }
456}
457
458impl<P, S: RenderSize> std::ops::IndexMut<(usize, usize)> for Image<P, S> {
459    fn index_mut(&mut self, pos: (usize, usize)) -> &mut Self::Output {
460        let index = self.decode_position(pos);
461        &mut self.data[index]
462    }
463}
464
465impl<P: Default + Copy + Clone> Image<P, voxel::RenderSize> {
466    /// Returns the image depth in voxels
467    pub fn depth(&self) -> usize {
468        self.size.depth() as usize
469    }
470}
471
472/// Three-channel color image
473pub type ColorImage = Image<[u8; 3]>;
474
475/// Error type for image builder
476#[derive(thiserror::Error, Debug, PartialEq)]
477#[error(
478    "bad pixel count: expected {expected} ({width} × {height}), got {actual}"
479)]
480pub struct BadPixelCount {
481    /// Expected pixel count from size
482    pub expected: u64,
483    /// Actual pixel count in data
484    pub actual: usize,
485    /// Expected width
486    pub width: u32,
487    /// Expected height
488    pub height: u32,
489}
490
491#[cfg(test)]
492mod test {
493    use super::*;
494
495    #[test]
496    fn image_construction() {
497        let i = Image::build(vec![1, 2, 3, 4, 5, 6], ImageSize::new(2, 3));
498        assert!(i.is_ok());
499
500        let i = Image::build(vec![1, 2, 3, 4, 5, 6], ImageSize::new(3, 2));
501        assert!(i.is_ok());
502
503        let i = Image::build(vec![1, 2, 3, 4, 5], ImageSize::new(2, 3));
504        let Err(e) = i else {
505            panic!("expected error, got valid image");
506        };
507        assert_eq!(
508            e,
509            BadPixelCount {
510                expected: 6,
511                actual: 5,
512                width: 2,
513                height: 3,
514            }
515        );
516    }
517}