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::lod::Lod;
48use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
49
50const SKY_MASK_SENTINEL: u32 = 0x00_DE_AD_BE;
65
66fn world_camera_to_grid_local(camera: &Camera, transform: &GridTransform) -> Camera {
79 let inv = transform.rotation.inverse();
80 let world_offset = DVec3::from_array(camera.pos) - transform.origin;
81 let local_pos = inv * world_offset;
82 let local_right = inv * DVec3::from_array(camera.right);
83 let local_down = inv * DVec3::from_array(camera.down);
84 let local_forward = inv * DVec3::from_array(camera.forward);
85 Camera {
86 pos: local_pos.to_array(),
87 right: local_right.to_array(),
88 down: local_down.to_array(),
89 forward: local_forward.to_array(),
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum RenderOutcome {
96 Rendered {
98 grids_drawn: usize,
101 },
102 Empty,
106}
107
108#[allow(clippy::too_many_arguments)]
124pub fn render_scene(
125 fb: &mut [u32],
126 zb: &mut [f32],
127 pitch_pixels: usize,
128 width: u32,
129 height: u32,
130 pool: &mut ScratchPool,
131 scene: &mut Scene,
132 camera: &Camera,
133 settings: &OpticastSettings,
134 sky: Option<&Sky>,
135) -> RenderOutcome {
136 debug_assert_eq!(fb.len(), zb.len());
137 let pixel_count = (width as usize) * (height as usize);
138 debug_assert_eq!(fb.len(), pixel_count);
139
140 let mut grids_drawn = 0usize;
141 for (_id, grid) in scene.grids_mut() {
142 let Some(backing) = grid.chunk_xyz_backing() else {
153 continue;
155 };
156 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
157 let cg = roxlap_core::ChunkGrid {
158 chunks: &backing.chunks,
159 origin_chunk_xy: backing.origin_chunk_xy,
160 origin_chunk_z: backing.origin_chunk_z,
161 chunks_x: backing.chunks_x,
162 chunks_y: backing.chunks_y,
163 chunks_z: backing.chunks_z,
164 };
165 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
166 let outcome = {
167 let mut rasterizer = ScalarRasterizer::new(fb, zb, pitch_pixels, grid_view);
168 if let Some(sky_ref) = sky {
169 rasterizer = rasterizer.with_sky(sky_ref);
170 }
171 opticast(&mut rasterizer, pool, &local_cam, settings, grid_view)
172 };
173 if outcome == OpticastOutcome::Rendered {
174 grids_drawn += 1;
175 }
176 }
177 if grids_drawn == 0 {
178 RenderOutcome::Empty
179 } else {
180 RenderOutcome::Rendered { grids_drawn }
181 }
182}
183
184pub fn compose_into(
197 shared_fb: &mut [u32],
198 shared_zb: &mut [f32],
199 temp_fb: &[u32],
200 temp_zb: &[f32],
201) {
202 debug_assert_eq!(shared_fb.len(), shared_zb.len());
203 debug_assert_eq!(shared_fb.len(), temp_fb.len());
204 debug_assert_eq!(shared_fb.len(), temp_zb.len());
205 for i in 0..shared_fb.len() {
206 if temp_zb[i] < shared_zb[i] {
207 shared_fb[i] = temp_fb[i];
208 shared_zb[i] = temp_zb[i];
209 }
210 }
211}
212
213#[allow(clippy::too_many_arguments)]
244pub fn render_scene_composed(
245 fb: &mut [u32],
246 zb: &mut [f32],
247 pitch_pixels: usize,
248 width: u32,
249 height: u32,
250 pool: &mut ScratchPool,
251 scene: &mut Scene,
252 camera: &Camera,
253 settings: &OpticastSettings,
254 sky_color: u32,
255 sky: Option<&Sky>,
256) -> RenderOutcome {
257 debug_assert_eq!(fb.len(), zb.len());
258 let pixel_count = (width as usize) * (height as usize);
259 debug_assert_eq!(fb.len(), pixel_count);
260
261 let mut grids_drawn = 0usize;
262 let mut temp_fb = vec![sky_color; pixel_count];
263 let mut temp_zb = vec![f32::INFINITY; pixel_count];
264
265 for (_id, grid) in scene.grids_mut() {
266 let lod = grid.select_lod(DVec3::from_array(camera.pos));
286
287 if lod == Lod::Far {
288 if grid.chunks.is_empty() {
294 continue;
295 }
296 if grid.billboards.is_none() {
299 let cache = BillboardCache::build(grid, BILLBOARD_RESOLUTION);
300 grid.billboards = Some(cache);
301 }
302 let bounds = billboard::grid_bounds(grid);
305 let centre_world = grid.transform.origin + grid.transform.rotation * bounds.centre;
306 let cam_pos = DVec3::from_array(camera.pos);
310 let centre_to_cam_world = cam_pos - centre_world;
311 let ctc_len = centre_to_cam_world.length();
312 if !ctc_len.is_finite() || ctc_len < 1e-9 {
313 continue;
317 }
318 let query_dir_world = centre_to_cam_world / ctc_len;
319 let query_dir_local = grid.transform.rotation.inverse() * query_dir_world;
320 let cache = grid.billboards.as_ref().unwrap();
322 let snapshot = cache
325 .pick_nearest(query_dir_local)
326 .expect("billboard cache populated above");
327 billboard::billboard_blit_into(
328 fb,
329 zb,
330 pitch_pixels,
331 width,
332 height,
333 snapshot,
334 centre_world,
335 bounds.radius,
336 camera,
337 settings,
338 );
339 grids_drawn += 1;
340 continue;
341 }
342
343 let Some(backing) = grid.chunk_xyz_backing() else {
349 continue;
350 };
351 let owns_sky = grid.render_sky;
361 let local_sky_color = if owns_sky {
362 sky_color
363 } else {
364 SKY_MASK_SENTINEL
365 };
366 if !owns_sky {
367 pool.set_skycast(SKY_MASK_SENTINEL as i32, 0);
374 }
375
376 temp_fb.fill(local_sky_color);
380 temp_zb.fill(f32::INFINITY);
381
382 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
383 let cg = roxlap_core::ChunkGrid {
384 chunks: &backing.chunks,
385 origin_chunk_xy: backing.origin_chunk_xy,
386 origin_chunk_z: backing.origin_chunk_z,
387 chunks_x: backing.chunks_x,
388 chunks_y: backing.chunks_y,
389 chunks_z: backing.chunks_z,
390 };
391 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
392
393 let per_grid_settings;
413 let active_settings = {
414 let base_mip_levels = settings.mip_levels;
415 let base_mip_scan = settings.mip_scan_dist;
416 let lod_mip_levels = match lod {
417 Lod::Mid => grid.lod_thresholds.mid_mip_levels,
418 Lod::Near | Lod::Far => None,
419 };
420 let lod_mip_scan = match lod {
421 Lod::Mid => grid.lod_thresholds.mid_mip_scan_dist,
422 Lod::Near | Lod::Far => None,
423 };
424 let global_mip_cap = grid.mip_levels_override;
425 let needs_override =
426 lod_mip_levels.is_some() || lod_mip_scan.is_some() || global_mip_cap.is_some();
427 if needs_override {
428 let mut mip_levels =
431 lod_mip_levels.map_or(base_mip_levels, |n| n.clamp(1, base_mip_levels));
432 if let Some(cap) = global_mip_cap {
433 mip_levels = mip_levels.min(cap.clamp(1, base_mip_levels));
434 }
435 let mip_scan_dist = lod_mip_scan.map_or(base_mip_scan, |d| base_mip_scan.min(d));
441 per_grid_settings = OpticastSettings {
442 mip_levels,
443 mip_scan_dist,
444 ..*settings
445 };
446 &per_grid_settings
447 } else {
448 settings
449 }
450 };
451
452 let outcome = {
453 let mut rasterizer =
454 ScalarRasterizer::new(&mut temp_fb, &mut temp_zb, pitch_pixels, grid_view);
455 if owns_sky {
459 if let Some(sky_ref) = sky {
460 rasterizer = rasterizer.with_sky(sky_ref);
461 }
462 }
463 opticast(
464 &mut rasterizer,
465 pool,
466 &local_cam,
467 active_settings,
468 grid_view,
469 )
470 };
471
472 if !owns_sky {
473 for (px, z) in temp_fb.iter().zip(temp_zb.iter_mut()) {
477 if *px == SKY_MASK_SENTINEL {
478 *z = f32::INFINITY;
479 }
480 }
481 pool.set_skycast(sky_color as i32, 0);
484 }
485
486 if outcome == OpticastOutcome::Rendered {
487 compose_into(fb, zb, &temp_fb, &temp_zb);
488 grids_drawn += 1;
489 }
490 }
491
492 if grids_drawn == 0 {
493 RenderOutcome::Empty
494 } else {
495 RenderOutcome::Rendered { grids_drawn }
496 }
497}
498
499#[cfg(test)]
500#[allow(clippy::float_cmp)]
501mod tests {
502 use super::*;
503 use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
504 use glam::{DVec3, IVec3};
505 use roxlap_core::opticast::{opticast as core_opticast, OpticastSettings};
506 use roxlap_core::rasterizer::ScratchPool;
507 use roxlap_core::scalar_rasterizer::ScalarRasterizer;
508 use roxlap_core::{Camera, Engine};
509
510 const XRES: u32 = 320;
511 const YRES: u32 = 200;
512
513 fn build_one_grid_scene(world_origin: DVec3) -> (Scene, crate::GridId) {
517 let mut scene = Scene::new();
518 let id = scene.add_grid(GridTransform::at(world_origin));
519 let grid = scene.grid_mut(id).unwrap();
520 grid.set_rect(
522 IVec3::new(40, 40, 40),
523 IVec3::new(55, 55, 55),
524 Some(0x80_88_88_88),
525 );
526 grid.set_sphere(IVec3::new(80, 80, 80), 6, Some(0x80_22_aa_22));
528 (scene, id)
529 }
530
531 fn camera_at(pos: [f64; 3]) -> Camera {
532 Camera {
535 pos,
536 right: [-1.0, 0.0, 0.0],
537 down: [0.0, 0.0, 1.0],
538 forward: [0.0, 1.0, 0.0],
539 }
540 }
541
542 fn render_setup(pool_vsid: u32) -> (Engine, ScratchPool, Vec<u32>, Vec<f32>) {
546 let engine = Engine::new();
547 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
548 let sky = engine.sky_color();
549 let sky_col_i = i32::from_ne_bytes(sky.to_ne_bytes());
550 pool.set_skycast(sky_col_i, 0);
551 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
552 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
553 pool.set_treat_z_max_as_air(true);
554 let pixel_count = (XRES as usize) * (YRES as usize);
555 let framebuffer = vec![sky; pixel_count];
556 let zbuffer = vec![0.0f32; pixel_count];
557 (engine, pool, framebuffer, zbuffer)
558 }
559
560 fn render_via_scene(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
563 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
564 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
565 let outcome = render_scene(
566 &mut fb,
567 &mut zb,
568 XRES as usize,
569 XRES,
570 YRES,
571 &mut pool,
572 scene,
573 camera,
574 &settings,
575 None,
576 );
577 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
578 fb
579 }
580
581 fn render_via_direct_opticast(scene: &Scene, local_camera: &Camera) -> Vec<u32> {
585 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
586 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
587 let grid = scene.grids().next().unwrap().1;
588 let chunk = grid.chunk(IVec3::ZERO).unwrap();
589 let grid_view = roxlap_core::GridView::from_single_vxl(chunk);
590 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
591 let _ = core_opticast(
592 &mut rasterizer,
593 &mut pool,
594 local_camera,
595 &settings,
596 grid_view,
597 );
598 drop(rasterizer);
599 fb
600 }
601
602 #[test]
607 fn world_camera_to_grid_local_identity_rotation_translates_pos_only() {
608 let camera = Camera {
609 pos: [110.0, 220.0, 330.0],
610 right: [1.0, 0.0, 0.0],
611 down: [0.0, 0.0, 1.0],
612 forward: [0.0, 1.0, 0.0],
613 };
614 let transform = GridTransform::at(DVec3::new(100.0, 200.0, 300.0));
615 let local = super::world_camera_to_grid_local(&camera, &transform);
616 assert_eq!(local.right, camera.right);
618 assert_eq!(local.down, camera.down);
619 assert_eq!(local.forward, camera.forward);
620 for (got, want) in local.pos.iter().zip([10.0, 20.0, 30.0].iter()) {
622 assert!((got - want).abs() < 1e-12, "pos got={got} want={want}");
623 }
624 }
625
626 #[test]
630 fn world_camera_to_grid_local_90deg_z_rotates_basis_and_pos() {
631 use glam::DQuat;
632 let camera = Camera {
633 pos: [0.0, 10.0, 0.0],
634 right: [1.0, 0.0, 0.0],
635 down: [0.0, 0.0, 1.0],
636 forward: [0.0, 1.0, 0.0],
637 };
638 let transform = GridTransform {
639 origin: DVec3::ZERO,
640 rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
641 };
642 let local = super::world_camera_to_grid_local(&camera, &transform);
643 let approx_eq =
645 |a: [f64; 3], b: [f64; 3]| a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-9);
646 assert!(
647 approx_eq(local.pos, [10.0, 0.0, 0.0]),
648 "pos={:?} expected ~(10, 0, 0)",
649 local.pos
650 );
651 assert!(
653 approx_eq(local.right, [0.0, -1.0, 0.0]),
654 "right={:?} expected ~(0, -1, 0)",
655 local.right
656 );
657 assert!(
659 approx_eq(local.down, [0.0, 0.0, 1.0]),
660 "down={:?} expected ~(0, 0, 1)",
661 local.down
662 );
663 assert!(
665 approx_eq(local.forward, [1.0, 0.0, 0.0]),
666 "forward={:?} expected ~(1, 0, 0)",
667 local.forward
668 );
669 }
670
671 #[test]
676 fn world_camera_to_grid_local_preserves_basis_orthonormality() {
677 use glam::DQuat;
678 let camera = Camera {
681 pos: [3.0, -5.0, 7.0],
682 right: [-1.0, 0.0, 0.0],
683 down: [0.0, 0.0, 1.0],
684 forward: [0.0, 1.0, 0.0],
685 };
686 let transform = GridTransform {
687 origin: DVec3::new(1.0, 2.0, 3.0),
688 rotation: DQuat::from_axis_angle(glam::DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
689 };
690 let local = super::world_camera_to_grid_local(&camera, &transform);
691 let r = DVec3::from_array(local.right);
692 let d = DVec3::from_array(local.down);
693 let f = DVec3::from_array(local.forward);
694 for v in [r, d, f] {
696 assert!(
697 (v.length_squared() - 1.0).abs() < 1e-12,
698 "basis vec {v:?} not unit length"
699 );
700 }
701 assert!(r.dot(d).abs() < 1e-12, "right·down = {}", r.dot(d));
703 assert!(r.dot(f).abs() < 1e-12, "right·forward = {}", r.dot(f));
704 assert!(d.dot(f).abs() < 1e-12, "down·forward = {}", d.dot(f));
705 let cross = r.cross(d);
707 assert!(
708 (cross - f).length() < 1e-12,
709 "right×down={cross:?} forward={f:?}"
710 );
711 }
712
713 fn build_one_grid_marker_scene(transform: GridTransform) -> (Scene, crate::GridId, u32) {
721 let mut scene = Scene::new();
722 let id = scene.add_grid(transform);
723 let grid = scene.grid_mut(id).unwrap();
724 grid.set_rect(
726 IVec3::new(40, 40, 40),
727 IVec3::new(55, 55, 55),
728 Some(0x80_55_aa_22), );
730 (scene, id, 0x80_55_aa_22)
731 }
732
733 #[test]
746 fn s5_1_180deg_z_rotated_grid_byte_identical_to_axis_aligned() {
747 use glam::DQuat;
748 let axis_aligned_camera = Camera {
750 pos: [40.0, -20.0, 50.0],
751 right: [-1.0, 0.0, 0.0],
752 down: [0.0, 0.0, 1.0],
753 forward: [0.0, 1.0, 0.0],
754 };
755 let rotated_camera = Camera {
757 pos: [-40.0, 20.0, 50.0],
758 right: [1.0, 0.0, 0.0],
759 down: [0.0, 0.0, 1.0],
760 forward: [0.0, -1.0, 0.0],
761 };
762 let q = DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0);
767 let rot_pos = q * DVec3::from_array(axis_aligned_camera.pos);
768 let rot_fwd = q * DVec3::from_array(axis_aligned_camera.forward);
769 assert_eq!(rot_pos.to_array(), rotated_camera.pos);
770 assert_eq!(rot_fwd.to_array(), rotated_camera.forward);
771
772 let (mut scene_a, _, _) = build_one_grid_marker_scene(GridTransform::identity());
773 let fb_a = render_via_scene(&mut scene_a, &axis_aligned_camera);
774
775 let (mut scene_b, _, _) = build_one_grid_marker_scene(GridTransform {
776 origin: DVec3::ZERO,
777 rotation: q,
778 });
779 let fb_b = render_via_scene(&mut scene_b, &rotated_camera);
780
781 assert_eq!(
782 fb_a, fb_b,
783 "rotating both grid and camera by R about the grid origin must leave the framebuffer unchanged"
784 );
785 }
786
787 #[test]
794 fn s5_1_45deg_z_rotated_grid_renders_marker() {
795 use glam::DQuat;
796 let rotation = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4);
797 let (mut scene, _, marker) = build_one_grid_marker_scene(GridTransform {
798 origin: DVec3::ZERO,
799 rotation,
800 });
801
802 let marker_world = rotation * DVec3::new(47.5, 47.5, 47.5);
807 let camera = Camera {
810 pos: [marker_world.x, marker_world.y - 80.0, marker_world.z],
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
816 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
817 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
818 let outcome = render_scene(
819 &mut fb,
820 &mut zb,
821 XRES as usize,
822 XRES,
823 YRES,
824 &mut pool,
825 &mut scene,
826 &camera,
827 &settings,
828 None,
829 );
830 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
831 let marker_count = fb.iter().filter(|&&p| p == marker).count();
832 assert!(
833 marker_count > 50,
834 "45°-rotated marker box should be visible — got {marker_count} marker pixels"
835 );
836 }
837
838 #[test]
850 fn render_sky_false_drops_grid_sky_pixels() {
851 use crate::{GridId, GridTransform};
852
853 let mut scene = Scene::new();
856 let _b_id: GridId = scene.add_grid(GridTransform::at(DVec3::new(0.0, 600.0, 0.0)));
857 let b_id = scene.grids().next().unwrap().0;
860 scene.grid_mut(b_id).unwrap().set_rect(
861 IVec3::new(0, 0, 100),
862 IVec3::new(127, 127, 110),
863 Some(0x80_22_88_22), );
865
866 let a_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
870 scene.grid_mut(a_id).unwrap().set_rect(
871 IVec3::new(60, 60, 60),
872 IVec3::new(67, 67, 67),
873 Some(0x80_aa_22_22), );
875 scene.grid_mut(a_id).unwrap().render_sky = false;
876
877 let unique_sky: u32 = 0xFF_AB_CD_EF;
878 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
879 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
880 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
881 let camera = camera_at([64.0, 0.0, 100.0]);
882 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
883 let outcome = render_scene_composed(
884 &mut fb,
885 &mut zb,
886 XRES as usize,
887 XRES,
888 YRES,
889 &mut pool,
890 &mut scene,
891 &camera,
892 &settings,
893 unique_sky,
894 None,
895 );
896 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
897
898 let leaked = fb
902 .iter()
903 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
904 .count();
905 assert_eq!(
906 leaked, 0,
907 "SKY_MASK_SENTINEL leaked into composed framebuffer ({leaked} pixels)"
908 );
909 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_22_22).count();
912 assert!(
913 red_count > 0,
914 "red cube from sky-disabled grid A is missing — render_sky=false should only mask sky"
915 );
916 let green_count = fb.iter().filter(|&&p| p == 0x80_22_88_22).count();
919 assert!(
920 green_count > 0,
921 "grid B's floor invisible — grid A's masked sky may have overwritten it"
922 );
923 }
924
925 #[test]
929 fn render_sky_false_single_grid_no_sentinel_leak() {
930 let (mut scene, id, _) = build_one_grid_marker_scene(GridTransform::identity());
931 scene.grid_mut(id).unwrap().render_sky = false;
932 let unique_sky: u32 = 0xFF_12_34_56;
933 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
934 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
935 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
936 let camera = camera_at([64.0, 0.0, 64.0]);
937 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
938 let outcome = render_scene_composed(
939 &mut fb,
940 &mut zb,
941 XRES as usize,
942 XRES,
943 YRES,
944 &mut pool,
945 &mut scene,
946 &camera,
947 &settings,
948 unique_sky,
949 None,
950 );
951 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
952 let leaked = fb
953 .iter()
954 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
955 .count();
956 assert_eq!(leaked, 0, "SKY_MASK_SENTINEL leaked ({leaked} pixels)");
957 let prefill_count = fb.iter().filter(|&&p| p == unique_sky).count();
960 assert!(
961 prefill_count > 0,
962 "no pre-fill pixels survived — render_sky=false should leave non-hit pixels untouched"
963 );
964 }
965
966 #[test]
967 fn render_scene_at_origin_matches_direct_opticast() {
968 let (mut scene, _) = build_one_grid_scene(DVec3::ZERO);
974 let cam = camera_at([64.0, 0.0, 64.0]);
975 let via_scene = render_via_scene(&mut scene, &cam);
976 let via_direct = render_via_direct_opticast(&scene, &cam);
977 assert_eq!(
978 via_scene, via_direct,
979 "render_scene with single 1-chunk grid at origin should match direct opticast"
980 );
981 }
982
983 #[test]
984 fn render_scene_translated_grid_matches_grid_local_opticast() {
985 let world_origin = DVec3::new(1000.0, 2000.0, 3000.0);
990 let (mut scene, _) = build_one_grid_scene(world_origin);
991 let world_cam = camera_at([1064.0, 2000.0, 3064.0]);
992 let local_cam = camera_at([64.0, 0.0, 64.0]);
993 let via_scene = render_via_scene(&mut scene, &world_cam);
994 let via_direct = render_via_direct_opticast(&scene, &local_cam);
995 assert_eq!(
996 via_scene, via_direct,
997 "render_scene of translated grid should match opticast with grid-local camera"
998 );
999 }
1000
1001 #[test]
1002 fn empty_scene_returns_empty_outcome() {
1003 let mut scene = Scene::new();
1004 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
1005 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1006 let outcome = render_scene(
1007 &mut fb,
1008 &mut zb,
1009 XRES as usize,
1010 XRES,
1011 YRES,
1012 &mut pool,
1013 &mut scene,
1014 &camera_at([0.0, 0.0, 0.0]),
1015 &settings,
1016 None,
1017 );
1018 assert_eq!(outcome, RenderOutcome::Empty);
1019 }
1020
1021 fn build_two_grid_side_by_side() -> (Scene, u32, u32) {
1029 let mut scene = Scene::new();
1030 let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1032 scene.grid_mut(g0).unwrap().set_rect(
1033 IVec3::new(56, 56, 92),
1034 IVec3::new(71, 71, 107),
1035 Some(0x80_88_22_22), );
1037 let _g1 = scene.add_grid(GridTransform::at(DVec3::new(200.0, 200.0, 0.0)));
1039 let g1_id = scene
1041 .grids()
1042 .filter(|(id, _)| *id != g0)
1043 .map(|(id, _)| id)
1044 .next()
1045 .unwrap();
1046 scene.grid_mut(g1_id).unwrap().set_rect(
1047 IVec3::new(56, 56, 92),
1048 IVec3::new(71, 71, 107),
1049 Some(0x80_22_22_88), );
1051 (scene, 0x80_88_22_22, 0x80_22_22_88)
1052 }
1053
1054 fn make_composed_pool(pool_vsid: u32) -> (Engine, ScratchPool, u32) {
1055 let engine = Engine::new();
1056 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
1057 let sky_color = engine.sky_color();
1058 let sky_col_i = i32::from_ne_bytes(sky_color.to_ne_bytes());
1059 pool.set_skycast(sky_col_i, 0);
1060 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
1061 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
1062 pool.set_treat_z_max_as_air(true);
1063 (engine, pool, sky_color)
1064 }
1065
1066 fn pixel_count(width: u32, height: u32) -> usize {
1067 (width as usize) * (height as usize)
1068 }
1069
1070 #[test]
1071 fn compose_into_takes_smaller_z() {
1072 let mut shared_fb = vec![0xff_ff_ff_ff_u32; 4];
1073 let mut shared_zb = vec![10.0f32; 4];
1074 let temp_fb = [0xaa_aa_aa_aa, 0x11_22_33_44, 0x55_66_77_88, 0xde_ad_be_ef];
1075 let temp_zb = [5.0f32, 20.0, 10.0, f32::INFINITY];
1076 compose_into(&mut shared_fb, &mut shared_zb, &temp_fb, &temp_zb);
1077 assert_eq!(shared_fb[0], 0xaa_aa_aa_aa);
1079 assert_eq!(shared_zb[0], 5.0);
1080 assert_eq!(shared_fb[1], 0xff_ff_ff_ff);
1082 assert_eq!(shared_zb[1], 10.0);
1083 assert_eq!(shared_fb[2], 0xff_ff_ff_ff);
1085 assert_eq!(shared_fb[3], 0xff_ff_ff_ff);
1087 }
1088
1089 #[test]
1090 fn render_scene_composed_two_grids_both_visible() {
1091 let (mut scene, red, blue) = build_two_grid_side_by_side();
1096 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1097 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1098 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1099
1100 let camera = camera_at([160.0, 100.0, 100.0]);
1101 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1102 let outcome = render_scene_composed(
1103 &mut fb,
1104 &mut zb,
1105 XRES as usize,
1106 XRES,
1107 YRES,
1108 &mut pool,
1109 &mut scene,
1110 &camera,
1111 &settings,
1112 sky_color,
1113 None,
1114 );
1115 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1116
1117 let red_count = fb.iter().filter(|&&p| p == red).count();
1119 let blue_count = fb.iter().filter(|&&p| p == blue).count();
1120 assert!(
1121 red_count > 0,
1122 "no red pixels: grid 0 (red box) not visible after compose"
1123 );
1124 assert!(
1125 blue_count > 0,
1126 "no blue pixels: grid 1 (blue box) not visible after compose"
1127 );
1128 }
1129
1130 #[test]
1131 fn render_scene_composed_grid_a_in_front_of_grid_b() {
1132 let mut scene = Scene::new();
1136 let g_a = scene.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1137 scene.grid_mut(g_a).unwrap().set_rect(
1138 IVec3::new(56, 56, 92),
1139 IVec3::new(71, 71, 107),
1140 Some(0x80_aa_00_00), );
1142 let _g_b = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1143 let g_b_id = scene
1144 .grids()
1145 .filter(|(id, _)| *id != g_a)
1146 .map(|(id, _)| id)
1147 .next()
1148 .unwrap();
1149 scene.grid_mut(g_b_id).unwrap().set_rect(
1150 IVec3::new(56, 56, 92),
1151 IVec3::new(71, 71, 107),
1152 Some(0x80_00_00_aa), );
1154
1155 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1156 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1157 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1158
1159 let camera = camera_at([64.0, -10.0, 100.0]);
1162 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1163 let outcome = render_scene_composed(
1164 &mut fb,
1165 &mut zb,
1166 XRES as usize,
1167 XRES,
1168 YRES,
1169 &mut pool,
1170 &mut scene,
1171 &camera,
1172 &settings,
1173 sky_color,
1174 None,
1175 );
1176 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1177
1178 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_00_00).count();
1182 assert!(
1183 red_count > 0,
1184 "expected red pixels (closer box should win z-test)"
1185 );
1186
1187 let mut scene2 = Scene::new();
1190 let g_b2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1191 scene2.grid_mut(g_b2).unwrap().set_rect(
1192 IVec3::new(56, 56, 92),
1193 IVec3::new(71, 71, 107),
1194 Some(0x80_00_00_aa),
1195 );
1196 let g_a2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1197 scene2.grid_mut(g_a2).unwrap().set_rect(
1198 IVec3::new(56, 56, 92),
1199 IVec3::new(71, 71, 107),
1200 Some(0x80_aa_00_00),
1201 );
1202
1203 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1204 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1205 let outcome2 = render_scene_composed(
1206 &mut fb2,
1207 &mut zb2,
1208 XRES as usize,
1209 XRES,
1210 YRES,
1211 &mut pool,
1212 &mut scene2,
1213 &camera,
1214 &settings,
1215 sky_color,
1216 None,
1217 );
1218 assert_eq!(outcome2, RenderOutcome::Rendered { grids_drawn: 2 });
1219 assert_eq!(
1220 fb, fb2,
1221 "composition should be order-independent — same scene in different add order should produce identical output"
1222 );
1223 }
1224
1225 fn build_mip_visible_grid(world_origin: DVec3) -> (Scene, crate::GridId) {
1240 let mut scene = Scene::new();
1241 let id = scene.add_grid(GridTransform::at(world_origin));
1242 let grid = scene.grid_mut(id).unwrap();
1243 grid.set_rect(
1245 IVec3::new(0, 0, 100),
1246 IVec3::new(127, 127, 254),
1247 Some(0x80_88_88_88),
1248 );
1249 grid.chunk_mut(IVec3::ZERO).unwrap().generate_mips(3);
1251 (scene, id)
1252 }
1253
1254 fn fb_hash(fb: &[u32]) -> u64 {
1256 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1257 for px in fb {
1258 for b in px.to_le_bytes() {
1259 h ^= u64::from(b);
1260 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1261 }
1262 }
1263 h
1264 }
1265
1266 fn render_with_multi_mip(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
1271 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1272 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1273 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1274 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1275 settings.mip_levels = 3;
1276 settings.mip_scan_dist = 32;
1277 let outcome = render_scene_composed(
1278 &mut fb,
1279 &mut zb,
1280 XRES as usize,
1281 XRES,
1282 YRES,
1283 &mut pool,
1284 scene,
1285 camera,
1286 &settings,
1287 sky_color,
1288 None,
1289 );
1290 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1291 fb
1292 }
1293
1294 #[test]
1299 fn s6_1_mid_overrides_produce_different_framebuffer_than_near() {
1300 let camera = camera_at([64.0, 0.0, 64.0]);
1302
1303 let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1306 let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1307
1308 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1316 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1317 r_near: 0.0,
1319 r_mid: f64::INFINITY,
1320 mid_mip_levels: Some(1),
1321 mid_mip_scan_dist: None,
1322 };
1323 let lod = scene_b
1325 .grid(b_id)
1326 .unwrap()
1327 .select_lod(DVec3::from_array(camera.pos));
1328 assert_eq!(lod, Lod::Mid, "expected Mid tier for forced thresholds");
1329 let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1330
1331 let (_engine, _, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1334 let non_sky_near = fb_near.iter().filter(|&&p| p != sky_color).count();
1335 let non_sky_mid = fb_mid.iter().filter(|&&p| p != sky_color).count();
1336 assert!(
1337 non_sky_near > 100,
1338 "Near render too sparse ({non_sky_near})"
1339 );
1340 assert!(non_sky_mid > 100, "Mid render too sparse ({non_sky_mid})");
1341
1342 let h_near = fb_hash(&fb_near);
1346 let h_mid = fb_hash(&fb_mid);
1347 assert_ne!(
1348 h_near, h_mid,
1349 "Mid tier with mid_mip_levels=Some(1) must differ from Near (h_near={h_near:016x})"
1350 );
1351 }
1352
1353 #[test]
1359 fn s6_1_mid_without_overrides_byte_identical_to_near() {
1360 let camera = camera_at([64.0, 0.0, 64.0]);
1361
1362 let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1364 let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1365
1366 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1368 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1369 r_near: 0.0,
1370 r_mid: f64::INFINITY,
1371 mid_mip_levels: None,
1372 mid_mip_scan_dist: None,
1373 };
1374 let lod = scene_b
1375 .grid(b_id)
1376 .unwrap()
1377 .select_lod(DVec3::from_array(camera.pos));
1378 assert_eq!(lod, Lod::Mid);
1379 let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1380
1381 assert_eq!(
1383 fb_near, fb_mid,
1384 "Mid with both overrides=None must byte-match Near"
1385 );
1386 }
1387
1388 #[test]
1400 fn s6_1_global_mip_cap_survives_mid_tier() {
1401 let camera = camera_at([64.0, 0.0, 64.0]);
1402
1403 let (mut scene_a, a_id) = build_mip_visible_grid(DVec3::ZERO);
1405 scene_a.grid_mut(a_id).unwrap().mip_levels_override = Some(1);
1406 let fb_a = render_with_multi_mip(&mut scene_a, &camera);
1407
1408 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1412 scene_b.grid_mut(b_id).unwrap().mip_levels_override = Some(1);
1413 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1414 r_near: 0.0,
1415 r_mid: f64::INFINITY,
1416 mid_mip_levels: Some(4),
1417 mid_mip_scan_dist: None,
1420 };
1421 let fb_b = render_with_multi_mip(&mut scene_b, &camera);
1422
1423 assert_eq!(
1424 fb_a, fb_b,
1425 "global mip_levels_override should clamp Mid override (ship workaround survives Mid tier)"
1426 );
1427 }
1428
1429 #[test]
1437 fn s6_3_far_tier_blits_non_sky_pixels() {
1438 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1439 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1440 r_near: 0.0,
1441 r_mid: 0.0,
1442 mid_mip_levels: None,
1443 mid_mip_scan_dist: None,
1444 };
1445
1446 let camera = camera_at([64.0, 0.0, 100.0]);
1447 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1448 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1449 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1450 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1451 let outcome = render_scene_composed(
1452 &mut fb,
1453 &mut zb,
1454 XRES as usize,
1455 XRES,
1456 YRES,
1457 &mut pool,
1458 &mut scene,
1459 &camera,
1460 &settings,
1461 sky_color,
1462 None,
1463 );
1464 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1465
1466 let lod = scene
1468 .grid(id)
1469 .unwrap()
1470 .select_lod(DVec3::from_array(camera.pos));
1471 assert_eq!(lod, Lod::Far);
1472
1473 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1475 assert!(
1476 non_sky > 0,
1477 "Far-tier render produced no non-sky pixels — billboard blit not firing"
1478 );
1479 }
1480
1481 #[test]
1484 fn s6_3_far_render_lazily_populates_cache() {
1485 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1486 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1487 r_near: 0.0,
1488 r_mid: 0.0,
1489 mid_mip_levels: None,
1490 mid_mip_scan_dist: None,
1491 };
1492 assert!(scene.grid(id).unwrap().billboards.is_none());
1493
1494 let camera = camera_at([64.0, 0.0, 100.0]);
1495 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1496 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1497 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1498 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1499 let _ = render_scene_composed(
1500 &mut fb,
1501 &mut zb,
1502 XRES as usize,
1503 XRES,
1504 YRES,
1505 &mut pool,
1506 &mut scene,
1507 &camera,
1508 &settings,
1509 sky_color,
1510 None,
1511 );
1512 let cache = scene
1513 .grid(id)
1514 .unwrap()
1515 .billboards
1516 .as_ref()
1517 .expect("Far render should have populated billboards");
1518 assert_eq!(cache.len(), 26);
1519 }
1520
1521 #[test]
1523 fn s6_3_edit_invalidates_then_far_render_rebuilds() {
1524 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1525 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1526 r_near: 0.0,
1527 r_mid: 0.0,
1528 mid_mip_levels: None,
1529 mid_mip_scan_dist: None,
1530 };
1531 let camera = camera_at([64.0, 0.0, 100.0]);
1532 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1533 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1534
1535 let mut fb1 = vec![sky_color; pixel_count(XRES, YRES)];
1537 let mut zb1 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1538 let _ = render_scene_composed(
1539 &mut fb1,
1540 &mut zb1,
1541 XRES as usize,
1542 XRES,
1543 YRES,
1544 &mut pool,
1545 &mut scene,
1546 &camera,
1547 &settings,
1548 sky_color,
1549 None,
1550 );
1551 assert!(scene.grid(id).unwrap().billboards.is_some());
1552
1553 scene
1555 .grid_mut(id)
1556 .unwrap()
1557 .set_voxel(IVec3::new(70, 70, 70), Some(0x80_aa_aa_22));
1558 assert!(scene.grid(id).unwrap().billboards.is_none());
1559
1560 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1562 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1563 let _ = render_scene_composed(
1564 &mut fb2,
1565 &mut zb2,
1566 XRES as usize,
1567 XRES,
1568 YRES,
1569 &mut pool,
1570 &mut scene,
1571 &camera,
1572 &settings,
1573 sky_color,
1574 None,
1575 );
1576 assert!(scene.grid(id).unwrap().billboards.is_some());
1577 }
1578
1579 #[test]
1584 fn s6_3_near_and_far_grids_in_same_scene() {
1585 let mut scene = Scene::new();
1586 let a_id = scene.add_grid(GridTransform::at(DVec3::new(-100.0, 200.0, 0.0)));
1589 scene.grid_mut(a_id).unwrap().set_rect(
1590 IVec3::new(70, 0, 50),
1591 IVec3::new(85, 15, 70),
1592 Some(0x80_22_88_22), );
1594 let b_id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1596 scene.grid_mut(b_id).unwrap().set_rect(
1597 IVec3::new(0, 0, 80),
1598 IVec3::new(20, 20, 110),
1599 Some(0x80_aa_22_22), );
1601 scene.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1602 r_near: 0.0,
1603 r_mid: 0.0,
1604 mid_mip_levels: None,
1605 mid_mip_scan_dist: None,
1606 };
1607
1608 let camera = camera_at([0.0, 0.0, 80.0]);
1609 assert_eq!(
1611 scene
1612 .grid(a_id)
1613 .unwrap()
1614 .select_lod(DVec3::from_array(camera.pos)),
1615 Lod::Near
1616 );
1617 assert_eq!(
1618 scene
1619 .grid(b_id)
1620 .unwrap()
1621 .select_lod(DVec3::from_array(camera.pos)),
1622 Lod::Far
1623 );
1624
1625 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1626 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1627 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1628 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1629 let outcome = render_scene_composed(
1630 &mut fb,
1631 &mut zb,
1632 XRES as usize,
1633 XRES,
1634 YRES,
1635 &mut pool,
1636 &mut scene,
1637 &camera,
1638 &settings,
1639 sky_color,
1640 None,
1641 );
1642 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1643
1644 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1646 assert!(
1647 non_sky > 20,
1648 "hybrid scene produced too few non-sky pixels ({non_sky}); one tier may have failed"
1649 );
1650 }
1651
1652 #[test]
1655 fn s6_3_empty_grid_at_far_is_skipped() {
1656 let mut scene = Scene::new();
1657 let id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1658 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1659 r_near: 0.0,
1660 r_mid: 0.0,
1661 mid_mip_levels: None,
1662 mid_mip_scan_dist: None,
1663 };
1664
1665 let camera = camera_at([0.0, 0.0, 100.0]);
1666 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1667 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1668 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1669 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1670 let outcome = render_scene_composed(
1671 &mut fb,
1672 &mut zb,
1673 XRES as usize,
1674 XRES,
1675 YRES,
1676 &mut pool,
1677 &mut scene,
1678 &camera,
1679 &settings,
1680 sky_color,
1681 None,
1682 );
1683 assert_eq!(outcome, RenderOutcome::Empty);
1685 assert!(scene.grid(id).unwrap().billboards.is_none());
1687 assert!(fb.iter().all(|&p| p == sky_color));
1689 }
1690
1691 #[test]
1700 fn render_scene_composed_lod_threshold_invariance() {
1701 let (mut scene_a, _a_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1703 let cam = camera_at([64.0, 0.0, 100.0]);
1704 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1705 let mut fb_a = vec![sky_color; pixel_count(XRES, YRES)];
1706 let mut zb_a = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1707 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1708 let outcome_a = render_scene_composed(
1709 &mut fb_a,
1710 &mut zb_a,
1711 XRES as usize,
1712 XRES,
1713 YRES,
1714 &mut pool,
1715 &mut scene_a,
1716 &cam,
1717 &settings,
1718 sky_color,
1719 None,
1720 );
1721 assert_eq!(outcome_a, RenderOutcome::Rendered { grids_drawn: 1 });
1722
1723 let (mut scene_b, b_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1728 let radius = scene_b.grid(b_id).unwrap().bounding_radius();
1729 assert!(
1730 radius > 0.0,
1731 "bounding_radius should be > 0 for a populated grid"
1732 );
1733 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds::from_radius(radius);
1734 let lod = scene_b
1737 .grid(b_id)
1738 .unwrap()
1739 .select_lod(DVec3::from_array(cam.pos));
1740 assert_ne!(
1741 lod,
1742 Lod::Near,
1743 "camera should land in Mid or Far for derived thresholds — got {lod:?}",
1744 );
1745
1746 let mut fb_b = vec![sky_color; pixel_count(XRES, YRES)];
1747 let mut zb_b = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1748 let outcome_b = render_scene_composed(
1749 &mut fb_b,
1750 &mut zb_b,
1751 XRES as usize,
1752 XRES,
1753 YRES,
1754 &mut pool,
1755 &mut scene_b,
1756 &cam,
1757 &settings,
1758 sky_color,
1759 None,
1760 );
1761 assert_eq!(outcome_b, RenderOutcome::Rendered { grids_drawn: 1 });
1762
1763 assert_eq!(
1766 fb_a, fb_b,
1767 "S6.0 framebuffer must be byte-identical regardless of LOD thresholds"
1768 );
1769 }
1770
1771 #[test]
1772 fn render_scene_composed_empty_scene_returns_empty() {
1773 let mut scene = Scene::new();
1774 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1775 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1776 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1777 let camera = camera_at([0.0, 0.0, 0.0]);
1778 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1779 let outcome = render_scene_composed(
1780 &mut fb,
1781 &mut zb,
1782 XRES as usize,
1783 XRES,
1784 YRES,
1785 &mut pool,
1786 &mut scene,
1787 &camera,
1788 &settings,
1789 sky_color,
1790 None,
1791 );
1792 assert_eq!(outcome, RenderOutcome::Empty);
1793 assert!(fb.iter().all(|&p| p == sky_color));
1795 }
1796
1797 fn fnv1a64(data: &[u8]) -> u64 {
1802 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1803 for &b in data {
1804 h ^= u64::from(b);
1805 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1806 }
1807 h
1808 }
1809
1810 #[test]
1816 fn render_scene_two_chunk_x_grid_no_seam() {
1817 let mut scene = Scene::new();
1818 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1819 let g = scene.grid_mut(id).unwrap();
1820 g.set_rect(
1826 IVec3::new(120, 60, 200),
1827 IVec3::new(136, 67, 215),
1828 Some(0x80_aa_55_22),
1829 );
1830 assert_eq!(g.chunk_count(), 2);
1832
1833 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1837 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1838 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1839 let camera = camera_at([128.0, 100.0, 207.0]);
1840 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1841 let outcome = render_scene_composed(
1842 &mut fb,
1843 &mut zb,
1844 XRES as usize,
1845 XRES,
1846 YRES,
1847 &mut pool,
1848 &mut scene,
1849 &camera,
1850 &settings,
1851 sky_color,
1852 None,
1853 );
1854 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1855
1856 let stripe = 0x80_aa_55_22;
1860 let stripe_count = fb.iter().filter(|&&p| p == stripe).count();
1861 assert!(
1862 stripe_count > 200,
1863 "stripe rendered too few pixels ({stripe_count}) — chunks may not be stitching"
1864 );
1865
1866 let centre_y = (YRES / 2) as usize;
1870 let row_start = centre_y * (XRES as usize);
1871 let row = &fb[row_start..row_start + (XRES as usize)];
1872 let mut in_stripe = false;
1873 let mut seam_gaps = 0usize;
1874 for &px in row {
1875 if px == stripe {
1876 in_stripe = true;
1877 } else if in_stripe && px == sky_color {
1878 if row.iter().skip_while(|&&p| p != px).any(|&p| p == stripe) {
1881 seam_gaps += 1;
1883 }
1884 in_stripe = false;
1885 }
1886 }
1887 assert!(
1891 seam_gaps <= 1,
1892 "centre row has {seam_gaps} disjoint stripe runs — expected 1 (chunk-edge seam suspected)"
1893 );
1894 }
1895
1896 #[test]
1912 fn vxl_generate_mips_on_set_voxel_chunk_renders() {
1913 let mut grid = crate::Grid::new(GridTransform::identity());
1914 grid.set_rect(
1917 IVec3::new(0, 0, 100),
1918 IVec3::new(127, 127, 254),
1919 Some(0x80_88_88_88),
1920 );
1921 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1922 chunk.generate_mips(3);
1923 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1924 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1925 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1926 let camera = camera_at([64.0, 0.0, 64.0]);
1927 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1928 settings.mip_levels = 3;
1929 settings.mip_scan_dist = 32;
1930 let grid_view = roxlap_core::GridView::from_single_vxl(&chunk);
1931 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
1932 let _ = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
1933 drop(rasterizer);
1934 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1935 assert!(
1936 non_sky > 0,
1937 "Vxl::generate_mips on a set_voxel-built chunk should render to something non-sky (got {non_sky})"
1938 );
1939 }
1940
1941 #[test]
1946 fn render_with_mips_present_still_renders_mip0() {
1947 let mut scene = Scene::new();
1948 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1949 scene.grid_mut(id).unwrap().set_rect(
1950 IVec3::new(40, 40, 40),
1951 IVec3::new(55, 55, 55),
1952 Some(0x80_88_88_88),
1953 );
1954 {
1960 let grid = scene.grid_mut(id).unwrap();
1961 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1962 chunk.generate_mips(3);
1963 }
1964
1965 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1966 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1967 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1968 let camera = camera_at([64.0, 0.0, 64.0]);
1969 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1972 settings.mip_scan_dist = 100_000;
1973 let outcome = render_scene_composed(
1974 &mut fb,
1975 &mut zb,
1976 XRES as usize,
1977 XRES,
1978 YRES,
1979 &mut pool,
1980 &mut scene,
1981 &camera,
1982 &settings,
1983 sky_color,
1984 None,
1985 );
1986 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1987 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1988 assert!(
1989 non_sky > 0,
1990 "render of single-grid scene with mips present rendered all-sky: mip-0 may be corrupted by generate_mips"
1991 );
1992 }
1993
1994 #[test]
1995 fn render_scene_two_chunk_x_grid_hash_is_stable() {
1996 const GOLDEN: u64 = 0x215e_d66d_7359_4725;
1998 let mut scene = Scene::new();
2002 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
2003 scene.grid_mut(id).unwrap().set_rect(
2004 IVec3::new(120, 60, 200),
2005 IVec3::new(136, 67, 215),
2006 Some(0x80_aa_55_22),
2007 );
2008 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2009 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2010 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2011 let camera = camera_at([128.0, 100.0, 207.0]);
2012 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2013 let outcome = render_scene_composed(
2014 &mut fb,
2015 &mut zb,
2016 XRES as usize,
2017 XRES,
2018 YRES,
2019 &mut pool,
2020 &mut scene,
2021 &camera,
2022 &settings,
2023 sky_color,
2024 None,
2025 );
2026 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2027
2028 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2029 let hash = fnv1a64(&bytes);
2030 if GOLDEN == SENTINEL {
2031 eprintln!("render_scene_two_chunk_x_grid_hash_is_stable: capture hash = 0x{hash:016x}");
2034 panic!("GOLDEN is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN above");
2035 }
2036 assert_eq!(
2037 hash, GOLDEN,
2038 "2-chunk render hash drifted: expected 0x{GOLDEN:016x}, got 0x{hash:016x}"
2039 );
2040 }
2041
2042 const SENTINEL: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2046
2047 #[test]
2066 fn approach_b_renders_two_chunk_x_stripe_via_chunk_grid() {
2067 const SENTINEL_B: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2068 const GOLDEN_B: u64 = 0x5ee1_e81c_66a8_d1f1;
2073
2074 let mut scene = Scene::new();
2075 let id = scene.add_grid(GridTransform::identity());
2076 let g = scene.grid_mut(id).unwrap();
2077 g.set_rect(
2080 IVec3::new(0, 0, 200),
2081 IVec3::new(127, 127, 205),
2082 Some(0x80_44_44_aa),
2083 );
2084 g.set_rect(
2087 IVec3::new(160, 50, 150),
2088 IVec3::new(170, 60, 165),
2089 Some(0x80_aa_55_22),
2090 );
2091 assert_eq!(g.chunk_count(), 2);
2092
2093 let backing = g.chunk_xyz_backing().expect("at least one chunk populated");
2095 assert_eq!(backing.chunks_x, 2);
2096 assert_eq!(backing.chunks_y, 1);
2097 assert_eq!(backing.origin_chunk_xy, [0, 0]);
2098 let cg = roxlap_core::ChunkGrid {
2099 chunks: &backing.chunks,
2100 origin_chunk_xy: backing.origin_chunk_xy,
2101 origin_chunk_z: backing.origin_chunk_z,
2102 chunks_x: backing.chunks_x,
2103 chunks_y: backing.chunks_y,
2104 chunks_z: backing.chunks_z,
2105 };
2106 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2107
2108 let camera = Camera {
2111 pos: [10.0, 64.0, 160.0],
2112 right: [0.0, 1.0, 0.0],
2113 down: [0.0, 0.0, 1.0],
2114 forward: [1.0, 0.0, 0.0],
2115 };
2116 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2117 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2118 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2119 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2120 drop(rasterizer);
2121 assert_eq!(outcome, OpticastOutcome::Rendered);
2122
2123 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2125 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2126 assert!(
2127 floor_count > 1000,
2128 "floor not visible — only {floor_count} floor pixels (single-chunk path?)"
2129 );
2130 assert!(
2131 box_count > 50,
2132 "box in chunk (1, 0) not visible — only {box_count} box pixels — cross-chunk DDA may have failed to fire"
2133 );
2134
2135 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2137 let hash = fnv1a64(&bytes);
2138 if GOLDEN_B == SENTINEL_B {
2139 eprintln!("approach_b_renders_two_chunk_x_stripe_via_chunk_grid: capture hash = 0x{hash:016x}");
2140 panic!(
2141 "GOLDEN_B is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN_B above"
2142 );
2143 }
2144 assert_eq!(
2145 hash, GOLDEN_B,
2146 "Approach B 2-chunk render hash drifted: expected 0x{GOLDEN_B:016x}, got 0x{hash:016x}"
2147 );
2148 }
2149
2150 #[test]
2163 fn approach_b_camera_in_chunk_1_0_renders_neighbour() {
2164 let mut scene = Scene::new();
2165 let id = scene.add_grid(GridTransform::identity());
2166 let g = scene.grid_mut(id).unwrap();
2167 g.set_rect(
2169 IVec3::new(128, 0, 200),
2170 IVec3::new(255, 127, 205),
2171 Some(0x80_44_44_aa),
2172 );
2173 g.set_rect(
2177 IVec3::new(20, 50, 150),
2178 IVec3::new(30, 60, 165),
2179 Some(0x80_aa_55_22),
2180 );
2181 assert_eq!(g.chunk_count(), 2);
2182
2183 let backing = g.chunk_xyz_backing().expect("populated");
2184 let cg = roxlap_core::ChunkGrid {
2185 chunks: &backing.chunks,
2186 origin_chunk_xy: backing.origin_chunk_xy,
2187 origin_chunk_z: backing.origin_chunk_z,
2188 chunks_x: backing.chunks_x,
2189 chunks_y: backing.chunks_y,
2190 chunks_z: backing.chunks_z,
2191 };
2192 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2193 let (aabb_min, aabb_max) = grid_view.aabb_xy();
2194 assert_eq!(aabb_min, [0, 0]);
2195 assert_eq!(aabb_max, [256, 128]);
2196
2197 let camera = Camera {
2201 pos: [200.0, 64.0, 160.0],
2202 right: [0.0, -1.0, 0.0],
2203 down: [0.0, 0.0, 1.0],
2204 forward: [-1.0, 0.0, 0.0],
2205 };
2206 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2207 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2208 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2209 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2210 drop(rasterizer);
2211 assert_eq!(outcome, OpticastOutcome::Rendered);
2212
2213 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2214 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2215 assert!(
2216 floor_count > 1000,
2217 "floor under camera in chunk (1, 0) not visible — only {floor_count} floor pixels — in_bounds_xy fix may not have taken effect"
2218 );
2219 assert!(
2220 box_count > 50,
2221 "box in chunk (0, 0) not visible — only {box_count} box pixels — westward cross-chunk DDA failed"
2222 );
2223 }
2224
2225 #[test]
2234 fn stacked_two_chunk_z_camera_in_chz1_sees_own_chunk_floor() {
2235 let mut scene = Scene::new();
2236 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2237 let g = scene.grid_mut(id).unwrap();
2238 g.ensure_chunk(IVec3::new(0, 0, 0));
2240 g.set_rect(
2242 IVec3::new(60, 60, 306),
2243 IVec3::new(72, 72, 310),
2244 Some(0x80_33_66_99),
2245 );
2246 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2247
2248 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2249 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2250 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2251 pool.set_treat_z_max_as_air(true);
2252 let camera = Camera {
2256 pos: [66.0, 66.0, 280.0],
2257 right: [1.0, 0.0, 0.0],
2258 down: [0.0, 1.0, 0.0],
2259 forward: [0.0, 0.0, 1.0],
2260 };
2261 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2262 let outcome = render_scene_composed(
2263 &mut fb,
2264 &mut zb,
2265 XRES as usize,
2266 XRES,
2267 YRES,
2268 &mut pool,
2269 &mut scene,
2270 &camera,
2271 &settings,
2272 sky_color,
2273 None,
2274 );
2275 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2276 let floor_count = fb.iter().filter(|&&p| p == 0x80_33_66_99).count();
2277 assert!(
2278 floor_count > 100,
2279 "camera at chz=1 with floor in same chunk should see it — got {floor_count} floor pixels"
2280 );
2281 }
2282
2283 #[test]
2292 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor() {
2293 let mut scene = Scene::new();
2294 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2295 let g = scene.grid_mut(id).unwrap();
2296 g.ensure_chunk(IVec3::new(0, 0, 0));
2299 g.set_rect(
2301 IVec3::new(60, 60, 306),
2302 IVec3::new(72, 72, 310),
2303 Some(0x80_77_aa_44),
2304 );
2305 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2306
2307 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2308 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2309 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2310 pool.set_treat_z_max_as_air(true);
2311 let camera = Camera {
2315 pos: [66.0, 66.0, 100.0],
2316 right: [1.0, 0.0, 0.0],
2317 down: [0.0, 1.0, 0.0],
2318 forward: [0.0, 0.0, 1.0],
2319 };
2320 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2321 let outcome = render_scene_composed(
2322 &mut fb,
2323 &mut zb,
2324 XRES as usize,
2325 XRES,
2326 YRES,
2327 &mut pool,
2328 &mut scene,
2329 &camera,
2330 &settings,
2331 sky_color,
2332 None,
2333 );
2334 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2335 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2336 assert!(
2337 floor_count > 50,
2338 "camera in chz=0 air-gap should see chz=1 floor via cross-chunk look-down — got {floor_count} floor pixels"
2339 );
2340 }
2341
2342 #[test]
2353 #[ignore = "S4B.6.l: known limitation — needs cf-splitting at chz boundaries"]
2354 fn stacked_chz0_distant_mountain_visible_from_chz0_camera() {
2355 let mut scene = Scene::new();
2356 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2357 let g = scene.grid_mut(id).unwrap();
2358 g.set_rect(
2362 IVec3::new(100, 100, 100),
2363 IVec3::new(124, 124, 200),
2364 Some(0x80_aa_55_22), );
2366 g.set_rect(
2370 IVec3::new(0, 0, 336),
2371 IVec3::new(128, 128, 360),
2372 Some(0x80_22_88_44),
2373 );
2374 g.set_rect(IVec3::new(100, 100, 336), IVec3::new(124, 124, 360), None);
2375 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2378 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2379
2380 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2381 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2382 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2383 pool.set_treat_z_max_as_air(true);
2384 let (sy, cy) = (std::f64::consts::FRAC_PI_4).sin_cos();
2390 let (sp, cp) = 0.72_f64.sin_cos();
2391 let camera = Camera {
2392 pos: [40.0, 40.0, 60.0],
2393 right: [-sy, cy, 0.0],
2394 down: [-cy * sp, -sy * sp, cp],
2395 forward: [cy * cp, sy * cp, sp],
2396 };
2397 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2398 let outcome = render_scene_composed(
2399 &mut fb,
2400 &mut zb,
2401 XRES as usize,
2402 XRES,
2403 YRES,
2404 &mut pool,
2405 &mut scene,
2406 &camera,
2407 &settings,
2408 sky_color,
2409 None,
2410 );
2411 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2412 let mountain_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2413 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2414 eprintln!("chz0-distant-mountain: mountain_chz0={mountain_count} hill_chz1={hill_count}");
2415 assert!(
2418 hill_count > 50,
2419 "expected chz=1 hills via cross-chunk look-down — got {hill_count}"
2420 );
2421 assert!(
2424 mountain_count > 50,
2425 "expected chz=0 distant mountain visible — got {mountain_count} (S4B.6.l limitation)"
2426 );
2427 }
2428
2429 #[test]
2440 fn mid_render_handoff_reveals_chz1_hills_under_mountain_camera() {
2441 let mut scene = Scene::new();
2442 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2443 let g = scene.grid_mut(id).unwrap();
2444 g.set_rect(
2447 IVec3::new(60, 60, 150),
2448 IVec3::new(72, 72, 200),
2449 Some(0x80_88_44_22), );
2451 g.set_rect(
2454 IVec3::new(0, 0, 336),
2455 IVec3::new(128, 128, 360),
2456 Some(0x80_22_88_44), );
2458 g.set_rect(IVec3::new(60, 60, 336), IVec3::new(72, 72, 360), None);
2461 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2462 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2463
2464 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2465 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2466 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2467 pool.set_treat_z_max_as_air(true);
2468 let camera = Camera {
2472 pos: [66.0, 66.0, 100.0],
2473 right: [1.0, 0.0, 0.0],
2474 down: [0.0, 1.0, 0.0],
2475 forward: [0.0, 0.0, 1.0],
2476 };
2477 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2478 let outcome = render_scene_composed(
2479 &mut fb,
2480 &mut zb,
2481 XRES as usize,
2482 XRES,
2483 YRES,
2484 &mut pool,
2485 &mut scene,
2486 &camera,
2487 &settings,
2488 sky_color,
2489 None,
2490 );
2491 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2492 let mountain_count = fb.iter().filter(|&&p| p == 0x80_88_44_22).count();
2493 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2494 let mut hill_depths: Vec<f32> = fb
2502 .iter()
2503 .zip(zb.iter())
2504 .filter_map(|(&p, &d)| if p == 0x80_22_88_44 { Some(d) } else { None })
2505 .collect();
2506 hill_depths.sort_by(|a, b| a.partial_cmp(b).unwrap());
2507 let median_hill_depth = hill_depths[hill_depths.len() / 2];
2508 eprintln!(
2509 "mid-render handoff: mountain={mountain_count} hill={hill_count} median_hill_depth={median_hill_depth:.1}"
2510 );
2511 assert!(
2512 mountain_count > 50,
2513 "should see mountain peak via chz=0 — got {mountain_count} mountain pixels"
2514 );
2515 assert!(
2516 hill_count > 50,
2517 "should see chz=1 hills via mid-render handoff — got {hill_count} hill pixels"
2518 );
2519 assert!(
2520 (median_hill_depth - 236.0).abs() < 80.0,
2521 "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"
2522 );
2523 }
2524
2525 #[test]
2534 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor_multi_mip() {
2535 let mut scene = Scene::new();
2536 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2537 let g = scene.grid_mut(id).unwrap();
2538 g.ensure_chunk(IVec3::new(0, 0, 0));
2539 g.set_rect(
2540 IVec3::new(60, 60, 306),
2541 IVec3::new(72, 72, 310),
2542 Some(0x80_77_aa_44),
2543 );
2544 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2545
2546 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2547 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2548 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2549 pool.set_treat_z_max_as_air(true);
2550 let camera = Camera {
2551 pos: [66.0, 66.0, 100.0],
2552 right: [1.0, 0.0, 0.0],
2553 down: [0.0, 1.0, 0.0],
2554 forward: [0.0, 0.0, 1.0],
2555 };
2556 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2557 settings.mip_levels = 2;
2558 settings.mip_scan_dist = 16;
2559 let outcome = render_scene_composed(
2560 &mut fb,
2561 &mut zb,
2562 XRES as usize,
2563 XRES,
2564 YRES,
2565 &mut pool,
2566 &mut scene,
2567 &camera,
2568 &settings,
2569 sky_color,
2570 None,
2571 );
2572 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2573 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2574 assert!(
2575 floor_count > 50,
2576 "multi-mip cross-chunk look-down should still see chz=1 floor — got {floor_count} floor pixels"
2577 );
2578 }
2579
2580 #[test]
2588 fn stacked_three_chunk_z_camera_in_chz2_sees_own_chunk_floor_multi_mip() {
2589 let mut scene = Scene::new();
2590 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2591 let g = scene.grid_mut(id).unwrap();
2592 g.ensure_chunk(IVec3::new(0, 0, 0));
2595 g.ensure_chunk(IVec3::new(0, 0, 1));
2596 g.set_rect(
2598 IVec3::new(60, 60, 562),
2599 IVec3::new(72, 72, 566),
2600 Some(0x80_aa_55_22),
2601 );
2602 assert!(g.chunk(IVec3::new(0, 0, 2)).is_some());
2603
2604 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2605 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2606 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2607 pool.set_treat_z_max_as_air(true);
2608 let camera = Camera {
2609 pos: [66.0, 66.0, 540.0],
2610 right: [1.0, 0.0, 0.0],
2611 down: [0.0, 1.0, 0.0],
2612 forward: [0.0, 0.0, 1.0],
2613 };
2614 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2616 settings.mip_levels = 2;
2617 settings.mip_scan_dist = 16;
2618 let outcome = render_scene_composed(
2619 &mut fb,
2620 &mut zb,
2621 XRES as usize,
2622 XRES,
2623 YRES,
2624 &mut pool,
2625 &mut scene,
2626 &camera,
2627 &settings,
2628 sky_color,
2629 None,
2630 );
2631 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2632 let floor_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2633 assert!(
2634 floor_count > 100,
2635 "camera at chz=2 with floor in same chunk should see it — got {floor_count} floor pixels"
2636 );
2637 }
2638
2639 #[derive(Debug)]
2647 struct FloorGenerator;
2648
2649 impl crate::ChunkGenerator for FloorGenerator {
2650 fn generate(&self, _chunk_idx: IVec3) -> roxlap_formats::vxl::Vxl {
2651 let mut tmp = crate::Grid::new(GridTransform::identity());
2655 tmp.ensure_chunk(IVec3::ZERO);
2656 let mut vxl = tmp.chunks.remove(&IVec3::ZERO).unwrap();
2657 #[allow(clippy::cast_possible_wrap)]
2658 roxlap_formats::edit::set_rect(
2659 &mut vxl,
2660 glam::IVec3::new(0, 0, 230).into(),
2661 glam::IVec3::new((CHUNK_SIZE_XY - 1) as i32, (CHUNK_SIZE_XY - 1) as i32, 239)
2662 .into(),
2663 Some(0x80_22_aa_22),
2664 );
2665 vxl
2666 }
2667 }
2668
2669 #[test]
2670 fn render_scene_composed_unpumped_streaming_grid_renders_all_sky() {
2671 use std::sync::Arc;
2677 let mut scene = Scene::new();
2678 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2679 let g = scene.grid_mut(id).unwrap();
2680 g.set_generator(Some(Arc::new(FloorGenerator)));
2681 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2682 assert!(g.chunks.is_empty(), "no pump yet → no chunks");
2683
2684 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2685 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2686 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2687 let camera = camera_at([64.0, -100.0, 200.0]);
2690 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2691 let _ = render_scene_composed(
2692 &mut fb,
2693 &mut zb,
2694 XRES as usize,
2695 XRES,
2696 YRES,
2697 &mut pool,
2698 &mut scene,
2699 &camera,
2700 &settings,
2701 sky_color,
2702 None,
2703 );
2704 assert!(
2706 fb.iter().all(|&p| p == sky_color),
2707 "unpumped streaming grid must render as all sky"
2708 );
2709 }
2710
2711 #[test]
2712 fn render_scene_composed_picks_up_streamed_chunks_after_sync_pump() {
2713 use std::sync::Arc;
2718 let mut scene = Scene::new();
2719 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2720 let g = scene.grid_mut(id).unwrap();
2721 g.set_generator(Some(Arc::new(FloorGenerator)));
2722 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2724
2725 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2727 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2728 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2729 let camera = camera_at([64.0, -100.0, 200.0]);
2730 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2731 let _ = render_scene_composed(
2732 &mut fb,
2733 &mut zb,
2734 XRES as usize,
2735 XRES,
2736 YRES,
2737 &mut pool,
2738 &mut scene,
2739 &camera,
2740 &settings,
2741 sky_color,
2742 None,
2743 );
2744 let pre_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2745 assert_eq!(pre_floor, 0, "pre-pump frame has no streamed chunks");
2746
2747 scene.pump_streaming_sync(DVec3::new(64.0, -100.0, 200.0));
2750 let g = scene.grid(id).unwrap();
2751 assert!(
2752 !g.chunks.is_empty(),
2753 "pump should have streamed at least one chunk"
2754 );
2755
2756 fb.iter_mut().for_each(|p| *p = sky_color);
2759 zb.iter_mut().for_each(|z| *z = f32::INFINITY);
2760 let outcome = render_scene_composed(
2761 &mut fb,
2762 &mut zb,
2763 XRES as usize,
2764 XRES,
2765 YRES,
2766 &mut pool,
2767 &mut scene,
2768 &camera,
2769 &settings,
2770 sky_color,
2771 None,
2772 );
2773 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2774 let post_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2775 assert!(
2776 post_floor > 100,
2777 "post-pump frame should show the streamed floor — got {post_floor} green pixels"
2778 );
2779 }
2780
2781 #[test]
2782 fn render_scene_composed_partial_streaming_renders_pending_chunks_as_air() {
2783 use std::sync::Arc;
2791 let mut scene = Scene::new();
2792 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2793 let g = scene.grid_mut(id).unwrap();
2794 g.set_generator(Some(Arc::new(FloorGenerator)));
2795 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2802
2803 let installed = g.ensure_chunk_generated(IVec3::ZERO);
2806 assert!(installed, "manual install of one chunk");
2807 assert_eq!(g.chunks.len(), 1);
2808 assert!(g.chunk(IVec3::new(0, 1, 0)).is_none());
2810 assert!(g.chunk(IVec3::new(0, 2, 0)).is_none());
2811
2812 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2813 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2814 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2815 let camera = camera_at([64.0, 32.0, 200.0]);
2820 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2821 let _ = render_scene_composed(
2822 &mut fb,
2823 &mut zb,
2824 XRES as usize,
2825 XRES,
2826 YRES,
2827 &mut pool,
2828 &mut scene,
2829 &camera,
2830 &settings,
2831 sky_color,
2832 None,
2833 );
2834 let floor_pixels = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2835 assert!(
2840 floor_pixels > 0,
2841 "should see at least some floor from the loaded chunk"
2842 );
2843 scene.pump_streaming_sync(DVec3::new(64.0, 32.0, 200.0));
2846 assert!(scene.grid(id).unwrap().chunk_count() >= 2);
2847 fb.iter_mut().for_each(|p| *p = sky_color);
2848 zb.iter_mut().for_each(|z| *z = f32::INFINITY);
2849 let _ = render_scene_composed(
2850 &mut fb,
2851 &mut zb,
2852 XRES as usize,
2853 XRES,
2854 YRES,
2855 &mut pool,
2856 &mut scene,
2857 &camera,
2858 &settings,
2859 sky_color,
2860 None,
2861 );
2862 let floor_pixels_full = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2863 assert!(
2864 floor_pixels_full > floor_pixels,
2865 "fully-streamed scene should show more floor than partial: \
2866 partial={floor_pixels} full={floor_pixels_full}"
2867 );
2868 }
2869}