1#![allow(clippy::similar_names)]
38
39use glam::DVec3;
40use roxlap_core::opticast::{opticast, OpticastOutcome, OpticastSettings};
41use roxlap_core::rasterizer::ScratchPool;
42use roxlap_core::scalar_rasterizer::ScalarRasterizer;
43use roxlap_core::sky::Sky;
44use roxlap_core::Camera;
45
46use crate::billboard::{self, BillboardCache, DEFAULT_RESOLUTION as BILLBOARD_RESOLUTION};
47use crate::chunks;
48use crate::lod::Lod;
49use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
50
51const SKY_MASK_SENTINEL: u32 = 0x00_DE_AD_BE;
66
67fn world_camera_to_grid_local(camera: &Camera, transform: &GridTransform) -> Camera {
80 let inv = transform.rotation.inverse();
81 let world_offset = DVec3::from_array(camera.pos) - transform.origin;
82 let local_pos = inv * world_offset;
83 let local_right = inv * DVec3::from_array(camera.right);
84 let local_down = inv * DVec3::from_array(camera.down);
85 let local_forward = inv * DVec3::from_array(camera.forward);
86 Camera {
87 pos: local_pos.to_array(),
88 right: local_right.to_array(),
89 down: local_down.to_array(),
90 forward: local_forward.to_array(),
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum RenderOutcome {
97 Rendered {
99 grids_drawn: usize,
102 },
103 Empty,
107}
108
109fn single_chunk_fast_path<'a>(
129 backing: &'a chunks::ChunkXyBacking<'a>,
130 cg: &'a roxlap_core::ChunkGrid<'a>,
131) -> roxlap_core::GridView<'a> {
132 if backing.chunks_x == 1
133 && backing.chunks_y == 1
134 && backing.chunks_z == 1
135 && backing.origin_chunk_xy == [0, 0]
136 && backing.origin_chunk_z == 0
137 {
138 if let Some(single) = backing.chunks[0] {
142 return single;
143 }
144 }
145 roxlap_core::GridView::from_chunk_grid(cg, CHUNK_SIZE_XY)
146}
147
148#[allow(clippy::too_many_arguments)]
157pub fn render_scene(
158 fb: &mut [u32],
159 zb: &mut [f32],
160 pitch_pixels: usize,
161 width: u32,
162 height: u32,
163 pool: &mut ScratchPool,
164 scene: &mut Scene,
165 camera: &Camera,
166 settings: &OpticastSettings,
167 sky: Option<&Sky>,
168) -> RenderOutcome {
169 debug_assert_eq!(fb.len(), zb.len());
170 let pixel_count = (width as usize) * (height as usize);
171 debug_assert_eq!(fb.len(), pixel_count);
172
173 let mut grids_drawn = 0usize;
174 for (_id, grid) in scene.grids_mut() {
175 let Some(backing) = grid.chunk_xyz_backing() else {
186 continue;
188 };
189 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
190 let cg = roxlap_core::ChunkGrid {
191 chunks: &backing.chunks,
192 origin_chunk_xy: backing.origin_chunk_xy,
193 origin_chunk_z: backing.origin_chunk_z,
194 chunks_x: backing.chunks_x,
195 chunks_y: backing.chunks_y,
196 chunks_z: backing.chunks_z,
197 };
198 let grid_view = single_chunk_fast_path(&backing, &cg);
199 let outcome = {
200 let mut rasterizer = ScalarRasterizer::new(fb, zb, pitch_pixels, grid_view);
201 if let Some(sky_ref) = sky {
202 rasterizer = rasterizer.with_sky(sky_ref);
203 }
204 opticast(&mut rasterizer, pool, &local_cam, settings, grid_view)
205 };
206 if outcome == OpticastOutcome::Rendered {
207 grids_drawn += 1;
208 }
209 }
210 if grids_drawn == 0 {
211 RenderOutcome::Empty
212 } else {
213 RenderOutcome::Rendered { grids_drawn }
214 }
215}
216
217pub fn compose_into(
230 shared_fb: &mut [u32],
231 shared_zb: &mut [f32],
232 temp_fb: &[u32],
233 temp_zb: &[f32],
234) {
235 debug_assert_eq!(shared_fb.len(), shared_zb.len());
236 debug_assert_eq!(shared_fb.len(), temp_fb.len());
237 debug_assert_eq!(shared_fb.len(), temp_zb.len());
238 for i in 0..shared_fb.len() {
239 if temp_zb[i] < shared_zb[i] {
240 shared_fb[i] = temp_fb[i];
241 shared_zb[i] = temp_zb[i];
242 }
243 }
244}
245
246#[derive(Clone, Copy, Debug)]
251struct ScreenRect {
252 x0: u32,
253 x1: u32,
254 y0: u32,
255 y1: u32,
256}
257
258impl ScreenRect {
259 fn is_empty(self) -> bool {
260 self.x0 >= self.x1 || self.y0 >= self.y1
261 }
262}
263
264fn project_sphere_to_screen(
281 camera: &Camera,
282 centre: DVec3,
283 radius: f64,
284 settings: &OpticastSettings,
285) -> Option<ScreenRect> {
286 let d = centre - DVec3::from_array(camera.pos);
287 let z = d.dot(DVec3::from_array(camera.forward));
288 if z <= radius {
289 return None; }
291 let x = d.dot(DVec3::from_array(camera.right));
292 let y = d.dot(DVec3::from_array(camera.down));
293 let (hx, hy, hz) = (
294 f64::from(settings.hx),
295 f64::from(settings.hy),
296 f64::from(settings.hz),
297 );
298 let sr = hz * radius / (z - radius); let sx = hx + x / z * hz;
300 let sy = hy + y / z * hz;
301 let pad = f64::from(settings.anginc) + 1.0;
302 let (xres, yres) = (f64::from(settings.xres), f64::from(settings.yres));
303 let clamp = |v: f64, hi: f64| v.clamp(0.0, hi);
304 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
305 Some(ScreenRect {
306 x0: clamp((sx - sr - pad).floor(), xres) as u32,
307 x1: clamp((sx + sr + pad).ceil(), xres) as u32,
308 y0: clamp((sy - sr - pad).floor(), yres) as u32,
309 y1: clamp((sy + sr + pad).ceil(), yres) as u32,
310 })
311}
312
313fn fill_rect_u32(buf: &mut [u32], pitch: usize, rect: ScreenRect, val: u32) {
316 for y in rect.y0..rect.y1 {
317 let row = y as usize * pitch;
318 buf[row + rect.x0 as usize..row + rect.x1 as usize].fill(val);
319 }
320}
321
322fn fill_rect_f32(buf: &mut [f32], pitch: usize, rect: ScreenRect, val: f32) {
324 for y in rect.y0..rect.y1 {
325 let row = y as usize * pitch;
326 buf[row + rect.x0 as usize..row + rect.x1 as usize].fill(val);
327 }
328}
329
330fn compose_rect(
334 fb: &mut [u32],
335 zb: &mut [f32],
336 temp_fb: &[u32],
337 temp_zb: &[f32],
338 pitch: usize,
339 rect: ScreenRect,
340) {
341 for y in rect.y0..rect.y1 {
342 let row = y as usize * pitch;
343 for i in row + rect.x0 as usize..row + rect.x1 as usize {
344 if temp_zb[i] < zb[i] {
345 zb[i] = temp_zb[i];
346 fb[i] = temp_fb[i];
347 }
348 }
349 }
350}
351
352#[allow(clippy::too_many_arguments)]
383pub fn render_scene_composed(
384 fb: &mut [u32],
385 zb: &mut [f32],
386 pitch_pixels: usize,
387 width: u32,
388 height: u32,
389 pool: &mut ScratchPool,
390 scene: &mut Scene,
391 camera: &Camera,
392 settings: &OpticastSettings,
393 sky_color: u32,
394 sky: Option<&Sky>,
395) -> RenderOutcome {
396 render_scene_composed_scissored(
397 fb,
398 zb,
399 pitch_pixels,
400 width,
401 height,
402 pool,
403 scene,
404 camera,
405 settings,
406 sky_color,
407 sky,
408 true,
409 )
410}
411
412#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
419fn render_scene_composed_scissored(
420 fb: &mut [u32],
421 zb: &mut [f32],
422 pitch_pixels: usize,
423 width: u32,
424 height: u32,
425 pool: &mut ScratchPool,
426 scene: &mut Scene,
427 camera: &Camera,
428 settings: &OpticastSettings,
429 sky_color: u32,
430 sky: Option<&Sky>,
431 scissor: bool,
432) -> RenderOutcome {
433 debug_assert_eq!(fb.len(), zb.len());
434 let pixel_count = (width as usize) * (height as usize);
435 debug_assert_eq!(fb.len(), pixel_count);
436
437 let mut grids_drawn = 0usize;
438 let mut temp_fb = vec![sky_color; pixel_count];
439 let mut temp_zb = vec![f32::INFINITY; pixel_count];
440
441 for (_id, grid) in scene.grids_mut() {
442 let lod = grid.select_lod(DVec3::from_array(camera.pos));
462
463 if lod == Lod::Far {
464 if grid.chunks.is_empty() {
470 continue;
471 }
472 if grid.billboards.is_none() {
475 let cache = BillboardCache::build(grid, BILLBOARD_RESOLUTION);
476 grid.billboards = Some(cache);
477 }
478 let bounds = billboard::grid_bounds(grid);
481 let centre_world = grid.transform.origin + grid.transform.rotation * bounds.centre;
482 let cam_pos = DVec3::from_array(camera.pos);
486 let centre_to_cam_world = cam_pos - centre_world;
487 let ctc_len = centre_to_cam_world.length();
488 if !ctc_len.is_finite() || ctc_len < 1e-9 {
489 continue;
493 }
494 let query_dir_world = centre_to_cam_world / ctc_len;
495 let query_dir_local = grid.transform.rotation.inverse() * query_dir_world;
496 let cache = grid.billboards.as_ref().unwrap();
498 let snapshot = cache
501 .pick_nearest(query_dir_local)
502 .expect("billboard cache populated above");
503 billboard::billboard_blit_into(
504 fb,
505 zb,
506 pitch_pixels,
507 width,
508 height,
509 snapshot,
510 centre_world,
511 bounds.radius,
512 camera,
513 settings,
514 );
515 grids_drawn += 1;
516 continue;
517 }
518
519 let Some(backing) = grid.chunk_xyz_backing() else {
525 continue;
526 };
527
528 let bounds = billboard::grid_bounds(grid);
541 let centre_world = grid.transform.origin + grid.transform.rotation * bounds.centre;
542 let cam_pos = DVec3::from_array(camera.pos);
543 let dist_to_centre = (centre_world - cam_pos).length();
544 if dist_to_centre - bounds.radius > f64::from(settings.max_scan_dist) {
545 continue;
546 }
547
548 let full_rect = ScreenRect {
563 x0: 0,
564 x1: width,
565 y0: 0,
566 y1: height,
567 };
568 let rect = if scissor {
569 match project_sphere_to_screen(camera, centre_world, bounds.radius, settings) {
570 Some(r) if r.is_empty() => continue,
572 Some(r) => ScreenRect {
575 x0: 0,
576 x1: width,
577 y0: r.y0,
578 y1: r.y1,
579 },
580 None => full_rect,
581 }
582 } else {
583 full_rect
584 };
585
586 let owns_sky = grid.render_sky;
596 let local_sky_color = if owns_sky {
597 sky_color
598 } else {
599 SKY_MASK_SENTINEL
600 };
601 if !owns_sky {
602 pool.set_skycast(SKY_MASK_SENTINEL as i32, 0);
609 }
610
611 fill_rect_u32(&mut temp_fb, pitch_pixels, rect, local_sky_color);
615 fill_rect_f32(&mut temp_zb, pitch_pixels, rect, f32::INFINITY);
616
617 let local_cam = world_camera_to_grid_local(camera, &grid.transform);
618 let cg = roxlap_core::ChunkGrid {
619 chunks: &backing.chunks,
620 origin_chunk_xy: backing.origin_chunk_xy,
621 origin_chunk_z: backing.origin_chunk_z,
622 chunks_x: backing.chunks_x,
623 chunks_y: backing.chunks_y,
624 chunks_z: backing.chunks_z,
625 };
626 let grid_view = single_chunk_fast_path(&backing, &cg);
627
628 let per_grid_settings;
648 let active_settings = {
649 let base_mip_levels = settings.mip_levels;
650 let base_mip_scan = settings.mip_scan_dist;
651 let lod_mip_levels = match lod {
652 Lod::Mid => grid.lod_thresholds.mid_mip_levels,
653 Lod::Near | Lod::Far => None,
654 };
655 let lod_mip_scan = match lod {
656 Lod::Mid => grid.lod_thresholds.mid_mip_scan_dist,
657 Lod::Near | Lod::Far => None,
658 };
659 let global_mip_cap = grid.mip_levels_override;
660 let needs_override =
661 lod_mip_levels.is_some() || lod_mip_scan.is_some() || global_mip_cap.is_some();
662 if needs_override {
663 let mut mip_levels =
666 lod_mip_levels.map_or(base_mip_levels, |n| n.clamp(1, base_mip_levels));
667 if let Some(cap) = global_mip_cap {
668 mip_levels = mip_levels.min(cap.clamp(1, base_mip_levels));
669 }
670 let mip_scan_dist = lod_mip_scan.map_or(base_mip_scan, |d| base_mip_scan.min(d));
676 per_grid_settings = OpticastSettings {
677 mip_levels,
678 mip_scan_dist,
679 ..*settings
680 };
681 &per_grid_settings
682 } else {
683 settings
684 }
685 };
686
687 let outcome = {
688 let mut rasterizer =
689 ScalarRasterizer::new(&mut temp_fb, &mut temp_zb, pitch_pixels, grid_view);
690 if owns_sky {
694 if let Some(sky_ref) = sky {
695 rasterizer = rasterizer.with_sky(sky_ref);
696 }
697 }
698 let scissored = (*active_settings).with_y_range(rect.y0, rect.y1);
708 opticast(&mut rasterizer, pool, &local_cam, &scissored, grid_view)
709 };
710
711 if !owns_sky {
712 for y in rect.y0..rect.y1 {
715 let row = y as usize * pitch_pixels;
716 for i in row + rect.x0 as usize..row + rect.x1 as usize {
717 if temp_fb[i] == SKY_MASK_SENTINEL {
718 temp_zb[i] = f32::INFINITY;
719 }
720 }
721 }
722 pool.set_skycast(sky_color as i32, 0);
725 }
726
727 if outcome == OpticastOutcome::Rendered {
728 compose_rect(fb, zb, &temp_fb, &temp_zb, pitch_pixels, rect);
729 grids_drawn += 1;
730 }
731 }
732
733 if grids_drawn == 0 {
734 RenderOutcome::Empty
735 } else {
736 RenderOutcome::Rendered { grids_drawn }
737 }
738}
739
740#[cfg(test)]
741#[allow(clippy::float_cmp)]
742mod tests {
743 use super::*;
744 use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
745 use glam::{DVec3, IVec3};
746 use roxlap_core::opticast::{opticast as core_opticast, OpticastSettings};
747 use roxlap_core::rasterizer::ScratchPool;
748 use roxlap_core::scalar_rasterizer::ScalarRasterizer;
749 use roxlap_core::{Camera, Engine};
750
751 const XRES: u32 = 320;
752 const YRES: u32 = 200;
753
754 fn build_one_grid_scene(world_origin: DVec3) -> (Scene, crate::GridId) {
758 let mut scene = Scene::new();
759 let id = scene.add_grid(GridTransform::at(world_origin));
760 let grid = scene.grid_mut(id).unwrap();
761 grid.set_rect(
763 IVec3::new(40, 40, 40),
764 IVec3::new(55, 55, 55),
765 Some(0x80_88_88_88),
766 );
767 grid.set_sphere(IVec3::new(80, 80, 80), 6, Some(0x80_22_aa_22));
769 (scene, id)
770 }
771
772 fn camera_at(pos: [f64; 3]) -> Camera {
773 Camera {
776 pos,
777 right: [-1.0, 0.0, 0.0],
778 down: [0.0, 0.0, 1.0],
779 forward: [0.0, 1.0, 0.0],
780 }
781 }
782
783 fn render_setup(pool_vsid: u32) -> (Engine, ScratchPool, Vec<u32>, Vec<f32>) {
787 let engine = Engine::new();
788 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
789 let sky = engine.sky_color();
790 let sky_col_i = i32::from_ne_bytes(sky.to_ne_bytes());
791 pool.set_skycast(sky_col_i, 0);
792 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
793 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
794 pool.set_treat_z_max_as_air(true);
795 let pixel_count = (XRES as usize) * (YRES as usize);
796 let framebuffer = vec![sky; pixel_count];
797 let zbuffer = vec![0.0f32; pixel_count];
798 (engine, pool, framebuffer, zbuffer)
799 }
800
801 fn render_via_scene(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
804 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
805 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
806 let outcome = render_scene(
807 &mut fb,
808 &mut zb,
809 XRES as usize,
810 XRES,
811 YRES,
812 &mut pool,
813 scene,
814 camera,
815 &settings,
816 None,
817 );
818 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
819 fb
820 }
821
822 fn render_via_direct_opticast(scene: &Scene, local_camera: &Camera) -> Vec<u32> {
826 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
827 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
828 let grid = scene.grids().next().unwrap().1;
829 let chunk = grid.chunk(IVec3::ZERO).unwrap();
830 let grid_view = roxlap_core::GridView::from_single_vxl(chunk);
831 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
832 let _ = core_opticast(
833 &mut rasterizer,
834 &mut pool,
835 local_camera,
836 &settings,
837 grid_view,
838 );
839 drop(rasterizer);
840 fb
841 }
842
843 #[test]
848 fn world_camera_to_grid_local_identity_rotation_translates_pos_only() {
849 let camera = Camera {
850 pos: [110.0, 220.0, 330.0],
851 right: [1.0, 0.0, 0.0],
852 down: [0.0, 0.0, 1.0],
853 forward: [0.0, 1.0, 0.0],
854 };
855 let transform = GridTransform::at(DVec3::new(100.0, 200.0, 300.0));
856 let local = super::world_camera_to_grid_local(&camera, &transform);
857 assert_eq!(local.right, camera.right);
859 assert_eq!(local.down, camera.down);
860 assert_eq!(local.forward, camera.forward);
861 for (got, want) in local.pos.iter().zip([10.0, 20.0, 30.0].iter()) {
863 assert!((got - want).abs() < 1e-12, "pos got={got} want={want}");
864 }
865 }
866
867 #[test]
871 fn world_camera_to_grid_local_90deg_z_rotates_basis_and_pos() {
872 use glam::DQuat;
873 let camera = Camera {
874 pos: [0.0, 10.0, 0.0],
875 right: [1.0, 0.0, 0.0],
876 down: [0.0, 0.0, 1.0],
877 forward: [0.0, 1.0, 0.0],
878 };
879 let transform = GridTransform {
880 origin: DVec3::ZERO,
881 rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
882 };
883 let local = super::world_camera_to_grid_local(&camera, &transform);
884 let approx_eq =
886 |a: [f64; 3], b: [f64; 3]| a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-9);
887 assert!(
888 approx_eq(local.pos, [10.0, 0.0, 0.0]),
889 "pos={:?} expected ~(10, 0, 0)",
890 local.pos
891 );
892 assert!(
894 approx_eq(local.right, [0.0, -1.0, 0.0]),
895 "right={:?} expected ~(0, -1, 0)",
896 local.right
897 );
898 assert!(
900 approx_eq(local.down, [0.0, 0.0, 1.0]),
901 "down={:?} expected ~(0, 0, 1)",
902 local.down
903 );
904 assert!(
906 approx_eq(local.forward, [1.0, 0.0, 0.0]),
907 "forward={:?} expected ~(1, 0, 0)",
908 local.forward
909 );
910 }
911
912 #[test]
917 fn world_camera_to_grid_local_preserves_basis_orthonormality() {
918 use glam::DQuat;
919 let camera = Camera {
922 pos: [3.0, -5.0, 7.0],
923 right: [-1.0, 0.0, 0.0],
924 down: [0.0, 0.0, 1.0],
925 forward: [0.0, 1.0, 0.0],
926 };
927 let transform = GridTransform {
928 origin: DVec3::new(1.0, 2.0, 3.0),
929 rotation: DQuat::from_axis_angle(glam::DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
930 };
931 let local = super::world_camera_to_grid_local(&camera, &transform);
932 let r = DVec3::from_array(local.right);
933 let d = DVec3::from_array(local.down);
934 let f = DVec3::from_array(local.forward);
935 for v in [r, d, f] {
937 assert!(
938 (v.length_squared() - 1.0).abs() < 1e-12,
939 "basis vec {v:?} not unit length"
940 );
941 }
942 assert!(r.dot(d).abs() < 1e-12, "right·down = {}", r.dot(d));
944 assert!(r.dot(f).abs() < 1e-12, "right·forward = {}", r.dot(f));
945 assert!(d.dot(f).abs() < 1e-12, "down·forward = {}", d.dot(f));
946 let cross = r.cross(d);
948 assert!(
949 (cross - f).length() < 1e-12,
950 "right×down={cross:?} forward={f:?}"
951 );
952 }
953
954 fn build_one_grid_marker_scene(transform: GridTransform) -> (Scene, crate::GridId, u32) {
962 let mut scene = Scene::new();
963 let id = scene.add_grid(transform);
964 let grid = scene.grid_mut(id).unwrap();
965 grid.set_rect(
967 IVec3::new(40, 40, 40),
968 IVec3::new(55, 55, 55),
969 Some(0x80_55_aa_22), );
971 (scene, id, 0x80_55_aa_22)
972 }
973
974 #[test]
987 fn s5_1_180deg_z_rotated_grid_byte_identical_to_axis_aligned() {
988 use glam::DQuat;
989 let axis_aligned_camera = Camera {
991 pos: [40.0, -20.0, 50.0],
992 right: [-1.0, 0.0, 0.0],
993 down: [0.0, 0.0, 1.0],
994 forward: [0.0, 1.0, 0.0],
995 };
996 let rotated_camera = Camera {
998 pos: [-40.0, 20.0, 50.0],
999 right: [1.0, 0.0, 0.0],
1000 down: [0.0, 0.0, 1.0],
1001 forward: [0.0, -1.0, 0.0],
1002 };
1003 let q = DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0);
1008 let rot_pos = q * DVec3::from_array(axis_aligned_camera.pos);
1009 let rot_fwd = q * DVec3::from_array(axis_aligned_camera.forward);
1010 assert_eq!(rot_pos.to_array(), rotated_camera.pos);
1011 assert_eq!(rot_fwd.to_array(), rotated_camera.forward);
1012
1013 let (mut scene_a, _, _) = build_one_grid_marker_scene(GridTransform::identity());
1014 let fb_a = render_via_scene(&mut scene_a, &axis_aligned_camera);
1015
1016 let (mut scene_b, _, _) = build_one_grid_marker_scene(GridTransform {
1017 origin: DVec3::ZERO,
1018 rotation: q,
1019 });
1020 let fb_b = render_via_scene(&mut scene_b, &rotated_camera);
1021
1022 assert_eq!(
1023 fb_a, fb_b,
1024 "rotating both grid and camera by R about the grid origin must leave the framebuffer unchanged"
1025 );
1026 }
1027
1028 #[test]
1035 fn s5_1_45deg_z_rotated_grid_renders_marker() {
1036 use glam::DQuat;
1037 let rotation = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4);
1038 let (mut scene, _, marker) = build_one_grid_marker_scene(GridTransform {
1039 origin: DVec3::ZERO,
1040 rotation,
1041 });
1042
1043 let marker_world = rotation * DVec3::new(47.5, 47.5, 47.5);
1048 let camera = Camera {
1051 pos: [marker_world.x, marker_world.y - 80.0, marker_world.z],
1052 right: [-1.0, 0.0, 0.0],
1053 down: [0.0, 0.0, 1.0],
1054 forward: [0.0, 1.0, 0.0],
1055 };
1056
1057 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
1058 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1059 let outcome = render_scene(
1060 &mut fb,
1061 &mut zb,
1062 XRES as usize,
1063 XRES,
1064 YRES,
1065 &mut pool,
1066 &mut scene,
1067 &camera,
1068 &settings,
1069 None,
1070 );
1071 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1072 let marker_count = fb.iter().filter(|&&p| p == marker).count();
1073 assert!(
1074 marker_count > 50,
1075 "45°-rotated marker box should be visible — got {marker_count} marker pixels"
1076 );
1077 }
1078
1079 #[test]
1091 fn render_sky_false_drops_grid_sky_pixels() {
1092 use crate::{GridId, GridTransform};
1093
1094 let mut scene = Scene::new();
1097 let _b_id: GridId = scene.add_grid(GridTransform::at(DVec3::new(0.0, 600.0, 0.0)));
1098 let b_id = scene.grids().next().unwrap().0;
1101 scene.grid_mut(b_id).unwrap().set_rect(
1102 IVec3::new(0, 0, 100),
1103 IVec3::new(127, 127, 110),
1104 Some(0x80_22_88_22), );
1106
1107 let a_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1111 scene.grid_mut(a_id).unwrap().set_rect(
1112 IVec3::new(60, 60, 60),
1113 IVec3::new(67, 67, 67),
1114 Some(0x80_aa_22_22), );
1116 scene.grid_mut(a_id).unwrap().render_sky = false;
1117
1118 let unique_sky: u32 = 0xFF_AB_CD_EF;
1119 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
1120 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
1121 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1122 let camera = camera_at([64.0, 0.0, 100.0]);
1123 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1124 let outcome = render_scene_composed(
1125 &mut fb,
1126 &mut zb,
1127 XRES as usize,
1128 XRES,
1129 YRES,
1130 &mut pool,
1131 &mut scene,
1132 &camera,
1133 &settings,
1134 unique_sky,
1135 None,
1136 );
1137 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1138
1139 let leaked = fb
1143 .iter()
1144 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
1145 .count();
1146 assert_eq!(
1147 leaked, 0,
1148 "SKY_MASK_SENTINEL leaked into composed framebuffer ({leaked} pixels)"
1149 );
1150 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_22_22).count();
1153 assert!(
1154 red_count > 0,
1155 "red cube from sky-disabled grid A is missing — render_sky=false should only mask sky"
1156 );
1157 let green_count = fb.iter().filter(|&&p| p == 0x80_22_88_22).count();
1160 assert!(
1161 green_count > 0,
1162 "grid B's floor invisible — grid A's masked sky may have overwritten it"
1163 );
1164 }
1165
1166 #[test]
1170 fn render_sky_false_single_grid_no_sentinel_leak() {
1171 let (mut scene, id, _) = build_one_grid_marker_scene(GridTransform::identity());
1172 scene.grid_mut(id).unwrap().render_sky = false;
1173 let unique_sky: u32 = 0xFF_12_34_56;
1174 let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
1175 let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
1176 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1177 let camera = camera_at([64.0, 0.0, 64.0]);
1178 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1179 let outcome = render_scene_composed(
1180 &mut fb,
1181 &mut zb,
1182 XRES as usize,
1183 XRES,
1184 YRES,
1185 &mut pool,
1186 &mut scene,
1187 &camera,
1188 &settings,
1189 unique_sky,
1190 None,
1191 );
1192 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1193 let leaked = fb
1194 .iter()
1195 .filter(|&&p| p == super::SKY_MASK_SENTINEL)
1196 .count();
1197 assert_eq!(leaked, 0, "SKY_MASK_SENTINEL leaked ({leaked} pixels)");
1198 let prefill_count = fb.iter().filter(|&&p| p == unique_sky).count();
1201 assert!(
1202 prefill_count > 0,
1203 "no pre-fill pixels survived — render_sky=false should leave non-hit pixels untouched"
1204 );
1205 }
1206
1207 #[test]
1208 fn render_scene_at_origin_matches_direct_opticast() {
1209 let (mut scene, _) = build_one_grid_scene(DVec3::ZERO);
1215 let cam = camera_at([64.0, 0.0, 64.0]);
1216 let via_scene = render_via_scene(&mut scene, &cam);
1217 let via_direct = render_via_direct_opticast(&scene, &cam);
1218 assert_eq!(
1219 via_scene, via_direct,
1220 "render_scene with single 1-chunk grid at origin should match direct opticast"
1221 );
1222 }
1223
1224 #[test]
1225 fn render_scene_translated_grid_matches_grid_local_opticast() {
1226 let world_origin = DVec3::new(1000.0, 2000.0, 3000.0);
1231 let (mut scene, _) = build_one_grid_scene(world_origin);
1232 let world_cam = camera_at([1064.0, 2000.0, 3064.0]);
1233 let local_cam = camera_at([64.0, 0.0, 64.0]);
1234 let via_scene = render_via_scene(&mut scene, &world_cam);
1235 let via_direct = render_via_direct_opticast(&scene, &local_cam);
1236 assert_eq!(
1237 via_scene, via_direct,
1238 "render_scene of translated grid should match opticast with grid-local camera"
1239 );
1240 }
1241
1242 #[test]
1243 fn empty_scene_returns_empty_outcome() {
1244 let mut scene = Scene::new();
1245 let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
1246 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1247 let outcome = render_scene(
1248 &mut fb,
1249 &mut zb,
1250 XRES as usize,
1251 XRES,
1252 YRES,
1253 &mut pool,
1254 &mut scene,
1255 &camera_at([0.0, 0.0, 0.0]),
1256 &settings,
1257 None,
1258 );
1259 assert_eq!(outcome, RenderOutcome::Empty);
1260 }
1261
1262 fn build_two_grid_side_by_side() -> (Scene, u32, u32) {
1270 let mut scene = Scene::new();
1271 let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1273 scene.grid_mut(g0).unwrap().set_rect(
1274 IVec3::new(56, 56, 92),
1275 IVec3::new(71, 71, 107),
1276 Some(0x80_88_22_22), );
1278 let _g1 = scene.add_grid(GridTransform::at(DVec3::new(200.0, 200.0, 0.0)));
1280 let g1_id = scene
1282 .grids()
1283 .filter(|(id, _)| *id != g0)
1284 .map(|(id, _)| id)
1285 .next()
1286 .unwrap();
1287 scene.grid_mut(g1_id).unwrap().set_rect(
1288 IVec3::new(56, 56, 92),
1289 IVec3::new(71, 71, 107),
1290 Some(0x80_22_22_88), );
1292 (scene, 0x80_88_22_22, 0x80_22_22_88)
1293 }
1294
1295 fn make_composed_pool(pool_vsid: u32) -> (Engine, ScratchPool, u32) {
1296 let engine = Engine::new();
1297 let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
1298 let sky_color = engine.sky_color();
1299 let sky_col_i = i32::from_ne_bytes(sky_color.to_ne_bytes());
1300 pool.set_skycast(sky_col_i, 0);
1301 let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
1302 pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
1303 pool.set_treat_z_max_as_air(true);
1304 (engine, pool, sky_color)
1305 }
1306
1307 fn pixel_count(width: u32, height: u32) -> usize {
1308 (width as usize) * (height as usize)
1309 }
1310
1311 #[test]
1312 fn compose_into_takes_smaller_z() {
1313 let mut shared_fb = vec![0xff_ff_ff_ff_u32; 4];
1314 let mut shared_zb = vec![10.0f32; 4];
1315 let temp_fb = [0xaa_aa_aa_aa, 0x11_22_33_44, 0x55_66_77_88, 0xde_ad_be_ef];
1316 let temp_zb = [5.0f32, 20.0, 10.0, f32::INFINITY];
1317 compose_into(&mut shared_fb, &mut shared_zb, &temp_fb, &temp_zb);
1318 assert_eq!(shared_fb[0], 0xaa_aa_aa_aa);
1320 assert_eq!(shared_zb[0], 5.0);
1321 assert_eq!(shared_fb[1], 0xff_ff_ff_ff);
1323 assert_eq!(shared_zb[1], 10.0);
1324 assert_eq!(shared_fb[2], 0xff_ff_ff_ff);
1326 assert_eq!(shared_fb[3], 0xff_ff_ff_ff);
1328 }
1329
1330 #[test]
1331 fn render_scene_composed_two_grids_both_visible() {
1332 let (mut scene, red, blue) = build_two_grid_side_by_side();
1337 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1338 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1339 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1340
1341 let camera = camera_at([160.0, 100.0, 100.0]);
1342 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1343 let outcome = render_scene_composed(
1344 &mut fb,
1345 &mut zb,
1346 XRES as usize,
1347 XRES,
1348 YRES,
1349 &mut pool,
1350 &mut scene,
1351 &camera,
1352 &settings,
1353 sky_color,
1354 None,
1355 );
1356 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1357
1358 let red_count = fb.iter().filter(|&&p| p == red).count();
1360 let blue_count = fb.iter().filter(|&&p| p == blue).count();
1361 assert!(
1362 red_count > 0,
1363 "no red pixels: grid 0 (red box) not visible after compose"
1364 );
1365 assert!(
1366 blue_count > 0,
1367 "no blue pixels: grid 1 (blue box) not visible after compose"
1368 );
1369 }
1370
1371 #[test]
1381 fn scissor_render_is_byte_identical_to_full_frame() {
1382 let (mut scene, red, blue) = build_two_grid_side_by_side();
1383 let g2 = scene.add_grid(GridTransform::at(DVec3::new(700.0, 130.0, 0.0)));
1388 let g2_id = scene
1389 .grids()
1390 .map(|(id, _)| id)
1391 .max_by_key(|id| id.raw())
1392 .unwrap();
1393 let _ = g2;
1394 scene.grid_mut(g2_id).unwrap().set_rect(
1395 IVec3::new(56, 56, 92),
1396 IVec3::new(71, 71, 107),
1397 Some(0x80_22_88_22), );
1399
1400 let camera = camera_at([160.0, 100.0, 100.0]);
1401 let render = |scene: &mut Scene, scissor: bool| -> Vec<u32> {
1402 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1403 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1404 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1405 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1406 render_scene_composed_scissored(
1407 &mut fb,
1408 &mut zb,
1409 XRES as usize,
1410 XRES,
1411 YRES,
1412 &mut pool,
1413 scene,
1414 &camera,
1415 &settings,
1416 sky_color,
1417 None,
1418 scissor,
1419 );
1420 fb
1421 };
1422
1423 let scissored = render(&mut scene, true);
1424 let full = render(&mut scene, false);
1425 assert_eq!(
1426 scissored, full,
1427 "the screen scissor changed the framebuffer — it must be a pure speed-up",
1428 );
1429 assert!(scissored.iter().any(|&p| p == red || p == blue));
1432 assert!(
1433 !scissored.contains(&0x80_22_88_22),
1434 "off-screen grid leaked pixels",
1435 );
1436 }
1437
1438 #[test]
1439 fn render_scene_composed_grid_a_in_front_of_grid_b() {
1440 let mut scene = Scene::new();
1444 let g_a = scene.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1445 scene.grid_mut(g_a).unwrap().set_rect(
1446 IVec3::new(56, 56, 92),
1447 IVec3::new(71, 71, 107),
1448 Some(0x80_aa_00_00), );
1450 let _g_b = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1451 let g_b_id = scene
1452 .grids()
1453 .filter(|(id, _)| *id != g_a)
1454 .map(|(id, _)| id)
1455 .next()
1456 .unwrap();
1457 scene.grid_mut(g_b_id).unwrap().set_rect(
1458 IVec3::new(56, 56, 92),
1459 IVec3::new(71, 71, 107),
1460 Some(0x80_00_00_aa), );
1462
1463 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1464 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1465 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1466
1467 let camera = camera_at([64.0, -10.0, 100.0]);
1470 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1471 let outcome = render_scene_composed(
1472 &mut fb,
1473 &mut zb,
1474 XRES as usize,
1475 XRES,
1476 YRES,
1477 &mut pool,
1478 &mut scene,
1479 &camera,
1480 &settings,
1481 sky_color,
1482 None,
1483 );
1484 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1485
1486 let red_count = fb.iter().filter(|&&p| p == 0x80_aa_00_00).count();
1490 assert!(
1491 red_count > 0,
1492 "expected red pixels (closer box should win z-test)"
1493 );
1494
1495 let mut scene2 = Scene::new();
1498 let g_b2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1499 scene2.grid_mut(g_b2).unwrap().set_rect(
1500 IVec3::new(56, 56, 92),
1501 IVec3::new(71, 71, 107),
1502 Some(0x80_00_00_aa),
1503 );
1504 let g_a2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1505 scene2.grid_mut(g_a2).unwrap().set_rect(
1506 IVec3::new(56, 56, 92),
1507 IVec3::new(71, 71, 107),
1508 Some(0x80_aa_00_00),
1509 );
1510
1511 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1512 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1513 let outcome2 = render_scene_composed(
1514 &mut fb2,
1515 &mut zb2,
1516 XRES as usize,
1517 XRES,
1518 YRES,
1519 &mut pool,
1520 &mut scene2,
1521 &camera,
1522 &settings,
1523 sky_color,
1524 None,
1525 );
1526 assert_eq!(outcome2, RenderOutcome::Rendered { grids_drawn: 2 });
1527 assert_eq!(
1528 fb, fb2,
1529 "composition should be order-independent — same scene in different add order should produce identical output"
1530 );
1531 }
1532
1533 fn build_mip_visible_grid(world_origin: DVec3) -> (Scene, crate::GridId) {
1548 let mut scene = Scene::new();
1549 let id = scene.add_grid(GridTransform::at(world_origin));
1550 let grid = scene.grid_mut(id).unwrap();
1551 grid.set_rect(
1553 IVec3::new(0, 0, 100),
1554 IVec3::new(127, 127, 254),
1555 Some(0x80_88_88_88),
1556 );
1557 grid.chunk_mut(IVec3::ZERO).unwrap().generate_mips(3);
1559 (scene, id)
1560 }
1561
1562 fn fb_hash(fb: &[u32]) -> u64 {
1564 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1565 for px in fb {
1566 for b in px.to_le_bytes() {
1567 h ^= u64::from(b);
1568 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1569 }
1570 }
1571 h
1572 }
1573
1574 fn render_with_multi_mip(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
1579 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1580 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1581 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1582 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1583 settings.mip_levels = 3;
1584 settings.mip_scan_dist = 32;
1585 let outcome = render_scene_composed(
1586 &mut fb,
1587 &mut zb,
1588 XRES as usize,
1589 XRES,
1590 YRES,
1591 &mut pool,
1592 scene,
1593 camera,
1594 &settings,
1595 sky_color,
1596 None,
1597 );
1598 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1599 fb
1600 }
1601
1602 #[test]
1607 fn s6_1_mid_overrides_produce_different_framebuffer_than_near() {
1608 let camera = camera_at([64.0, 0.0, 64.0]);
1610
1611 let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1614 let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1615
1616 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1624 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1625 r_near: 0.0,
1627 r_mid: f64::INFINITY,
1628 mid_mip_levels: Some(1),
1629 mid_mip_scan_dist: None,
1630 };
1631 let lod = scene_b
1633 .grid(b_id)
1634 .unwrap()
1635 .select_lod(DVec3::from_array(camera.pos));
1636 assert_eq!(lod, Lod::Mid, "expected Mid tier for forced thresholds");
1637 let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1638
1639 let (_engine, _, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1642 let non_sky_near = fb_near.iter().filter(|&&p| p != sky_color).count();
1643 let non_sky_mid = fb_mid.iter().filter(|&&p| p != sky_color).count();
1644 assert!(
1645 non_sky_near > 100,
1646 "Near render too sparse ({non_sky_near})"
1647 );
1648 assert!(non_sky_mid > 100, "Mid render too sparse ({non_sky_mid})");
1649
1650 let h_near = fb_hash(&fb_near);
1654 let h_mid = fb_hash(&fb_mid);
1655 assert_ne!(
1656 h_near, h_mid,
1657 "Mid tier with mid_mip_levels=Some(1) must differ from Near (h_near={h_near:016x})"
1658 );
1659 }
1660
1661 #[test]
1667 fn s6_1_mid_without_overrides_byte_identical_to_near() {
1668 let camera = camera_at([64.0, 0.0, 64.0]);
1669
1670 let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1672 let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1673
1674 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1676 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1677 r_near: 0.0,
1678 r_mid: f64::INFINITY,
1679 mid_mip_levels: None,
1680 mid_mip_scan_dist: None,
1681 };
1682 let lod = scene_b
1683 .grid(b_id)
1684 .unwrap()
1685 .select_lod(DVec3::from_array(camera.pos));
1686 assert_eq!(lod, Lod::Mid);
1687 let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1688
1689 assert_eq!(
1691 fb_near, fb_mid,
1692 "Mid with both overrides=None must byte-match Near"
1693 );
1694 }
1695
1696 #[test]
1708 fn s6_1_global_mip_cap_survives_mid_tier() {
1709 let camera = camera_at([64.0, 0.0, 64.0]);
1710
1711 let (mut scene_a, a_id) = build_mip_visible_grid(DVec3::ZERO);
1713 scene_a.grid_mut(a_id).unwrap().mip_levels_override = Some(1);
1714 let fb_a = render_with_multi_mip(&mut scene_a, &camera);
1715
1716 let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1720 scene_b.grid_mut(b_id).unwrap().mip_levels_override = Some(1);
1721 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1722 r_near: 0.0,
1723 r_mid: f64::INFINITY,
1724 mid_mip_levels: Some(4),
1725 mid_mip_scan_dist: None,
1728 };
1729 let fb_b = render_with_multi_mip(&mut scene_b, &camera);
1730
1731 assert_eq!(
1732 fb_a, fb_b,
1733 "global mip_levels_override should clamp Mid override (ship workaround survives Mid tier)"
1734 );
1735 }
1736
1737 #[test]
1745 fn s6_3_far_tier_blits_non_sky_pixels() {
1746 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1747 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1748 r_near: 0.0,
1749 r_mid: 0.0,
1750 mid_mip_levels: None,
1751 mid_mip_scan_dist: None,
1752 };
1753
1754 let camera = camera_at([64.0, 0.0, 100.0]);
1755 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1756 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1757 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1758 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1759 let outcome = render_scene_composed(
1760 &mut fb,
1761 &mut zb,
1762 XRES as usize,
1763 XRES,
1764 YRES,
1765 &mut pool,
1766 &mut scene,
1767 &camera,
1768 &settings,
1769 sky_color,
1770 None,
1771 );
1772 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1773
1774 let lod = scene
1776 .grid(id)
1777 .unwrap()
1778 .select_lod(DVec3::from_array(camera.pos));
1779 assert_eq!(lod, Lod::Far);
1780
1781 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1783 assert!(
1784 non_sky > 0,
1785 "Far-tier render produced no non-sky pixels — billboard blit not firing"
1786 );
1787 }
1788
1789 #[test]
1792 fn s6_3_far_render_lazily_populates_cache() {
1793 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1794 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1795 r_near: 0.0,
1796 r_mid: 0.0,
1797 mid_mip_levels: None,
1798 mid_mip_scan_dist: None,
1799 };
1800 assert!(scene.grid(id).unwrap().billboards.is_none());
1801
1802 let camera = camera_at([64.0, 0.0, 100.0]);
1803 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1804 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1805 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1806 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1807 let _ = render_scene_composed(
1808 &mut fb,
1809 &mut zb,
1810 XRES as usize,
1811 XRES,
1812 YRES,
1813 &mut pool,
1814 &mut scene,
1815 &camera,
1816 &settings,
1817 sky_color,
1818 None,
1819 );
1820 let cache = scene
1821 .grid(id)
1822 .unwrap()
1823 .billboards
1824 .as_ref()
1825 .expect("Far render should have populated billboards");
1826 assert_eq!(cache.len(), 26);
1827 }
1828
1829 #[test]
1831 fn s6_3_edit_invalidates_then_far_render_rebuilds() {
1832 let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1833 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1834 r_near: 0.0,
1835 r_mid: 0.0,
1836 mid_mip_levels: None,
1837 mid_mip_scan_dist: None,
1838 };
1839 let camera = camera_at([64.0, 0.0, 100.0]);
1840 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1841 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1842
1843 let mut fb1 = vec![sky_color; pixel_count(XRES, YRES)];
1845 let mut zb1 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1846 let _ = render_scene_composed(
1847 &mut fb1,
1848 &mut zb1,
1849 XRES as usize,
1850 XRES,
1851 YRES,
1852 &mut pool,
1853 &mut scene,
1854 &camera,
1855 &settings,
1856 sky_color,
1857 None,
1858 );
1859 assert!(scene.grid(id).unwrap().billboards.is_some());
1860
1861 scene
1863 .grid_mut(id)
1864 .unwrap()
1865 .set_voxel(IVec3::new(70, 70, 70), Some(0x80_aa_aa_22));
1866 assert!(scene.grid(id).unwrap().billboards.is_none());
1867
1868 let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1870 let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1871 let _ = render_scene_composed(
1872 &mut fb2,
1873 &mut zb2,
1874 XRES as usize,
1875 XRES,
1876 YRES,
1877 &mut pool,
1878 &mut scene,
1879 &camera,
1880 &settings,
1881 sky_color,
1882 None,
1883 );
1884 assert!(scene.grid(id).unwrap().billboards.is_some());
1885 }
1886
1887 #[test]
1892 fn s6_3_near_and_far_grids_in_same_scene() {
1893 let mut scene = Scene::new();
1894 let a_id = scene.add_grid(GridTransform::at(DVec3::new(-100.0, 200.0, 0.0)));
1897 scene.grid_mut(a_id).unwrap().set_rect(
1898 IVec3::new(70, 0, 50),
1899 IVec3::new(85, 15, 70),
1900 Some(0x80_22_88_22), );
1902 let b_id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1904 scene.grid_mut(b_id).unwrap().set_rect(
1905 IVec3::new(0, 0, 80),
1906 IVec3::new(20, 20, 110),
1907 Some(0x80_aa_22_22), );
1909 scene.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1910 r_near: 0.0,
1911 r_mid: 0.0,
1912 mid_mip_levels: None,
1913 mid_mip_scan_dist: None,
1914 };
1915
1916 let camera = camera_at([0.0, 0.0, 80.0]);
1917 assert_eq!(
1919 scene
1920 .grid(a_id)
1921 .unwrap()
1922 .select_lod(DVec3::from_array(camera.pos)),
1923 Lod::Near
1924 );
1925 assert_eq!(
1926 scene
1927 .grid(b_id)
1928 .unwrap()
1929 .select_lod(DVec3::from_array(camera.pos)),
1930 Lod::Far
1931 );
1932
1933 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1934 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1935 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1936 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1937 let outcome = render_scene_composed(
1938 &mut fb,
1939 &mut zb,
1940 XRES as usize,
1941 XRES,
1942 YRES,
1943 &mut pool,
1944 &mut scene,
1945 &camera,
1946 &settings,
1947 sky_color,
1948 None,
1949 );
1950 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1951
1952 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1954 assert!(
1955 non_sky > 20,
1956 "hybrid scene produced too few non-sky pixels ({non_sky}); one tier may have failed"
1957 );
1958 }
1959
1960 #[test]
1963 fn s6_3_empty_grid_at_far_is_skipped() {
1964 let mut scene = Scene::new();
1965 let id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1966 scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1967 r_near: 0.0,
1968 r_mid: 0.0,
1969 mid_mip_levels: None,
1970 mid_mip_scan_dist: None,
1971 };
1972
1973 let camera = camera_at([0.0, 0.0, 100.0]);
1974 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1975 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1976 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1977 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1978 let outcome = render_scene_composed(
1979 &mut fb,
1980 &mut zb,
1981 XRES as usize,
1982 XRES,
1983 YRES,
1984 &mut pool,
1985 &mut scene,
1986 &camera,
1987 &settings,
1988 sky_color,
1989 None,
1990 );
1991 assert_eq!(outcome, RenderOutcome::Empty);
1993 assert!(scene.grid(id).unwrap().billboards.is_none());
1995 assert!(fb.iter().all(|&p| p == sky_color));
1997 }
1998
1999 #[test]
2008 fn render_scene_composed_lod_threshold_invariance() {
2009 let (mut scene_a, _a_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
2011 let cam = camera_at([64.0, 0.0, 100.0]);
2012 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2013 let mut fb_a = vec![sky_color; pixel_count(XRES, YRES)];
2014 let mut zb_a = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2015 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2016 let outcome_a = render_scene_composed(
2017 &mut fb_a,
2018 &mut zb_a,
2019 XRES as usize,
2020 XRES,
2021 YRES,
2022 &mut pool,
2023 &mut scene_a,
2024 &cam,
2025 &settings,
2026 sky_color,
2027 None,
2028 );
2029 assert_eq!(outcome_a, RenderOutcome::Rendered { grids_drawn: 1 });
2030
2031 let (mut scene_b, b_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
2036 let radius = scene_b.grid(b_id).unwrap().bounding_radius();
2037 assert!(
2038 radius > 0.0,
2039 "bounding_radius should be > 0 for a populated grid"
2040 );
2041 scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds::from_radius(radius);
2042 let lod = scene_b
2045 .grid(b_id)
2046 .unwrap()
2047 .select_lod(DVec3::from_array(cam.pos));
2048 assert_ne!(
2049 lod,
2050 Lod::Near,
2051 "camera should land in Mid or Far for derived thresholds — got {lod:?}",
2052 );
2053
2054 let mut fb_b = vec![sky_color; pixel_count(XRES, YRES)];
2055 let mut zb_b = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2056 let outcome_b = render_scene_composed(
2057 &mut fb_b,
2058 &mut zb_b,
2059 XRES as usize,
2060 XRES,
2061 YRES,
2062 &mut pool,
2063 &mut scene_b,
2064 &cam,
2065 &settings,
2066 sky_color,
2067 None,
2068 );
2069 assert_eq!(outcome_b, RenderOutcome::Rendered { grids_drawn: 1 });
2070
2071 assert_eq!(
2074 fb_a, fb_b,
2075 "S6.0 framebuffer must be byte-identical regardless of LOD thresholds"
2076 );
2077 }
2078
2079 #[test]
2080 fn render_scene_composed_empty_scene_returns_empty() {
2081 let mut scene = Scene::new();
2082 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2083 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2084 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2085 let camera = camera_at([0.0, 0.0, 0.0]);
2086 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2087 let outcome = render_scene_composed(
2088 &mut fb,
2089 &mut zb,
2090 XRES as usize,
2091 XRES,
2092 YRES,
2093 &mut pool,
2094 &mut scene,
2095 &camera,
2096 &settings,
2097 sky_color,
2098 None,
2099 );
2100 assert_eq!(outcome, RenderOutcome::Empty);
2101 assert!(fb.iter().all(|&p| p == sky_color));
2103 }
2104
2105 fn fnv1a64(data: &[u8]) -> u64 {
2110 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
2111 for &b in data {
2112 h ^= u64::from(b);
2113 h = h.wrapping_mul(0x0000_0100_0000_01b3);
2114 }
2115 h
2116 }
2117
2118 #[test]
2124 fn render_scene_two_chunk_x_grid_no_seam() {
2125 let mut scene = Scene::new();
2126 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
2127 let g = scene.grid_mut(id).unwrap();
2128 g.set_rect(
2134 IVec3::new(120, 60, 200),
2135 IVec3::new(136, 67, 215),
2136 Some(0x80_aa_55_22),
2137 );
2138 assert_eq!(g.chunk_count(), 2);
2140
2141 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2145 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2146 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2147 let camera = camera_at([128.0, 100.0, 207.0]);
2148 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2149 let outcome = render_scene_composed(
2150 &mut fb,
2151 &mut zb,
2152 XRES as usize,
2153 XRES,
2154 YRES,
2155 &mut pool,
2156 &mut scene,
2157 &camera,
2158 &settings,
2159 sky_color,
2160 None,
2161 );
2162 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2163
2164 let stripe = 0x80_aa_55_22;
2168 let stripe_count = fb.iter().filter(|&&p| p == stripe).count();
2169 assert!(
2170 stripe_count > 200,
2171 "stripe rendered too few pixels ({stripe_count}) — chunks may not be stitching"
2172 );
2173
2174 let centre_y = (YRES / 2) as usize;
2178 let row_start = centre_y * (XRES as usize);
2179 let row = &fb[row_start..row_start + (XRES as usize)];
2180 let mut in_stripe = false;
2181 let mut seam_gaps = 0usize;
2182 for &px in row {
2183 if px == stripe {
2184 in_stripe = true;
2185 } else if in_stripe && px == sky_color {
2186 if row.iter().skip_while(|&&p| p != px).any(|&p| p == stripe) {
2189 seam_gaps += 1;
2191 }
2192 in_stripe = false;
2193 }
2194 }
2195 assert!(
2199 seam_gaps <= 1,
2200 "centre row has {seam_gaps} disjoint stripe runs — expected 1 (chunk-edge seam suspected)"
2201 );
2202 }
2203
2204 #[test]
2220 fn vxl_generate_mips_on_set_voxel_chunk_renders() {
2221 let mut grid = crate::Grid::new(GridTransform::identity());
2222 grid.set_rect(
2225 IVec3::new(0, 0, 100),
2226 IVec3::new(127, 127, 254),
2227 Some(0x80_88_88_88),
2228 );
2229 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
2230 chunk.generate_mips(3);
2231 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2232 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2233 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2234 let camera = camera_at([64.0, 0.0, 64.0]);
2235 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2236 settings.mip_levels = 3;
2237 settings.mip_scan_dist = 32;
2238 let grid_view = roxlap_core::GridView::from_single_vxl(chunk);
2239 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2240 let _ = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2241 drop(rasterizer);
2242 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
2243 assert!(
2244 non_sky > 0,
2245 "Vxl::generate_mips on a set_voxel-built chunk should render to something non-sky (got {non_sky})"
2246 );
2247 }
2248
2249 #[test]
2254 fn render_with_mips_present_still_renders_mip0() {
2255 let mut scene = Scene::new();
2256 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2257 scene.grid_mut(id).unwrap().set_rect(
2258 IVec3::new(40, 40, 40),
2259 IVec3::new(55, 55, 55),
2260 Some(0x80_88_88_88),
2261 );
2262 {
2268 let grid = scene.grid_mut(id).unwrap();
2269 let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
2270 chunk.generate_mips(3);
2271 }
2272
2273 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2274 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2275 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2276 let camera = camera_at([64.0, 0.0, 64.0]);
2277 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2280 settings.mip_scan_dist = 100_000;
2281 let outcome = render_scene_composed(
2282 &mut fb,
2283 &mut zb,
2284 XRES as usize,
2285 XRES,
2286 YRES,
2287 &mut pool,
2288 &mut scene,
2289 &camera,
2290 &settings,
2291 sky_color,
2292 None,
2293 );
2294 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2295 let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
2296 assert!(
2297 non_sky > 0,
2298 "render of single-grid scene with mips present rendered all-sky: mip-0 may be corrupted by generate_mips"
2299 );
2300 }
2301
2302 #[test]
2303 fn render_scene_two_chunk_x_grid_hash_is_stable() {
2304 const GOLDEN: u64 = 0x215e_d66d_7359_4725;
2306 let mut scene = Scene::new();
2310 let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
2311 scene.grid_mut(id).unwrap().set_rect(
2312 IVec3::new(120, 60, 200),
2313 IVec3::new(136, 67, 215),
2314 Some(0x80_aa_55_22),
2315 );
2316 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2317 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2318 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2319 let camera = camera_at([128.0, 100.0, 207.0]);
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
2336 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2337 let hash = fnv1a64(&bytes);
2338 if GOLDEN == SENTINEL {
2339 eprintln!("render_scene_two_chunk_x_grid_hash_is_stable: capture hash = 0x{hash:016x}");
2342 panic!("GOLDEN is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN above");
2343 }
2344 assert_eq!(
2345 hash, GOLDEN,
2346 "2-chunk render hash drifted: expected 0x{GOLDEN:016x}, got 0x{hash:016x}"
2347 );
2348 }
2349
2350 const SENTINEL: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2354
2355 #[test]
2374 fn approach_b_renders_two_chunk_x_stripe_via_chunk_grid() {
2375 const SENTINEL_B: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2376 const GOLDEN_B: u64 = 0x5ee1_e81c_66a8_d1f1;
2381
2382 let mut scene = Scene::new();
2383 let id = scene.add_grid(GridTransform::identity());
2384 let g = scene.grid_mut(id).unwrap();
2385 g.set_rect(
2388 IVec3::new(0, 0, 200),
2389 IVec3::new(127, 127, 205),
2390 Some(0x80_44_44_aa),
2391 );
2392 g.set_rect(
2395 IVec3::new(160, 50, 150),
2396 IVec3::new(170, 60, 165),
2397 Some(0x80_aa_55_22),
2398 );
2399 assert_eq!(g.chunk_count(), 2);
2400
2401 let backing = g.chunk_xyz_backing().expect("at least one chunk populated");
2403 assert_eq!(backing.chunks_x, 2);
2404 assert_eq!(backing.chunks_y, 1);
2405 assert_eq!(backing.origin_chunk_xy, [0, 0]);
2406 let cg = roxlap_core::ChunkGrid {
2407 chunks: &backing.chunks,
2408 origin_chunk_xy: backing.origin_chunk_xy,
2409 origin_chunk_z: backing.origin_chunk_z,
2410 chunks_x: backing.chunks_x,
2411 chunks_y: backing.chunks_y,
2412 chunks_z: backing.chunks_z,
2413 };
2414 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2415
2416 let camera = Camera {
2419 pos: [10.0, 64.0, 160.0],
2420 right: [0.0, 1.0, 0.0],
2421 down: [0.0, 0.0, 1.0],
2422 forward: [1.0, 0.0, 0.0],
2423 };
2424 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2425 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2426 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2427 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2428 drop(rasterizer);
2429 assert_eq!(outcome, OpticastOutcome::Rendered);
2430
2431 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2433 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2434 assert!(
2435 floor_count > 1000,
2436 "floor not visible — only {floor_count} floor pixels (single-chunk path?)"
2437 );
2438 assert!(
2439 box_count > 50,
2440 "box in chunk (1, 0) not visible — only {box_count} box pixels — cross-chunk DDA may have failed to fire"
2441 );
2442
2443 let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2445 let hash = fnv1a64(&bytes);
2446 if GOLDEN_B == SENTINEL_B {
2447 eprintln!("approach_b_renders_two_chunk_x_stripe_via_chunk_grid: capture hash = 0x{hash:016x}");
2448 panic!(
2449 "GOLDEN_B is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN_B above"
2450 );
2451 }
2452 assert_eq!(
2453 hash, GOLDEN_B,
2454 "Approach B 2-chunk render hash drifted: expected 0x{GOLDEN_B:016x}, got 0x{hash:016x}"
2455 );
2456 }
2457
2458 #[test]
2471 fn approach_b_camera_in_chunk_1_0_renders_neighbour() {
2472 let mut scene = Scene::new();
2473 let id = scene.add_grid(GridTransform::identity());
2474 let g = scene.grid_mut(id).unwrap();
2475 g.set_rect(
2477 IVec3::new(128, 0, 200),
2478 IVec3::new(255, 127, 205),
2479 Some(0x80_44_44_aa),
2480 );
2481 g.set_rect(
2485 IVec3::new(20, 50, 150),
2486 IVec3::new(30, 60, 165),
2487 Some(0x80_aa_55_22),
2488 );
2489 assert_eq!(g.chunk_count(), 2);
2490
2491 let backing = g.chunk_xyz_backing().expect("populated");
2492 let cg = roxlap_core::ChunkGrid {
2493 chunks: &backing.chunks,
2494 origin_chunk_xy: backing.origin_chunk_xy,
2495 origin_chunk_z: backing.origin_chunk_z,
2496 chunks_x: backing.chunks_x,
2497 chunks_y: backing.chunks_y,
2498 chunks_z: backing.chunks_z,
2499 };
2500 let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2501 let (aabb_min, aabb_max) = grid_view.aabb_xy();
2502 assert_eq!(aabb_min, [0, 0]);
2503 assert_eq!(aabb_max, [256, 128]);
2504
2505 let camera = Camera {
2509 pos: [200.0, 64.0, 160.0],
2510 right: [0.0, -1.0, 0.0],
2511 down: [0.0, 0.0, 1.0],
2512 forward: [-1.0, 0.0, 0.0],
2513 };
2514 let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2515 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2516 let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2517 let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2518 drop(rasterizer);
2519 assert_eq!(outcome, OpticastOutcome::Rendered);
2520
2521 let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2522 let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2523 assert!(
2524 floor_count > 1000,
2525 "floor under camera in chunk (1, 0) not visible — only {floor_count} floor pixels — in_bounds_xy fix may not have taken effect"
2526 );
2527 assert!(
2528 box_count > 50,
2529 "box in chunk (0, 0) not visible — only {box_count} box pixels — westward cross-chunk DDA failed"
2530 );
2531 }
2532
2533 #[test]
2542 fn stacked_two_chunk_z_camera_in_chz1_sees_own_chunk_floor() {
2543 let mut scene = Scene::new();
2544 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2545 let g = scene.grid_mut(id).unwrap();
2546 g.ensure_chunk(IVec3::new(0, 0, 0));
2548 g.set_rect(
2550 IVec3::new(60, 60, 306),
2551 IVec3::new(72, 72, 310),
2552 Some(0x80_33_66_99),
2553 );
2554 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2555
2556 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2557 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2558 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2559 pool.set_treat_z_max_as_air(true);
2560 let camera = Camera {
2564 pos: [66.0, 66.0, 280.0],
2565 right: [1.0, 0.0, 0.0],
2566 down: [0.0, 1.0, 0.0],
2567 forward: [0.0, 0.0, 1.0],
2568 };
2569 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2570 let outcome = render_scene_composed(
2571 &mut fb,
2572 &mut zb,
2573 XRES as usize,
2574 XRES,
2575 YRES,
2576 &mut pool,
2577 &mut scene,
2578 &camera,
2579 &settings,
2580 sky_color,
2581 None,
2582 );
2583 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2584 let floor_count = fb.iter().filter(|&&p| p == 0x80_33_66_99).count();
2585 assert!(
2586 floor_count > 100,
2587 "camera at chz=1 with floor in same chunk should see it — got {floor_count} floor pixels"
2588 );
2589 }
2590
2591 #[test]
2600 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor() {
2601 let mut scene = Scene::new();
2602 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2603 let g = scene.grid_mut(id).unwrap();
2604 g.ensure_chunk(IVec3::new(0, 0, 0));
2607 g.set_rect(
2609 IVec3::new(60, 60, 306),
2610 IVec3::new(72, 72, 310),
2611 Some(0x80_77_aa_44),
2612 );
2613 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2614
2615 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2616 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2617 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2618 pool.set_treat_z_max_as_air(true);
2619 let camera = Camera {
2623 pos: [66.0, 66.0, 100.0],
2624 right: [1.0, 0.0, 0.0],
2625 down: [0.0, 1.0, 0.0],
2626 forward: [0.0, 0.0, 1.0],
2627 };
2628 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2629 let outcome = render_scene_composed(
2630 &mut fb,
2631 &mut zb,
2632 XRES as usize,
2633 XRES,
2634 YRES,
2635 &mut pool,
2636 &mut scene,
2637 &camera,
2638 &settings,
2639 sky_color,
2640 None,
2641 );
2642 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2643 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2644 assert!(
2645 floor_count > 50,
2646 "camera in chz=0 air-gap should see chz=1 floor via cross-chunk look-down — got {floor_count} floor pixels"
2647 );
2648 }
2649
2650 #[test]
2662 fn stacked_chz0_distant_mountain_visible_from_chz0_camera() {
2663 let mut scene = Scene::new();
2664 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2665 let g = scene.grid_mut(id).unwrap();
2666 g.set_rect(
2670 IVec3::new(100, 100, 100),
2671 IVec3::new(124, 124, 200),
2672 Some(0x80_aa_55_22), );
2674 g.set_rect(
2678 IVec3::new(0, 0, 336),
2679 IVec3::new(128, 128, 360),
2680 Some(0x80_22_88_44),
2681 );
2682 g.set_rect(IVec3::new(100, 100, 336), IVec3::new(124, 124, 360), None);
2683 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2686 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2687
2688 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2689 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2690 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2691 pool.set_treat_z_max_as_air(true);
2692 let (sy, cy) = (std::f64::consts::FRAC_PI_4).sin_cos();
2698 let (sp, cp) = 0.72_f64.sin_cos();
2699 let camera = Camera {
2700 pos: [40.0, 40.0, 60.0],
2701 right: [-sy, cy, 0.0],
2702 down: [-cy * sp, -sy * sp, cp],
2703 forward: [cy * cp, sy * cp, sp],
2704 };
2705 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2706 let outcome = render_scene_composed(
2707 &mut fb,
2708 &mut zb,
2709 XRES as usize,
2710 XRES,
2711 YRES,
2712 &mut pool,
2713 &mut scene,
2714 &camera,
2715 &settings,
2716 sky_color,
2717 None,
2718 );
2719 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2720 let mountain_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2721 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2722 eprintln!("chz0-distant-mountain: mountain_chz0={mountain_count} hill_chz1={hill_count}");
2723 assert!(
2726 hill_count > 50,
2727 "expected chz=1 hills via cross-chunk look-down — got {hill_count}"
2728 );
2729 assert!(
2732 mountain_count > 50,
2733 "expected chz=0 distant mountain visible — got {mountain_count} (S4B.6.l limitation)"
2734 );
2735 }
2736
2737 #[test]
2748 fn mid_render_handoff_reveals_chz1_hills_under_mountain_camera() {
2749 let mut scene = Scene::new();
2750 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2751 let g = scene.grid_mut(id).unwrap();
2752 g.set_rect(
2755 IVec3::new(60, 60, 150),
2756 IVec3::new(72, 72, 200),
2757 Some(0x80_88_44_22), );
2759 g.set_rect(
2762 IVec3::new(0, 0, 336),
2763 IVec3::new(128, 128, 360),
2764 Some(0x80_22_88_44), );
2766 g.set_rect(IVec3::new(60, 60, 336), IVec3::new(72, 72, 360), None);
2769 assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2770 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2771
2772 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2773 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2774 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2775 pool.set_treat_z_max_as_air(true);
2776 let camera = Camera {
2780 pos: [66.0, 66.0, 100.0],
2781 right: [1.0, 0.0, 0.0],
2782 down: [0.0, 1.0, 0.0],
2783 forward: [0.0, 0.0, 1.0],
2784 };
2785 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2786 let outcome = render_scene_composed(
2787 &mut fb,
2788 &mut zb,
2789 XRES as usize,
2790 XRES,
2791 YRES,
2792 &mut pool,
2793 &mut scene,
2794 &camera,
2795 &settings,
2796 sky_color,
2797 None,
2798 );
2799 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2800 let mountain_count = fb.iter().filter(|&&p| p == 0x80_88_44_22).count();
2801 let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2802 let mut hill_depths: Vec<f32> = fb
2810 .iter()
2811 .zip(zb.iter())
2812 .filter_map(|(&p, &d)| if p == 0x80_22_88_44 { Some(d) } else { None })
2813 .collect();
2814 hill_depths.sort_by(|a, b| a.partial_cmp(b).unwrap());
2815 let median_hill_depth = hill_depths[hill_depths.len() / 2];
2816 eprintln!(
2817 "mid-render handoff: mountain={mountain_count} hill={hill_count} median_hill_depth={median_hill_depth:.1}"
2818 );
2819 assert!(
2820 mountain_count > 50,
2821 "should see mountain peak via chz=0 — got {mountain_count} mountain pixels"
2822 );
2823 assert!(
2824 hill_count > 50,
2825 "should see chz=1 hills via mid-render handoff — got {hill_count} hill pixels"
2826 );
2827 assert!(
2828 (median_hill_depth - 236.0).abs() < 80.0,
2829 "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"
2830 );
2831 }
2832
2833 #[test]
2842 fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor_multi_mip() {
2843 let mut scene = Scene::new();
2844 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2845 let g = scene.grid_mut(id).unwrap();
2846 g.ensure_chunk(IVec3::new(0, 0, 0));
2847 g.set_rect(
2848 IVec3::new(60, 60, 306),
2849 IVec3::new(72, 72, 310),
2850 Some(0x80_77_aa_44),
2851 );
2852 assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2853
2854 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2855 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2856 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2857 pool.set_treat_z_max_as_air(true);
2858 let camera = Camera {
2859 pos: [66.0, 66.0, 100.0],
2860 right: [1.0, 0.0, 0.0],
2861 down: [0.0, 1.0, 0.0],
2862 forward: [0.0, 0.0, 1.0],
2863 };
2864 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2865 settings.mip_levels = 2;
2866 settings.mip_scan_dist = 16;
2867 let outcome = render_scene_composed(
2868 &mut fb,
2869 &mut zb,
2870 XRES as usize,
2871 XRES,
2872 YRES,
2873 &mut pool,
2874 &mut scene,
2875 &camera,
2876 &settings,
2877 sky_color,
2878 None,
2879 );
2880 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2881 let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2882 assert!(
2883 floor_count > 50,
2884 "multi-mip cross-chunk look-down should still see chz=1 floor — got {floor_count} floor pixels"
2885 );
2886 }
2887
2888 #[test]
2896 fn stacked_three_chunk_z_camera_in_chz2_sees_own_chunk_floor_multi_mip() {
2897 let mut scene = Scene::new();
2898 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2899 let g = scene.grid_mut(id).unwrap();
2900 g.ensure_chunk(IVec3::new(0, 0, 0));
2903 g.ensure_chunk(IVec3::new(0, 0, 1));
2904 g.set_rect(
2906 IVec3::new(60, 60, 562),
2907 IVec3::new(72, 72, 566),
2908 Some(0x80_aa_55_22),
2909 );
2910 assert!(g.chunk(IVec3::new(0, 0, 2)).is_some());
2911
2912 let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2913 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2914 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2915 pool.set_treat_z_max_as_air(true);
2916 let camera = Camera {
2917 pos: [66.0, 66.0, 540.0],
2918 right: [1.0, 0.0, 0.0],
2919 down: [0.0, 1.0, 0.0],
2920 forward: [0.0, 0.0, 1.0],
2921 };
2922 let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2924 settings.mip_levels = 2;
2925 settings.mip_scan_dist = 16;
2926 let outcome = render_scene_composed(
2927 &mut fb,
2928 &mut zb,
2929 XRES as usize,
2930 XRES,
2931 YRES,
2932 &mut pool,
2933 &mut scene,
2934 &camera,
2935 &settings,
2936 sky_color,
2937 None,
2938 );
2939 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2940 let floor_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2941 assert!(
2942 floor_count > 100,
2943 "camera at chz=2 with floor in same chunk should see it — got {floor_count} floor pixels"
2944 );
2945 }
2946
2947 #[derive(Debug)]
2955 struct FloorGenerator;
2956
2957 impl crate::ChunkGenerator for FloorGenerator {
2958 fn generate(&self, _chunk_idx: IVec3) -> roxlap_formats::vxl::Vxl {
2959 let mut tmp = crate::Grid::new(GridTransform::identity());
2963 tmp.ensure_chunk(IVec3::ZERO);
2964 let mut vxl = tmp.chunks.remove(&IVec3::ZERO).unwrap();
2965 #[allow(clippy::cast_possible_wrap)]
2966 roxlap_formats::edit::set_rect(
2967 &mut vxl,
2968 glam::IVec3::new(0, 0, 230).into(),
2969 glam::IVec3::new((CHUNK_SIZE_XY - 1) as i32, (CHUNK_SIZE_XY - 1) as i32, 239)
2970 .into(),
2971 Some(0x80_22_aa_22),
2972 );
2973 vxl
2974 }
2975 }
2976
2977 #[test]
2978 fn render_scene_composed_unpumped_streaming_grid_renders_all_sky() {
2979 use std::sync::Arc;
2985 let mut scene = Scene::new();
2986 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2987 let g = scene.grid_mut(id).unwrap();
2988 g.set_generator(Some(Arc::new(FloorGenerator)));
2989 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2990 assert!(g.chunks.is_empty(), "no pump yet → no chunks");
2991
2992 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2993 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2994 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2995 let camera = camera_at([64.0, -100.0, 200.0]);
2998 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2999 let _ = render_scene_composed(
3000 &mut fb,
3001 &mut zb,
3002 XRES as usize,
3003 XRES,
3004 YRES,
3005 &mut pool,
3006 &mut scene,
3007 &camera,
3008 &settings,
3009 sky_color,
3010 None,
3011 );
3012 assert!(
3014 fb.iter().all(|&p| p == sky_color),
3015 "unpumped streaming grid must render as all sky"
3016 );
3017 }
3018
3019 #[test]
3020 fn render_scene_composed_picks_up_streamed_chunks_after_sync_pump() {
3021 use std::sync::Arc;
3026 let mut scene = Scene::new();
3027 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
3028 let g = scene.grid_mut(id).unwrap();
3029 g.set_generator(Some(Arc::new(FloorGenerator)));
3030 g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
3032
3033 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
3035 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
3036 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
3037 let camera = camera_at([64.0, -100.0, 200.0]);
3038 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
3039 let _ = render_scene_composed(
3040 &mut fb,
3041 &mut zb,
3042 XRES as usize,
3043 XRES,
3044 YRES,
3045 &mut pool,
3046 &mut scene,
3047 &camera,
3048 &settings,
3049 sky_color,
3050 None,
3051 );
3052 let pre_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
3053 assert_eq!(pre_floor, 0, "pre-pump frame has no streamed chunks");
3054
3055 scene.pump_streaming_sync(DVec3::new(64.0, -100.0, 200.0));
3058 let g = scene.grid(id).unwrap();
3059 assert!(
3060 !g.chunks.is_empty(),
3061 "pump should have streamed at least one chunk"
3062 );
3063
3064 fb.iter_mut().for_each(|p| *p = sky_color);
3067 zb.iter_mut().for_each(|z| *z = f32::INFINITY);
3068 let outcome = render_scene_composed(
3069 &mut fb,
3070 &mut zb,
3071 XRES as usize,
3072 XRES,
3073 YRES,
3074 &mut pool,
3075 &mut scene,
3076 &camera,
3077 &settings,
3078 sky_color,
3079 None,
3080 );
3081 assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
3082 let post_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
3083 assert!(
3084 post_floor > 100,
3085 "post-pump frame should show the streamed floor — got {post_floor} green pixels"
3086 );
3087 }
3088
3089 #[test]
3090 fn render_scene_composed_partial_streaming_renders_pending_chunks_as_air() {
3091 use std::sync::Arc;
3099 let mut scene = Scene::new();
3100 let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
3101 let g = scene.grid_mut(id).unwrap();
3102 g.set_generator(Some(Arc::new(FloorGenerator)));
3103 g.stream_radius = crate::StreamRadius::new(400.0, 800.0);
3106
3107 let installed = g.ensure_chunk_generated(IVec3::ZERO);
3110 assert!(installed, "manual install of one chunk");
3111 assert_eq!(g.chunks.len(), 1);
3112 assert!(g.chunk(IVec3::new(0, 1, 0)).is_none());
3114 assert!(g.chunk(IVec3::new(0, 2, 0)).is_none());
3115
3116 let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
3117 let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
3118 let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
3119 let camera = camera_at([64.0, 32.0, 200.0]);
3124 let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
3125 let _ = render_scene_composed(
3126 &mut fb,
3127 &mut zb,
3128 XRES as usize,
3129 XRES,
3130 YRES,
3131 &mut pool,
3132 &mut scene,
3133 &camera,
3134 &settings,
3135 sky_color,
3136 None,
3137 );
3138 let floor_pixels = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
3139 assert!(
3144 floor_pixels > 0,
3145 "should see at least some floor from the loaded chunk"
3146 );
3147 scene.pump_streaming_sync(DVec3::new(64.0, 32.0, 200.0));
3150 assert!(scene.grid(id).unwrap().chunk_count() >= 2);
3151 fb.iter_mut().for_each(|p| *p = sky_color);
3152 zb.iter_mut().for_each(|z| *z = f32::INFINITY);
3153 let _ = render_scene_composed(
3154 &mut fb,
3155 &mut zb,
3156 XRES as usize,
3157 XRES,
3158 YRES,
3159 &mut pool,
3160 &mut scene,
3161 &camera,
3162 &settings,
3163 sky_color,
3164 None,
3165 );
3166 let floor_pixels_full = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
3167 assert!(
3168 floor_pixels_full > floor_pixels,
3169 "fully-streamed scene should show more floor than partial: \
3170 partial={floor_pixels} full={floor_pixels_full}"
3171 );
3172 }
3173}