Skip to content

Commit

Permalink
Add ShapeVars helper type; use it in rendering + meshing (#190)
Browse files Browse the repository at this point in the history
- Add new `ShapeVars<T>` type, representing a map from `VarIndex -> T`.  This
  type is used for high-level rendering and meshing of `Shape` objects that
  include supplementary variables
- Add `Octree::build_with_vars` and `Image/VoxelRenderConfig::run_with_vars`
  functions for shapes with supplementary variables
- Change `ShapeBulkEval::eval_v` to take **single** variables (i.e. `x`, `y`,
  `z` vary but each variable has a constant value across the whole slice).  Add
  `ShapeBulkEval::eval_vs` if `x`, `y`, `z` and variables are _all_ changing
  through the slices.
  • Loading branch information
mkeeter authored Nov 17, 2024
1 parent f6da7b7 commit 3b94b8c
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 203 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
- Remove fine-grained features from `fidget` crate, because we aren't actually
testing the power-set of feature combinations in CI (and some were breaking!).
The only remaining features are `rhai`, `jit` and `eval-tests`.
- Add new `ShapeVars<T>` type, representing a map from `VarIndex -> T`. This
type is used for high-level rendering and meshing of `Shape` objects that
include supplementary variables
- Add `Octree::build_with_vars` and `Image/VoxelRenderConfig::run_with_vars`
functions for shapes with supplementary variables
- Change `ShapeBulkEval::eval_v` to take **single** variables (i.e. `x`, `y`,
`z` vary but each variable has a constant value across the whole slice). Add
`ShapeBulkEval::eval_vs` if `x`, `y`, `z` and variables are _all_ changing
through the slices.

# 0.3.3
- `Function` and evaluator types now produce multiple outputs
Expand Down
18 changes: 9 additions & 9 deletions fidget/src/core/eval/test/float_slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ use super::{
use crate::{
context::Context,
eval::{BulkEvaluator, Function, MathFunction, Tape},
shape::{EzShape, Shape},
var::{Var, VarIndex},
shape::{EzShape, Shape, ShapeVars},
var::Var,
Error,
};
use std::collections::HashMap;

/// Helper struct to put constrains on our `Shape` object
pub struct TestFloatSlice<F>(std::marker::PhantomData<*const F>);
Expand Down Expand Up @@ -126,29 +125,30 @@ impl<F: Function + MathFunction> TestFloatSlice<F> {
assert!(eval
.eval(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0])
.is_err());
let mut h: HashMap<VarIndex, &[f32]> = HashMap::new();
let mut h: ShapeVars<&[f32]> = ShapeVars::new();
assert!(eval
.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h)
.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h)
.is_err());
let index = v.index().unwrap();
h.insert(index, &[4.0, 5.0]);
assert_eq!(
eval.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h)
eval.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h)
.unwrap(),
&[7.0, 10.0]
);

h.insert(index, &[4.0, 5.0, 6.0]);
assert!(matches!(
eval.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h),
Err(Error::MismatchedSlices)
eval.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h),
Err(Error::MismatchedSlices),
));

// Get a new var index that isn't valid for this tape
let v2 = Var::new();
h.insert(index, &[4.0, 5.0]);
h.insert(v2.index().unwrap(), &[4.0, 5.0]);
assert!(matches!(
eval.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h),
eval.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h),
Err(Error::BadVarSlice(..))
));
}
Expand Down
5 changes: 2 additions & 3 deletions fidget/src/core/eval/test/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ use super::{
use crate::{
context::Context,
eval::{Function, MathFunction, Tape, TracingEvaluator},
shape::{EzShape, Shape},
shape::{EzShape, Shape, ShapeVars},
var::Var,
vm::Choice,
};
use std::collections::HashMap;

/// Helper struct to put constrains on our `Shape` object
pub struct TestPoint<F>(std::marker::PhantomData<*const F>);
Expand Down Expand Up @@ -369,7 +368,7 @@ where
let tape = s.ez_point_tape();
assert!(eval.eval(&tape, 1.0, 2.0, 0.0).is_err());

let mut h = HashMap::new();
let mut h = ShapeVars::new();
assert!(eval.eval_v(&tape, 1.0, 2.0, 0.0, &h).is_err());

let index = v.index().unwrap();
Expand Down
145 changes: 128 additions & 17 deletions fidget/src/core/shape/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,53 @@ impl<F> Shape<F> {
}
}

/// Variables bound to values for shape evaluation
///
/// Note that this cannot store `X`, `Y`, `Z` variables (which are passed in as
/// first-class arguments); it only stores [`Var::V`] values (identified by
/// their inner [`VarIndex`]).
pub struct ShapeVars<F>(HashMap<VarIndex, F>);

impl<F> Default for ShapeVars<F> {
fn default() -> Self {
Self(HashMap::default())
}
}

impl<F> ShapeVars<F> {
/// Builds a new, empty variable set
pub fn new() -> Self {
Self(HashMap::default())
}
/// Returns the number of variables stored in the set
pub fn len(&self) -> usize {
self.0.len()
}
/// Checks whether the variable set is empty
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Inserts a new variable
///
/// Returns the previous value (if present)
pub fn insert(&mut self, v: VarIndex, f: F) -> Option<F> {
self.0.insert(v, f)
}

/// Iterates over values
pub fn values(&self) -> impl Iterator<Item = &F> {
self.0.values()
}
}

impl<'a, F> IntoIterator for &'a ShapeVars<F> {
type Item = (&'a VarIndex, &'a F);
type IntoIter = std::collections::hash_map::Iter<'a, VarIndex, F>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}

/// Extension trait for working with a shape without thinking much about memory
///
/// All of the [`Shape`] functions that use significant amounts of memory
Expand Down Expand Up @@ -372,20 +419,20 @@ where
y: F,
z: F,
) -> Result<(E::Data, Option<&E::Trace>), Error> {
let h = HashMap::default();
let h = ShapeVars::<f32>::new();
self.eval_v(tape, x, y, z, &h)
}

/// Tracing evaluation of a single sample
///
/// Before evaluation, the tape's transform matrix is applied (if present).
pub fn eval_v<F: Into<E::Data> + Copy>(
pub fn eval_v<F: Into<E::Data> + Copy, V: Into<E::Data> + Copy>(
&mut self,
tape: &ShapeTape<E::Tape>,
x: F,
y: F,
z: F,
vars: &HashMap<VarIndex, F>,
vars: &ShapeVars<V>,
) -> Result<(E::Data, Option<&E::Trace>), Error> {
assert_eq!(
tape.tape.output_count(),
Expand Down Expand Up @@ -455,8 +502,8 @@ where
/// Bulk evaluation of many samples, without any variables
///
/// If the shape includes variables other than `X`, `Y`, `Z`,
/// [`eval_v`](Self::eval_v) should be used instead (and this function will
/// return an error).
/// [`eval_v`](Self::eval_v) or [`eval_vs`](Self::eval_vs) should be used
/// instead (and this function will return an error).
///
/// Before evaluation, the tape's transform matrix is applied (if present).
pub fn eval(
Expand All @@ -466,21 +513,19 @@ where
y: &[E::Data],
z: &[E::Data],
) -> Result<&[E::Data], Error> {
let h: HashMap<VarIndex, &[E::Data]> = HashMap::default();
self.eval_v(tape, x, y, z, &h)
let h: ShapeVars<&[E::Data]> = ShapeVars::new();
self.eval_vs(tape, x, y, z, &h)
}

/// Bulk evaluation of many samples, with variables
///
/// Before evaluation, the tape's transform matrix is applied (if present).
pub fn eval_v<V: std::ops::Deref<Target = [E::Data]>>(
/// Helper function to do common setup
fn setup<V>(
&mut self,
tape: &ShapeTape<E::Tape>,
x: &[E::Data],
y: &[E::Data],
z: &[E::Data],
vars: &HashMap<VarIndex, V>,
) -> Result<&[E::Data], Error> {
vars: &ShapeVars<V>,
) -> Result<usize, Error> {
assert_eq!(
tape.tape.output_count(),
1,
Expand All @@ -492,9 +537,7 @@ where
return Err(Error::MismatchedSlices);
}
let n = x.len();
if vars.values().any(|vs| vs.len() != n) {
return Err(Error::MismatchedSlices);
}

let vs = tape.vars();
let expected_vars = vs.len()
- vs.get(&Var::X).is_some() as usize
Expand Down Expand Up @@ -539,10 +582,78 @@ where
// TODO fast path if there are no extra vars, reusing slices
};

Ok(n)
}
/// Bulk evaluation of many samples, with slices of variables
///
/// Each variable is a slice (or `Vec`) of values, which must be the same
/// length as the `x`, `y`, `z` slices. This is in contrast with
/// [`eval_vs`](Self::eval_v), where variables have a single value used for
/// every position in the `x`, `y,` `z` slices.
///
///
/// Before evaluation, the tape's transform matrix is applied (if present).
pub fn eval_vs<
V: std::ops::Deref<Target = [G]>,
G: Into<E::Data> + Copy,
>(
&mut self,
tape: &ShapeTape<E::Tape>,
x: &[E::Data],
y: &[E::Data],
z: &[E::Data],
vars: &ShapeVars<V>,
) -> Result<&[E::Data], Error> {
let n = self.setup(tape, x, y, z, vars)?;

if vars.values().any(|vs| vs.len() != n) {
return Err(Error::MismatchedSlices);
}

let vs = tape.vars();
for (var, value) in vars {
if let Some(i) = vs.get(&Var::V(*var)) {
if i < self.scratch.len() {
for (a, b) in
self.scratch[i].iter_mut().zip(value.deref().iter())
{
*a = (*b).into();
}
// TODO fast path if we can use the slices directly?
} else {
return Err(Error::BadVarIndex(i, self.scratch.len()));
}
} else {
// Passing in Bonus Variables is allowed (for now)
}
}

let out = self.eval.eval(&tape.tape, &self.scratch)?;
Ok(out.borrow(0))
}

/// Bulk evaluation of many samples, with fixed variables
///
/// Each variable has a single value, which is used for every position in
/// the `x`, `y`, `z` slices. This is in contrast with
/// [`eval_vs`](Self::eval_vs), where variables can be different for every
/// position in the `x`, `y,` `z` slices.
///
/// Before evaluation, the tape's transform matrix is applied (if present).
pub fn eval_v<G: Into<E::Data> + Copy>(
&mut self,
tape: &ShapeTape<E::Tape>,
x: &[E::Data],
y: &[E::Data],
z: &[E::Data],
vars: &ShapeVars<G>,
) -> Result<&[E::Data], Error> {
self.setup(tape, x, y, z, vars)?;
let vs = tape.vars();
for (var, value) in vars {
if let Some(i) = vs.get(&Var::V(*var)) {
if i < self.scratch.len() {
self.scratch[i].copy_from_slice(value);
self.scratch[i].fill((*value).into());
} else {
return Err(Error::BadVarIndex(i, self.scratch.len()));
}
Expand Down
21 changes: 17 additions & 4 deletions fidget/src/mesh/mt/octree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
Octree,
},
render::RenderHints,
shape::ShapeVars,
};
use std::sync::{mpsc::TryRecvError, Arc};

Expand Down Expand Up @@ -123,6 +124,7 @@ pub struct OctreeWorker<F: Function + RenderHints> {
impl<F: Function + RenderHints> OctreeWorker<F> {
pub fn scheduler(
eval: Arc<EvalGroup<F>>,
vars: &ShapeVars<f32>,
settings: MultithreadedSettings,
) -> Octree {
let thread_count = settings.threads.get();
Expand Down Expand Up @@ -151,7 +153,9 @@ impl<F: Function + RenderHints> OctreeWorker<F> {
.collect::<Vec<_>>();

let root = CellIndex::default();
let r = workers[0].octree.eval_cell(&eval, root, settings.depth);
let r = workers[0]
.octree
.eval_cell(&eval, vars, root, settings.depth);
let c = match r {
CellResult::Done(cell) => Some(cell),
CellResult::Recurse(eval) => {
Expand All @@ -168,7 +172,9 @@ impl<F: Function + RenderHints> OctreeWorker<F> {
let out: Vec<Octree> = std::thread::scope(|s| {
let mut handles = vec![];
for w in workers {
handles.push(s.spawn(move || w.run(pool, settings.depth)));
handles.push(
s.spawn(move || w.run(vars, pool, settings.depth)),
);
}
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
Expand All @@ -177,7 +183,12 @@ impl<F: Function + RenderHints> OctreeWorker<F> {
}

/// Runs a single worker to completion as part of a worker group
pub fn run(mut self, threads: &ThreadPool, max_depth: u8) -> Octree {
pub fn run(
mut self,
vars: &ShapeVars<f32>,
threads: &ThreadPool,
max_depth: u8,
) -> Octree {
let mut ctx = threads.start(self.thread_index);
loop {
// First, check to see if anyone has finished a task and sent us
Expand Down Expand Up @@ -212,7 +223,9 @@ impl<F: Function + RenderHints> OctreeWorker<F> {
for i in Corner::iter() {
let sub_cell = task.target_cell.child(index, i);

match self.octree.eval_cell(&task.eval, sub_cell, max_depth)
match self
.octree
.eval_cell(&task.eval, vars, sub_cell, max_depth)
{
// If this child is finished, then record it locally.
// If it's a branching cell, then we'll let a caller
Expand Down
Loading

0 comments on commit 3b94b8c

Please sign in to comment.