1use super::RenderHandle;
3use crate::{
4 Image as GenericImage, RenderSize as _, RenderWorker, Tile, TileSizesRef,
5};
6use fidget_core::{
7 eval::Function,
8 render::{CancelToken, RenderHints, ThreadPool, TileSizes},
9 shape::{BoundShape, ShapeBulkEval, ShapeTracingEval, ShapeVars},
10 types::{Grad, Interval},
11};
12
13use nalgebra::{Matrix4, Point3, Vector2, Vector3};
14use zerocopy::{FromBytes, Immutable, IntoBytes};
15
16pub type Image = GenericImage<GeometryPixel, RenderSize>;
18
19pub type RenderSize = fidget_core::render::VoxelSize;
21
22#[derive(Copy, Clone, Debug)]
26pub struct RenderConfig {
27 pub image_size: RenderSize,
33
34 pub world_to_model: Matrix4<f32>,
36}
37
38pub struct EvalConfig<'a> {
40 pub tile_sizes: Option<TileSizes>,
45
46 pub threads: Option<&'a ThreadPool>,
51
52 pub cancel: CancelToken,
54}
55
56impl crate::EvalConfig for EvalConfig<'_> {
57 fn threads(&self) -> Option<&ThreadPool> {
58 self.threads
59 }
60 fn is_cancelled(&self) -> bool {
61 self.cancel.is_cancelled()
62 }
63}
64
65impl Default for EvalConfig<'_> {
66 fn default() -> Self {
67 Self {
68 tile_sizes: None,
69 threads: Some(&ThreadPool::Global),
70 cancel: CancelToken::new(),
71 }
72 }
73}
74
75impl crate::RenderSize for RenderConfig {
76 fn width(&self) -> u32 {
77 self.image_size.width()
78 }
79 fn height(&self) -> u32 {
80 self.image_size.height()
81 }
82}
83
84impl RenderConfig {
85 pub fn from_size(image_size: RenderSize) -> Self {
87 Self {
88 image_size,
89 world_to_model: Matrix4::identity(),
90 }
91 }
92
93 pub fn run<F: Function + RenderHints>(
99 &self,
100 shape: BoundShape<F, f32>,
101 ) -> Image {
102 render(shape, self, &Default::default())
103 .expect("no cancellation is possible")
104 }
105
106 pub fn mat(&self) -> Matrix4<f32> {
108 self.world_to_model * self.image_size.screen_to_world()
109 }
110}
111
112#[repr(C)]
123#[derive(
124 Debug, Default, Copy, Clone, IntoBytes, FromBytes, Immutable, PartialEq,
125)]
126pub struct GeometryPixel {
127 pub normal: [f32; 3],
129 pub depth: u32,
134}
135
136impl GeometryPixel {
137 pub fn to_color(&self) -> [u8; 3] {
139 let [dx, dy, dz] = self.normal;
140 let s = (dx.powi(2) + dy.powi(2) + dz.powi(2)).sqrt();
141 if s != 0.0 {
142 let scale = u8::MAX as f32 / s;
143 [
144 (dx.abs() * scale) as u8,
145 (dy.abs() * scale) as u8,
146 (dz.abs() * scale) as u8,
147 ]
148 } else {
149 [0; 3]
150 }
151 }
152}
153
154struct Scratch {
157 x: Vec<f32>,
158 y: Vec<f32>,
159 z: Vec<f32>,
160
161 xg: Vec<Grad>,
162 yg: Vec<Grad>,
163 zg: Vec<Grad>,
164
165 columns: Vec<usize>,
167}
168
169impl Scratch {
170 fn new(tile_size: usize) -> Self {
171 let size2 = tile_size.pow(2);
172 let size3 = tile_size.pow(3);
173
174 Self {
175 x: vec![0.0; size3],
176 y: vec![0.0; size3],
177 z: vec![0.0; size3],
178
179 xg: vec![Grad::from(0.0); size2],
180 yg: vec![Grad::from(0.0); size2],
181 zg: vec![Grad::from(0.0); size2],
182
183 columns: vec![0; size2],
184 }
185 }
186}
187
188struct Worker<'a, F: Function> {
191 tile_sizes: TileSizesRef<'a>,
192 vars: &'a ShapeVars<f32>,
193
194 transform: nalgebra::Matrix4<f32>,
195 image_size: RenderSize,
196
197 scratch: Scratch,
199
200 eval_float_slice: ShapeBulkEval<F::FloatSliceEval>,
201 eval_grad_slice: ShapeBulkEval<F::GradSliceEval>,
202 eval_interval: ShapeTracingEval<F::IntervalEval>,
203
204 tape_storage: Vec<F::TapeStorage>,
205 shape_storage: Vec<F::Storage>,
206 workspace: F::Workspace,
207
208 out: Image,
210}
211
212impl<'a, F: Function> RenderWorker<'a, F> for Worker<'a, F> {
213 type Config = RenderConfig;
214 type Output = Image;
215
216 fn new(
217 cfg: &'a Self::Config,
218 tile_sizes: TileSizesRef<'a>,
219 vars: &'a ShapeVars<f32>,
220 ) -> Self {
221 let transform = cfg.mat();
222 let buf_size = tile_sizes.last();
223 let scratch = Scratch::new(buf_size);
224 Worker {
225 tile_sizes,
226 vars,
227
228 scratch,
229 out: Default::default(),
230
231 transform,
232 image_size: cfg.image_size,
233
234 eval_float_slice: Default::default(),
235 eval_interval: Default::default(),
236 eval_grad_slice: Default::default(),
237
238 tape_storage: vec![],
239 shape_storage: vec![],
240 workspace: Default::default(),
241 }
242 }
243
244 fn render_tile(
245 &mut self,
246 shape: &mut RenderHandle<F>,
247 tile: Tile<2>,
248 ) -> Self::Output {
249 let root_tile_size = self.tile_sizes[0];
251 self.out = Image::new(RenderSize::from(root_tile_size as u32));
252 for k in (0..self.image_size[2].div_ceil(root_tile_size as u32)).rev() {
253 let tile = Tile::new(Point3::new(
254 tile.corner.x,
255 tile.corner.y,
256 k as usize * root_tile_size,
257 ));
258 if !self.render_tile_recurse(shape, 0, tile) {
259 break;
260 }
261 }
262 std::mem::take(&mut self.out)
263 }
264}
265
266impl<F: Function> Worker<'_, F> {
267 pub(crate) fn tile_row_offset(&self, tile: Tile<3>, row: usize) -> usize {
269 self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, row)))
270 }
271
272 fn render_tile_recurse(
276 &mut self,
277 shape: &mut RenderHandle<F>,
278 depth: usize,
279 tile: Tile<3>,
280 ) -> bool {
281 let tile_size = self.tile_sizes[depth];
283 let fill_z = (tile.corner[2] + tile_size + 1).try_into().unwrap();
284 if (0..tile_size).all(|y| {
285 let i = self.tile_row_offset(tile, y);
286 (0..tile_size).all(|x| self.out[i + x].depth >= fill_z)
287 }) {
288 return false;
289 }
290
291 let base = Point3::from(tile.corner).cast::<f32>();
292 let x = Interval::new(base.x, base.x + tile_size as f32);
293 let y = Interval::new(base.y, base.y + tile_size as f32);
294 let z = Interval::new(base.z, base.z + tile_size as f32);
295
296 let (i, trace) = self
297 .eval_interval
298 .eval_with_transform_and_vars(
299 shape.i_tape(&mut self.tape_storage),
300 x,
301 y,
302 z,
303 &self.transform,
304 self.vars,
305 )
306 .unwrap();
307
308 if i.upper() < 0.0 {
311 for y in 0..tile_size {
312 let i = self.tile_row_offset(tile, y);
313 for x in 0..tile_size {
314 self.out[i + x].depth = self.out[i + x].depth.max(fill_z);
315 }
316 }
317 return false; } else if i.lower() > 0.0 {
319 return true; }
321
322 let sub_tape = if let Some(trace) = trace.as_ref() {
324 shape.simplify(
325 trace,
326 &mut self.workspace,
327 &mut self.shape_storage,
328 &mut self.tape_storage,
329 )
330 } else {
331 shape
332 };
333
334 if let Some(next_tile_size) = self.tile_sizes.get(depth + 1) {
336 let n = tile_size / next_tile_size;
337
338 for j in 0..n {
339 for i in 0..n {
340 for k in (0..n).rev() {
341 self.render_tile_recurse(
342 sub_tape,
343 depth + 1,
344 Tile::new(
345 tile.corner
346 + Vector3::new(i, j, k) * next_tile_size,
347 ),
348 );
349 }
350 }
351 }
352 } else {
353 self.render_tile_pixels(sub_tape, tile_size, tile);
354 };
355 true }
358
359 fn render_tile_pixels(
360 &mut self,
361 shape: &mut RenderHandle<F>,
362 tile_size: usize,
363 tile: Tile<3>,
364 ) {
365 let mut index = 0;
367 assert!(self.scratch.x.len() >= tile_size.pow(3));
368 assert!(self.scratch.y.len() >= tile_size.pow(3));
369 assert!(self.scratch.z.len() >= tile_size.pow(3));
370 self.scratch.columns.clear();
371 for xy in 0..tile_size.pow(2) {
372 let i = xy % tile_size;
373 let j = xy / tile_size;
374
375 let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(i, j)));
376
377 let zmax = (tile.corner[2] + tile_size).try_into().unwrap();
379 if self.out[o].depth >= zmax {
380 continue;
381 }
382
383 for k in (0..tile_size).rev() {
384 unsafe {
392 *self.scratch.x.get_unchecked_mut(index) =
393 (tile.corner[0] + i) as f32;
394 *self.scratch.y.get_unchecked_mut(index) =
395 (tile.corner[1] + j) as f32;
396 *self.scratch.z.get_unchecked_mut(index) =
397 (tile.corner[2] + k) as f32;
398 }
399 index += 1;
400 }
401 self.scratch.columns.push(xy);
402 }
403 let size = index;
404 assert!(size > 0);
405
406 let out = self
407 .eval_float_slice
408 .eval_with_transform_and_vars(
409 shape.f_tape(&mut self.tape_storage),
410 &self.scratch.x[..index],
411 &self.scratch.y[..index],
412 &self.scratch.z[..index],
413 &self.transform,
414 self.vars,
415 )
416 .unwrap();
417
418 let mut grad = 0;
422 let mut depth = out.chunks(tile_size);
423 for col in 0..self.scratch.columns.len() {
424 let depth = depth.next().unwrap();
426 let k = match depth.iter().enumerate().find(|(_, d)| **d < 0.0) {
427 Some((i, _)) => i,
428 None => continue,
429 };
430
431 let xy = self.scratch.columns[col];
435 let i = xy % tile_size;
436 let j = xy / tile_size;
437
438 let k = tile_size - 1 - k;
440
441 let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(i, j)));
443 let z = (tile.corner[2] + k + 1).try_into().unwrap();
444 assert!(self.out[o].depth < z);
445 self.out[o].depth = z;
446
447 self.scratch.xg[grad] =
452 Grad::new((tile.corner[0] + i) as f32, 1.0, 0.0, 0.0);
453 self.scratch.yg[grad] =
454 Grad::new((tile.corner[1] + j) as f32, 0.0, 1.0, 0.0);
455 self.scratch.zg[grad] =
456 Grad::new((tile.corner[2] + k) as f32, 0.0, 0.0, 1.0);
457
458 self.scratch.columns[grad] = o;
462 grad += 1;
463 }
464
465 if grad > 0 {
466 let out = self
467 .eval_grad_slice
468 .eval_with_transform_and_vars(
469 shape.g_tape(&mut self.tape_storage),
470 &self.scratch.xg[..grad],
471 &self.scratch.yg[..grad],
472 &self.scratch.zg[..grad],
473 &self.transform,
474 self.vars,
475 )
476 .unwrap();
477
478 for (index, o) in self.scratch.columns[0..grad].iter().enumerate() {
479 let g = out[index];
480 self.out[*o].normal = [g.dx, g.dy, g.dz];
481 }
482 }
483 }
484}
485
486pub fn render<F: Function + RenderHints>(
501 b: BoundShape<F, f32>,
502 render_config: &RenderConfig,
503 eval_config: &EvalConfig,
504) -> Option<Image> {
505 let shape = b.shape().clone();
506 let vars = b.vars();
507 let max_size = render_config.width().max(render_config.height()) as usize;
508 let default_tile_sizes;
509
510 let tile_sizes = if let Some(ts) = &eval_config.tile_sizes {
511 TileSizesRef::new(ts, max_size)
512 } else {
513 default_tile_sizes = F::tile_sizes_3d();
514 TileSizesRef::new(&default_tile_sizes, max_size)
515 };
516 let tiles = super::render_tiles::<F, Worker<F>, _>(
517 shape,
518 vars,
519 render_config,
520 eval_config,
521 tile_sizes,
522 )?;
523
524 let width = render_config.image_size.width() as usize;
525 let height = render_config.image_size.height() as usize;
526 let mut image = Image::new(render_config.image_size);
527 for (tile, out) in tiles {
528 let mut index = 0;
529 for j in 0..tile_sizes[0] {
530 let y = j + tile.corner.y;
531 for i in 0..tile_sizes[0] {
532 let x = i + tile.corner.x;
533 if x < width && y < height {
534 let o = y * width + x;
535 if out[index].depth >= image[o].depth {
536 let d = render_config.image_size.depth() - 1;
538 if out[index].depth >= d {
539 image[o] = GeometryPixel {
540 depth: d + 1,
541 normal: [0.0, 0.0, 1.0],
542 };
543 } else {
544 image[o] = out[index];
545 }
546 }
547 }
548 index += 1;
549 }
550 }
551 }
552 Some(image)
553}
554
555#[cfg(test)]
556mod test {
557 use super::*;
558 use fidget_core::{Context, var::Var, vm::VmShape};
559
560 #[test]
562 fn test_tile_queues() {
563 let mut ctx = Context::new();
564 let x = ctx.x();
565 let shape = VmShape::new(&ctx, x).unwrap().try_into().unwrap();
566
567 let cfg = RenderConfig::from_size(128.into()); let image = cfg.run(shape);
569 assert_eq!(image.len(), 128 * 128);
570 }
571
572 #[test]
573 fn cancel_render() {
574 let mut ctx = Context::new();
575 let x = ctx.x();
576 let shape = VmShape::new(&ctx, x).unwrap().try_into().unwrap();
577
578 let render_cfg = RenderConfig::from_size(64.into());
579 let eval_cfg = EvalConfig::default();
580 let cancel = eval_cfg.cancel.clone();
581 cancel.cancel();
582 assert!(render(shape, &render_cfg, &eval_cfg).is_none());
583 }
584
585 #[test]
586 fn shape_with_var() {
587 let mut ctx = Context::new();
588 let x = ctx.x();
589 let var = Var::new();
590 let v = ctx.var(var);
591 let s = ctx.sub(x, v).unwrap();
592 let shape = VmShape::new(&ctx, s).unwrap();
593
594 let cfg = RenderConfig::from_size(64.into());
595
596 let mut vars = ShapeVars::new();
597 let i = var.index().expect("expected Var::V");
598 vars.insert(i, 1.0);
599 cfg.run(shape.bind(&vars).expect("all vars present"));
600 }
601}