1use alloc::boxed::Box;
2use alloc::string::String;
3use alloc::sync::Arc;
4use core::fmt;
5
6use all_is_cubes::character::Cursor;
7use all_is_cubes::content::palette;
8use all_is_cubes::euclid::{self, point2, vec2};
9use all_is_cubes::listen::{self, Source as _};
10use all_is_cubes::math::{Rgba, ZeroOne};
11use all_is_cubes::space::Space;
12use all_is_cubes::universe::{Handle, ReadTicket};
13
14use crate::camera::{
15 Camera, GraphicsOptions, Layers, Ndc, NdcPoint2, StandardCameras, Viewport, area_usize,
16};
17use crate::raytracer::{
18 Accumulate, ColorBuf, RaytraceInfo, RtBlockData, RtOptionsRef, SpaceRaytracer,
19 UpdatingSpaceRaytracer,
20};
21use crate::{Flaws, RenderError, Rendering};
22
23#[cfg(any(doc, feature = "std"))]
24use crate::HeadlessRenderer;
25
26type CustomOptionsValues<D> = Layers<Arc<<D as RtBlockData>::Options>>;
29
30pub struct RtRenderer<D: RtBlockData = ()> {
33 rts: Layers<Option<UpdatingSpaceRaytracer<D>>>,
34
35 cameras: StandardCameras,
36
37 size_policy: Box<dyn Fn(Viewport) -> Viewport + Send + Sync>,
40
41 ui_graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
43
44 custom_options: listen::DynSource<CustomOptionsValues<D>>,
45 custom_options_cache: CustomOptionsValues<D>,
47
48 had_cursor: bool,
51}
52
53impl<D> RtRenderer<D>
54where
55 D: RtBlockData<Options: Clone + Sync + 'static>,
56{
57 pub fn new(
63 cameras: StandardCameras,
64 size_policy: Box<dyn Fn(Viewport) -> Viewport + Send + Sync>,
65 custom_options: listen::DynSource<CustomOptionsValues<D>>,
66 ) -> Self {
67 RtRenderer {
68 rts: Layers::<Option<_>>::default(),
69 ui_graphics_options: Arc::new(
70 cameras.ui_view_source().map(|view| view.graphics_options.clone()),
71 ),
72 cameras,
73 size_policy,
74 custom_options_cache: custom_options.get(),
75 custom_options,
76 had_cursor: false,
77 }
78 }
79
80 pub fn update(
92 &mut self,
93 read_tickets: Layers<ReadTicket<'_>>,
94 cursor: Option<&Cursor>,
95 ) -> Result<bool, RenderError> {
96 let mut anything_changed = false;
97
98 self.had_cursor = cursor.is_some();
100 anything_changed |= self.cameras.update(read_tickets);
101 self.custom_options_cache = self.custom_options.get();
102
103 fn sync_space<D>(
104 read_ticket: ReadTicket<'_>,
105 cached_rt: &mut Option<UpdatingSpaceRaytracer<D>>,
106 optional_space: Option<&Handle<Space>>,
107 graphics_options_source: listen::DynSource<Arc<GraphicsOptions>>,
108 custom_options_source_factory: impl FnOnce() -> listen::DynSource<Arc<D::Options>>,
109 anything_changed: &mut bool,
110 ) -> Result<(), RenderError>
111 where
112 D: RtBlockData<Options: Clone + Sync + 'static>,
113 {
114 match (optional_space, &mut *cached_rt) {
118 (Some(space), Some(rt)) if space == rt.space() => {}
120 (Some(space), rt) => {
122 *anything_changed = true;
123 *rt = Some(UpdatingSpaceRaytracer::new(
124 space.clone(),
125 graphics_options_source,
126 custom_options_source_factory(),
127 ));
128 }
129 (None, c) => *c = None,
131 }
132 if let Some(rt) = cached_rt {
134 *anything_changed |= rt.update(read_ticket).map_err(RenderError::Read)?;
135 }
136 Ok(())
137 }
138 sync_space(
139 read_tickets.world,
140 &mut self.rts.world,
141 Option::as_ref(&self.cameras.world_space().get()),
142 self.cameras.graphics_options_source(),
143 || Arc::new(self.custom_options.clone().map(|layers| layers.world.clone())),
144 &mut anything_changed,
145 )?;
146 sync_space(
147 read_tickets.ui,
148 &mut self.rts.ui,
149 self.cameras.ui_space(),
150 self.ui_graphics_options.clone(),
151 || Arc::new(self.custom_options.clone().map(|layers| layers.ui.clone())),
152 &mut anything_changed,
153 )?;
154
155 Ok(anything_changed)
156 }
157
158 pub fn draw<P, E, O, IF>(&self, info_text_fn: IF, encoder: E, output: &mut [O]) -> RaytraceInfo
175 where
176 P: Accumulate<BlockData = D> + Default,
177 E: Fn(P) -> O + Send + Sync,
178 O: Clone + Send + Sync, IF: FnOnce(&RaytraceInfo) -> String,
180 {
181 let scene = self.scene();
182 let viewport = scene.cameras.world.viewport();
183
184 assert_eq!(
185 viewport.pixel_count(),
186 Some(output.len()),
187 "Viewport size does not match output buffer length",
188 );
189
190 let info = trace_image::trace_scene_to_image_impl(&scene, &encoder, output);
191
192 let info_text: String = info_text_fn(&info);
193 if !info_text.is_empty() && self.cameras.cameras().world.options().debug_info_text {
194 eg::draw_info_text(
195 output,
196 viewport,
197 [
198 encoder(P::paint(Rgba::BLACK, scene.options_refs().ui)),
199 encoder(P::paint(Rgba::WHITE, scene.options_refs().ui)),
200 ],
201 &info_text,
202 );
203 }
204
205 info
206 }
207
208 pub fn scene<P>(&self) -> RtScene<'_, P>
213 where
214 P: Accumulate<BlockData = D>,
215 {
216 let mut cameras = self.cameras.cameras().clone();
217 let viewport = (self.size_policy)(cameras.world.viewport());
218 cameras.world.set_viewport(viewport);
219 cameras.ui.set_viewport(viewport);
220
221 RtScene {
222 rts: self
223 .rts
224 .as_refs()
225 .map(|opt_urt| opt_urt.as_ref().map(UpdatingSpaceRaytracer::get)),
226 cameras,
227 custom_options: &self.custom_options_cache,
228 }
229 }
230
231 pub fn cameras(&self) -> &StandardCameras {
236 &self.cameras
237 }
238
239 pub fn modified_viewport(&self) -> Viewport {
243 (self.size_policy)(self.cameras.viewport())
244 }
245}
246
247impl RtRenderer<()> {
248 pub fn draw_rgba(
253 &self,
254 info_text_fn: impl FnOnce(&RaytraceInfo) -> String,
255 ) -> (Rendering, RaytraceInfo) {
256 let camera = self.cameras.cameras().world.clone();
257 let size = self.modified_viewport().framebuffer_size;
258
259 let mut data = vec![[0; 4]; area_usize(size).unwrap()];
260 let info = self.draw::<ColorBuf, _, [u8; 4], _>(
261 info_text_fn,
262 |pixel_buf| camera.post_process_color(Rgba::from(pixel_buf)).to_srgb8(),
263 &mut data,
264 );
265
266 let options = self.cameras.graphics_options();
267 let mut flaws = Flaws::empty();
268 if options.bloom_intensity != ZeroOne::ZERO {
269 flaws |= Flaws::NO_BLOOM;
270 }
271 if self.had_cursor {
272 flaws |= Flaws::NO_CURSOR;
273 }
274
275 (Rendering { size, data, flaws }, info)
276 }
277}
278
279impl<D> fmt::Debug for RtRenderer<D>
281where
282 D: RtBlockData<Options: fmt::Debug>,
283{
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 let Self {
286 rts,
287 cameras,
288 size_policy: _, ui_graphics_options: _, custom_options,
291 custom_options_cache: _, had_cursor,
293 } = self;
294 f.debug_struct("RtRenderer")
296 .field("cameras", cameras)
297 .field("rts", rts)
298 .field("custom_options", custom_options)
299 .field("had_cursor", had_cursor)
300 .finish_non_exhaustive()
301 }
302}
303
304#[cfg(feature = "std")] impl HeadlessRenderer for RtRenderer<()> {
307 fn update(
308 &mut self,
309 read_tickets: Layers<ReadTicket<'_>>,
310 cursor: Option<&Cursor>,
311 ) -> Result<(), RenderError> {
312 let _anything_changed = self.update(read_tickets, cursor)?;
313 Ok(())
314 }
315
316 fn draw<'a>(
317 &'a mut self,
318 info_text: &'a str,
319 ) -> futures_core::future::BoxFuture<'a, Result<Rendering, RenderError>> {
320 use alloc::string::ToString as _;
321
322 Box::pin(async {
323 let (rendering, _rt_info) = self.draw_rgba(|_| info_text.to_string());
324
325 Ok(rendering)
326 })
327 }
328}
329
330pub struct RtScene<'a, P: Accumulate> {
341 rts: Layers<Option<&'a SpaceRaytracer<P::BlockData>>>,
342 cameras: Layers<Camera>,
344 custom_options: &'a CustomOptionsValues<P::BlockData>,
346}
347
348impl<P: Accumulate> fmt::Debug for RtScene<'_, P>
349where
350 <P::BlockData as RtBlockData>::Options: fmt::Debug,
351{
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 let Self {
354 rts,
355 cameras,
356 custom_options,
357 } = self;
358 f.debug_struct("RtScene")
359 .field("rts", rts)
360 .field("cameras", cameras)
361 .field("custom_options", custom_options)
362 .finish()
363 }
364}
365
366impl<P: Accumulate> Clone for RtScene<'_, P> {
367 fn clone(&self) -> Self {
368 Self {
369 rts: self.rts,
370 cameras: self.cameras.clone(),
371 custom_options: self.custom_options,
372 }
373 }
374}
375
376impl<P: Accumulate + Default> RtScene<'_, P> {
377 fn options_refs(&self) -> Layers<RtOptionsRef<'_, <P::BlockData as RtBlockData>::Options>> {
379 Layers {
380 world: RtOptionsRef::_new_but_please_do_not_construct_this_if_you_are_not_all_is_cubes_itself(
381 self.cameras.world.options(),
382 &*self.custom_options.world,
383 ),
384 ui: RtOptionsRef::_new_but_please_do_not_construct_this_if_you_are_not_all_is_cubes_itself(
385 self.cameras.ui.options(),
386 &*self.custom_options.ui,
387 ),
388 }
389 }
390
391 #[inline]
397 pub fn trace_patch(&self, patch: NdcRect) -> (P, RaytraceInfo) {
398 let mut info = RaytraceInfo::default();
399 let pixel = if self.cameras.world.options().antialiasing.is_strongly_enabled() {
400 const N: usize = 4;
401 const SAMPLE_POINTS: [euclid::default::Vector2D<f64>; N] = [
402 vec2(1. / 8., 5. / 8.),
403 vec2(3. / 8., 1. / 8.),
404 vec2(5. / 8., 7. / 8.),
405 vec2(7. / 8., 3. / 8.),
406 ];
407
408 let samples: [P; N] = core::array::from_fn(|i| {
409 let mut accum = P::default();
410 self.trace_ray_through_layers(
411 &mut info,
412 &mut accum,
413 point_within_patch(patch, SAMPLE_POINTS[i]),
414 );
415 accum
416 });
417 P::mean(samples)
418 } else {
419 let mut pixel = P::default();
420 self.trace_ray_through_layers(&mut info, &mut pixel, patch.center());
421 pixel
422 };
423 (pixel, info)
424 }
425
426 fn trace_ray_through_layers(&self, info: &mut RaytraceInfo, accum: &mut P, point: NdcPoint2) {
428 if let Some(ui) = self.rts.ui {
429 *info += ui.trace_ray(self.cameras.ui.project_ndc_into_world(point), accum, false);
430 }
431 if let Some(world) = self.rts.world {
432 *info += world.trace_ray(
433 self.cameras.world.project_ndc_into_world(point),
434 accum,
435 true,
436 );
437 }
438 if !accum.opaque() {
439 *accum = P::paint(palette::NO_WORLD_TO_SHOW, self.options_refs().world);
441 }
442 }
443
444 #[doc(hidden)] pub fn cameras(&self) -> &Layers<Camera> {
446 &self.cameras
447 }
448}
449
450type NdcRect = euclid::Box2D<f64, Ndc>;
452
453fn point_within_patch(patch: NdcRect, uv: euclid::default::Vector2D<f64>) -> NdcPoint2 {
454 patch.min + (patch.max - patch.min).component_mul(uv.cast_unit())
455}
456
457mod trace_image {
461 use super::*;
462
463 #[cfg(feature = "auto-threads")]
476 pub(super) fn trace_scene_to_image_impl<P, E, O>(
477 scene: &RtScene<'_, P>,
478 encoder: E,
479 output: &mut [O],
480 ) -> RaytraceInfo
481 where
482 P: Accumulate + Default,
483 E: Fn(P) -> O + Send + Sync,
484 O: Send + Sync,
485 {
486 use rayon::iter::{
487 IndexedParallelIterator as _, IntoParallelIterator as _, ParallelIterator as _,
488 };
489 use rayon::slice::ParallelSliceMut as _;
490
491 let viewport = scene.cameras.world.viewport();
492 let viewport_size = viewport.framebuffer_size.to_usize();
493 let encoder = &encoder; output
498 .par_chunks_mut(viewport_size.width.max(1))
499 .enumerate()
500 .map(move |(ych, raster_row)| {
501 let y0 = viewport.normalize_fb_y_edge(ych);
502 let y1 = viewport.normalize_fb_y_edge(ych + 1);
503 raster_row.into_par_iter().enumerate().map(move |(xch, pixel_out)| {
504 let x0 = viewport.normalize_fb_x_edge(xch);
505 let x1 = viewport.normalize_fb_x_edge(xch + 1);
506 let (pixel, info) = scene.trace_patch(NdcRect {
507 min: point2(x0, y0),
508 max: point2(x1, y1),
509 });
510 *pixel_out = encoder(pixel);
511 info
512 })
513 })
514 .flatten()
515 .sum() }
517
518 #[cfg(not(feature = "auto-threads"))]
531 pub(super) fn trace_scene_to_image_impl<P, E, O>(
532 scene: &RtScene<'_, P>,
533 encoder: E,
534 output: &mut [O],
535 ) -> RaytraceInfo
536 where
537 P: Accumulate + Default,
538 E: Fn(P) -> O + Send + Sync,
539 O: Send + Sync,
540 {
541 let viewport = scene.cameras.world.viewport();
542 let viewport_size = viewport.framebuffer_size.to_usize();
543
544 let mut total_info = RaytraceInfo::default();
545 let mut index = 0;
546 let mut y0 = viewport.normalize_fb_y_edge(0);
547 for y_edge in 1..=viewport_size.height {
548 let y1 = viewport.normalize_fb_y_edge(y_edge);
549 let mut x0 = viewport.normalize_fb_x_edge(0);
550 for x_edge in 1..=viewport_size.width {
551 let x1 = viewport.normalize_fb_x_edge(x_edge);
552 let (pixel, info) = scene.trace_patch(NdcRect {
553 min: point2(x0, y0),
554 max: point2(x1, y1),
555 });
556 output[index] = encoder(pixel);
557 total_info += info;
558 index += 1;
559 x0 = x1;
560 }
561 y0 = y1;
562 }
563
564 total_info
565 }
566}
567
568mod eg {
569 use super::*;
570 use crate::info_text_drawable;
571 use embedded_graphics::Drawable;
572 use embedded_graphics::Pixel;
573 use embedded_graphics::draw_target::DrawTarget;
574 use embedded_graphics::draw_target::DrawTargetExt;
575 use embedded_graphics::pixelcolor::BinaryColor;
576 use embedded_graphics::prelude::{OriginDimensions, Point, Size};
577 use embedded_graphics::primitives::Rectangle;
578
579 pub fn draw_info_text<T: Clone>(
580 output: &mut [T],
581 viewport: Viewport,
582 paint: [T; 2],
583 info_text: &str,
584 ) {
585 let target = &mut EgImageTarget {
586 data: output,
587 paint,
588 size: Size {
589 width: viewport.framebuffer_size.width,
590 height: viewport.framebuffer_size.height,
591 },
592 };
593 let shadow = info_text_drawable(info_text, BinaryColor::Off);
594 shadow.draw(&mut target.translated(Point::new(0, -1))).unwrap();
596 shadow.draw(&mut target.translated(Point::new(0, 1))).unwrap();
597 shadow.draw(&mut target.translated(Point::new(-1, 0))).unwrap();
598 shadow.draw(&mut target.translated(Point::new(1, 0))).unwrap();
599 info_text_drawable(info_text, BinaryColor::On).draw(target).unwrap();
600 }
601
602 pub(crate) struct EgImageTarget<'a, T> {
604 data: &'a mut [T],
605 paint: [T; 2],
606 size: Size,
607 }
608
609 impl<T: Clone> DrawTarget for EgImageTarget<'_, T> {
610 type Color = BinaryColor;
611 type Error = core::convert::Infallible;
612
613 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
614 where
615 I: IntoIterator<Item = Pixel<Self::Color>>,
616 {
617 let bounds = Rectangle {
618 top_left: Point::zero(),
619 size: self.size,
620 };
621 for Pixel(point, color) in pixels {
622 if bounds.contains(point) {
623 self.data[point.y as usize * self.size.width as usize + point.x as usize] =
624 match color {
625 BinaryColor::Off => &self.paint[0],
626 BinaryColor::On => &self.paint[1],
627 }
628 .clone();
629 }
630 }
631 Ok(())
632 }
633 }
634
635 impl<T> OriginDimensions for EgImageTarget<'_, T> {
636 fn size(&self) -> Size {
637 self.size
638 }
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::raytracer;
646 use all_is_cubes::universe::Universe;
647 use all_is_cubes::util::assert_conditional_send_sync;
648 use core::convert::identity;
649
650 #[test]
651 fn renderer_is_send_sync() {
652 assert_conditional_send_sync::<RtRenderer>()
653 }
654
655 #[test]
656 fn custom_options_are_updated() {
657 #[derive(Clone, Copy, Debug, Default, PartialEq)]
658 struct CatchCustomOptions {
659 custom_options: &'static str,
660 }
661 impl RtBlockData for CatchCustomOptions {
662 type Options = &'static str;
663 fn from_block(
664 options: RtOptionsRef<'_, Self::Options>,
665 _: &all_is_cubes::space::SpaceBlockData,
666 ) -> Self {
667 CatchCustomOptions {
668 custom_options: options.custom_options,
669 }
670 }
671 fn exception(
672 _: raytracer::Exception,
673 options: RtOptionsRef<'_, Self::Options>,
674 ) -> Self {
675 CatchCustomOptions {
676 custom_options: options.custom_options,
677 }
678 }
679 }
680 impl Accumulate for CatchCustomOptions {
681 type BlockData = CatchCustomOptions;
682 fn opaque(&self) -> bool {
683 !self.custom_options.is_empty()
684 }
685 fn add(&mut self, hit: raytracer::Hit<'_, Self::BlockData>) {
686 if self.custom_options.is_empty() {
687 *self = *hit.block;
688 }
689 }
690 fn mean<const N: usize>(_: [Self; N]) -> Self {
691 unimplemented!()
692 }
693 }
694
695 let universe = Universe::new();
696 let cameras = StandardCameras::from_constant_for_test(
697 GraphicsOptions::UNALTERED_COLORS,
698 Viewport::with_scale(1.0, [1, 1]),
699 &universe,
700 );
701
702 let custom_options = listen::Cell::new(Layers {
704 world: Arc::new("world before"),
705 ui: Arc::new("ui before"),
706 });
707 let mut renderer = RtRenderer::new(cameras, Box::new(identity), custom_options.as_source());
708 custom_options.set(Layers {
709 world: Arc::new("world after"),
710 ui: Arc::new("ui after"),
711 });
712
713 let mut result = [CatchCustomOptions::default()];
715 renderer
716 .update(
717 Layers {
718 world: universe.read_ticket(),
719 ui: ReadTicket::stub(),
720 },
721 None,
722 )
723 .unwrap();
724 renderer.draw(|_| String::new(), identity, &mut result);
725
726 assert_eq!(
727 result,
728 [CatchCustomOptions {
729 custom_options: "world after"
730 }]
731 )
732 }
733}