Skip to main content

fidget_core/render/
mod.rs

1//! Common types for all kinds of rendering
2use crate::{
3    eval::{BulkEvaluator, Function, Trace, TracingEvaluator},
4    shape::{Shape, ShapeTape},
5};
6
7mod config;
8mod region;
9
10pub use config::{CancelToken, ThreadPool};
11pub use region::{ImageSize, RegionSize, VoxelSize};
12
13/// A `RenderHandle` contains lazily-populated tapes for rendering
14///
15/// This can be cheaply cloned, although it is _usually_ passed by mutable
16/// reference to a recursive function.
17///
18/// The most recent simplification is cached for reuse (if the trace matches).
19pub struct RenderHandle<F: Function> {
20    shape: Shape<F>,
21
22    i_tape: Option<ShapeTape<<F::IntervalEval as TracingEvaluator>::Tape>>,
23    f_tape: Option<ShapeTape<<F::FloatSliceEval as BulkEvaluator>::Tape>>,
24    g_tape: Option<ShapeTape<<F::GradSliceEval as BulkEvaluator>::Tape>>,
25
26    next: Option<(F::Trace, Box<Self>)>,
27}
28
29impl<F: Function> Clone for RenderHandle<F> {
30    #[inline]
31    fn clone(&self) -> Self {
32        Self {
33            shape: self.shape.clone(),
34            i_tape: self.i_tape.clone(),
35            f_tape: self.f_tape.clone(),
36            g_tape: self.g_tape.clone(),
37            next: None,
38        }
39    }
40}
41
42impl<F: Function> RenderHandle<F> {
43    /// Build a new [`RenderHandle`] for the given shape
44    ///
45    /// None of the tapes are populated here.
46    pub fn new(shape: Shape<F>) -> Self {
47        Self {
48            shape,
49            i_tape: None,
50            f_tape: None,
51            g_tape: None,
52            next: None,
53        }
54    }
55
56    /// Returns a tape for tracing interval evaluation
57    #[inline]
58    pub fn i_tape(
59        &mut self,
60        storage: &mut Vec<F::TapeStorage>,
61    ) -> &ShapeTape<<F::IntervalEval as TracingEvaluator>::Tape> {
62        self.i_tape.get_or_insert_with(|| {
63            self.shape.interval_tape(storage.pop().unwrap_or_default())
64        })
65    }
66
67    /// Returns a tape for bulk float evaluation
68    #[inline]
69    pub fn f_tape(
70        &mut self,
71        storage: &mut Vec<F::TapeStorage>,
72    ) -> &ShapeTape<<F::FloatSliceEval as BulkEvaluator>::Tape> {
73        self.f_tape.get_or_insert_with(|| {
74            self.shape
75                .float_slice_tape(storage.pop().unwrap_or_default())
76        })
77    }
78
79    /// Returns a tape for bulk gradient evaluation
80    #[inline]
81    pub fn g_tape(
82        &mut self,
83        storage: &mut Vec<F::TapeStorage>,
84    ) -> &ShapeTape<<F::GradSliceEval as BulkEvaluator>::Tape> {
85        self.g_tape.get_or_insert_with(|| {
86            self.shape
87                .grad_slice_tape(storage.pop().unwrap_or_default())
88        })
89    }
90
91    /// Simplifies the shape with the given trace
92    ///
93    /// As an internal optimization, this may reuse a previous simplification if
94    /// the trace matches.
95    #[inline]
96    pub fn simplify(
97        &mut self,
98        trace: &F::Trace,
99        workspace: &mut F::Workspace,
100        shape_storage: &mut Vec<F::Storage>,
101        tape_storage: &mut Vec<F::TapeStorage>,
102    ) -> &mut Self {
103        // Free self.next if it doesn't match our new set of choices
104        let mut trace_storage = if let Some(neighbor) = &self.next {
105            if &neighbor.0 != trace {
106                let (trace, neighbor) = self.next.take().unwrap();
107                neighbor.recycle(shape_storage, tape_storage);
108                Some(trace)
109                // continue with simplification
110            } else {
111                None
112            }
113        } else {
114            None
115        };
116
117        // Ordering is a little weird here, to persuade the borrow checker to be
118        // happy about things.  At this point, `next` is empty if we can't reuse
119        // it, and `Some(..)` if we can.  Clippy can't quite handle this
120        // ordering, rust-lang/rust-clippy#16467 and #16182
121        #[allow(clippy::panicking_unwrap, clippy::unnecessary_unwrap)]
122        if self.next.is_none() {
123            let s = shape_storage.pop().unwrap_or_default();
124            let next = self.shape.simplify(trace, s, workspace).unwrap();
125            if next.size() >= self.shape.size() {
126                // Optimization: if the simplified shape isn't any shorter, then
127                // don't use it (this saves time spent generating tapes)
128                shape_storage.extend(next.recycle());
129                self
130            } else {
131                assert!(self.next.is_none());
132                if let Some(t) = trace_storage.as_mut() {
133                    t.copy_from(trace);
134                } else {
135                    trace_storage = Some(trace.clone());
136                }
137                self.next = Some((
138                    trace_storage.unwrap(),
139                    Box::new(RenderHandle {
140                        shape: next,
141                        i_tape: None,
142                        f_tape: None,
143                        g_tape: None,
144                        next: None,
145                    }),
146                ));
147                &mut self.next.as_mut().unwrap().1
148            }
149        } else {
150            &mut self.next.as_mut().unwrap().1
151        }
152    }
153
154    /// Recycles the entire handle into the given storage vectors
155    #[inline]
156    pub fn recycle(
157        mut self,
158        shape_storage: &mut Vec<F::Storage>,
159        tape_storage: &mut Vec<F::TapeStorage>,
160    ) {
161        // Recycle the child first, in case it borrowed from us
162        if let Some((_trace, shape)) = self.next.take() {
163            shape.recycle(shape_storage, tape_storage);
164        }
165
166        if let Some(i_tape) = self.i_tape.take() {
167            tape_storage.extend(i_tape.recycle());
168        }
169        if let Some(g_tape) = self.g_tape.take() {
170            tape_storage.extend(g_tape.recycle());
171        }
172        if let Some(f_tape) = self.f_tape.take() {
173            tape_storage.extend(f_tape.recycle());
174        }
175
176        // Do this step last because the evaluators may borrow the shape
177        shape_storage.extend(self.shape.recycle());
178    }
179}
180
181/// Container representing an ordered, checked list of tile sizes
182///
183/// This object wraps a `Vec<usize>`, guaranteeing three invariants:
184///
185/// - There must be at least one tile size
186/// - Tiles must be ordered from largest to smallest
187/// - Each tile size must be exactly divisible by subsequent tile sizes
188#[derive(Debug, Eq, PartialEq)]
189pub struct TileSizes(Vec<usize>);
190
191/// Error type when constructing a [`TileSizes`] list
192#[derive(thiserror::Error, Debug)]
193pub enum TileSizeError {
194    /// Each tile must be divisible by subsequent tiles
195    #[error("bad tile sizes; {0} is not divisible by {1}")]
196    BadTileSize(usize, usize),
197
198    /// Tile size list must be in descending order
199    #[error("bad tile order; {0} is not larger than {1}")]
200    BadTileOrder(usize, usize),
201
202    /// Tile size list must not be empty
203    #[error("tile size list must not be empty")]
204    EmptyTileSizes,
205}
206
207impl TileSizes {
208    /// Builds a new tile size list, checking invariants
209    pub fn new(sizes: &[usize]) -> Result<Self, TileSizeError> {
210        if sizes.is_empty() {
211            return Err(TileSizeError::EmptyTileSizes);
212        }
213        for i in 1..sizes.len() {
214            if sizes[i - 1] <= sizes[i] {
215                return Err(TileSizeError::BadTileOrder(
216                    sizes[i - 1],
217                    sizes[i],
218                ));
219            } else if !sizes[i - 1].is_multiple_of(sizes[i]) {
220                return Err(TileSizeError::BadTileSize(sizes[i - 1], sizes[i]));
221            }
222        }
223        Ok(Self(sizes.to_vec()))
224    }
225
226    /// Returns the length of the tile list
227    #[allow(clippy::len_without_is_empty)]
228    pub fn len(&self) -> usize {
229        self.0.len()
230    }
231
232    /// Returns an iterator over tile sizes (largest to smallest)
233    pub fn iter(&self) -> impl Iterator<Item = &usize> {
234        self.0.iter()
235    }
236}
237
238impl std::ops::Index<usize> for TileSizes {
239    type Output = usize;
240
241    fn index(&self, i: usize) -> &Self::Output {
242        &self.0[i]
243    }
244}
245
246impl std::ops::Index<std::ops::RangeFrom<usize>> for TileSizes {
247    type Output = [usize];
248    fn index(&self, index: std::ops::RangeFrom<usize>) -> &Self::Output {
249        &self.0[index]
250    }
251}
252
253/// Hints for how to render this particular type
254///
255/// This is a bit of a grab-bag trait for both rasterization and meshing; it's
256/// in `fidget-core` so that other evaluators can implement it without needing
257/// to depend on `fidget-raster` or `fidget-mesh`.
258pub trait RenderHints {
259    /// Recommended tile sizes for 3D rendering
260    fn tile_sizes_3d() -> TileSizes;
261
262    /// Recommended tile sizes for 2D rendering
263    fn tile_sizes_2d() -> TileSizes;
264
265    /// Indicates whether we run tape simplification at the given cell depth
266    /// during meshing.
267    ///
268    /// By default, this is always true; for evaluators where simplification is
269    /// more expensive than evaluation (i.e. the JIT), it may only be true at
270    /// certain depths.
271    fn simplify_tree_during_meshing(_d: usize) -> bool {
272        true
273    }
274}