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::{GridTransform, Scene, CHUNK_SIZE_XY};
47
48const SKY_MASK_SENTINEL: u32 = 0x00_DE_AD_BE;
63
64fn world_camera_to_grid_local(camera: &Camera, transform: &GridTransform) -> Camera {
77 let inv = transform.rotation.inverse();
78 let world_offset = DVec3::from_array(camera.pos) - transform.origin;
79 let local_pos = inv * world_offset;
80 let local_right = inv * DVec3::from_array(camera.right);
81 let local_down = inv * DVec3::from_array(camera.down);
82 let local_forward = inv * DVec3::from_array(camera.forward);
83 Camera {
84 pos: local_pos.to_array(),
85 right: local_right.to_array(),
86 down: local_down.to_array(),
87 forward: local_forward.to_array(),
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum RenderOutcome {
94 Rendered {
96 grids_drawn: usize,
99 },
100 Empty,
104}
105
106#[allow(clippy::too_many_arguments)]
122pub fn render_scene(
123 fb: &mut [u32],
124 zb: &mut [f32],
125 pitch_pixels: usize,
126 width: u32,
127 height: u32,
128 pool: &mut ScratchPool,
129 scene: &mut Scene,
130 camera: &Camera,
131 settings: &OpticastSettings,
132 sky: Option<&Sky>,
133) -> RenderOutcome {
134 debug_assert_eq!(fb.len(), zb.len());
135 let pixel_count = (width as usize) * (height as usize);
136 debug_assert_eq!(fb.len(), pixel_count);
137
138 let mut grids_drawn = 0usize;
139 for (_id, grid) in scene.grids_mut() {
140 let Some(backing) = grid.chunk_xyz_backing() else {
151 continue;
153 };
154 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
155 let cg = roxlap_core::ChunkGrid {
156 chunks: &backing.chunks,
157 origin_chunk_xy: backing.origin_chunk_xy,
158 origin_chunk_z: backing.origin_chunk_z,
159 chunks_x: backing.chunks_x,
160 chunks_y: backing.chunks_y,
161 chunks_z: backing.chunks_z,
162 };
163 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
164 let outcome = {
165 let mut rasterizer = ScalarRasterizer::new(fb, zb, pitch_pixels, grid_view);
166 if let Some(sky_ref) = sky {
167 rasterizer = rasterizer.with_sky(sky_ref);
168 }
169 opticast(&mut rasterizer, pool, &local_cam, settings, grid_view)
170 };
171 if outcome == OpticastOutcome::Rendered {
172 grids_drawn += 1;
173 }
174 }
175 if grids_drawn == 0 {
176 RenderOutcome::Empty
177 } else {
178 RenderOutcome::Rendered { grids_drawn }
179 }
180}
181
182pub fn compose_into(
195 shared_fb: &mut [u32],
196 shared_zb: &mut [f32],
197 temp_fb: &[u32],
198 temp_zb: &[f32],
199) {
200 debug_assert_eq!(shared_fb.len(), shared_zb.len());
201 debug_assert_eq!(shared_fb.len(), temp_fb.len());
202 debug_assert_eq!(shared_fb.len(), temp_zb.len());
203 for i in 0..shared_fb.len() {
204 if temp_zb[i] < shared_zb[i] {
205 shared_fb[i] = temp_fb[i];
206 shared_zb[i] = temp_zb[i];
207 }
208 }
209}
210
211#[allow(clippy::too_many_arguments)]
242pub fn render_scene_composed(
243 fb: &mut [u32],
244 zb: &mut [f32],
245 pitch_pixels: usize,
246 width: u32,
247 height: u32,
248 pool: &mut ScratchPool,
249 scene: &mut Scene,
250 camera: &Camera,
251 settings: &OpticastSettings,
252 sky_color: u32,
253 sky: Option<&Sky>,
254) -> RenderOutcome {
255 debug_assert_eq!(fb.len(), zb.len());
256 let pixel_count = (width as usize) * (height as usize);
257 debug_assert_eq!(fb.len(), pixel_count);
258
259 let mut grids_drawn = 0usize;
260 let mut temp_fb = vec![sky_color; pixel_count];
261 let mut temp_zb = vec![f32::INFINITY; pixel_count];
262
263 for (_id, grid) in scene.grids_mut() {
264 let Some(backing) = grid.chunk_xyz_backing() else {
270 continue;
271 };
272 let owns_sky = grid.render_sky;
282 let local_sky_color = if owns_sky {
283 sky_color
284 } else {
285 SKY_MASK_SENTINEL
286 };
287 if !owns_sky {
288 pool.set_skycast(SKY_MASK_SENTINEL as i32, 0);
295 }
296
297 temp_fb.fill(local_sky_color);
301 temp_zb.fill(f32::INFINITY);
302
303 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
304 let cg = roxlap_core::ChunkGrid {
305 chunks: &backing.chunks,
306 origin_chunk_xy: backing.origin_chunk_xy,
307 origin_chunk_z: backing.origin_chunk_z,
308 chunks_x: backing.chunks_x,
309 chunks_y: backing.chunks_y,
310 chunks_z: backing.chunks_z,
311 };
312 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
313
314 let per_grid_settings;
322 let active_settings = match grid.mip_levels_override {
323 Some(cap) => {
324 let clamped = cap.clamp(1, settings.mip_levels);
325 per_grid_settings = OpticastSettings {
326 mip_levels: clamped,
327 ..*settings
328 };
329 &per_grid_settings
330 }
331 None => settings,
332 };
333
334 let outcome = {
335 let mut rasterizer =
336 ScalarRasterizer::new(&mut temp_fb, &mut temp_zb, pitch_pixels, grid_view);
337 if owns_sky {
341 if let Some(sky_ref) = sky {
342 rasterizer = rasterizer.with_sky(sky_ref);
343 }
344 }
345 opticast(
346 &mut rasterizer,
347 pool,
348 &local_cam,
349 active_settings,
350 grid_view,
351 )
352 };
353
354 if !owns_sky {
355 for (px, z) in temp_fb.iter().zip(temp_zb.iter_mut()) {
359 if *px == SKY_MASK_SENTINEL {
360 *z = f32::INFINITY;
361 }
362 }
363 pool.set_skycast(sky_color as i32, 0);
366 }
367
368 if outcome == OpticastOutcome::Rendered {
369 compose_into(fb, zb, &temp_fb, &temp_zb);
370 grids_drawn += 1;
371 }
372 }
373
374 if grids_drawn == 0 {
375 RenderOutcome::Empty
376 } else {
377 RenderOutcome::Rendered { grids_drawn }
378 }
379}
380
381#[cfg(test)]
382#[allow(clippy::float_cmp)]
383mod tests {
384 use super::*;
385 use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
386 use glam::{DVec3, IVec3};
387 use roxlap_core::opticast::{opticast as core_opticast, OpticastSettings};
388 use roxlap_core::rasterizer::ScratchPool;
389 use roxlap_core::scalar_rasterizer::ScalarRasterizer;
390 use roxlap_core::{Camera, Engine};
391
392 const XRES: u32 = 320;
393 const YRES: u32 = 200;
394
395 fn build_one_grid_scene(world_origin: DVec3) -> (Scene, crate::GridId) {
399 let mut scene = Scene::new();
400 let id = scene.add_grid(GridTransform::at(world_origin));
401 let grid = scene.grid_mut(id).unwrap();
402 grid.set_rect(
404 IVec3::new(40, 40, 40),
405 IVec3::new(55, 55, 55),
406 Some(0x80_88_88_88),
407 );
408 grid.set_sphere(IVec3::new(80, 80, 80), 6, Some(0x80_22_aa_22));
410 (scene, id)
411 }
412
413 fn camera_at(pos: [f64; 3]) -> Camera {
414 Camera {
417 pos,
418 right: [-1.0, 0.0, 0.0],
419 down: [0.0, 0.0, 1.0],
420 forward: [0.0, 1.0, 0.0],
421 }
422 }
423
424 fn render_setup(pool_vsid: u32) -> (Engine, ScratchPool, Vec<u32>, Vec<f32>) {
428 let engine = Engine::new();
429 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
430 let sky = engine.sky_color();
431 let sky_col_i = i32::from_ne_bytes(sky.to_ne_bytes());
432 pool.set_skycast(sky_col_i, 0);
433 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
434 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
435 pool.set_treat_z_max_as_air(true);
436 let pixel_count = (XRES as usize) * (YRES as usize);
437 let framebuffer = vec![sky; pixel_count];
438 let zbuffer = vec![0.0f32; pixel_count];
439 (engine, pool, framebuffer, zbuffer)
440 }
441
442 fn render_via_scene(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
445 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
446 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
447 let outcome = render_scene(
448 &mut fb,
449 &mut zb,
450 XRES as usize,
451 XRES,
452 YRES,
453 &mut pool,
454 scene,
455 camera,
456 &settings,
457 None,
458 );
459 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
460 fb
461 }
462
463 fn render_via_direct_opticast(scene: &Scene, local_camera: &Camera) -> Vec<u32> {
467 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
468 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
469 let grid = scene.grids().next().unwrap().1;
470 let chunk = grid.chunk(IVec3::ZERO).unwrap();
471 let grid_view = roxlap_core::GridView::from_single_vxl(chunk);
472 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
473 let _ = core_opticast(
474 &mut rasterizer,
475 &mut pool,
476 local_camera,
477 &settings,
478 grid_view,
479 );
480 drop(rasterizer);
481 fb
482 }
483
484 #[test]
489 fn world_camera_to_grid_local_identity_rotation_translates_pos_only() {
490 let camera = Camera {
491 pos: [110.0, 220.0, 330.0],
492 right: [1.0, 0.0, 0.0],
493 down: [0.0, 0.0, 1.0],
494 forward: [0.0, 1.0, 0.0],
495 };
496 let transform = GridTransform::at(DVec3::new(100.0, 200.0, 300.0));
497 let local = super::world_camera_to_grid_local(&camera, &transform);
498 assert_eq!(local.right, camera.right);
500 assert_eq!(local.down, camera.down);
501 assert_eq!(local.forward, camera.forward);
502 for (got, want) in local.pos.iter().zip([10.0, 20.0, 30.0].iter()) {
504 assert!((got - want).abs() < 1e-12, "pos got={got} want={want}");
505 }
506 }
507
508 #[test]
512 fn world_camera_to_grid_local_90deg_z_rotates_basis_and_pos() {
513 use glam::DQuat;
514 let camera = Camera {
515 pos: [0.0, 10.0, 0.0],
516 right: [1.0, 0.0, 0.0],
517 down: [0.0, 0.0, 1.0],
518 forward: [0.0, 1.0, 0.0],
519 };
520 let transform = GridTransform {
521 origin: DVec3::ZERO,
522 rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
523 };
524 let local = super::world_camera_to_grid_local(&camera, &transform);
525 let approx_eq =
527 |a: [f64; 3], b: [f64; 3]| a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-9);
528 assert!(
529 approx_eq(local.pos, [10.0, 0.0, 0.0]),
530 "pos={:?} expected ~(10, 0, 0)",
531 local.pos
532 );
533 assert!(
535 approx_eq(local.right, [0.0, -1.0, 0.0]),
536 "right={:?} expected ~(0, -1, 0)",
537 local.right
538 );
539 assert!(
541 approx_eq(local.down, [0.0, 0.0, 1.0]),
542 "down={:?} expected ~(0, 0, 1)",
543 local.down
544 );
545 assert!(
547 approx_eq(local.forward, [1.0, 0.0, 0.0]),
548 "forward={:?} expected ~(1, 0, 0)",
549 local.forward
550 );
551 }
552
553 #[test]
558 fn world_camera_to_grid_local_preserves_basis_orthonormality() {
559 use glam::DQuat;
560 let camera = Camera {
563 pos: [3.0, -5.0, 7.0],
564 right: [-1.0, 0.0, 0.0],
565 down: [0.0, 0.0, 1.0],
566 forward: [0.0, 1.0, 0.0],
567 };
568 let transform = GridTransform {
569 origin: DVec3::new(1.0, 2.0, 3.0),
570 rotation: DQuat::from_axis_angle(glam::DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
571 };
572 let local = super::world_camera_to_grid_local(&camera, &transform);
573 let r = DVec3::from_array(local.right);
574 let d = DVec3::from_array(local.down);
575 let f = DVec3::from_array(local.forward);
576 for v in [r, d, f] {
578 assert!(
579 (v.length_squared() - 1.0).abs() < 1e-12,
580 "basis vec {v:?} not unit length"
581 );
582 }
583 assert!(r.dot(d).abs() < 1e-12, "right·down = {}", r.dot(d));
585 assert!(r.dot(f).abs() < 1e-12, "right·forward = {}", r.dot(f));
586 assert!(d.dot(f).abs() < 1e-12, "down·forward = {}", d.dot(f));
587 let cross = r.cross(d);
589 assert!(
590 (cross - f).length() < 1e-12,
591 "right×down={cross:?} forward={f:?}"
592 );
593 }
594
595 fn build_one_grid_marker_scene(transform: GridTransform) -> (Scene, crate::GridId, u32) {
603 let mut scene = Scene::new();
604 let id = scene.add_grid(transform);
605 let grid = scene.grid_mut(id).unwrap();
606 grid.set_rect(
608 IVec3::new(40, 40, 40),
609 IVec3::new(55, 55, 55),
610 Some(0x80_55_aa_22), );
612 (scene, id, 0x80_55_aa_22)
613 }
614
615 #[test]
628 fn s5_1_180deg_z_rotated_grid_byte_identical_to_axis_aligned() {
629 use glam::DQuat;
630 let axis_aligned_camera = Camera {
632 pos: [40.0, -20.0, 50.0],
633 right: [-1.0, 0.0, 0.0],
634 down: [0.0, 0.0, 1.0],
635 forward: [0.0, 1.0, 0.0],
636 };
637 let rotated_camera = Camera {
639 pos: [-40.0, 20.0, 50.0],
640 right: [1.0, 0.0, 0.0],
641 down: [0.0, 0.0, 1.0],
642 forward: [0.0, -1.0, 0.0],
643 };
644 let q = DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0);
649 let rot_pos = q * DVec3::from_array(axis_aligned_camera.pos);
650 let rot_fwd = q * DVec3::from_array(axis_aligned_camera.forward);
651 assert_eq!(rot_pos.to_array(), rotated_camera.pos);
652 assert_eq!(rot_fwd.to_array(), rotated_camera.forward);
653
654 let (mut scene_a, _, _) = build_one_grid_marker_scene(GridTransform::identity());
655 let fb_a = render_via_scene(&mut scene_a, &axis_aligned_camera);
656
657 let (mut scene_b, _, _) = build_one_grid_marker_scene(GridTransform {
658 origin: DVec3::ZERO,
659 rotation: q,
660 });
661 let fb_b = render_via_scene(&mut scene_b, &rotated_camera);
662
663 assert_eq!(
664 fb_a, fb_b,
665 "rotating both grid and camera by R about the grid origin must leave the framebuffer unchanged"
666 );
667 }
668
669 #[test]
676 fn s5_1_45deg_z_rotated_grid_renders_marker() {
677 use glam::DQuat;
678 let rotation = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4);
679 let (mut scene, _, marker) = build_one_grid_marker_scene(GridTransform {
680 origin: DVec3::ZERO,
681 rotation,
682 });
683
684 let marker_world = rotation * DVec3::new(47.5, 47.5, 47.5);
689 let camera = Camera {
692 pos: [marker_world.x, marker_world.y - 80.0, marker_world.z],
693 right: [-1.0, 0.0, 0.0],
694 down: [0.0, 0.0, 1.0],
695 forward: [0.0, 1.0, 0.0],
696 };
697
698 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
699 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
700 let outcome = render_scene(
701 &mut fb,
702 &mut zb,
703 XRES as usize,
704 XRES,
705 YRES,
706 &mut pool,
707 &mut scene,
708 &camera,
709 &settings,
710 None,
711 );
712 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
713 let marker_count = fb.iter().filter(|&&p| p == marker).count();
714 assert!(
715 marker_count > 50,
716 "45°-rotated marker box should be visible — got {marker_count} marker pixels"
717 );
718 }
719
720 #[test]
732 fn render_sky_false_drops_grid_sky_pixels() {
733 use crate::{GridId, GridTransform};
734
735 let mut scene = Scene::new();
738 let _b_id: GridId = scene.add_grid(GridTransform::at(DVec3::new(0.0, 600.0, 0.0)));
739 let b_id = scene.grids().next().unwrap().0;
742 scene.grid_mut(b_id).unwrap().set_rect(
743 IVec3::new(0, 0, 100),
744 IVec3::new(127, 127, 110),
745 Some(0x80_22_88_22), );
747
748 let a_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
752 scene.grid_mut(a_id).unwrap().set_rect(
753 IVec3::new(60, 60, 60),
754 IVec3::new(67, 67, 67),
755 Some(0x80_aa_22_22), );
757 scene.grid_mut(a_id).unwrap().render_sky = false;
758
759 let unique_sky: u32 = 0xFF_AB_CD_EF;
760 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
761 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
762 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
763 let camera = camera_at([64.0, 0.0, 100.0]);
764 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
765 let outcome = render_scene_composed(
766 &mut fb,
767 &mut zb,
768 XRES as usize,
769 XRES,
770 YRES,
771 &mut pool,
772 &mut scene,
773 &camera,
774 &settings,
775 unique_sky,
776 None,
777 );
778 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
779
780 let leaked = fb
784 .iter()
785 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
786 .count();
787 assert_eq!(
788 leaked, 0,
789 "SKY_MASK_SENTINEL leaked into composed framebuffer ({leaked} pixels)"
790 );
791 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_22_22).count();
794 assert!(
795 red_count > 0,
796 "red cube from sky-disabled grid A is missing — render_sky=false should only mask sky"
797 );
798 let green_count = fb.iter().filter(|&&p| p == 0x80_22_88_22).count();
801 assert!(
802 green_count > 0,
803 "grid B's floor invisible — grid A's masked sky may have overwritten it"
804 );
805 }
806
807 #[test]
811 fn render_sky_false_single_grid_no_sentinel_leak() {
812 let (mut scene, id, _) = build_one_grid_marker_scene(GridTransform::identity());
813 scene.grid_mut(id).unwrap().render_sky = false;
814 let unique_sky: u32 = 0xFF_12_34_56;
815 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
816 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
817 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
818 let camera = camera_at([64.0, 0.0, 64.0]);
819 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
820 let outcome = render_scene_composed(
821 &mut fb,
822 &mut zb,
823 XRES as usize,
824 XRES,
825 YRES,
826 &mut pool,
827 &mut scene,
828 &camera,
829 &settings,
830 unique_sky,
831 None,
832 );
833 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
834 let leaked = fb
835 .iter()
836 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
837 .count();
838 assert_eq!(leaked, 0, "SKY_MASK_SENTINEL leaked ({leaked} pixels)");
839 let prefill_count = fb.iter().filter(|&&p| p == unique_sky).count();
842 assert!(
843 prefill_count > 0,
844 "no pre-fill pixels survived — render_sky=false should leave non-hit pixels untouched"
845 );
846 }
847
848 #[test]
849 fn render_scene_at_origin_matches_direct_opticast() {
850 let (mut scene, _) = build_one_grid_scene(DVec3::ZERO);
856 let cam = camera_at([64.0, 0.0, 64.0]);
857 let via_scene = render_via_scene(&mut scene, &cam);
858 let via_direct = render_via_direct_opticast(&scene, &cam);
859 assert_eq!(
860 via_scene, via_direct,
861 "render_scene with single 1-chunk grid at origin should match direct opticast"
862 );
863 }
864
865 #[test]
866 fn render_scene_translated_grid_matches_grid_local_opticast() {
867 let world_origin = DVec3::new(1000.0, 2000.0, 3000.0);
872 let (mut scene, _) = build_one_grid_scene(world_origin);
873 let world_cam = camera_at([1064.0, 2000.0, 3064.0]);
874 let local_cam = camera_at([64.0, 0.0, 64.0]);
875 let via_scene = render_via_scene(&mut scene, &world_cam);
876 let via_direct = render_via_direct_opticast(&scene, &local_cam);
877 assert_eq!(
878 via_scene, via_direct,
879 "render_scene of translated grid should match opticast with grid-local camera"
880 );
881 }
882
883 #[test]
884 fn empty_scene_returns_empty_outcome() {
885 let mut scene = Scene::new();
886 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
887 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
888 let outcome = render_scene(
889 &mut fb,
890 &mut zb,
891 XRES as usize,
892 XRES,
893 YRES,
894 &mut pool,
895 &mut scene,
896 &camera_at([0.0, 0.0, 0.0]),
897 &settings,
898 None,
899 );
900 assert_eq!(outcome, RenderOutcome::Empty);
901 }
902
903 fn build_two_grid_side_by_side() -> (Scene, u32, u32) {
911 let mut scene = Scene::new();
912 let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
914 scene.grid_mut(g0).unwrap().set_rect(
915 IVec3::new(56, 56, 92),
916 IVec3::new(71, 71, 107),
917 Some(0x80_88_22_22), );
919 let _g1 = scene.add_grid(GridTransform::at(DVec3::new(200.0, 200.0, 0.0)));
921 let g1_id = scene
923 .grids()
924 .filter(|(id, _)| *id != g0)
925 .map(|(id, _)| id)
926 .next()
927 .unwrap();
928 scene.grid_mut(g1_id).unwrap().set_rect(
929 IVec3::new(56, 56, 92),
930 IVec3::new(71, 71, 107),
931 Some(0x80_22_22_88), );
933 (scene, 0x80_88_22_22, 0x80_22_22_88)
934 }
935
936 fn make_composed_pool(pool_vsid: u32) -> (Engine, ScratchPool, u32) {
937 let engine = Engine::new();
938 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
939 let sky_color = engine.sky_color();
940 let sky_col_i = i32::from_ne_bytes(sky_color.to_ne_bytes());
941 pool.set_skycast(sky_col_i, 0);
942 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
943 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
944 pool.set_treat_z_max_as_air(true);
945 (engine, pool, sky_color)
946 }
947
948 fn pixel_count(width: u32, height: u32) -> usize {
949 (width as usize) * (height as usize)
950 }
951
952 #[test]
953 fn compose_into_takes_smaller_z() {
954 let mut shared_fb = vec![0xff_ff_ff_ff_u32; 4];
955 let mut shared_zb = vec![10.0f32; 4];
956 let temp_fb = [0xaa_aa_aa_aa, 0x11_22_33_44, 0x55_66_77_88, 0xde_ad_be_ef];
957 let temp_zb = [5.0f32, 20.0, 10.0, f32::INFINITY];
958 compose_into(&mut shared_fb, &mut shared_zb, &temp_fb, &temp_zb);
959 assert_eq!(shared_fb[0], 0xaa_aa_aa_aa);
961 assert_eq!(shared_zb[0], 5.0);
962 assert_eq!(shared_fb[1], 0xff_ff_ff_ff);
964 assert_eq!(shared_zb[1], 10.0);
965 assert_eq!(shared_fb[2], 0xff_ff_ff_ff);
967 assert_eq!(shared_fb[3], 0xff_ff_ff_ff);
969 }
970
971 #[test]
972 fn render_scene_composed_two_grids_both_visible() {
973 let (mut scene, red, blue) = build_two_grid_side_by_side();
978 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
979 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
980 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
981
982 let camera = camera_at([160.0, 100.0, 100.0]);
983 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
984 let outcome = render_scene_composed(
985 &mut fb,
986 &mut zb,
987 XRES as usize,
988 XRES,
989 YRES,
990 &mut pool,
991 &mut scene,
992 &camera,
993 &settings,
994 sky_color,
995 None,
996 );
997 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
998
999 let red_count = fb.iter().filter(|&&p| p == red).count();
1001 let blue_count = fb.iter().filter(|&&p| p == blue).count();
1002 assert!(
1003 red_count > 0,
1004 "no red pixels: grid 0 (red box) not visible after compose"
1005 );
1006 assert!(
1007 blue_count > 0,
1008 "no blue pixels: grid 1 (blue box) not visible after compose"
1009 );
1010 }
1011
1012 #[test]
1013 fn render_scene_composed_grid_a_in_front_of_grid_b() {
1014 let mut scene = Scene::new();
1018 let g_a = scene.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1019 scene.grid_mut(g_a).unwrap().set_rect(
1020 IVec3::new(56, 56, 92),
1021 IVec3::new(71, 71, 107),
1022 Some(0x80_aa_00_00), );
1024 let _g_b = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1025 let g_b_id = scene
1026 .grids()
1027 .filter(|(id, _)| *id != g_a)
1028 .map(|(id, _)| id)
1029 .next()
1030 .unwrap();
1031 scene.grid_mut(g_b_id).unwrap().set_rect(
1032 IVec3::new(56, 56, 92),
1033 IVec3::new(71, 71, 107),
1034 Some(0x80_00_00_aa), );
1036
1037 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1038 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1039 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1040
1041 let camera = camera_at([64.0, -10.0, 100.0]);
1044 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1045 let outcome = render_scene_composed(
1046 &mut fb,
1047 &mut zb,
1048 XRES as usize,
1049 XRES,
1050 YRES,
1051 &mut pool,
1052 &mut scene,
1053 &camera,
1054 &settings,
1055 sky_color,
1056 None,
1057 );
1058 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1059
1060 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_00_00).count();
1064 assert!(
1065 red_count > 0,
1066 "expected red pixels (closer box should win z-test)"
1067 );
1068
1069 let mut scene2 = Scene::new();
1072 let g_b2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1073 scene2.grid_mut(g_b2).unwrap().set_rect(
1074 IVec3::new(56, 56, 92),
1075 IVec3::new(71, 71, 107),
1076 Some(0x80_00_00_aa),
1077 );
1078 let g_a2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1079 scene2.grid_mut(g_a2).unwrap().set_rect(
1080 IVec3::new(56, 56, 92),
1081 IVec3::new(71, 71, 107),
1082 Some(0x80_aa_00_00),
1083 );
1084
1085 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1086 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1087 let outcome2 = render_scene_composed(
1088 &mut fb2,
1089 &mut zb2,
1090 XRES as usize,
1091 XRES,
1092 YRES,
1093 &mut pool,
1094 &mut scene2,
1095 &camera,
1096 &settings,
1097 sky_color,
1098 None,
1099 );
1100 assert_eq!(outcome2, RenderOutcome::Rendered { grids_drawn: 2 });
1101 assert_eq!(
1102 fb, fb2,
1103 "composition should be order-independent — same scene in different add order should produce identical output"
1104 );
1105 }
1106
1107 #[test]
1108 fn render_scene_composed_empty_scene_returns_empty() {
1109 let mut scene = Scene::new();
1110 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1111 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1112 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1113 let camera = camera_at([0.0, 0.0, 0.0]);
1114 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1115 let outcome = render_scene_composed(
1116 &mut fb,
1117 &mut zb,
1118 XRES as usize,
1119 XRES,
1120 YRES,
1121 &mut pool,
1122 &mut scene,
1123 &camera,
1124 &settings,
1125 sky_color,
1126 None,
1127 );
1128 assert_eq!(outcome, RenderOutcome::Empty);
1129 assert!(fb.iter().all(|&p| p == sky_color));
1131 }
1132
1133 fn fnv1a64(data: &[u8]) -> u64 {
1138 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1139 for &b in data {
1140 h ^= u64::from(b);
1141 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1142 }
1143 h
1144 }
1145
1146 #[test]
1152 fn render_scene_two_chunk_x_grid_no_seam() {
1153 let mut scene = Scene::new();
1154 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1155 let g = scene.grid_mut(id).unwrap();
1156 g.set_rect(
1162 IVec3::new(120, 60, 200),
1163 IVec3::new(136, 67, 215),
1164 Some(0x80_aa_55_22),
1165 );
1166 assert_eq!(g.chunk_count(), 2);
1168
1169 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1173 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1174 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1175 let camera = camera_at([128.0, 100.0, 207.0]);
1176 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1177 let outcome = render_scene_composed(
1178 &mut fb,
1179 &mut zb,
1180 XRES as usize,
1181 XRES,
1182 YRES,
1183 &mut pool,
1184 &mut scene,
1185 &camera,
1186 &settings,
1187 sky_color,
1188 None,
1189 );
1190 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1191
1192 let stripe = 0x80_aa_55_22;
1196 let stripe_count = fb.iter().filter(|&&p| p == stripe).count();
1197 assert!(
1198 stripe_count > 200,
1199 "stripe rendered too few pixels ({stripe_count}) — chunks may not be stitching"
1200 );
1201
1202 let centre_y = (YRES / 2) as usize;
1206 let row_start = centre_y * (XRES as usize);
1207 let row = &fb[row_start..row_start + (XRES as usize)];
1208 let mut in_stripe = false;
1209 let mut seam_gaps = 0usize;
1210 for &px in row {
1211 if px == stripe {
1212 in_stripe = true;
1213 } else if in_stripe && px == sky_color {
1214 if row.iter().skip_while(|&&p| p != px).any(|&p| p == stripe) {
1217 seam_gaps += 1;
1219 }
1220 in_stripe = false;
1221 }
1222 }
1223 assert!(
1227 seam_gaps <= 1,
1228 "centre row has {seam_gaps} disjoint stripe runs — expected 1 (chunk-edge seam suspected)"
1229 );
1230 }
1231
1232 #[test]
1248 fn vxl_generate_mips_on_set_voxel_chunk_renders() {
1249 let mut grid = crate::Grid::new(GridTransform::identity());
1250 grid.set_rect(
1253 IVec3::new(0, 0, 100),
1254 IVec3::new(127, 127, 254),
1255 Some(0x80_88_88_88),
1256 );
1257 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1258 chunk.generate_mips(3);
1259 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1260 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1261 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1262 let camera = camera_at([64.0, 0.0, 64.0]);
1263 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1264 settings.mip_levels = 3;
1265 settings.mip_scan_dist = 32;
1266 let grid_view = roxlap_core::GridView::from_single_vxl(&chunk);
1267 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
1268 let _ = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
1269 drop(rasterizer);
1270 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1271 assert!(
1272 non_sky > 0,
1273 "Vxl::generate_mips on a set_voxel-built chunk should render to something non-sky (got {non_sky})"
1274 );
1275 }
1276
1277 #[test]
1282 fn render_with_mips_present_still_renders_mip0() {
1283 let mut scene = Scene::new();
1284 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1285 scene.grid_mut(id).unwrap().set_rect(
1286 IVec3::new(40, 40, 40),
1287 IVec3::new(55, 55, 55),
1288 Some(0x80_88_88_88),
1289 );
1290 {
1296 let grid = scene.grid_mut(id).unwrap();
1297 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1298 chunk.generate_mips(3);
1299 }
1300
1301 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1302 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1303 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1304 let camera = camera_at([64.0, 0.0, 64.0]);
1305 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1308 settings.mip_scan_dist = 100_000;
1309 let outcome = render_scene_composed(
1310 &mut fb,
1311 &mut zb,
1312 XRES as usize,
1313 XRES,
1314 YRES,
1315 &mut pool,
1316 &mut scene,
1317 &camera,
1318 &settings,
1319 sky_color,
1320 None,
1321 );
1322 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1323 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1324 assert!(
1325 non_sky > 0,
1326 "render of single-grid scene with mips present rendered all-sky: mip-0 may be corrupted by generate_mips"
1327 );
1328 }
1329
1330 #[test]
1331 fn render_scene_two_chunk_x_grid_hash_is_stable() {
1332 const GOLDEN: u64 = 0x215e_d66d_7359_4725;
1334 let mut scene = Scene::new();
1338 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1339 scene.grid_mut(id).unwrap().set_rect(
1340 IVec3::new(120, 60, 200),
1341 IVec3::new(136, 67, 215),
1342 Some(0x80_aa_55_22),
1343 );
1344 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1345 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1346 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1347 let camera = camera_at([128.0, 100.0, 207.0]);
1348 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1349 let outcome = render_scene_composed(
1350 &mut fb,
1351 &mut zb,
1352 XRES as usize,
1353 XRES,
1354 YRES,
1355 &mut pool,
1356 &mut scene,
1357 &camera,
1358 &settings,
1359 sky_color,
1360 None,
1361 );
1362 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1363
1364 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
1365 let hash = fnv1a64(&bytes);
1366 if GOLDEN == SENTINEL {
1367 eprintln!("render_scene_two_chunk_x_grid_hash_is_stable: capture hash = 0x{hash:016x}");
1370 panic!("GOLDEN is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN above");
1371 }
1372 assert_eq!(
1373 hash, GOLDEN,
1374 "2-chunk render hash drifted: expected 0x{GOLDEN:016x}, got 0x{hash:016x}"
1375 );
1376 }
1377
1378 const SENTINEL: u64 = 0xDEAD_BEEF_DEAD_BEEF;
1382
1383 #[test]
1402 fn approach_b_renders_two_chunk_x_stripe_via_chunk_grid() {
1403 const SENTINEL_B: u64 = 0xDEAD_BEEF_DEAD_BEEF;
1404 const GOLDEN_B: u64 = 0x5ee1_e81c_66a8_d1f1;
1409
1410 let mut scene = Scene::new();
1411 let id = scene.add_grid(GridTransform::identity());
1412 let g = scene.grid_mut(id).unwrap();
1413 g.set_rect(
1416 IVec3::new(0, 0, 200),
1417 IVec3::new(127, 127, 205),
1418 Some(0x80_44_44_aa),
1419 );
1420 g.set_rect(
1423 IVec3::new(160, 50, 150),
1424 IVec3::new(170, 60, 165),
1425 Some(0x80_aa_55_22),
1426 );
1427 assert_eq!(g.chunk_count(), 2);
1428
1429 let backing = g.chunk_xyz_backing().expect("at least one chunk populated");
1431 assert_eq!(backing.chunks_x, 2);
1432 assert_eq!(backing.chunks_y, 1);
1433 assert_eq!(backing.origin_chunk_xy, [0, 0]);
1434 let cg = roxlap_core::ChunkGrid {
1435 chunks: &backing.chunks,
1436 origin_chunk_xy: backing.origin_chunk_xy,
1437 origin_chunk_z: backing.origin_chunk_z,
1438 chunks_x: backing.chunks_x,
1439 chunks_y: backing.chunks_y,
1440 chunks_z: backing.chunks_z,
1441 };
1442 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
1443
1444 let camera = Camera {
1447 pos: [10.0, 64.0, 160.0],
1448 right: [0.0, 1.0, 0.0],
1449 down: [0.0, 0.0, 1.0],
1450 forward: [1.0, 0.0, 0.0],
1451 };
1452 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
1453 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1454 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
1455 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
1456 drop(rasterizer);
1457 assert_eq!(outcome, OpticastOutcome::Rendered);
1458
1459 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
1461 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
1462 assert!(
1463 floor_count > 1000,
1464 "floor not visible — only {floor_count} floor pixels (single-chunk path?)"
1465 );
1466 assert!(
1467 box_count > 50,
1468 "box in chunk (1, 0) not visible — only {box_count} box pixels — cross-chunk DDA may have failed to fire"
1469 );
1470
1471 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
1473 let hash = fnv1a64(&bytes);
1474 if GOLDEN_B == SENTINEL_B {
1475 eprintln!("approach_b_renders_two_chunk_x_stripe_via_chunk_grid: capture hash = 0x{hash:016x}");
1476 panic!(
1477 "GOLDEN_B is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN_B above"
1478 );
1479 }
1480 assert_eq!(
1481 hash, GOLDEN_B,
1482 "Approach B 2-chunk render hash drifted: expected 0x{GOLDEN_B:016x}, got 0x{hash:016x}"
1483 );
1484 }
1485
1486 #[test]
1499 fn approach_b_camera_in_chunk_1_0_renders_neighbour() {
1500 let mut scene = Scene::new();
1501 let id = scene.add_grid(GridTransform::identity());
1502 let g = scene.grid_mut(id).unwrap();
1503 g.set_rect(
1505 IVec3::new(128, 0, 200),
1506 IVec3::new(255, 127, 205),
1507 Some(0x80_44_44_aa),
1508 );
1509 g.set_rect(
1513 IVec3::new(20, 50, 150),
1514 IVec3::new(30, 60, 165),
1515 Some(0x80_aa_55_22),
1516 );
1517 assert_eq!(g.chunk_count(), 2);
1518
1519 let backing = g.chunk_xyz_backing().expect("populated");
1520 let cg = roxlap_core::ChunkGrid {
1521 chunks: &backing.chunks,
1522 origin_chunk_xy: backing.origin_chunk_xy,
1523 origin_chunk_z: backing.origin_chunk_z,
1524 chunks_x: backing.chunks_x,
1525 chunks_y: backing.chunks_y,
1526 chunks_z: backing.chunks_z,
1527 };
1528 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
1529 let (aabb_min, aabb_max) = grid_view.aabb_xy();
1530 assert_eq!(aabb_min, [0, 0]);
1531 assert_eq!(aabb_max, [256, 128]);
1532
1533 let camera = Camera {
1537 pos: [200.0, 64.0, 160.0],
1538 right: [0.0, -1.0, 0.0],
1539 down: [0.0, 0.0, 1.0],
1540 forward: [-1.0, 0.0, 0.0],
1541 };
1542 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
1543 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1544 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
1545 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
1546 drop(rasterizer);
1547 assert_eq!(outcome, OpticastOutcome::Rendered);
1548
1549 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
1550 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
1551 assert!(
1552 floor_count > 1000,
1553 "floor under camera in chunk (1, 0) not visible — only {floor_count} floor pixels — in_bounds_xy fix may not have taken effect"
1554 );
1555 assert!(
1556 box_count > 50,
1557 "box in chunk (0, 0) not visible — only {box_count} box pixels — westward cross-chunk DDA failed"
1558 );
1559 }
1560
1561 #[test]
1570 fn stacked_two_chunk_z_camera_in_chz1_sees_own_chunk_floor() {
1571 let mut scene = Scene::new();
1572 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1573 let g = scene.grid_mut(id).unwrap();
1574 g.ensure_chunk(IVec3::new(0, 0, 0));
1576 g.set_rect(
1578 IVec3::new(60, 60, 306),
1579 IVec3::new(72, 72, 310),
1580 Some(0x80_33_66_99),
1581 );
1582 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
1583
1584 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1585 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1586 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1587 pool.set_treat_z_max_as_air(true);
1588 let camera = Camera {
1592 pos: [66.0, 66.0, 280.0],
1593 right: [1.0, 0.0, 0.0],
1594 down: [0.0, 1.0, 0.0],
1595 forward: [0.0, 0.0, 1.0],
1596 };
1597 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1598 let outcome = render_scene_composed(
1599 &mut fb,
1600 &mut zb,
1601 XRES as usize,
1602 XRES,
1603 YRES,
1604 &mut pool,
1605 &mut scene,
1606 &camera,
1607 &settings,
1608 sky_color,
1609 None,
1610 );
1611 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1612 let floor_count = fb.iter().filter(|&&p| p == 0x80_33_66_99).count();
1613 assert!(
1614 floor_count > 100,
1615 "camera at chz=1 with floor in same chunk should see it — got {floor_count} floor pixels"
1616 );
1617 }
1618
1619 #[test]
1628 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor() {
1629 let mut scene = Scene::new();
1630 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1631 let g = scene.grid_mut(id).unwrap();
1632 g.ensure_chunk(IVec3::new(0, 0, 0));
1635 g.set_rect(
1637 IVec3::new(60, 60, 306),
1638 IVec3::new(72, 72, 310),
1639 Some(0x80_77_aa_44),
1640 );
1641 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
1642
1643 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1644 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1645 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1646 pool.set_treat_z_max_as_air(true);
1647 let camera = Camera {
1651 pos: [66.0, 66.0, 100.0],
1652 right: [1.0, 0.0, 0.0],
1653 down: [0.0, 1.0, 0.0],
1654 forward: [0.0, 0.0, 1.0],
1655 };
1656 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1657 let outcome = render_scene_composed(
1658 &mut fb,
1659 &mut zb,
1660 XRES as usize,
1661 XRES,
1662 YRES,
1663 &mut pool,
1664 &mut scene,
1665 &camera,
1666 &settings,
1667 sky_color,
1668 None,
1669 );
1670 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1671 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
1672 assert!(
1673 floor_count > 50,
1674 "camera in chz=0 air-gap should see chz=1 floor via cross-chunk look-down — got {floor_count} floor pixels"
1675 );
1676 }
1677
1678 #[test]
1689 #[ignore = "S4B.6.l: known limitation — needs cf-splitting at chz boundaries"]
1690 fn stacked_chz0_distant_mountain_visible_from_chz0_camera() {
1691 let mut scene = Scene::new();
1692 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1693 let g = scene.grid_mut(id).unwrap();
1694 g.set_rect(
1698 IVec3::new(100, 100, 100),
1699 IVec3::new(124, 124, 200),
1700 Some(0x80_aa_55_22), );
1702 g.set_rect(
1706 IVec3::new(0, 0, 336),
1707 IVec3::new(128, 128, 360),
1708 Some(0x80_22_88_44),
1709 );
1710 g.set_rect(IVec3::new(100, 100, 336), IVec3::new(124, 124, 360), None);
1711 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
1714 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
1715
1716 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1717 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1718 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1719 pool.set_treat_z_max_as_air(true);
1720 let (sy, cy) = (std::f64::consts::FRAC_PI_4).sin_cos();
1726 let (sp, cp) = 0.72_f64.sin_cos();
1727 let camera = Camera {
1728 pos: [40.0, 40.0, 60.0],
1729 right: [-sy, cy, 0.0],
1730 down: [-cy * sp, -sy * sp, cp],
1731 forward: [cy * cp, sy * cp, sp],
1732 };
1733 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1734 let outcome = render_scene_composed(
1735 &mut fb,
1736 &mut zb,
1737 XRES as usize,
1738 XRES,
1739 YRES,
1740 &mut pool,
1741 &mut scene,
1742 &camera,
1743 &settings,
1744 sky_color,
1745 None,
1746 );
1747 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1748 let mountain_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
1749 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
1750 eprintln!("chz0-distant-mountain: mountain_chz0={mountain_count} hill_chz1={hill_count}");
1751 assert!(
1754 hill_count > 50,
1755 "expected chz=1 hills via cross-chunk look-down — got {hill_count}"
1756 );
1757 assert!(
1760 mountain_count > 50,
1761 "expected chz=0 distant mountain visible — got {mountain_count} (S4B.6.l limitation)"
1762 );
1763 }
1764
1765 #[test]
1776 fn mid_render_handoff_reveals_chz1_hills_under_mountain_camera() {
1777 let mut scene = Scene::new();
1778 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1779 let g = scene.grid_mut(id).unwrap();
1780 g.set_rect(
1783 IVec3::new(60, 60, 150),
1784 IVec3::new(72, 72, 200),
1785 Some(0x80_88_44_22), );
1787 g.set_rect(
1790 IVec3::new(0, 0, 336),
1791 IVec3::new(128, 128, 360),
1792 Some(0x80_22_88_44), );
1794 g.set_rect(IVec3::new(60, 60, 336), IVec3::new(72, 72, 360), None);
1797 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
1798 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
1799
1800 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1801 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1802 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1803 pool.set_treat_z_max_as_air(true);
1804 let camera = Camera {
1808 pos: [66.0, 66.0, 100.0],
1809 right: [1.0, 0.0, 0.0],
1810 down: [0.0, 1.0, 0.0],
1811 forward: [0.0, 0.0, 1.0],
1812 };
1813 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1814 let outcome = render_scene_composed(
1815 &mut fb,
1816 &mut zb,
1817 XRES as usize,
1818 XRES,
1819 YRES,
1820 &mut pool,
1821 &mut scene,
1822 &camera,
1823 &settings,
1824 sky_color,
1825 None,
1826 );
1827 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1828 let mountain_count = fb.iter().filter(|&&p| p == 0x80_88_44_22).count();
1829 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
1830 let mut hill_depths: Vec<f32> = fb
1838 .iter()
1839 .zip(zb.iter())
1840 .filter_map(|(&p, &d)| if p == 0x80_22_88_44 { Some(d) } else { None })
1841 .collect();
1842 hill_depths.sort_by(|a, b| a.partial_cmp(b).unwrap());
1843 let median_hill_depth = hill_depths[hill_depths.len() / 2];
1844 eprintln!(
1845 "mid-render handoff: mountain={mountain_count} hill={hill_count} median_hill_depth={median_hill_depth:.1}"
1846 );
1847 assert!(
1848 mountain_count > 50,
1849 "should see mountain peak via chz=0 — got {mountain_count} mountain pixels"
1850 );
1851 assert!(
1852 hill_count > 50,
1853 "should see chz=1 hills via mid-render handoff — got {hill_count} hill pixels"
1854 );
1855 assert!(
1856 (median_hill_depth - 236.0).abs() < 80.0,
1857 "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"
1858 );
1859 }
1860
1861 #[test]
1870 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor_multi_mip() {
1871 let mut scene = Scene::new();
1872 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1873 let g = scene.grid_mut(id).unwrap();
1874 g.ensure_chunk(IVec3::new(0, 0, 0));
1875 g.set_rect(
1876 IVec3::new(60, 60, 306),
1877 IVec3::new(72, 72, 310),
1878 Some(0x80_77_aa_44),
1879 );
1880 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
1881
1882 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1883 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1884 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1885 pool.set_treat_z_max_as_air(true);
1886 let camera = Camera {
1887 pos: [66.0, 66.0, 100.0],
1888 right: [1.0, 0.0, 0.0],
1889 down: [0.0, 1.0, 0.0],
1890 forward: [0.0, 0.0, 1.0],
1891 };
1892 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1893 settings.mip_levels = 2;
1894 settings.mip_scan_dist = 16;
1895 let outcome = render_scene_composed(
1896 &mut fb,
1897 &mut zb,
1898 XRES as usize,
1899 XRES,
1900 YRES,
1901 &mut pool,
1902 &mut scene,
1903 &camera,
1904 &settings,
1905 sky_color,
1906 None,
1907 );
1908 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1909 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
1910 assert!(
1911 floor_count > 50,
1912 "multi-mip cross-chunk look-down should still see chz=1 floor — got {floor_count} floor pixels"
1913 );
1914 }
1915
1916 #[test]
1924 fn stacked_three_chunk_z_camera_in_chz2_sees_own_chunk_floor_multi_mip() {
1925 let mut scene = Scene::new();
1926 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1927 let g = scene.grid_mut(id).unwrap();
1928 g.ensure_chunk(IVec3::new(0, 0, 0));
1931 g.ensure_chunk(IVec3::new(0, 0, 1));
1932 g.set_rect(
1934 IVec3::new(60, 60, 562),
1935 IVec3::new(72, 72, 566),
1936 Some(0x80_aa_55_22),
1937 );
1938 assert!(g.chunk(IVec3::new(0, 0, 2)).is_some());
1939
1940 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1941 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1942 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1943 pool.set_treat_z_max_as_air(true);
1944 let camera = Camera {
1945 pos: [66.0, 66.0, 540.0],
1946 right: [1.0, 0.0, 0.0],
1947 down: [0.0, 1.0, 0.0],
1948 forward: [0.0, 0.0, 1.0],
1949 };
1950 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1952 settings.mip_levels = 2;
1953 settings.mip_scan_dist = 16;
1954 let outcome = render_scene_composed(
1955 &mut fb,
1956 &mut zb,
1957 XRES as usize,
1958 XRES,
1959 YRES,
1960 &mut pool,
1961 &mut scene,
1962 &camera,
1963 &settings,
1964 sky_color,
1965 None,
1966 );
1967 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1968 let floor_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
1969 assert!(
1970 floor_count > 100,
1971 "camera at chz=2 with floor in same chunk should see it — got {floor_count} floor pixels"
1972 );
1973 }
1974}