1#![allow(clippy::similar_names)]
38
39use glam::DVec3;
40use roxlap_core::opticast::{opticast, OpticastOutcome, OpticastSettings};
41use roxlap_core::rasterizer::ScratchPool;
42use roxlap_core::scalar_rasterizer::ScalarRasterizer;
43use roxlap_core::sky::Sky;
44use roxlap_core::Camera;
45
46use crate::billboard::{self, BillboardCache, DEFAULT_RESOLUTION as BILLBOARD_RESOLUTION};
47use crate::chunks;
48use crate::lod::Lod;
49use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
50
51const SKY_MASK_SENTINEL: u32 = 0x00_DE_AD_BE;
66
67fn world_camera_to_grid_local(camera: &Camera, transform: &GridTransform) -> Camera {
80 let inv = transform.rotation.inverse();
81 let world_offset = DVec3::from_array(camera.pos) - transform.origin;
82 let local_pos = inv * world_offset;
83 let local_right = inv * DVec3::from_array(camera.right);
84 let local_down = inv * DVec3::from_array(camera.down);
85 let local_forward = inv * DVec3::from_array(camera.forward);
86 Camera {
87 pos: local_pos.to_array(),
88 right: local_right.to_array(),
89 down: local_down.to_array(),
90 forward: local_forward.to_array(),
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum RenderOutcome {
97 Rendered {
99 grids_drawn: usize,
102 },
103 Empty,
107}
108
109fn single_chunk_fast_path<'a>(
129 backing: &'a chunks::ChunkXyBacking<'a>,
130 cg: &'a roxlap_core::ChunkGrid<'a>,
131) -> roxlap_core::GridView<'a> {
132 if backing.chunks_x == 1
133 && backing.chunks_y == 1
134 && backing.chunks_z == 1
135 && backing.origin_chunk_xy == [0, 0]
136 && backing.origin_chunk_z == 0
137 {
138 if let Some(single) = backing.chunks[0] {
142 return single;
143 }
144 }
145 roxlap_core::GridView::from_chunk_grid(cg, CHUNK_SIZE_XY)
146}
147
148#[allow(clippy::too_many_arguments)]
157pub fn render_scene(
158 fb: &mut [u32],
159 zb: &mut [f32],
160 pitch_pixels: usize,
161 width: u32,
162 height: u32,
163 pool: &mut ScratchPool,
164 scene: &mut Scene,
165 camera: &Camera,
166 settings: &OpticastSettings,
167 sky: Option<&Sky>,
168) -> RenderOutcome {
169 debug_assert_eq!(fb.len(), zb.len());
170 let pixel_count = (width as usize) * (height as usize);
171 debug_assert_eq!(fb.len(), pixel_count);
172
173 let mut grids_drawn = 0usize;
174 for (_id, grid) in scene.grids_mut() {
175 let Some(backing) = grid.chunk_xyz_backing() else {
186 continue;
188 };
189 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
190 let cg = roxlap_core::ChunkGrid {
191 chunks: &backing.chunks,
192 origin_chunk_xy: backing.origin_chunk_xy,
193 origin_chunk_z: backing.origin_chunk_z,
194 chunks_x: backing.chunks_x,
195 chunks_y: backing.chunks_y,
196 chunks_z: backing.chunks_z,
197 };
198 let grid_view = single_chunk_fast_path(&backing, &cg);
199 let outcome = {
200 let mut rasterizer = ScalarRasterizer::new(fb, zb, pitch_pixels, grid_view);
201 if let Some(sky_ref) = sky {
202 rasterizer = rasterizer.with_sky(sky_ref);
203 }
204 opticast(&mut rasterizer, pool, &local_cam, settings, grid_view)
205 };
206 if outcome == OpticastOutcome::Rendered {
207 grids_drawn += 1;
208 }
209 }
210 if grids_drawn == 0 {
211 RenderOutcome::Empty
212 } else {
213 RenderOutcome::Rendered { grids_drawn }
214 }
215}
216
217pub fn compose_into(
230 shared_fb: &mut [u32],
231 shared_zb: &mut [f32],
232 temp_fb: &[u32],
233 temp_zb: &[f32],
234) {
235 debug_assert_eq!(shared_fb.len(), shared_zb.len());
236 debug_assert_eq!(shared_fb.len(), temp_fb.len());
237 debug_assert_eq!(shared_fb.len(), temp_zb.len());
238 for i in 0..shared_fb.len() {
239 if temp_zb[i] < shared_zb[i] {
240 shared_fb[i] = temp_fb[i];
241 shared_zb[i] = temp_zb[i];
242 }
243 }
244}
245
246#[allow(clippy::too_many_arguments)]
277pub fn render_scene_composed(
278 fb: &mut [u32],
279 zb: &mut [f32],
280 pitch_pixels: usize,
281 width: u32,
282 height: u32,
283 pool: &mut ScratchPool,
284 scene: &mut Scene,
285 camera: &Camera,
286 settings: &OpticastSettings,
287 sky_color: u32,
288 sky: Option<&Sky>,
289) -> RenderOutcome {
290 debug_assert_eq!(fb.len(), zb.len());
291 let pixel_count = (width as usize) * (height as usize);
292 debug_assert_eq!(fb.len(), pixel_count);
293
294 let mut grids_drawn = 0usize;
295 let mut temp_fb = vec![sky_color; pixel_count];
296 let mut temp_zb = vec![f32::INFINITY; pixel_count];
297
298 for (_id, grid) in scene.grids_mut() {
299 let lod = grid.select_lod(DVec3::from_array(camera.pos));
319
320 if lod == Lod::Far {
321 if grid.chunks.is_empty() {
327 continue;
328 }
329 if grid.billboards.is_none() {
332 let cache = BillboardCache::build(grid, BILLBOARD_RESOLUTION);
333 grid.billboards = Some(cache);
334 }
335 let bounds = billboard::grid_bounds(grid);
338 let centre_world = grid.transform.origin + grid.transform.rotation * bounds.centre;
339 let cam_pos = DVec3::from_array(camera.pos);
343 let centre_to_cam_world = cam_pos - centre_world;
344 let ctc_len = centre_to_cam_world.length();
345 if !ctc_len.is_finite() || ctc_len < 1e-9 {
346 continue;
350 }
351 let query_dir_world = centre_to_cam_world / ctc_len;
352 let query_dir_local = grid.transform.rotation.inverse() * query_dir_world;
353 let cache = grid.billboards.as_ref().unwrap();
355 let snapshot = cache
358 .pick_nearest(query_dir_local)
359 .expect("billboard cache populated above");
360 billboard::billboard_blit_into(
361 fb,
362 zb,
363 pitch_pixels,
364 width,
365 height,
366 snapshot,
367 centre_world,
368 bounds.radius,
369 camera,
370 settings,
371 );
372 grids_drawn += 1;
373 continue;
374 }
375
376 let Some(backing) = grid.chunk_xyz_backing() else {
382 continue;
383 };
384
385 let bounds = billboard::grid_bounds(grid);
398 let centre_world = grid.transform.origin + grid.transform.rotation * bounds.centre;
399 let cam_pos = DVec3::from_array(camera.pos);
400 let dist_to_centre = (centre_world - cam_pos).length();
401 if dist_to_centre - bounds.radius > f64::from(settings.max_scan_dist) {
402 continue;
403 }
404 let owns_sky = grid.render_sky;
414 let local_sky_color = if owns_sky {
415 sky_color
416 } else {
417 SKY_MASK_SENTINEL
418 };
419 if !owns_sky {
420 pool.set_skycast(SKY_MASK_SENTINEL as i32, 0);
427 }
428
429 temp_fb.fill(local_sky_color);
433 temp_zb.fill(f32::INFINITY);
434
435 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
436 let cg = roxlap_core::ChunkGrid {
437 chunks: &backing.chunks,
438 origin_chunk_xy: backing.origin_chunk_xy,
439 origin_chunk_z: backing.origin_chunk_z,
440 chunks_x: backing.chunks_x,
441 chunks_y: backing.chunks_y,
442 chunks_z: backing.chunks_z,
443 };
444 let grid_view = single_chunk_fast_path(&backing, &cg);
445
446 let per_grid_settings;
466 let active_settings = {
467 let base_mip_levels = settings.mip_levels;
468 let base_mip_scan = settings.mip_scan_dist;
469 let lod_mip_levels = match lod {
470 Lod::Mid => grid.lod_thresholds.mid_mip_levels,
471 Lod::Near | Lod::Far => None,
472 };
473 let lod_mip_scan = match lod {
474 Lod::Mid => grid.lod_thresholds.mid_mip_scan_dist,
475 Lod::Near | Lod::Far => None,
476 };
477 let global_mip_cap = grid.mip_levels_override;
478 let needs_override =
479 lod_mip_levels.is_some() || lod_mip_scan.is_some() || global_mip_cap.is_some();
480 if needs_override {
481 let mut mip_levels =
484 lod_mip_levels.map_or(base_mip_levels, |n| n.clamp(1, base_mip_levels));
485 if let Some(cap) = global_mip_cap {
486 mip_levels = mip_levels.min(cap.clamp(1, base_mip_levels));
487 }
488 let mip_scan_dist = lod_mip_scan.map_or(base_mip_scan, |d| base_mip_scan.min(d));
494 per_grid_settings = OpticastSettings {
495 mip_levels,
496 mip_scan_dist,
497 ..*settings
498 };
499 &per_grid_settings
500 } else {
501 settings
502 }
503 };
504
505 let outcome = {
506 let mut rasterizer =
507 ScalarRasterizer::new(&mut temp_fb, &mut temp_zb, pitch_pixels, grid_view);
508 if owns_sky {
512 if let Some(sky_ref) = sky {
513 rasterizer = rasterizer.with_sky(sky_ref);
514 }
515 }
516 opticast(
517 &mut rasterizer,
518 pool,
519 &local_cam,
520 active_settings,
521 grid_view,
522 )
523 };
524
525 if !owns_sky {
526 for (px, z) in temp_fb.iter().zip(temp_zb.iter_mut()) {
530 if *px == SKY_MASK_SENTINEL {
531 *z = f32::INFINITY;
532 }
533 }
534 pool.set_skycast(sky_color as i32, 0);
537 }
538
539 if outcome == OpticastOutcome::Rendered {
540 compose_into(fb, zb, &temp_fb, &temp_zb);
541 grids_drawn += 1;
542 }
543 }
544
545 if grids_drawn == 0 {
546 RenderOutcome::Empty
547 } else {
548 RenderOutcome::Rendered { grids_drawn }
549 }
550}
551
552#[cfg(test)]
553#[allow(clippy::float_cmp)]
554mod tests {
555 use super::*;
556 use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
557 use glam::{DVec3, IVec3};
558 use roxlap_core::opticast::{opticast as core_opticast, OpticastSettings};
559 use roxlap_core::rasterizer::ScratchPool;
560 use roxlap_core::scalar_rasterizer::ScalarRasterizer;
561 use roxlap_core::{Camera, Engine};
562
563 const XRES: u32 = 320;
564 const YRES: u32 = 200;
565
566 fn build_one_grid_scene(world_origin: DVec3) -> (Scene, crate::GridId) {
570 let mut scene = Scene::new();
571 let id = scene.add_grid(GridTransform::at(world_origin));
572 let grid = scene.grid_mut(id).unwrap();
573 grid.set_rect(
575 IVec3::new(40, 40, 40),
576 IVec3::new(55, 55, 55),
577 Some(0x80_88_88_88),
578 );
579 grid.set_sphere(IVec3::new(80, 80, 80), 6, Some(0x80_22_aa_22));
581 (scene, id)
582 }
583
584 fn camera_at(pos: [f64; 3]) -> Camera {
585 Camera {
588 pos,
589 right: [-1.0, 0.0, 0.0],
590 down: [0.0, 0.0, 1.0],
591 forward: [0.0, 1.0, 0.0],
592 }
593 }
594
595 fn render_setup(pool_vsid: u32) -> (Engine, ScratchPool, Vec<u32>, Vec<f32>) {
599 let engine = Engine::new();
600 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
601 let sky = engine.sky_color();
602 let sky_col_i = i32::from_ne_bytes(sky.to_ne_bytes());
603 pool.set_skycast(sky_col_i, 0);
604 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
605 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
606 pool.set_treat_z_max_as_air(true);
607 let pixel_count = (XRES as usize) * (YRES as usize);
608 let framebuffer = vec![sky; pixel_count];
609 let zbuffer = vec![0.0f32; pixel_count];
610 (engine, pool, framebuffer, zbuffer)
611 }
612
613 fn render_via_scene(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
616 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
617 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
618 let outcome = render_scene(
619 &mut fb,
620 &mut zb,
621 XRES as usize,
622 XRES,
623 YRES,
624 &mut pool,
625 scene,
626 camera,
627 &settings,
628 None,
629 );
630 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
631 fb
632 }
633
634 fn render_via_direct_opticast(scene: &Scene, local_camera: &Camera) -> Vec<u32> {
638 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
639 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
640 let grid = scene.grids().next().unwrap().1;
641 let chunk = grid.chunk(IVec3::ZERO).unwrap();
642 let grid_view = roxlap_core::GridView::from_single_vxl(chunk);
643 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
644 let _ = core_opticast(
645 &mut rasterizer,
646 &mut pool,
647 local_camera,
648 &settings,
649 grid_view,
650 );
651 drop(rasterizer);
652 fb
653 }
654
655 #[test]
660 fn world_camera_to_grid_local_identity_rotation_translates_pos_only() {
661 let camera = Camera {
662 pos: [110.0, 220.0, 330.0],
663 right: [1.0, 0.0, 0.0],
664 down: [0.0, 0.0, 1.0],
665 forward: [0.0, 1.0, 0.0],
666 };
667 let transform = GridTransform::at(DVec3::new(100.0, 200.0, 300.0));
668 let local = super::world_camera_to_grid_local(&camera, &transform);
669 assert_eq!(local.right, camera.right);
671 assert_eq!(local.down, camera.down);
672 assert_eq!(local.forward, camera.forward);
673 for (got, want) in local.pos.iter().zip([10.0, 20.0, 30.0].iter()) {
675 assert!((got - want).abs() < 1e-12, "pos got={got} want={want}");
676 }
677 }
678
679 #[test]
683 fn world_camera_to_grid_local_90deg_z_rotates_basis_and_pos() {
684 use glam::DQuat;
685 let camera = Camera {
686 pos: [0.0, 10.0, 0.0],
687 right: [1.0, 0.0, 0.0],
688 down: [0.0, 0.0, 1.0],
689 forward: [0.0, 1.0, 0.0],
690 };
691 let transform = GridTransform {
692 origin: DVec3::ZERO,
693 rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
694 };
695 let local = super::world_camera_to_grid_local(&camera, &transform);
696 let approx_eq =
698 |a: [f64; 3], b: [f64; 3]| a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-9);
699 assert!(
700 approx_eq(local.pos, [10.0, 0.0, 0.0]),
701 "pos={:?} expected ~(10, 0, 0)",
702 local.pos
703 );
704 assert!(
706 approx_eq(local.right, [0.0, -1.0, 0.0]),
707 "right={:?} expected ~(0, -1, 0)",
708 local.right
709 );
710 assert!(
712 approx_eq(local.down, [0.0, 0.0, 1.0]),
713 "down={:?} expected ~(0, 0, 1)",
714 local.down
715 );
716 assert!(
718 approx_eq(local.forward, [1.0, 0.0, 0.0]),
719 "forward={:?} expected ~(1, 0, 0)",
720 local.forward
721 );
722 }
723
724 #[test]
729 fn world_camera_to_grid_local_preserves_basis_orthonormality() {
730 use glam::DQuat;
731 let camera = Camera {
734 pos: [3.0, -5.0, 7.0],
735 right: [-1.0, 0.0, 0.0],
736 down: [0.0, 0.0, 1.0],
737 forward: [0.0, 1.0, 0.0],
738 };
739 let transform = GridTransform {
740 origin: DVec3::new(1.0, 2.0, 3.0),
741 rotation: DQuat::from_axis_angle(glam::DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
742 };
743 let local = super::world_camera_to_grid_local(&camera, &transform);
744 let r = DVec3::from_array(local.right);
745 let d = DVec3::from_array(local.down);
746 let f = DVec3::from_array(local.forward);
747 for v in [r, d, f] {
749 assert!(
750 (v.length_squared() - 1.0).abs() < 1e-12,
751 "basis vec {v:?} not unit length"
752 );
753 }
754 assert!(r.dot(d).abs() < 1e-12, "right·down = {}", r.dot(d));
756 assert!(r.dot(f).abs() < 1e-12, "right·forward = {}", r.dot(f));
757 assert!(d.dot(f).abs() < 1e-12, "down·forward = {}", d.dot(f));
758 let cross = r.cross(d);
760 assert!(
761 (cross - f).length() < 1e-12,
762 "right×down={cross:?} forward={f:?}"
763 );
764 }
765
766 fn build_one_grid_marker_scene(transform: GridTransform) -> (Scene, crate::GridId, u32) {
774 let mut scene = Scene::new();
775 let id = scene.add_grid(transform);
776 let grid = scene.grid_mut(id).unwrap();
777 grid.set_rect(
779 IVec3::new(40, 40, 40),
780 IVec3::new(55, 55, 55),
781 Some(0x80_55_aa_22), );
783 (scene, id, 0x80_55_aa_22)
784 }
785
786 #[test]
799 fn s5_1_180deg_z_rotated_grid_byte_identical_to_axis_aligned() {
800 use glam::DQuat;
801 let axis_aligned_camera = Camera {
803 pos: [40.0, -20.0, 50.0],
804 right: [-1.0, 0.0, 0.0],
805 down: [0.0, 0.0, 1.0],
806 forward: [0.0, 1.0, 0.0],
807 };
808 let rotated_camera = Camera {
810 pos: [-40.0, 20.0, 50.0],
811 right: [1.0, 0.0, 0.0],
812 down: [0.0, 0.0, 1.0],
813 forward: [0.0, -1.0, 0.0],
814 };
815 let q = DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0);
820 let rot_pos = q * DVec3::from_array(axis_aligned_camera.pos);
821 let rot_fwd = q * DVec3::from_array(axis_aligned_camera.forward);
822 assert_eq!(rot_pos.to_array(), rotated_camera.pos);
823 assert_eq!(rot_fwd.to_array(), rotated_camera.forward);
824
825 let (mut scene_a, _, _) = build_one_grid_marker_scene(GridTransform::identity());
826 let fb_a = render_via_scene(&mut scene_a, &axis_aligned_camera);
827
828 let (mut scene_b, _, _) = build_one_grid_marker_scene(GridTransform {
829 origin: DVec3::ZERO,
830 rotation: q,
831 });
832 let fb_b = render_via_scene(&mut scene_b, &rotated_camera);
833
834 assert_eq!(
835 fb_a, fb_b,
836 "rotating both grid and camera by R about the grid origin must leave the framebuffer unchanged"
837 );
838 }
839
840 #[test]
847 fn s5_1_45deg_z_rotated_grid_renders_marker() {
848 use glam::DQuat;
849 let rotation = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4);
850 let (mut scene, _, marker) = build_one_grid_marker_scene(GridTransform {
851 origin: DVec3::ZERO,
852 rotation,
853 });
854
855 let marker_world = rotation * DVec3::new(47.5, 47.5, 47.5);
860 let camera = Camera {
863 pos: [marker_world.x, marker_world.y - 80.0, marker_world.z],
864 right: [-1.0, 0.0, 0.0],
865 down: [0.0, 0.0, 1.0],
866 forward: [0.0, 1.0, 0.0],
867 };
868
869 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
870 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
871 let outcome = render_scene(
872 &mut fb,
873 &mut zb,
874 XRES as usize,
875 XRES,
876 YRES,
877 &mut pool,
878 &mut scene,
879 &camera,
880 &settings,
881 None,
882 );
883 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
884 let marker_count = fb.iter().filter(|&&p| p == marker).count();
885 assert!(
886 marker_count > 50,
887 "45°-rotated marker box should be visible — got {marker_count} marker pixels"
888 );
889 }
890
891 #[test]
903 fn render_sky_false_drops_grid_sky_pixels() {
904 use crate::{GridId, GridTransform};
905
906 let mut scene = Scene::new();
909 let _b_id: GridId = scene.add_grid(GridTransform::at(DVec3::new(0.0, 600.0, 0.0)));
910 let b_id = scene.grids().next().unwrap().0;
913 scene.grid_mut(b_id).unwrap().set_rect(
914 IVec3::new(0, 0, 100),
915 IVec3::new(127, 127, 110),
916 Some(0x80_22_88_22), );
918
919 let a_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
923 scene.grid_mut(a_id).unwrap().set_rect(
924 IVec3::new(60, 60, 60),
925 IVec3::new(67, 67, 67),
926 Some(0x80_aa_22_22), );
928 scene.grid_mut(a_id).unwrap().render_sky = false;
929
930 let unique_sky: u32 = 0xFF_AB_CD_EF;
931 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
932 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
933 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
934 let camera = camera_at([64.0, 0.0, 100.0]);
935 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
936 let outcome = render_scene_composed(
937 &mut fb,
938 &mut zb,
939 XRES as usize,
940 XRES,
941 YRES,
942 &mut pool,
943 &mut scene,
944 &camera,
945 &settings,
946 unique_sky,
947 None,
948 );
949 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
950
951 let leaked = fb
955 .iter()
956 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
957 .count();
958 assert_eq!(
959 leaked, 0,
960 "SKY_MASK_SENTINEL leaked into composed framebuffer ({leaked} pixels)"
961 );
962 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_22_22).count();
965 assert!(
966 red_count > 0,
967 "red cube from sky-disabled grid A is missing — render_sky=false should only mask sky"
968 );
969 let green_count = fb.iter().filter(|&&p| p == 0x80_22_88_22).count();
972 assert!(
973 green_count > 0,
974 "grid B's floor invisible — grid A's masked sky may have overwritten it"
975 );
976 }
977
978 #[test]
982 fn render_sky_false_single_grid_no_sentinel_leak() {
983 let (mut scene, id, _) = build_one_grid_marker_scene(GridTransform::identity());
984 scene.grid_mut(id).unwrap().render_sky = false;
985 let unique_sky: u32 = 0xFF_12_34_56;
986 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
987 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
988 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
989 let camera = camera_at([64.0, 0.0, 64.0]);
990 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
991 let outcome = render_scene_composed(
992 &mut fb,
993 &mut zb,
994 XRES as usize,
995 XRES,
996 YRES,
997 &mut pool,
998 &mut scene,
999 &camera,
1000 &settings,
1001 unique_sky,
1002 None,
1003 );
1004 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1005 let leaked = fb
1006 .iter()
1007 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
1008 .count();
1009 assert_eq!(leaked, 0, "SKY_MASK_SENTINEL leaked ({leaked} pixels)");
1010 let prefill_count = fb.iter().filter(|&&p| p == unique_sky).count();
1013 assert!(
1014 prefill_count > 0,
1015 "no pre-fill pixels survived — render_sky=false should leave non-hit pixels untouched"
1016 );
1017 }
1018
1019 #[test]
1020 fn render_scene_at_origin_matches_direct_opticast() {
1021 let (mut scene, _) = build_one_grid_scene(DVec3::ZERO);
1027 let cam = camera_at([64.0, 0.0, 64.0]);
1028 let via_scene = render_via_scene(&mut scene, &cam);
1029 let via_direct = render_via_direct_opticast(&scene, &cam);
1030 assert_eq!(
1031 via_scene, via_direct,
1032 "render_scene with single 1-chunk grid at origin should match direct opticast"
1033 );
1034 }
1035
1036 #[test]
1037 fn render_scene_translated_grid_matches_grid_local_opticast() {
1038 let world_origin = DVec3::new(1000.0, 2000.0, 3000.0);
1043 let (mut scene, _) = build_one_grid_scene(world_origin);
1044 let world_cam = camera_at([1064.0, 2000.0, 3064.0]);
1045 let local_cam = camera_at([64.0, 0.0, 64.0]);
1046 let via_scene = render_via_scene(&mut scene, &world_cam);
1047 let via_direct = render_via_direct_opticast(&scene, &local_cam);
1048 assert_eq!(
1049 via_scene, via_direct,
1050 "render_scene of translated grid should match opticast with grid-local camera"
1051 );
1052 }
1053
1054 #[test]
1055 fn empty_scene_returns_empty_outcome() {
1056 let mut scene = Scene::new();
1057 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
1058 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1059 let outcome = render_scene(
1060 &mut fb,
1061 &mut zb,
1062 XRES as usize,
1063 XRES,
1064 YRES,
1065 &mut pool,
1066 &mut scene,
1067 &camera_at([0.0, 0.0, 0.0]),
1068 &settings,
1069 None,
1070 );
1071 assert_eq!(outcome, RenderOutcome::Empty);
1072 }
1073
1074 fn build_two_grid_side_by_side() -> (Scene, u32, u32) {
1082 let mut scene = Scene::new();
1083 let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1085 scene.grid_mut(g0).unwrap().set_rect(
1086 IVec3::new(56, 56, 92),
1087 IVec3::new(71, 71, 107),
1088 Some(0x80_88_22_22), );
1090 let _g1 = scene.add_grid(GridTransform::at(DVec3::new(200.0, 200.0, 0.0)));
1092 let g1_id = scene
1094 .grids()
1095 .filter(|(id, _)| *id != g0)
1096 .map(|(id, _)| id)
1097 .next()
1098 .unwrap();
1099 scene.grid_mut(g1_id).unwrap().set_rect(
1100 IVec3::new(56, 56, 92),
1101 IVec3::new(71, 71, 107),
1102 Some(0x80_22_22_88), );
1104 (scene, 0x80_88_22_22, 0x80_22_22_88)
1105 }
1106
1107 fn make_composed_pool(pool_vsid: u32) -> (Engine, ScratchPool, u32) {
1108 let engine = Engine::new();
1109 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
1110 let sky_color = engine.sky_color();
1111 let sky_col_i = i32::from_ne_bytes(sky_color.to_ne_bytes());
1112 pool.set_skycast(sky_col_i, 0);
1113 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
1114 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
1115 pool.set_treat_z_max_as_air(true);
1116 (engine, pool, sky_color)
1117 }
1118
1119 fn pixel_count(width: u32, height: u32) -> usize {
1120 (width as usize) * (height as usize)
1121 }
1122
1123 #[test]
1124 fn compose_into_takes_smaller_z() {
1125 let mut shared_fb = vec![0xff_ff_ff_ff_u32; 4];
1126 let mut shared_zb = vec![10.0f32; 4];
1127 let temp_fb = [0xaa_aa_aa_aa, 0x11_22_33_44, 0x55_66_77_88, 0xde_ad_be_ef];
1128 let temp_zb = [5.0f32, 20.0, 10.0, f32::INFINITY];
1129 compose_into(&mut shared_fb, &mut shared_zb, &temp_fb, &temp_zb);
1130 assert_eq!(shared_fb[0], 0xaa_aa_aa_aa);
1132 assert_eq!(shared_zb[0], 5.0);
1133 assert_eq!(shared_fb[1], 0xff_ff_ff_ff);
1135 assert_eq!(shared_zb[1], 10.0);
1136 assert_eq!(shared_fb[2], 0xff_ff_ff_ff);
1138 assert_eq!(shared_fb[3], 0xff_ff_ff_ff);
1140 }
1141
1142 #[test]
1143 fn render_scene_composed_two_grids_both_visible() {
1144 let (mut scene, red, blue) = build_two_grid_side_by_side();
1149 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1150 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1151 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1152
1153 let camera = camera_at([160.0, 100.0, 100.0]);
1154 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1155 let outcome = render_scene_composed(
1156 &mut fb,
1157 &mut zb,
1158 XRES as usize,
1159 XRES,
1160 YRES,
1161 &mut pool,
1162 &mut scene,
1163 &camera,
1164 &settings,
1165 sky_color,
1166 None,
1167 );
1168 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1169
1170 let red_count = fb.iter().filter(|&&p| p == red).count();
1172 let blue_count = fb.iter().filter(|&&p| p == blue).count();
1173 assert!(
1174 red_count > 0,
1175 "no red pixels: grid 0 (red box) not visible after compose"
1176 );
1177 assert!(
1178 blue_count > 0,
1179 "no blue pixels: grid 1 (blue box) not visible after compose"
1180 );
1181 }
1182
1183 #[test]
1184 fn render_scene_composed_grid_a_in_front_of_grid_b() {
1185 let mut scene = Scene::new();
1189 let g_a = scene.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1190 scene.grid_mut(g_a).unwrap().set_rect(
1191 IVec3::new(56, 56, 92),
1192 IVec3::new(71, 71, 107),
1193 Some(0x80_aa_00_00), );
1195 let _g_b = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1196 let g_b_id = scene
1197 .grids()
1198 .filter(|(id, _)| *id != g_a)
1199 .map(|(id, _)| id)
1200 .next()
1201 .unwrap();
1202 scene.grid_mut(g_b_id).unwrap().set_rect(
1203 IVec3::new(56, 56, 92),
1204 IVec3::new(71, 71, 107),
1205 Some(0x80_00_00_aa), );
1207
1208 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1209 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1210 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1211
1212 let camera = camera_at([64.0, -10.0, 100.0]);
1215 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1216 let outcome = render_scene_composed(
1217 &mut fb,
1218 &mut zb,
1219 XRES as usize,
1220 XRES,
1221 YRES,
1222 &mut pool,
1223 &mut scene,
1224 &camera,
1225 &settings,
1226 sky_color,
1227 None,
1228 );
1229 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1230
1231 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_00_00).count();
1235 assert!(
1236 red_count > 0,
1237 "expected red pixels (closer box should win z-test)"
1238 );
1239
1240 let mut scene2 = Scene::new();
1243 let g_b2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1244 scene2.grid_mut(g_b2).unwrap().set_rect(
1245 IVec3::new(56, 56, 92),
1246 IVec3::new(71, 71, 107),
1247 Some(0x80_00_00_aa),
1248 );
1249 let g_a2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1250 scene2.grid_mut(g_a2).unwrap().set_rect(
1251 IVec3::new(56, 56, 92),
1252 IVec3::new(71, 71, 107),
1253 Some(0x80_aa_00_00),
1254 );
1255
1256 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1257 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1258 let outcome2 = render_scene_composed(
1259 &mut fb2,
1260 &mut zb2,
1261 XRES as usize,
1262 XRES,
1263 YRES,
1264 &mut pool,
1265 &mut scene2,
1266 &camera,
1267 &settings,
1268 sky_color,
1269 None,
1270 );
1271 assert_eq!(outcome2, RenderOutcome::Rendered { grids_drawn: 2 });
1272 assert_eq!(
1273 fb, fb2,
1274 "composition should be order-independent — same scene in different add order should produce identical output"
1275 );
1276 }
1277
1278 fn build_mip_visible_grid(world_origin: DVec3) -> (Scene, crate::GridId) {
1293 let mut scene = Scene::new();
1294 let id = scene.add_grid(GridTransform::at(world_origin));
1295 let grid = scene.grid_mut(id).unwrap();
1296 grid.set_rect(
1298 IVec3::new(0, 0, 100),
1299 IVec3::new(127, 127, 254),
1300 Some(0x80_88_88_88),
1301 );
1302 grid.chunk_mut(IVec3::ZERO).unwrap().generate_mips(3);
1304 (scene, id)
1305 }
1306
1307 fn fb_hash(fb: &[u32]) -> u64 {
1309 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1310 for px in fb {
1311 for b in px.to_le_bytes() {
1312 h ^= u64::from(b);
1313 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1314 }
1315 }
1316 h
1317 }
1318
1319 fn render_with_multi_mip(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
1324 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1325 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1326 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1327 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1328 settings.mip_levels = 3;
1329 settings.mip_scan_dist = 32;
1330 let outcome = render_scene_composed(
1331 &mut fb,
1332 &mut zb,
1333 XRES as usize,
1334 XRES,
1335 YRES,
1336 &mut pool,
1337 scene,
1338 camera,
1339 &settings,
1340 sky_color,
1341 None,
1342 );
1343 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1344 fb
1345 }
1346
1347 #[test]
1352 fn s6_1_mid_overrides_produce_different_framebuffer_than_near() {
1353 let camera = camera_at([64.0, 0.0, 64.0]);
1355
1356 let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1359 let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1360
1361 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1369 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1370 r_near: 0.0,
1372 r_mid: f64::INFINITY,
1373 mid_mip_levels: Some(1),
1374 mid_mip_scan_dist: None,
1375 };
1376 let lod = scene_b
1378 .grid(b_id)
1379 .unwrap()
1380 .select_lod(DVec3::from_array(camera.pos));
1381 assert_eq!(lod, Lod::Mid, "expected Mid tier for forced thresholds");
1382 let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1383
1384 let (_engine, _, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1387 let non_sky_near = fb_near.iter().filter(|&&p| p != sky_color).count();
1388 let non_sky_mid = fb_mid.iter().filter(|&&p| p != sky_color).count();
1389 assert!(
1390 non_sky_near > 100,
1391 "Near render too sparse ({non_sky_near})"
1392 );
1393 assert!(non_sky_mid > 100, "Mid render too sparse ({non_sky_mid})");
1394
1395 let h_near = fb_hash(&fb_near);
1399 let h_mid = fb_hash(&fb_mid);
1400 assert_ne!(
1401 h_near, h_mid,
1402 "Mid tier with mid_mip_levels=Some(1) must differ from Near (h_near={h_near:016x})"
1403 );
1404 }
1405
1406 #[test]
1412 fn s6_1_mid_without_overrides_byte_identical_to_near() {
1413 let camera = camera_at([64.0, 0.0, 64.0]);
1414
1415 let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1417 let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1418
1419 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1421 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1422 r_near: 0.0,
1423 r_mid: f64::INFINITY,
1424 mid_mip_levels: None,
1425 mid_mip_scan_dist: None,
1426 };
1427 let lod = scene_b
1428 .grid(b_id)
1429 .unwrap()
1430 .select_lod(DVec3::from_array(camera.pos));
1431 assert_eq!(lod, Lod::Mid);
1432 let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1433
1434 assert_eq!(
1436 fb_near, fb_mid,
1437 "Mid with both overrides=None must byte-match Near"
1438 );
1439 }
1440
1441 #[test]
1453 fn s6_1_global_mip_cap_survives_mid_tier() {
1454 let camera = camera_at([64.0, 0.0, 64.0]);
1455
1456 let (mut scene_a, a_id) = build_mip_visible_grid(DVec3::ZERO);
1458 scene_a.grid_mut(a_id).unwrap().mip_levels_override = Some(1);
1459 let fb_a = render_with_multi_mip(&mut scene_a, &camera);
1460
1461 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1465 scene_b.grid_mut(b_id).unwrap().mip_levels_override = Some(1);
1466 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1467 r_near: 0.0,
1468 r_mid: f64::INFINITY,
1469 mid_mip_levels: Some(4),
1470 mid_mip_scan_dist: None,
1473 };
1474 let fb_b = render_with_multi_mip(&mut scene_b, &camera);
1475
1476 assert_eq!(
1477 fb_a, fb_b,
1478 "global mip_levels_override should clamp Mid override (ship workaround survives Mid tier)"
1479 );
1480 }
1481
1482 #[test]
1490 fn s6_3_far_tier_blits_non_sky_pixels() {
1491 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1492 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1493 r_near: 0.0,
1494 r_mid: 0.0,
1495 mid_mip_levels: None,
1496 mid_mip_scan_dist: None,
1497 };
1498
1499 let camera = camera_at([64.0, 0.0, 100.0]);
1500 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1501 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1502 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1503 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1504 let outcome = render_scene_composed(
1505 &mut fb,
1506 &mut zb,
1507 XRES as usize,
1508 XRES,
1509 YRES,
1510 &mut pool,
1511 &mut scene,
1512 &camera,
1513 &settings,
1514 sky_color,
1515 None,
1516 );
1517 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1518
1519 let lod = scene
1521 .grid(id)
1522 .unwrap()
1523 .select_lod(DVec3::from_array(camera.pos));
1524 assert_eq!(lod, Lod::Far);
1525
1526 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1528 assert!(
1529 non_sky > 0,
1530 "Far-tier render produced no non-sky pixels — billboard blit not firing"
1531 );
1532 }
1533
1534 #[test]
1537 fn s6_3_far_render_lazily_populates_cache() {
1538 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1539 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1540 r_near: 0.0,
1541 r_mid: 0.0,
1542 mid_mip_levels: None,
1543 mid_mip_scan_dist: None,
1544 };
1545 assert!(scene.grid(id).unwrap().billboards.is_none());
1546
1547 let camera = camera_at([64.0, 0.0, 100.0]);
1548 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1549 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1550 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1551 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1552 let _ = render_scene_composed(
1553 &mut fb,
1554 &mut zb,
1555 XRES as usize,
1556 XRES,
1557 YRES,
1558 &mut pool,
1559 &mut scene,
1560 &camera,
1561 &settings,
1562 sky_color,
1563 None,
1564 );
1565 let cache = scene
1566 .grid(id)
1567 .unwrap()
1568 .billboards
1569 .as_ref()
1570 .expect("Far render should have populated billboards");
1571 assert_eq!(cache.len(), 26);
1572 }
1573
1574 #[test]
1576 fn s6_3_edit_invalidates_then_far_render_rebuilds() {
1577 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1578 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1579 r_near: 0.0,
1580 r_mid: 0.0,
1581 mid_mip_levels: None,
1582 mid_mip_scan_dist: None,
1583 };
1584 let camera = camera_at([64.0, 0.0, 100.0]);
1585 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1586 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1587
1588 let mut fb1 = vec![sky_color; pixel_count(XRES, YRES)];
1590 let mut zb1 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1591 let _ = render_scene_composed(
1592 &mut fb1,
1593 &mut zb1,
1594 XRES as usize,
1595 XRES,
1596 YRES,
1597 &mut pool,
1598 &mut scene,
1599 &camera,
1600 &settings,
1601 sky_color,
1602 None,
1603 );
1604 assert!(scene.grid(id).unwrap().billboards.is_some());
1605
1606 scene
1608 .grid_mut(id)
1609 .unwrap()
1610 .set_voxel(IVec3::new(70, 70, 70), Some(0x80_aa_aa_22));
1611 assert!(scene.grid(id).unwrap().billboards.is_none());
1612
1613 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1615 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1616 let _ = render_scene_composed(
1617 &mut fb2,
1618 &mut zb2,
1619 XRES as usize,
1620 XRES,
1621 YRES,
1622 &mut pool,
1623 &mut scene,
1624 &camera,
1625 &settings,
1626 sky_color,
1627 None,
1628 );
1629 assert!(scene.grid(id).unwrap().billboards.is_some());
1630 }
1631
1632 #[test]
1637 fn s6_3_near_and_far_grids_in_same_scene() {
1638 let mut scene = Scene::new();
1639 let a_id = scene.add_grid(GridTransform::at(DVec3::new(-100.0, 200.0, 0.0)));
1642 scene.grid_mut(a_id).unwrap().set_rect(
1643 IVec3::new(70, 0, 50),
1644 IVec3::new(85, 15, 70),
1645 Some(0x80_22_88_22), );
1647 let b_id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1649 scene.grid_mut(b_id).unwrap().set_rect(
1650 IVec3::new(0, 0, 80),
1651 IVec3::new(20, 20, 110),
1652 Some(0x80_aa_22_22), );
1654 scene.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1655 r_near: 0.0,
1656 r_mid: 0.0,
1657 mid_mip_levels: None,
1658 mid_mip_scan_dist: None,
1659 };
1660
1661 let camera = camera_at([0.0, 0.0, 80.0]);
1662 assert_eq!(
1664 scene
1665 .grid(a_id)
1666 .unwrap()
1667 .select_lod(DVec3::from_array(camera.pos)),
1668 Lod::Near
1669 );
1670 assert_eq!(
1671 scene
1672 .grid(b_id)
1673 .unwrap()
1674 .select_lod(DVec3::from_array(camera.pos)),
1675 Lod::Far
1676 );
1677
1678 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1679 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1680 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1681 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1682 let outcome = render_scene_composed(
1683 &mut fb,
1684 &mut zb,
1685 XRES as usize,
1686 XRES,
1687 YRES,
1688 &mut pool,
1689 &mut scene,
1690 &camera,
1691 &settings,
1692 sky_color,
1693 None,
1694 );
1695 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1696
1697 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1699 assert!(
1700 non_sky > 20,
1701 "hybrid scene produced too few non-sky pixels ({non_sky}); one tier may have failed"
1702 );
1703 }
1704
1705 #[test]
1708 fn s6_3_empty_grid_at_far_is_skipped() {
1709 let mut scene = Scene::new();
1710 let id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1711 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1712 r_near: 0.0,
1713 r_mid: 0.0,
1714 mid_mip_levels: None,
1715 mid_mip_scan_dist: None,
1716 };
1717
1718 let camera = camera_at([0.0, 0.0, 100.0]);
1719 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1720 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1721 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1722 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1723 let outcome = render_scene_composed(
1724 &mut fb,
1725 &mut zb,
1726 XRES as usize,
1727 XRES,
1728 YRES,
1729 &mut pool,
1730 &mut scene,
1731 &camera,
1732 &settings,
1733 sky_color,
1734 None,
1735 );
1736 assert_eq!(outcome, RenderOutcome::Empty);
1738 assert!(scene.grid(id).unwrap().billboards.is_none());
1740 assert!(fb.iter().all(|&p| p == sky_color));
1742 }
1743
1744 #[test]
1753 fn render_scene_composed_lod_threshold_invariance() {
1754 let (mut scene_a, _a_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1756 let cam = camera_at([64.0, 0.0, 100.0]);
1757 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1758 let mut fb_a = vec![sky_color; pixel_count(XRES, YRES)];
1759 let mut zb_a = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1760 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1761 let outcome_a = render_scene_composed(
1762 &mut fb_a,
1763 &mut zb_a,
1764 XRES as usize,
1765 XRES,
1766 YRES,
1767 &mut pool,
1768 &mut scene_a,
1769 &cam,
1770 &settings,
1771 sky_color,
1772 None,
1773 );
1774 assert_eq!(outcome_a, RenderOutcome::Rendered { grids_drawn: 1 });
1775
1776 let (mut scene_b, b_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1781 let radius = scene_b.grid(b_id).unwrap().bounding_radius();
1782 assert!(
1783 radius > 0.0,
1784 "bounding_radius should be > 0 for a populated grid"
1785 );
1786 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds::from_radius(radius);
1787 let lod = scene_b
1790 .grid(b_id)
1791 .unwrap()
1792 .select_lod(DVec3::from_array(cam.pos));
1793 assert_ne!(
1794 lod,
1795 Lod::Near,
1796 "camera should land in Mid or Far for derived thresholds — got {lod:?}",
1797 );
1798
1799 let mut fb_b = vec![sky_color; pixel_count(XRES, YRES)];
1800 let mut zb_b = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1801 let outcome_b = render_scene_composed(
1802 &mut fb_b,
1803 &mut zb_b,
1804 XRES as usize,
1805 XRES,
1806 YRES,
1807 &mut pool,
1808 &mut scene_b,
1809 &cam,
1810 &settings,
1811 sky_color,
1812 None,
1813 );
1814 assert_eq!(outcome_b, RenderOutcome::Rendered { grids_drawn: 1 });
1815
1816 assert_eq!(
1819 fb_a, fb_b,
1820 "S6.0 framebuffer must be byte-identical regardless of LOD thresholds"
1821 );
1822 }
1823
1824 #[test]
1825 fn render_scene_composed_empty_scene_returns_empty() {
1826 let mut scene = Scene::new();
1827 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1828 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1829 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1830 let camera = camera_at([0.0, 0.0, 0.0]);
1831 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1832 let outcome = render_scene_composed(
1833 &mut fb,
1834 &mut zb,
1835 XRES as usize,
1836 XRES,
1837 YRES,
1838 &mut pool,
1839 &mut scene,
1840 &camera,
1841 &settings,
1842 sky_color,
1843 None,
1844 );
1845 assert_eq!(outcome, RenderOutcome::Empty);
1846 assert!(fb.iter().all(|&p| p == sky_color));
1848 }
1849
1850 fn fnv1a64(data: &[u8]) -> u64 {
1855 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1856 for &b in data {
1857 h ^= u64::from(b);
1858 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1859 }
1860 h
1861 }
1862
1863 #[test]
1869 fn render_scene_two_chunk_x_grid_no_seam() {
1870 let mut scene = Scene::new();
1871 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1872 let g = scene.grid_mut(id).unwrap();
1873 g.set_rect(
1879 IVec3::new(120, 60, 200),
1880 IVec3::new(136, 67, 215),
1881 Some(0x80_aa_55_22),
1882 );
1883 assert_eq!(g.chunk_count(), 2);
1885
1886 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1890 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1891 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1892 let camera = camera_at([128.0, 100.0, 207.0]);
1893 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1894 let outcome = render_scene_composed(
1895 &mut fb,
1896 &mut zb,
1897 XRES as usize,
1898 XRES,
1899 YRES,
1900 &mut pool,
1901 &mut scene,
1902 &camera,
1903 &settings,
1904 sky_color,
1905 None,
1906 );
1907 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1908
1909 let stripe = 0x80_aa_55_22;
1913 let stripe_count = fb.iter().filter(|&&p| p == stripe).count();
1914 assert!(
1915 stripe_count > 200,
1916 "stripe rendered too few pixels ({stripe_count}) — chunks may not be stitching"
1917 );
1918
1919 let centre_y = (YRES / 2) as usize;
1923 let row_start = centre_y * (XRES as usize);
1924 let row = &fb[row_start..row_start + (XRES as usize)];
1925 let mut in_stripe = false;
1926 let mut seam_gaps = 0usize;
1927 for &px in row {
1928 if px == stripe {
1929 in_stripe = true;
1930 } else if in_stripe && px == sky_color {
1931 if row.iter().skip_while(|&&p| p != px).any(|&p| p == stripe) {
1934 seam_gaps += 1;
1936 }
1937 in_stripe = false;
1938 }
1939 }
1940 assert!(
1944 seam_gaps <= 1,
1945 "centre row has {seam_gaps} disjoint stripe runs — expected 1 (chunk-edge seam suspected)"
1946 );
1947 }
1948
1949 #[test]
1965 fn vxl_generate_mips_on_set_voxel_chunk_renders() {
1966 let mut grid = crate::Grid::new(GridTransform::identity());
1967 grid.set_rect(
1970 IVec3::new(0, 0, 100),
1971 IVec3::new(127, 127, 254),
1972 Some(0x80_88_88_88),
1973 );
1974 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1975 chunk.generate_mips(3);
1976 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1977 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1978 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1979 let camera = camera_at([64.0, 0.0, 64.0]);
1980 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1981 settings.mip_levels = 3;
1982 settings.mip_scan_dist = 32;
1983 let grid_view = roxlap_core::GridView::from_single_vxl(&chunk);
1984 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
1985 let _ = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
1986 drop(rasterizer);
1987 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1988 assert!(
1989 non_sky > 0,
1990 "Vxl::generate_mips on a set_voxel-built chunk should render to something non-sky (got {non_sky})"
1991 );
1992 }
1993
1994 #[test]
1999 fn render_with_mips_present_still_renders_mip0() {
2000 let mut scene = Scene::new();
2001 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2002 scene.grid_mut(id).unwrap().set_rect(
2003 IVec3::new(40, 40, 40),
2004 IVec3::new(55, 55, 55),
2005 Some(0x80_88_88_88),
2006 );
2007 {
2013 let grid = scene.grid_mut(id).unwrap();
2014 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
2015 chunk.generate_mips(3);
2016 }
2017
2018 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2019 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2020 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2021 let camera = camera_at([64.0, 0.0, 64.0]);
2022 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2025 settings.mip_scan_dist = 100_000;
2026 let outcome = render_scene_composed(
2027 &mut fb,
2028 &mut zb,
2029 XRES as usize,
2030 XRES,
2031 YRES,
2032 &mut pool,
2033 &mut scene,
2034 &camera,
2035 &settings,
2036 sky_color,
2037 None,
2038 );
2039 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2040 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
2041 assert!(
2042 non_sky > 0,
2043 "render of single-grid scene with mips present rendered all-sky: mip-0 may be corrupted by generate_mips"
2044 );
2045 }
2046
2047 #[test]
2048 fn render_scene_two_chunk_x_grid_hash_is_stable() {
2049 const GOLDEN: u64 = 0x215e_d66d_7359_4725;
2051 let mut scene = Scene::new();
2055 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
2056 scene.grid_mut(id).unwrap().set_rect(
2057 IVec3::new(120, 60, 200),
2058 IVec3::new(136, 67, 215),
2059 Some(0x80_aa_55_22),
2060 );
2061 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2062 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2063 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2064 let camera = camera_at([128.0, 100.0, 207.0]);
2065 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2066 let outcome = render_scene_composed(
2067 &mut fb,
2068 &mut zb,
2069 XRES as usize,
2070 XRES,
2071 YRES,
2072 &mut pool,
2073 &mut scene,
2074 &camera,
2075 &settings,
2076 sky_color,
2077 None,
2078 );
2079 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2080
2081 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2082 let hash = fnv1a64(&bytes);
2083 if GOLDEN == SENTINEL {
2084 eprintln!("render_scene_two_chunk_x_grid_hash_is_stable: capture hash = 0x{hash:016x}");
2087 panic!("GOLDEN is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN above");
2088 }
2089 assert_eq!(
2090 hash, GOLDEN,
2091 "2-chunk render hash drifted: expected 0x{GOLDEN:016x}, got 0x{hash:016x}"
2092 );
2093 }
2094
2095 const SENTINEL: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2099
2100 #[test]
2119 fn approach_b_renders_two_chunk_x_stripe_via_chunk_grid() {
2120 const SENTINEL_B: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2121 const GOLDEN_B: u64 = 0x5ee1_e81c_66a8_d1f1;
2126
2127 let mut scene = Scene::new();
2128 let id = scene.add_grid(GridTransform::identity());
2129 let g = scene.grid_mut(id).unwrap();
2130 g.set_rect(
2133 IVec3::new(0, 0, 200),
2134 IVec3::new(127, 127, 205),
2135 Some(0x80_44_44_aa),
2136 );
2137 g.set_rect(
2140 IVec3::new(160, 50, 150),
2141 IVec3::new(170, 60, 165),
2142 Some(0x80_aa_55_22),
2143 );
2144 assert_eq!(g.chunk_count(), 2);
2145
2146 let backing = g.chunk_xyz_backing().expect("at least one chunk populated");
2148 assert_eq!(backing.chunks_x, 2);
2149 assert_eq!(backing.chunks_y, 1);
2150 assert_eq!(backing.origin_chunk_xy, [0, 0]);
2151 let cg = roxlap_core::ChunkGrid {
2152 chunks: &backing.chunks,
2153 origin_chunk_xy: backing.origin_chunk_xy,
2154 origin_chunk_z: backing.origin_chunk_z,
2155 chunks_x: backing.chunks_x,
2156 chunks_y: backing.chunks_y,
2157 chunks_z: backing.chunks_z,
2158 };
2159 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2160
2161 let camera = Camera {
2164 pos: [10.0, 64.0, 160.0],
2165 right: [0.0, 1.0, 0.0],
2166 down: [0.0, 0.0, 1.0],
2167 forward: [1.0, 0.0, 0.0],
2168 };
2169 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2170 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2171 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2172 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2173 drop(rasterizer);
2174 assert_eq!(outcome, OpticastOutcome::Rendered);
2175
2176 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2178 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2179 assert!(
2180 floor_count > 1000,
2181 "floor not visible — only {floor_count} floor pixels (single-chunk path?)"
2182 );
2183 assert!(
2184 box_count > 50,
2185 "box in chunk (1, 0) not visible — only {box_count} box pixels — cross-chunk DDA may have failed to fire"
2186 );
2187
2188 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2190 let hash = fnv1a64(&bytes);
2191 if GOLDEN_B == SENTINEL_B {
2192 eprintln!("approach_b_renders_two_chunk_x_stripe_via_chunk_grid: capture hash = 0x{hash:016x}");
2193 panic!(
2194 "GOLDEN_B is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN_B above"
2195 );
2196 }
2197 assert_eq!(
2198 hash, GOLDEN_B,
2199 "Approach B 2-chunk render hash drifted: expected 0x{GOLDEN_B:016x}, got 0x{hash:016x}"
2200 );
2201 }
2202
2203 #[test]
2216 fn approach_b_camera_in_chunk_1_0_renders_neighbour() {
2217 let mut scene = Scene::new();
2218 let id = scene.add_grid(GridTransform::identity());
2219 let g = scene.grid_mut(id).unwrap();
2220 g.set_rect(
2222 IVec3::new(128, 0, 200),
2223 IVec3::new(255, 127, 205),
2224 Some(0x80_44_44_aa),
2225 );
2226 g.set_rect(
2230 IVec3::new(20, 50, 150),
2231 IVec3::new(30, 60, 165),
2232 Some(0x80_aa_55_22),
2233 );
2234 assert_eq!(g.chunk_count(), 2);
2235
2236 let backing = g.chunk_xyz_backing().expect("populated");
2237 let cg = roxlap_core::ChunkGrid {
2238 chunks: &backing.chunks,
2239 origin_chunk_xy: backing.origin_chunk_xy,
2240 origin_chunk_z: backing.origin_chunk_z,
2241 chunks_x: backing.chunks_x,
2242 chunks_y: backing.chunks_y,
2243 chunks_z: backing.chunks_z,
2244 };
2245 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2246 let (aabb_min, aabb_max) = grid_view.aabb_xy();
2247 assert_eq!(aabb_min, [0, 0]);
2248 assert_eq!(aabb_max, [256, 128]);
2249
2250 let camera = Camera {
2254 pos: [200.0, 64.0, 160.0],
2255 right: [0.0, -1.0, 0.0],
2256 down: [0.0, 0.0, 1.0],
2257 forward: [-1.0, 0.0, 0.0],
2258 };
2259 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2260 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2261 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2262 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2263 drop(rasterizer);
2264 assert_eq!(outcome, OpticastOutcome::Rendered);
2265
2266 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2267 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2268 assert!(
2269 floor_count > 1000,
2270 "floor under camera in chunk (1, 0) not visible — only {floor_count} floor pixels — in_bounds_xy fix may not have taken effect"
2271 );
2272 assert!(
2273 box_count > 50,
2274 "box in chunk (0, 0) not visible — only {box_count} box pixels — westward cross-chunk DDA failed"
2275 );
2276 }
2277
2278 #[test]
2287 fn stacked_two_chunk_z_camera_in_chz1_sees_own_chunk_floor() {
2288 let mut scene = Scene::new();
2289 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2290 let g = scene.grid_mut(id).unwrap();
2291 g.ensure_chunk(IVec3::new(0, 0, 0));
2293 g.set_rect(
2295 IVec3::new(60, 60, 306),
2296 IVec3::new(72, 72, 310),
2297 Some(0x80_33_66_99),
2298 );
2299 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2300
2301 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2302 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2303 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2304 pool.set_treat_z_max_as_air(true);
2305 let camera = Camera {
2309 pos: [66.0, 66.0, 280.0],
2310 right: [1.0, 0.0, 0.0],
2311 down: [0.0, 1.0, 0.0],
2312 forward: [0.0, 0.0, 1.0],
2313 };
2314 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2315 let outcome = render_scene_composed(
2316 &mut fb,
2317 &mut zb,
2318 XRES as usize,
2319 XRES,
2320 YRES,
2321 &mut pool,
2322 &mut scene,
2323 &camera,
2324 &settings,
2325 sky_color,
2326 None,
2327 );
2328 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2329 let floor_count = fb.iter().filter(|&&p| p == 0x80_33_66_99).count();
2330 assert!(
2331 floor_count > 100,
2332 "camera at chz=1 with floor in same chunk should see it — got {floor_count} floor pixels"
2333 );
2334 }
2335
2336 #[test]
2345 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor() {
2346 let mut scene = Scene::new();
2347 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2348 let g = scene.grid_mut(id).unwrap();
2349 g.ensure_chunk(IVec3::new(0, 0, 0));
2352 g.set_rect(
2354 IVec3::new(60, 60, 306),
2355 IVec3::new(72, 72, 310),
2356 Some(0x80_77_aa_44),
2357 );
2358 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2359
2360 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2361 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2362 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2363 pool.set_treat_z_max_as_air(true);
2364 let camera = Camera {
2368 pos: [66.0, 66.0, 100.0],
2369 right: [1.0, 0.0, 0.0],
2370 down: [0.0, 1.0, 0.0],
2371 forward: [0.0, 0.0, 1.0],
2372 };
2373 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2374 let outcome = render_scene_composed(
2375 &mut fb,
2376 &mut zb,
2377 XRES as usize,
2378 XRES,
2379 YRES,
2380 &mut pool,
2381 &mut scene,
2382 &camera,
2383 &settings,
2384 sky_color,
2385 None,
2386 );
2387 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2388 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2389 assert!(
2390 floor_count > 50,
2391 "camera in chz=0 air-gap should see chz=1 floor via cross-chunk look-down — got {floor_count} floor pixels"
2392 );
2393 }
2394
2395 #[test]
2407 fn stacked_chz0_distant_mountain_visible_from_chz0_camera() {
2408 let mut scene = Scene::new();
2409 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2410 let g = scene.grid_mut(id).unwrap();
2411 g.set_rect(
2415 IVec3::new(100, 100, 100),
2416 IVec3::new(124, 124, 200),
2417 Some(0x80_aa_55_22), );
2419 g.set_rect(
2423 IVec3::new(0, 0, 336),
2424 IVec3::new(128, 128, 360),
2425 Some(0x80_22_88_44),
2426 );
2427 g.set_rect(IVec3::new(100, 100, 336), IVec3::new(124, 124, 360), None);
2428 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2431 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2432
2433 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2434 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2435 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2436 pool.set_treat_z_max_as_air(true);
2437 let (sy, cy) = (std::f64::consts::FRAC_PI_4).sin_cos();
2443 let (sp, cp) = 0.72_f64.sin_cos();
2444 let camera = Camera {
2445 pos: [40.0, 40.0, 60.0],
2446 right: [-sy, cy, 0.0],
2447 down: [-cy * sp, -sy * sp, cp],
2448 forward: [cy * cp, sy * cp, sp],
2449 };
2450 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2451 let outcome = render_scene_composed(
2452 &mut fb,
2453 &mut zb,
2454 XRES as usize,
2455 XRES,
2456 YRES,
2457 &mut pool,
2458 &mut scene,
2459 &camera,
2460 &settings,
2461 sky_color,
2462 None,
2463 );
2464 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2465 let mountain_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2466 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2467 eprintln!("chz0-distant-mountain: mountain_chz0={mountain_count} hill_chz1={hill_count}");
2468 assert!(
2471 hill_count > 50,
2472 "expected chz=1 hills via cross-chunk look-down — got {hill_count}"
2473 );
2474 assert!(
2477 mountain_count > 50,
2478 "expected chz=0 distant mountain visible — got {mountain_count} (S4B.6.l limitation)"
2479 );
2480 }
2481
2482 #[test]
2493 fn mid_render_handoff_reveals_chz1_hills_under_mountain_camera() {
2494 let mut scene = Scene::new();
2495 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2496 let g = scene.grid_mut(id).unwrap();
2497 g.set_rect(
2500 IVec3::new(60, 60, 150),
2501 IVec3::new(72, 72, 200),
2502 Some(0x80_88_44_22), );
2504 g.set_rect(
2507 IVec3::new(0, 0, 336),
2508 IVec3::new(128, 128, 360),
2509 Some(0x80_22_88_44), );
2511 g.set_rect(IVec3::new(60, 60, 336), IVec3::new(72, 72, 360), None);
2514 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2515 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2516
2517 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2518 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2519 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2520 pool.set_treat_z_max_as_air(true);
2521 let camera = Camera {
2525 pos: [66.0, 66.0, 100.0],
2526 right: [1.0, 0.0, 0.0],
2527 down: [0.0, 1.0, 0.0],
2528 forward: [0.0, 0.0, 1.0],
2529 };
2530 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2531 let outcome = render_scene_composed(
2532 &mut fb,
2533 &mut zb,
2534 XRES as usize,
2535 XRES,
2536 YRES,
2537 &mut pool,
2538 &mut scene,
2539 &camera,
2540 &settings,
2541 sky_color,
2542 None,
2543 );
2544 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2545 let mountain_count = fb.iter().filter(|&&p| p == 0x80_88_44_22).count();
2546 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2547 let mut hill_depths: Vec<f32> = fb
2555 .iter()
2556 .zip(zb.iter())
2557 .filter_map(|(&p, &d)| if p == 0x80_22_88_44 { Some(d) } else { None })
2558 .collect();
2559 hill_depths.sort_by(|a, b| a.partial_cmp(b).unwrap());
2560 let median_hill_depth = hill_depths[hill_depths.len() / 2];
2561 eprintln!(
2562 "mid-render handoff: mountain={mountain_count} hill={hill_count} median_hill_depth={median_hill_depth:.1}"
2563 );
2564 assert!(
2565 mountain_count > 50,
2566 "should see mountain peak via chz=0 — got {mountain_count} mountain pixels"
2567 );
2568 assert!(
2569 hill_count > 50,
2570 "should see chz=1 hills via mid-render handoff — got {hill_count} hill pixels"
2571 );
2572 assert!(
2573 (median_hill_depth - 236.0).abs() < 80.0,
2574 "hill median depth should be ≈236 (camera→z=336); got {median_hill_depth:.1} — state.z1 may be stale at the mountain peak's z"
2575 );
2576 }
2577
2578 #[test]
2587 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor_multi_mip() {
2588 let mut scene = Scene::new();
2589 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2590 let g = scene.grid_mut(id).unwrap();
2591 g.ensure_chunk(IVec3::new(0, 0, 0));
2592 g.set_rect(
2593 IVec3::new(60, 60, 306),
2594 IVec3::new(72, 72, 310),
2595 Some(0x80_77_aa_44),
2596 );
2597 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2598
2599 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2600 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2601 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2602 pool.set_treat_z_max_as_air(true);
2603 let camera = Camera {
2604 pos: [66.0, 66.0, 100.0],
2605 right: [1.0, 0.0, 0.0],
2606 down: [0.0, 1.0, 0.0],
2607 forward: [0.0, 0.0, 1.0],
2608 };
2609 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2610 settings.mip_levels = 2;
2611 settings.mip_scan_dist = 16;
2612 let outcome = render_scene_composed(
2613 &mut fb,
2614 &mut zb,
2615 XRES as usize,
2616 XRES,
2617 YRES,
2618 &mut pool,
2619 &mut scene,
2620 &camera,
2621 &settings,
2622 sky_color,
2623 None,
2624 );
2625 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2626 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2627 assert!(
2628 floor_count > 50,
2629 "multi-mip cross-chunk look-down should still see chz=1 floor — got {floor_count} floor pixels"
2630 );
2631 }
2632
2633 #[test]
2641 fn stacked_three_chunk_z_camera_in_chz2_sees_own_chunk_floor_multi_mip() {
2642 let mut scene = Scene::new();
2643 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2644 let g = scene.grid_mut(id).unwrap();
2645 g.ensure_chunk(IVec3::new(0, 0, 0));
2648 g.ensure_chunk(IVec3::new(0, 0, 1));
2649 g.set_rect(
2651 IVec3::new(60, 60, 562),
2652 IVec3::new(72, 72, 566),
2653 Some(0x80_aa_55_22),
2654 );
2655 assert!(g.chunk(IVec3::new(0, 0, 2)).is_some());
2656
2657 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2658 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2659 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2660 pool.set_treat_z_max_as_air(true);
2661 let camera = Camera {
2662 pos: [66.0, 66.0, 540.0],
2663 right: [1.0, 0.0, 0.0],
2664 down: [0.0, 1.0, 0.0],
2665 forward: [0.0, 0.0, 1.0],
2666 };
2667 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2669 settings.mip_levels = 2;
2670 settings.mip_scan_dist = 16;
2671 let outcome = render_scene_composed(
2672 &mut fb,
2673 &mut zb,
2674 XRES as usize,
2675 XRES,
2676 YRES,
2677 &mut pool,
2678 &mut scene,
2679 &camera,
2680 &settings,
2681 sky_color,
2682 None,
2683 );
2684 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2685 let floor_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2686 assert!(
2687 floor_count > 100,
2688 "camera at chz=2 with floor in same chunk should see it — got {floor_count} floor pixels"
2689 );
2690 }
2691
2692 #[derive(Debug)]
2700 struct FloorGenerator;
2701
2702 impl crate::ChunkGenerator for FloorGenerator {
2703 fn generate(&self, _chunk_idx: IVec3) -> roxlap_formats::vxl::Vxl {
2704 let mut tmp = crate::Grid::new(GridTransform::identity());
2708 tmp.ensure_chunk(IVec3::ZERO);
2709 let mut vxl = tmp.chunks.remove(&IVec3::ZERO).unwrap();
2710 #[allow(clippy::cast_possible_wrap)]
2711 roxlap_formats::edit::set_rect(
2712 &mut vxl,
2713 glam::IVec3::new(0, 0, 230).into(),
2714 glam::IVec3::new((CHUNK_SIZE_XY - 1) as i32, (CHUNK_SIZE_XY - 1) as i32, 239)
2715 .into(),
2716 Some(0x80_22_aa_22),
2717 );
2718 vxl
2719 }
2720 }
2721
2722 #[test]
2723 fn render_scene_composed_unpumped_streaming_grid_renders_all_sky() {
2724 use std::sync::Arc;
2730 let mut scene = Scene::new();
2731 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2732 let g = scene.grid_mut(id).unwrap();
2733 g.set_generator(Some(Arc::new(FloorGenerator)));
2734 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2735 assert!(g.chunks.is_empty(), "no pump yet → no chunks");
2736
2737 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2738 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2739 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2740 let camera = camera_at([64.0, -100.0, 200.0]);
2743 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2744 let _ = render_scene_composed(
2745 &mut fb,
2746 &mut zb,
2747 XRES as usize,
2748 XRES,
2749 YRES,
2750 &mut pool,
2751 &mut scene,
2752 &camera,
2753 &settings,
2754 sky_color,
2755 None,
2756 );
2757 assert!(
2759 fb.iter().all(|&p| p == sky_color),
2760 "unpumped streaming grid must render as all sky"
2761 );
2762 }
2763
2764 #[test]
2765 fn render_scene_composed_picks_up_streamed_chunks_after_sync_pump() {
2766 use std::sync::Arc;
2771 let mut scene = Scene::new();
2772 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2773 let g = scene.grid_mut(id).unwrap();
2774 g.set_generator(Some(Arc::new(FloorGenerator)));
2775 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2777
2778 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2780 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2781 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2782 let camera = camera_at([64.0, -100.0, 200.0]);
2783 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2784 let _ = render_scene_composed(
2785 &mut fb,
2786 &mut zb,
2787 XRES as usize,
2788 XRES,
2789 YRES,
2790 &mut pool,
2791 &mut scene,
2792 &camera,
2793 &settings,
2794 sky_color,
2795 None,
2796 );
2797 let pre_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2798 assert_eq!(pre_floor, 0, "pre-pump frame has no streamed chunks");
2799
2800 scene.pump_streaming_sync(DVec3::new(64.0, -100.0, 200.0));
2803 let g = scene.grid(id).unwrap();
2804 assert!(
2805 !g.chunks.is_empty(),
2806 "pump should have streamed at least one chunk"
2807 );
2808
2809 fb.iter_mut().for_each(|p| *p = sky_color);
2812 zb.iter_mut().for_each(|z| *z = f32::INFINITY);
2813 let outcome = render_scene_composed(
2814 &mut fb,
2815 &mut zb,
2816 XRES as usize,
2817 XRES,
2818 YRES,
2819 &mut pool,
2820 &mut scene,
2821 &camera,
2822 &settings,
2823 sky_color,
2824 None,
2825 );
2826 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2827 let post_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2828 assert!(
2829 post_floor > 100,
2830 "post-pump frame should show the streamed floor — got {post_floor} green pixels"
2831 );
2832 }
2833
2834 #[test]
2835 fn render_scene_composed_partial_streaming_renders_pending_chunks_as_air() {
2836 use std::sync::Arc;
2844 let mut scene = Scene::new();
2845 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2846 let g = scene.grid_mut(id).unwrap();
2847 g.set_generator(Some(Arc::new(FloorGenerator)));
2848 g.stream_radius = crate::StreamRadius::new(400.0, 800.0);
2851
2852 let installed = g.ensure_chunk_generated(IVec3::ZERO);
2855 assert!(installed, "manual install of one chunk");
2856 assert_eq!(g.chunks.len(), 1);
2857 assert!(g.chunk(IVec3::new(0, 1, 0)).is_none());
2859 assert!(g.chunk(IVec3::new(0, 2, 0)).is_none());
2860
2861 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2862 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2863 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2864 let camera = camera_at([64.0, 32.0, 200.0]);
2869 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2870 let _ = render_scene_composed(
2871 &mut fb,
2872 &mut zb,
2873 XRES as usize,
2874 XRES,
2875 YRES,
2876 &mut pool,
2877 &mut scene,
2878 &camera,
2879 &settings,
2880 sky_color,
2881 None,
2882 );
2883 let floor_pixels = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2884 assert!(
2889 floor_pixels > 0,
2890 "should see at least some floor from the loaded chunk"
2891 );
2892 scene.pump_streaming_sync(DVec3::new(64.0, 32.0, 200.0));
2895 assert!(scene.grid(id).unwrap().chunk_count() >= 2);
2896 fb.iter_mut().for_each(|p| *p = sky_color);
2897 zb.iter_mut().for_each(|z| *z = f32::INFINITY);
2898 let _ = render_scene_composed(
2899 &mut fb,
2900 &mut zb,
2901 XRES as usize,
2902 XRES,
2903 YRES,
2904 &mut pool,
2905 &mut scene,
2906 &camera,
2907 &settings,
2908 sky_color,
2909 None,
2910 );
2911 let floor_pixels_full = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2912 assert!(
2913 floor_pixels_full > floor_pixels,
2914 "fully-streamed scene should show more floor than partial: \
2915 partial={floor_pixels} full={floor_pixels_full}"
2916 );
2917 }
2918}