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::{
10 BoundShape, ShapeBulkEval, ShapeBulkEvalError, ShapeTracingEval,
11 ShapeTracingEvalError, ShapeVars,
12 },
13 types::Interval,
14};
15use nalgebra::{Matrix3, Point2, Vector2};
16
17pub type Image = GenericImage<RawDistancePixel>;
21
22pub type RenderSize = fidget_core::render::ImageSize;
24
25pub struct RenderConfig<'a> {
27 pub image_size: RenderSize,
29
30 pub world_to_model: Matrix3<f32>,
32
33 pub pixel_perfect: bool,
35
36 pub tile_sizes: Option<TileSizes>,
41
42 pub threads: Option<&'a ThreadPool>,
47
48 pub cancel: CancelToken,
50}
51
52impl crate::RenderConfig for RenderConfig<'_> {
53 fn threads(&self) -> Option<&ThreadPool> {
54 self.threads
55 }
56 fn is_cancelled(&self) -> bool {
57 self.cancel.is_cancelled()
58 }
59}
60
61impl crate::RenderSize for RenderConfig<'_> {
62 fn width(&self) -> u32 {
63 self.image_size.width()
64 }
65 fn height(&self) -> u32 {
66 self.image_size.height()
67 }
68}
69
70impl RenderConfig<'_> {
71 pub fn from_size(image_size: RenderSize) -> Self {
88 Self {
89 image_size,
90 tile_sizes: None,
91 world_to_model: Matrix3::identity(),
92 pixel_perfect: false,
93 threads: Some(&ThreadPool::Global),
94 cancel: CancelToken::new(),
95 }
96 }
97
98 pub fn run<F: Function + RenderHints>(
103 &self,
104 shape: BoundShape<F, f32>,
105 ) -> Option<Image> {
106 render(shape, self)
107 }
108
109 pub fn mat(&self) -> Matrix3<f32> {
111 self.world_to_model * self.image_size.screen_to_world()
112 }
113}
114
115struct Scratch {
118 x: Vec<f32>,
119 y: Vec<f32>,
120 z: Vec<f32>,
121}
122
123impl Scratch {
124 fn new(size: usize) -> Self {
125 Self {
126 x: vec![0.0; size],
127 y: vec![0.0; size],
128 z: vec![0.0; size],
129 }
130 }
131}
132
133#[derive(Copy, Clone, Debug, Default)]
148pub struct RawDistancePixel(f32);
149
150#[derive(Copy, Clone, Debug)]
152pub enum DistancePixel {
153 Value(f32),
155 Fill {
157 depth: u8,
159 inside: bool,
161 },
162}
163
164impl RawDistancePixel {
165 const KEY: u32 = 0b1111_0110 << 9;
166 const KEY_MASK: u32 = 0b1111_1111 << 9;
167
168 #[inline]
173 pub fn inside(self) -> bool {
174 match self.unpack() {
175 DistancePixel::Fill { inside, .. } => inside,
176 DistancePixel::Value(v) => v < 0.0,
177 }
178 }
179
180 #[inline]
182 pub fn is_distance(self) -> bool {
183 if !self.0.is_nan() {
184 return true;
185 }
186 let bits = self.0.to_bits();
187 (bits & Self::KEY_MASK) != Self::KEY
188 }
189
190 #[inline]
192 pub fn unpack(self) -> DistancePixel {
193 if self.is_distance() {
194 DistancePixel::Value(self.0)
195 } else {
196 let bits = self.0.to_bits();
197 let inside = (bits & 1) == 1;
198 let depth = (bits >> 1) as u8;
199 DistancePixel::Fill { inside, depth }
200 }
201 }
202}
203
204impl From<DistancePixel> for RawDistancePixel {
205 #[inline]
206 fn from(p: DistancePixel) -> Self {
207 match p {
208 DistancePixel::Value(f) => Self::from(f),
209 DistancePixel::Fill { depth, inside } => {
210 let bits = 0x7FC00000
211 | (u32::from(depth) << 1)
212 | u32::from(inside)
213 | Self::KEY;
214 Self(f32::from_bits(bits))
215 }
216 }
217 }
218}
219
220impl From<f32> for RawDistancePixel {
221 #[inline]
222 fn from(p: f32) -> Self {
223 Self(if p.is_nan() { f32::NAN } else { p })
225 }
226}
227
228struct Worker<'a, F: Function> {
232 tile_sizes: TileSizesRef<'a>,
233 vars: &'a ShapeVars<f32>,
234
235 pixel_perfect: bool,
236 scratch: Scratch,
237 transform: nalgebra::Matrix4<f32>,
238
239 eval_float_slice: ShapeBulkEval<F::FloatSliceEval>,
240 eval_interval: ShapeTracingEval<F::IntervalEval>,
241
242 tape_storage: Vec<F::TapeStorage>,
244
245 shape_storage: Vec<F::Storage>,
247
248 workspace: F::Workspace,
250
251 image: Image,
255}
256
257impl<'a, F: Function> RenderWorker<'a, F> for Worker<'a, F> {
258 type Config = RenderConfig<'a>;
259 type Output = Image;
260 fn new(
261 cfg: &'a Self::Config,
262 tile_sizes: TileSizesRef<'a>,
263 vars: &'a ShapeVars<f32>,
264 ) -> Self {
265 let transform = cfg.mat();
267 let transform = transform.insert_row(2, 0.0);
268 let transform = transform.insert_column(2, 0.0);
269
270 Worker::<F> {
271 tile_sizes,
272 transform,
273 vars,
274
275 scratch: Scratch::new(tile_sizes.last().pow(2)),
276 pixel_perfect: cfg.pixel_perfect,
277 image: Default::default(),
278 eval_float_slice: Default::default(),
279 eval_interval: Default::default(),
280 tape_storage: vec![],
281 shape_storage: vec![],
282 workspace: Default::default(),
283 }
284 }
285
286 fn render_tile(
287 &mut self,
288 shape: &mut RenderHandle<F>,
289 tile: Tile<2>,
290 ) -> Self::Output {
291 self.image = Image::new((self.tile_sizes[0] as u32).into());
292 self.render_tile_recurse(shape, 0, tile);
293 std::mem::take(&mut self.image)
294 }
295}
296
297impl<F: Function> Worker<'_, F> {
298 fn render_tile_recurse(
299 &mut self,
300 shape: &mut RenderHandle<F>,
301 depth: usize,
302 tile: Tile<2>,
303 ) {
304 let tile_size = self.tile_sizes[depth];
305
306 let base = Point2::from(tile.corner).cast::<f32>();
308 let x = Interval::new(base.x, base.x + tile_size as f32);
309 let y = Interval::new(base.y, base.y + tile_size as f32);
310 let z = Interval::new(0.0, 0.0);
311
312 let (i, simplify) =
315 match self.eval_interval.eval_with_transform_and_vars(
316 shape.i_tape(&mut self.tape_storage),
317 x,
318 y,
319 z,
320 &self.transform,
321 self.vars,
322 ) {
323 Ok(v) => v,
324 Err(ShapeTracingEvalError::MissingVar(..)) => unreachable!(),
325 };
326
327 if !self.pixel_perfect {
328 let pixel = if i.upper() < 0.0 {
329 Some(DistancePixel::Fill {
330 inside: true,
331 depth: depth as u8,
332 })
333 } else if i.lower() > 0.0 {
334 Some(DistancePixel::Fill {
335 inside: false,
336 depth: depth as u8,
337 })
338 } else {
339 None
340 };
341 if let Some(pixel) = pixel {
342 let fill = pixel.into();
343 for y in 0..tile_size {
344 let start = self
345 .tile_sizes
346 .pixel_offset(tile.add(Vector2::new(0, y)));
347 self.image[start..][..tile_size].fill(fill);
348 }
349 return;
350 }
351 }
352
353 let sub_tape = if let Some(trace) = simplify.as_ref() {
354 shape.simplify(
355 trace,
356 &mut self.workspace,
357 &mut self.shape_storage,
358 &mut self.tape_storage,
359 )
360 } else {
361 shape
362 };
363
364 if let Some(next_tile_size) = self.tile_sizes.get(depth + 1) {
365 let n = tile_size / next_tile_size;
366 for j in 0..n {
367 for i in 0..n {
368 self.render_tile_recurse(
369 sub_tape,
370 depth + 1,
371 Tile::new(
372 tile.corner + Vector2::new(i, j) * next_tile_size,
373 ),
374 );
375 }
376 }
377 } else {
378 self.render_tile_pixels(sub_tape, tile_size, tile);
379 }
380 }
381
382 fn render_tile_pixels(
383 &mut self,
384 shape: &mut RenderHandle<F>,
385 tile_size: usize,
386 tile: Tile<2>,
387 ) {
388 let mut index = 0;
389 for j in 0..tile_size {
390 for i in 0..tile_size {
391 self.scratch.x[index] = (tile.corner[0] + i) as f32;
392 self.scratch.y[index] = (tile.corner[1] + j) as f32;
393 index += 1;
394 }
395 }
396
397 let out = match self.eval_float_slice.eval_with_transform_and_vars(
398 shape.f_tape(&mut self.tape_storage),
399 &self.scratch.x,
400 &self.scratch.y,
401 &self.scratch.z,
402 &self.transform,
403 self.vars,
404 ) {
405 Ok(v) => v,
406 Err(ShapeBulkEvalError::MissingVar(..))
408 | Err(ShapeBulkEvalError::MismatchedVarSlices { .. })
410 => unreachable!(),
411 };
412
413 let mut index = 0;
414 for j in 0..tile_size {
415 let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, j)));
416 for i in 0..tile_size {
417 self.image[o + i] = out[index].into();
418 index += 1;
419 }
420 }
421 }
422}
423
424pub fn render<F: Function + RenderHints>(
434 b: BoundShape<F, f32>,
435 config: &RenderConfig,
436) -> Option<Image> {
437 let shape = b.shape();
438 let vars = b.vars();
439 let max_size = config.width().max(config.height()) as usize;
440 let default_tile_sizes;
441 let tile_sizes = if let Some(ts) = &config.tile_sizes {
442 TileSizesRef::new(ts, max_size)
443 } else {
444 default_tile_sizes = F::tile_sizes_2d();
445 TileSizesRef::new(&default_tile_sizes, max_size)
446 };
447 let tiles = super::render_tiles::<F, Worker<F>>(
448 shape.clone(),
449 vars,
450 config,
451 tile_sizes,
452 )?;
453
454 let width = config.image_size.width() as usize;
455 let height = config.image_size.height() as usize;
456 let mut image = Image::new(config.image_size);
457 for (tile, data) in tiles.iter() {
458 let mut index = 0;
459 for j in 0..tile_sizes[0] {
460 let y = j + tile.corner.y;
461 for i in 0..tile_sizes[0] {
462 let x = i + tile.corner.x;
463 if y < height && x < width {
464 image[(y, x)] = data[index];
465 }
466 index += 1;
467 }
468 }
469 }
470 Some(image)
471}
472
473#[cfg(test)]
474mod test {
475 use super::*;
476 use fidget_core::{
477 Context,
478 shape::Shape,
479 var::Var,
480 vm::{VmFunction, VmShape},
481 };
482
483 const HI: &str =
484 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../models/hi.vm"));
485
486 #[test]
487 fn render2d_cancel() {
488 let (ctx, root) = Context::from_text(HI.as_bytes()).unwrap();
489 let shape = Shape::<VmFunction>::new(&ctx, root)
490 .unwrap()
491 .try_into()
492 .expect("no vars");
493
494 let cfg = RenderConfig::from_size(64.into());
495 let cancel = cfg.cancel.clone();
496 cancel.cancel();
497 assert!(cfg.run(shape).is_none());
498 }
499
500 #[test]
501 fn shape_with_var() {
502 let mut ctx = Context::new();
503 let x = ctx.x();
504 let var = Var::new();
505 let v = ctx.var(var);
506 let s = ctx.sub(x, v).unwrap();
507 let shape = VmShape::new(&ctx, s).unwrap();
508
509 let cfg = RenderConfig::from_size(64.into());
510 let mut vars = ShapeVars::new();
511 let i = var.index().expect("expected Var::V");
512 vars.insert(i, 1.0);
513 cfg.run::<_>(shape.bind(&vars).expect("all vars present"))
514 .expect("not cancelled");
515 }
516
517 #[test]
518 fn test_render_config_transforms() {
519 let config = RenderConfig::from_size(512.into());
520 let mat = config.mat();
521 assert_eq!(
522 mat.transform_point(&Point2::new(0.0, -1.0)),
523 Point2::new(-1.0, 1.0)
524 );
525 assert_eq!(
526 mat.transform_point(&Point2::new(512.0, -1.0)),
527 Point2::new(1.0, 1.0)
528 );
529 assert_eq!(
530 mat.transform_point(&Point2::new(512.0, 511.0)),
531 Point2::new(1.0, -1.0)
532 );
533
534 let config = RenderConfig::from_size(575.into());
535 let mat = config.mat();
536 assert_eq!(
537 mat.transform_point(&Point2::new(0.0, -1.0)),
538 Point2::new(-1.0, 1.0)
539 );
540 assert_eq!(
541 mat.transform_point(&Point2::new(
542 config.image_size.width() as f32,
543 -1.0
544 )),
545 Point2::new(1.0, 1.0)
546 );
547 assert_eq!(
548 mat.transform_point(&Point2::new(
549 config.image_size.width() as f32,
550 config.image_size.height() as f32 - 1.0,
551 )),
552 Point2::new(1.0, -1.0)
553 );
554 }
555}