all_is_cubes_render/raytracer/
renderer.rs

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
26// -------------------------------------------------------------------------------------------------
27
28type CustomOptionsValues<D> = Layers<Arc<<D as RtBlockData>::Options>>;
29
30/// Builds upon [`UpdatingSpaceRaytracer`] to make a complete [`HeadlessRenderer`],
31/// following the scene and camera information in a [`StandardCameras`].
32pub struct RtRenderer<D: RtBlockData = ()> {
33    rts: Layers<Option<UpdatingSpaceRaytracer<D>>>,
34
35    cameras: StandardCameras,
36
37    /// Adjusts the `cameras` viewport to control how many pixels are actually traced.
38    /// The output images will alway
39    size_policy: Box<dyn Fn(Viewport) -> Viewport + Send + Sync>,
40
41    // TODO: this oughta be just provided by `StandardCameras`
42    ui_graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
43
44    custom_options: listen::DynSource<CustomOptionsValues<D>>,
45    /// Borrowable copy of the value in `custom_options`.
46    custom_options_cache: CustomOptionsValues<D>,
47
48    /// Whether there was a [`Cursor`] to be drawn.
49    /// Raytracing doesn't yet support cursors but we need to report that.
50    had_cursor: bool,
51}
52
53impl<D> RtRenderer<D>
54where
55    D: RtBlockData<Options: Clone + Sync + 'static>,
56{
57    /// * `cameras`: Scene to draw.
58    /// * `size_policy`: Modifier to the `cameras`' provided viewport to control how many
59    ///   pixels are actually traced.
60    /// * `custom_options`: The custom options for the `D` block data type; see
61    ///   [`RtBlockData`].
62    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    /// Update the renderer's internal copy of the scene from the data sources
81    /// (`Handle<Character>` etc.) it is tracking.
82    ///
83    /// On success, returns whether any of the scene actually changed.
84    ///
85    /// Returns [`RenderError::Read`] if said sources are in use.
86    /// In that case, the renderer is still functional but will have stale data.
87    ///
88    /// This method is equivalent to [`HeadlessRenderer::update()`] except for
89    /// fitting the raytracer's needs and capabilities (works with all types;
90    /// not `async`).
91    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        // TODO: raytracer needs to implement drawing the cursor
99        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            // TODO: this Option-synchronization pattern is recurring in renderers but also ugly ... look for ways to make it nicer
115
116            // Check whether we need to replace the raytracer:
117            match (optional_space, &mut *cached_rt) {
118                // Matches already
119                (Some(space), Some(rt)) if space == rt.space() => {}
120                // Needs replacement
121                (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                // Space is None, so drop raytracer if any
130                (None, c) => *c = None,
131            }
132            // Now that we have one if we should have one, update it.
133            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    /// Produce an image of the current state of the scene this renderer was created to
159    /// track, as of the last call to [`Self::update()`], with the given overlaid text.
160    ///
161    /// The image's dimensions are determined by the previously supplied
162    /// [`StandardCameras`]’ viewport value as of the last call to [`Self::update()`],
163    /// as affected by the `size_policy`. The provided `output` buffer must have exactly
164    /// that length.
165    ///
166    /// This operation does not attempt to access the scene objects and therefore may be
167    /// called while the [`Universe`] is being stepped, etc.
168    ///
169    /// This method is equivalent to [`HeadlessRenderer::draw()`] except that it works
170    /// with any [`Accumulate`] instead of requiring [`ColorBuf`] and [`Rgba`] output,
171    /// is not async, and does not require `&mut self`.
172    ///
173    /// [`Universe`]: all_is_cubes::universe::Universe
174    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, // Clone is used in the no-data case
179        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    /// Returns a [`RtScene`] which may be used to compute individual image pixels.
209    ///
210    /// This is the setup operation which [`RtRenderer::draw()`] is built upon;
211    /// use it if you want to render partially or incrementally.
212    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    /// Returns the [`StandardCameras`] this renderer contains.
232    ///
233    /// TODO: Should this be a standard part of [`HeadlessRenderer`] and/or other traits?
234    /// It's likely to be useful for dealing with cursors and such matters, I think.
235    pub fn cameras(&self) -> &StandardCameras {
236        &self.cameras
237    }
238
239    /// Returns the [`Viewport`] as of the last [`Self::update()`] as modified by the
240    /// `size_policy`. That is, this reports the size of images that will be actually
241    /// drawn.
242    pub fn modified_viewport(&self) -> Viewport {
243        (self.size_policy)(self.cameras.viewport())
244    }
245}
246
247impl RtRenderer<()> {
248    /// As [`Self::draw()`], but the output is a [`Rendering`], and
249    /// [`Camera::post_process_color()`] is applied to the pixels.
250    ///
251    ///  [`Camera::post_process_color()`]: crate::camera::Camera::post_process_color
252    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
279// manual impl avoids `D: Debug` bound
280impl<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: _,         // can't print a function
289            ui_graphics_options: _, // derived
290            custom_options,
291            custom_options_cache: _, // not printed because its value is not meaningful when not in use
292            had_cursor,
293        } = self;
294        // TODO: missing fields
295        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/// This implementation is only available if the `std` feature is enabled.
305#[cfg(feature = "std")] // can't provide `Sync` futures otherwise
306impl 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
330/// Scene to be raytraced.
331///
332/// This may be obtained from [`RtRenderer::scene()`] and used to trace individual rays,
333/// rather than an entire image.
334///
335/// Differs from [`SpaceRaytracer`]
336/// in that it includes the [`Camera`]s (thus accepting screen-space coordinates
337/// rather than a world-space ray) and [`Layers`] rather than one space.
338///
339/// Obtain this by calling [`RtRenderer::scene()`].
340pub struct RtScene<'a, P: Accumulate> {
341    rts: Layers<Option<&'a SpaceRaytracer<P::BlockData>>>,
342    /// Cameras *with* `size_policy` applied.
343    cameras: Layers<Camera>,
344    /// Custom options for `P`, per layer.
345    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    /// Constructs [`RtOptionsRef`] referring to the options stored in `self`.
378    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    /// Given the `patch` which is the bounding box of a single image pixel in normalized device
392    /// coordinates (range -1 to 1), produce the [`Accumulate`]d value of that pixel in this scene.
393    ///
394    /// The depth axis of the rays used, and hence the depth information provided to `P`,
395    /// corresponds to that specified by [`Camera::project_ndc_into_world()`].
396    #[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    /// Trace only one ray, regardless of the antialiasing option, through all layers.
427    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            // TODO: this should be another blending but paint() doesn't present the right interface
440            *accum = P::paint(palette::NO_WORLD_TO_SHOW, self.options_refs().world);
441        }
442    }
443
444    #[doc(hidden)] // TODO: good public API? Required by raytrace_to_texture.
445    pub fn cameras(&self) -> &Layers<Camera> {
446        &self.cameras
447    }
448}
449
450/// A rectangle in normalized device coordinates (-1 to 1 is the viewport).
451type 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
457/// Threaded and non-threaded implementations of generating a full image.
458/// TODO: The design of this code (and its documentation) are slightly residual from
459/// when `trace_scene_to_image()` was a public interface. Revisit them.
460mod trace_image {
461    use super::*;
462
463    /// Compute a full image, writing it into `output`.
464    ///
465    /// The produced data is in the usual left-right then top-bottom raster order;
466    /// its dimensions are `camera.framebuffer_size`.
467    ///
468    /// `encoder` may be used to transform the output of the [`Accumulate`] into the stored
469    /// representation.
470    ///
471    /// Panics if `output`'s length does not match the area of `camera.framebuffer_size`.
472    ///
473    /// TODO: Add a mechanism for incrementally rendering (not 100% of pixels) for
474    /// interactive use.
475    #[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; // make shareable
494
495        // x.max(1) is zero-sized-viewport protection; the chunk size will be wrong, but there
496        // will be zero chunks anyway.
497        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() // sum of info
516    }
517
518    /// Compute a full image, writing it into `output`.
519    ///
520    /// The produced data is in the usual left-right then top-bottom raster order;
521    /// its dimensions are `camera.framebuffer_size`.
522    ///
523    /// `encoder` may be used to transform the output of the [`Accumulate`] into the stored
524    /// representation.
525    ///
526    /// Panics if `output`'s length does not match the area of `camera.framebuffer_size`.
527    ///
528    /// TODO: Add a mechanism for incrementally rendering (not 100% of pixels) for
529    /// interactive use.
530    #[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        // TODO: use .into_ok() when stable for infallible drawing
595        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    /// Just enough [`DrawTarget`] to implement info text drawing.
603    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        // Change the options after the renderer is created.
703        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        // See what options value is used.
714        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}