Skip to main content

fidget_core/render/
mod.rs

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