Skip to main content

roxlap_scene/
render.rs

1//! Scene-level rendering — drives [`roxlap_core::opticast::opticast`]
2//! across the grids of a [`Scene`].
3//!
4//! Two entry points:
5//!
6//! - [`render_scene_composed`] (recommended for multi-grid scenes):
7//!   per grid, allocates a temporary framebuffer + zbuffer, runs
8//!   opticast into the temp, then merges into the shared output via
9//!   per-pixel min-z. Correctly composites overlapping grid output.
10//! - [`render_scene`] (single-grid trusting caller): writes every
11//!   grid directly into the shared rasterizer. For single-grid
12//!   scenes this matches a direct opticast call byte-for-byte; for
13//!   multi-grid it's last-grid-wins (sky writes from grid B
14//!   overwrite grid A's hits). Useful for tests / single-grid
15//!   sanity checks.
16//!
17//! ## S4B.2.e: Approach B multi-chunk dispatch
18//!
19//! Both APIs route per-grid rendering through
20//! [`crate::Grid::chunk_xy_backing`] → [`roxlap_core::ChunkGrid`] →
21//! [`roxlap_core::GridView::from_chunk_grid`] → [`opticast`].
22//! `opticast`'s prelude looks up the camera's chunk via
23//! [`roxlap_core::GridView::chunk_at_xy`]; the grouscan column-step
24//! swaps the active per-chunk `(slab_buf, column_offsets)` when
25//! rays cross a chunk-XY boundary. The combined-world stitch
26//! (Approach C, S4.0..S4.2) is no longer in the render path — the
27//! lighting bake still uses it until S4B.4 lands a per-chunk bake.
28//!
29//! Per-grid rotation (S5) and per-grid LOD (S6) plug in at the
30//! same dispatch point: rotate the world camera into grid-local
31//! before the chunk-grid lookup, then dispatch coarse / fine /
32//! billboard based on grid-camera distance.
33
34// `fb` / `zb` (framebuffer / zbuffer) and the `_fb` / `_zb` suffixes
35// throughout this module are voxlap-canonical pairs — drilling them
36// apart with longer names just hurts readability.
37#![allow(clippy::similar_names)]
38
39use glam::DVec3;
40use roxlap_core::opticast::{opticast, OpticastOutcome, OpticastSettings};
41use roxlap_core::rasterizer::ScratchPool;
42use roxlap_core::scalar_rasterizer::ScalarRasterizer;
43use roxlap_core::sky::Sky;
44use roxlap_core::Camera;
45
46use crate::billboard::{self, BillboardCache, DEFAULT_RESOLUTION as BILLBOARD_RESOLUTION};
47use crate::lod::Lod;
48use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
49
50/// Sentinel colour stamped into a `render_sky = false` grid's
51/// temporary framebuffer wherever the rasterizer would have drawn
52/// sky. After opticast, [`render_scene_composed`] walks the temp
53/// buffer and resets `temp_zb` to [`f32::INFINITY`] for any pixel
54/// still carrying this value — those pixels then always lose
55/// [`compose_into`]'s min-z test and the underlying grid's sky
56/// (or another grid's hit) wins.
57///
58/// Alpha byte is `0x00`. Voxlap voxel slabs carry an alpha-encoded
59/// shade in `[0x00, 0x80]`, but a `0x00` alpha **with this exact
60/// RGB pattern** is exceedingly unlikely to occur on a real hit
61/// (the lit-voxel path produces alpha ≥ 0x40 in practice). Bit
62/// pattern is also visually distinct (cyan-ish neon) if anything
63/// ever leaks through to the screen, making the bug obvious.
64const SKY_MASK_SENTINEL: u32 = 0x00_DE_AD_BE;
65
66/// Project a world-space [`Camera`] into a grid's local frame:
67/// translate by `-transform.origin`, then apply
68/// `transform.rotation.inverse()` to the position and the
69/// orthonormal basis (`right` / `down` / `forward`).
70///
71/// Identity rotation collapses to pure translation, byte-identical
72/// to the pre-S5 path (`DQuat::IDENTITY * v == v`). For a rotated
73/// grid the rasterizer still sees an axis-aligned chunk grid —
74/// rotation is invisible below this layer per PORTING-SCENE.md § S5.
75///
76/// The basis is rotated as a free vector (no translation
77/// component); position is rotated about the grid origin.
78fn world_camera_to_grid_local(camera: &Camera, transform: &GridTransform) -> Camera {
79    let inv = transform.rotation.inverse();
80    let world_offset = DVec3::from_array(camera.pos) - transform.origin;
81    let local_pos = inv * world_offset;
82    let local_right = inv * DVec3::from_array(camera.right);
83    let local_down = inv * DVec3::from_array(camera.down);
84    let local_forward = inv * DVec3::from_array(camera.forward);
85    Camera {
86        pos: local_pos.to_array(),
87        right: local_right.to_array(),
88        down: local_down.to_array(),
89        forward: local_forward.to_array(),
90    }
91}
92
93/// Outcome of a [`render_scene`] / [`render_scene_composed`] call.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum RenderOutcome {
96    /// At least one grid produced a render.
97    Rendered {
98        /// Number of grids whose opticast pass returned
99        /// [`OpticastOutcome::Rendered`].
100        grids_drawn: usize,
101    },
102    /// No grid rendered. Either the scene was empty or every
103    /// per-grid opticast call returned
104    /// [`OpticastOutcome::SkippedCameraInSolid`].
105    Empty,
106}
107
108/// Render every grid in `scene` directly into `(fb, zb)` — no
109/// per-grid temp buffer, no compose merge. For multi-grid scenes
110/// this is last-grid-wins (later grids' opticast writes overwrite
111/// earlier grids' pixels indiscriminately, including sky), so it's
112/// only correct for single-grid scenes.
113///
114/// Use this when you have one grid and want the byte-stable
115/// matches-direct-opticast property — the test suite uses it as a
116/// sanity check that the combined-world stitch + render harness
117/// doesn't drift vs. a raw [`opticast`] call.
118///
119/// Caller pre-fills `fb` with the desired sky colour and `zb` with
120/// any value (typically `0.0` matching the per-chunk renderer's
121/// convention or `f32::INFINITY` for compose-friendly init); the
122/// rasterizer overwrites both per pixel that gets a hit.
123#[allow(clippy::too_many_arguments)]
124pub fn render_scene(
125    fb: &mut [u32],
126    zb: &mut [f32],
127    pitch_pixels: usize,
128    width: u32,
129    height: u32,
130    pool: &mut ScratchPool,
131    scene: &mut Scene,
132    camera: &Camera,
133    settings: &OpticastSettings,
134    sky: Option<&Sky>,
135) -> RenderOutcome {
136    debug_assert_eq!(fb.len(), zb.len());
137    let pixel_count = (width as usize) * (height as usize);
138    debug_assert_eq!(fb.len(), pixel_count);
139
140    let mut grids_drawn = 0usize;
141    for (_id, grid) in scene.grids_mut() {
142        // S4B.2.e: Approach B render path. World → grid-local
143        // camera transform doesn't need a voxel-offset adjustment
144        // anymore — Approach B's chunks live at their signed
145        // (chx, chy) indices and `chunk_at_xy` handles negative-
146        // index lookups natively.
147        //
148        // S5.0: per-grid arbitrary rotation. The local camera is
149        // built by `world_camera_to_grid_local` — translation +
150        // inverse-rotation of the basis. Identity rotation keeps
151        // this byte-identical to the pre-S5 translate-only form.
152        let Some(backing) = grid.chunk_xyz_backing() else {
153            // Empty grid (no populated chz=0 chunks) — skip.
154            continue;
155        };
156        let local_cam = world_camera_to_grid_local(camera, &grid.transform);
157        let cg = roxlap_core::ChunkGrid {
158            chunks: &backing.chunks,
159            origin_chunk_xy: backing.origin_chunk_xy,
160            origin_chunk_z: backing.origin_chunk_z,
161            chunks_x: backing.chunks_x,
162            chunks_y: backing.chunks_y,
163            chunks_z: backing.chunks_z,
164        };
165        let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
166        let outcome = {
167            let mut rasterizer = ScalarRasterizer::new(fb, zb, pitch_pixels, grid_view);
168            if let Some(sky_ref) = sky {
169                rasterizer = rasterizer.with_sky(sky_ref);
170            }
171            opticast(&mut rasterizer, pool, &local_cam, settings, grid_view)
172        };
173        if outcome == OpticastOutcome::Rendered {
174            grids_drawn += 1;
175        }
176    }
177    if grids_drawn == 0 {
178        RenderOutcome::Empty
179    } else {
180        RenderOutcome::Rendered { grids_drawn }
181    }
182}
183
184/// Per-pixel "min-z wins" merge of `(temp_fb, temp_zb)` into
185/// `(shared_fb, shared_zb)`.
186///
187/// Voxlap's z-buffer convention: `z` = perpendicular distance from
188/// camera; **smaller `z` = closer to camera**. This helper picks
189/// the closer pixel per slot. Sky pixels emerge with a large `z`
190/// (`scratch.skycast.dist`, set to `gxmax` or `i32::MAX` per
191/// `phase_startsky`) so they always lose to any hit's finite
192/// distance.
193///
194/// `temp_fb` / `temp_zb` are read-only inputs; both must have the
195/// same length as `shared_fb` / `shared_zb` (debug-asserted).
196pub fn compose_into(
197    shared_fb: &mut [u32],
198    shared_zb: &mut [f32],
199    temp_fb: &[u32],
200    temp_zb: &[f32],
201) {
202    debug_assert_eq!(shared_fb.len(), shared_zb.len());
203    debug_assert_eq!(shared_fb.len(), temp_fb.len());
204    debug_assert_eq!(shared_fb.len(), temp_zb.len());
205    for i in 0..shared_fb.len() {
206        if temp_zb[i] < shared_zb[i] {
207            shared_fb[i] = temp_fb[i];
208            shared_zb[i] = temp_zb[i];
209        }
210    }
211}
212
213/// Render every grid in `scene` with per-grid temporary buffers +
214/// z-buffer composition. The canonical multi-grid scene render
215/// path.
216///
217/// Algorithm:
218/// 1. Caller pre-fills `fb` with the desired sky colour and `zb`
219///    with [`f32::INFINITY`] (so any rendered pixel wins the
220///    initial composition).
221/// 2. For each grid, allocate a temporary `(temp_fb, temp_zb)` of
222///    the same size, pre-fill them with sky / `INFINITY`, and run
223///    [`opticast`] into them via a [`ScalarRasterizer`] over the
224///    temporary buffers AND the grid's combined-world view (S4.0).
225/// 3. Merge the temporary buffers into the shared `(fb, zb)` via
226///    [`compose_into`] — closer pixels (smaller `z`) win.
227///
228/// Pixel correctness across overlapping grids: sky pixels emerge
229/// with `z` = `gxmax` / `i32::MAX` (a very large value), so they
230/// always lose to any hit. Hits compete on actual perpendicular
231/// distance — the closer grid's surface is what gets composited.
232///
233/// `pitch_pixels` is the framebuffer's row stride in pixels (×4 for
234/// bytes). `width` × `height` must equal `fb.len()` /
235/// `zb.len()`. `sky` is the optional textured sky resource the
236/// rasterizer threads through to `phase_startsky`; `None` ⇒ solid
237/// `pool.skycast` fill.
238///
239/// **Heap allocation per call:** two `Vec` allocations per grid (a
240/// temp framebuffer and zbuffer). For repeated frame rendering an
241/// owned scratch struct that pre-allocates these is the obvious
242/// optimisation; deferred until profiling shows it matters.
243#[allow(clippy::too_many_arguments)]
244pub fn render_scene_composed(
245    fb: &mut [u32],
246    zb: &mut [f32],
247    pitch_pixels: usize,
248    width: u32,
249    height: u32,
250    pool: &mut ScratchPool,
251    scene: &mut Scene,
252    camera: &Camera,
253    settings: &OpticastSettings,
254    sky_color: u32,
255    sky: Option<&Sky>,
256) -> RenderOutcome {
257    debug_assert_eq!(fb.len(), zb.len());
258    let pixel_count = (width as usize) * (height as usize);
259    debug_assert_eq!(fb.len(), pixel_count);
260
261    let mut grids_drawn = 0usize;
262    let mut temp_fb = vec![sky_color; pixel_count];
263    let mut temp_zb = vec![f32::INFINITY; pixel_count];
264
265    for (_id, grid) in scene.grids_mut() {
266        // S6.0/S6.1: per-grid LOD tier dispatch. The picker keys
267        // off the grid's `lod_thresholds` and the world-space
268        // camera. Default thresholds are `always_near` so every
269        // grid lands on `Lod::Near` and the framebuffer stays
270        // byte-identical to the pre-S6 path.
271        //
272        // S6.1: `Mid` applies the grid's `mid_mip_levels` /
273        // `mid_mip_scan_dist` overrides (if `Some`) on top of the
274        // base settings, biasing the grid into coarser mips. With
275        // both `None`, Mid renders identically to Near (graceful
276        // degrade — callers opt into the Mid plumbing via
277        // `LodThresholds::from_radius_with_mid_mip`).
278        //
279        // S6.3: `Far` skips the opticast path entirely — render
280        // dispatches into the billboard impostor blit (below). The
281        // LOD enum is computed before `chunk_xyz_backing` because
282        // the Far branch needs `&mut grid` for the lazy cache
283        // populate, which conflicts with the `&grid` lifetime
284        // backing's tied to.
285        let lod = grid.select_lod(DVec3::from_array(camera.pos));
286
287        if lod == Lod::Far {
288            // S6.3: Far-tier billboard blit.
289            //
290            // Empty grids have nothing to impostor; skip without
291            // touching `billboards` so a later edit + Far re-entry
292            // still builds a fresh cache.
293            if grid.chunks.is_empty() {
294                continue;
295            }
296            // Lazy populate: cleared by edits (see `edit.rs`),
297            // rebuilt on first Far entry after each edit cycle.
298            if grid.billboards.is_none() {
299                let cache = BillboardCache::build(grid, BILLBOARD_RESOLUTION);
300                grid.billboards = Some(cache);
301            }
302            // Grid bounds + world-space centre. Rotation preserves
303            // length, so `bounds.radius` is the world-space radius.
304            let bounds = billboard::grid_bounds(grid);
305            let centre_world = grid.transform.origin + grid.transform.rotation * bounds.centre;
306            // Query direction = unit vector from grid centre TO
307            // camera, in grid-local space (snapshots' `view_dir`s
308            // live in that frame).
309            let cam_pos = DVec3::from_array(camera.pos);
310            let centre_to_cam_world = cam_pos - centre_world;
311            let ctc_len = centre_to_cam_world.length();
312            if !ctc_len.is_finite() || ctc_len < 1e-9 {
313                // Camera essentially at grid centre — pick_nearest
314                // is ill-defined. Skip; a future frame at a
315                // resolvable pose will render normally.
316                continue;
317            }
318            let query_dir_world = centre_to_cam_world / ctc_len;
319            let query_dir_local = grid.transform.rotation.inverse() * query_dir_world;
320            // Cache is guaranteed Some here (populated above).
321            let cache = grid.billboards.as_ref().unwrap();
322            // pick_nearest only returns None for empty caches;
323            // we just built a 26-snapshot cache so unwrap is safe.
324            let snapshot = cache
325                .pick_nearest(query_dir_local)
326                .expect("billboard cache populated above");
327            billboard::billboard_blit_into(
328                fb,
329                zb,
330                pitch_pixels,
331                width,
332                height,
333                snapshot,
334                centre_world,
335                bounds.radius,
336                camera,
337                settings,
338            );
339            grids_drawn += 1;
340            continue;
341        }
342
343        // S4B.2.e: Approach B render path. See `render_scene`'s
344        // body for the camera transform + ChunkGrid construction
345        // commentary; the only difference is this writes to
346        // (temp_fb, temp_zb) and composes via `compose_into`.
347        // S5.0: per-grid rotation flows via the shared helper.
348        let Some(backing) = grid.chunk_xyz_backing() else {
349            continue;
350        };
351        // S5.2-followup: per-grid sky opt-out. Grids with
352        // `render_sky = false` (e.g. a rotating ship) must not
353        // contribute sky pixels — the grid-local sky lookup
354        // rotates with the grid and visibly fights the world's
355        // sky during compose. Implementation: stamp a sentinel
356        // colour into temp_fb everywhere the rasterizer would
357        // paint sky, then walk the buffer post-opticast and
358        // mark sentinel pixels as `INFINITY` in temp_zb so
359        // [`compose_into`]'s min-z test always drops them.
360        let owns_sky = grid.render_sky;
361        let local_sky_color = if owns_sky {
362            sky_color
363        } else {
364            SKY_MASK_SENTINEL
365        };
366        if !owns_sky {
367            // Override the pool's skycast colour just for this
368            // grid so the solid-fill sky path stamps the sentinel.
369            // Restored after the grid's compose. `dist = 0` mirrors
370            // the caller's typical setup; the rasterizer's prelude
371            // overwrites the dist field with `gxmax` or `i32::MAX`
372            // anyway, so the dist arg is cosmetic here.
373            pool.set_skycast(SKY_MASK_SENTINEL as i32, 0);
374        }
375
376        // Reset temp to sky / INFINITY so each grid starts fresh.
377        // The reset cost is O(pixels) per grid; for small grid counts
378        // this is negligible vs the opticast work.
379        temp_fb.fill(local_sky_color);
380        temp_zb.fill(f32::INFINITY);
381
382        let local_cam = world_camera_to_grid_local(camera, &grid.transform);
383        let cg = roxlap_core::ChunkGrid {
384            chunks: &backing.chunks,
385            origin_chunk_xy: backing.origin_chunk_xy,
386            origin_chunk_z: backing.origin_chunk_z,
387            chunks_x: backing.chunks_x,
388            chunks_y: backing.chunks_y,
389            chunks_z: backing.chunks_z,
390        };
391        let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
392
393        // Build the per-grid settings by layering three opt-in
394        // overrides on top of the caller's `settings`:
395        //
396        //   1. (S6.1) `lod_thresholds.mid_mip_levels` /
397        //      `mid_mip_scan_dist` — applied iff `lod == Mid`.
398        //      Biases the grid into coarser mips via the existing
399        //      multi-mip path. None ⇒ Mid degrades to Near's
400        //      settings (graceful).
401        //   2. (S5.2-followup) `Grid::mip_levels_override` — global
402        //      per-grid cap applied at ALL tiers. Preserves the
403        //      ship anti-axis-aligned-beam workaround through Mid
404        //      tier (so a rotating ship pinned at mip-0 stays at
405        //      mip-0 even when distant).
406        //
407        // Layer order: Mid overrides first, then global cap. Both
408        // mip_levels overrides are clamped to `[1, base.mip_levels]`
409        // since the base is the maximum the renderer can use
410        // (chunk's `chunk_mips`-min logic inside scalar_rasterizer
411        // applies further per-chunk).
412        let per_grid_settings;
413        let active_settings = {
414            let base_mip_levels = settings.mip_levels;
415            let base_mip_scan = settings.mip_scan_dist;
416            let lod_mip_levels = match lod {
417                Lod::Mid => grid.lod_thresholds.mid_mip_levels,
418                Lod::Near | Lod::Far => None,
419            };
420            let lod_mip_scan = match lod {
421                Lod::Mid => grid.lod_thresholds.mid_mip_scan_dist,
422                Lod::Near | Lod::Far => None,
423            };
424            let global_mip_cap = grid.mip_levels_override;
425            let needs_override =
426                lod_mip_levels.is_some() || lod_mip_scan.is_some() || global_mip_cap.is_some();
427            if needs_override {
428                // Resolve mip_levels: start with base, apply LOD
429                // override (clamped to base), then apply global cap.
430                let mut mip_levels =
431                    lod_mip_levels.map_or(base_mip_levels, |n| n.clamp(1, base_mip_levels));
432                if let Some(cap) = global_mip_cap {
433                    mip_levels = mip_levels.min(cap.clamp(1, base_mip_levels));
434                }
435                // Resolve mip_scan_dist: LOD override clamps to
436                // `min(base, override)` — the override only makes
437                // transitions kick in CLOSER, never farther. The
438                // renderer floors at 4 internally so we don't
439                // bottom-clamp here.
440                let mip_scan_dist = lod_mip_scan.map_or(base_mip_scan, |d| base_mip_scan.min(d));
441                per_grid_settings = OpticastSettings {
442                    mip_levels,
443                    mip_scan_dist,
444                    ..*settings
445                };
446                &per_grid_settings
447            } else {
448                settings
449            }
450        };
451
452        let outcome = {
453            let mut rasterizer =
454                ScalarRasterizer::new(&mut temp_fb, &mut temp_zb, pitch_pixels, grid_view);
455            // Sky texture is suppressed for `!owns_sky` grids so
456            // the textured-sky branch doesn't bypass the sentinel;
457            // only the solid-fill path stamps `skycast.col`.
458            if owns_sky {
459                if let Some(sky_ref) = sky {
460                    rasterizer = rasterizer.with_sky(sky_ref);
461                }
462            }
463            opticast(
464                &mut rasterizer,
465                pool,
466                &local_cam,
467                active_settings,
468                grid_view,
469            )
470        };
471
472        if !owns_sky {
473            // Mask sentinel pixels so compose drops them. One linear
474            // sweep of the temp framebuffer; sentinel pixels become
475            // `INFINITY` in temp_zb (= always lose min-z).
476            for (px, z) in temp_fb.iter().zip(temp_zb.iter_mut()) {
477                if *px == SKY_MASK_SENTINEL {
478                    *z = f32::INFINITY;
479                }
480            }
481            // Restore the pool's sky colour so subsequent grids
482            // (and the next frame) see the caller's value.
483            pool.set_skycast(sky_color as i32, 0);
484        }
485
486        if outcome == OpticastOutcome::Rendered {
487            compose_into(fb, zb, &temp_fb, &temp_zb);
488            grids_drawn += 1;
489        }
490    }
491
492    if grids_drawn == 0 {
493        RenderOutcome::Empty
494    } else {
495        RenderOutcome::Rendered { grids_drawn }
496    }
497}
498
499#[cfg(test)]
500#[allow(clippy::float_cmp)]
501mod tests {
502    use super::*;
503    use crate::{GridTransform, Scene, CHUNK_SIZE_XY};
504    use glam::{DVec3, IVec3};
505    use roxlap_core::opticast::{opticast as core_opticast, OpticastSettings};
506    use roxlap_core::rasterizer::ScratchPool;
507    use roxlap_core::scalar_rasterizer::ScalarRasterizer;
508    use roxlap_core::{Camera, Engine};
509
510    const XRES: u32 = 320;
511    const YRES: u32 = 200;
512
513    /// Build a single-grid scene at the given world origin with a
514    /// recognisable shape inside its chunk (0, 0, 0): a 16-voxel
515    /// box plus a 6-radius sphere. Returns `(scene, grid_id)`.
516    fn build_one_grid_scene(world_origin: DVec3) -> (Scene, crate::GridId) {
517        let mut scene = Scene::new();
518        let id = scene.add_grid(GridTransform::at(world_origin));
519        let grid = scene.grid_mut(id).unwrap();
520        // Box covering [40..56]³ in chunk-local coords.
521        grid.set_rect(
522            IVec3::new(40, 40, 40),
523            IVec3::new(55, 55, 55),
524            Some(0x80_88_88_88),
525        );
526        // Sphere at (80, 80, 80) radius 6.
527        grid.set_sphere(IVec3::new(80, 80, 80), 6, Some(0x80_22_aa_22));
528        (scene, id)
529    }
530
531    fn camera_at(pos: [f64; 3]) -> Camera {
532        // Look +y axis; voxlap z-down convention. Right-handed:
533        // right × down == forward.
534        Camera {
535            pos,
536            right: [-1.0, 0.0, 0.0],
537            down: [0.0, 0.0, 1.0],
538            forward: [0.0, 1.0, 0.0],
539        }
540    }
541
542    /// Spin up an engine + `ScratchPool` + framebuffers ready for
543    /// one `opticast` / `render_scene` pass. `pool_vsid` should
544    /// cover the largest grid's combined vsid.
545    fn render_setup(pool_vsid: u32) -> (Engine, ScratchPool, Vec<u32>, Vec<f32>) {
546        let engine = Engine::new();
547        let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
548        let sky = engine.sky_color();
549        let sky_col_i = i32::from_ne_bytes(sky.to_ne_bytes());
550        pool.set_skycast(sky_col_i, 0);
551        let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
552        pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
553        pool.set_treat_z_max_as_air(true);
554        let pixel_count = (XRES as usize) * (YRES as usize);
555        let framebuffer = vec![sky; pixel_count];
556        let zbuffer = vec![0.0f32; pixel_count];
557        (engine, pool, framebuffer, zbuffer)
558    }
559
560    /// Render `scene` via [`render_scene`] (single-grid no-compose
561    /// path) and return the resulting framebuffer.
562    fn render_via_scene(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
563        let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
564        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
565        let outcome = render_scene(
566            &mut fb,
567            &mut zb,
568            XRES as usize,
569            XRES,
570            YRES,
571            &mut pool,
572            scene,
573            camera,
574            &settings,
575            None,
576        );
577        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
578        fb
579    }
580
581    /// Render the same chunk as a direct opticast call, with the
582    /// camera already in grid-local frame. The reference output
583    /// for the round-trip test.
584    fn render_via_direct_opticast(scene: &Scene, local_camera: &Camera) -> Vec<u32> {
585        let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
586        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
587        let grid = scene.grids().next().unwrap().1;
588        let chunk = grid.chunk(IVec3::ZERO).unwrap();
589        let grid_view = roxlap_core::GridView::from_single_vxl(chunk);
590        let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
591        let _ = core_opticast(
592            &mut rasterizer,
593            &mut pool,
594            local_camera,
595            &settings,
596            grid_view,
597        );
598        drop(rasterizer);
599        fb
600    }
601
602    // ---- S5.0: world_camera_to_grid_local helper ----
603
604    /// Identity rotation: pos translates by `-origin`; basis is
605    /// untouched. This is the byte-identical-to-pre-S5 contract.
606    #[test]
607    fn world_camera_to_grid_local_identity_rotation_translates_pos_only() {
608        let camera = Camera {
609            pos: [110.0, 220.0, 330.0],
610            right: [1.0, 0.0, 0.0],
611            down: [0.0, 0.0, 1.0],
612            forward: [0.0, 1.0, 0.0],
613        };
614        let transform = GridTransform::at(DVec3::new(100.0, 200.0, 300.0));
615        let local = super::world_camera_to_grid_local(&camera, &transform);
616        // Basis must be bit-for-bit unchanged for the identity case.
617        assert_eq!(local.right, camera.right);
618        assert_eq!(local.down, camera.down);
619        assert_eq!(local.forward, camera.forward);
620        // Pos translates by `-origin`.
621        for (got, want) in local.pos.iter().zip([10.0, 20.0, 30.0].iter()) {
622            assert!((got - want).abs() < 1e-12, "pos got={got} want={want}");
623        }
624    }
625
626    /// 90° rotation about +Z: grid-local `+x` aligns with world `+y`.
627    /// World camera at `(0, 10, 0)` looking world `+y` lives in
628    /// grid-local at `(10, 0, 0)` looking grid-local `+x`.
629    #[test]
630    fn world_camera_to_grid_local_90deg_z_rotates_basis_and_pos() {
631        use glam::DQuat;
632        let camera = Camera {
633            pos: [0.0, 10.0, 0.0],
634            right: [1.0, 0.0, 0.0],
635            down: [0.0, 0.0, 1.0],
636            forward: [0.0, 1.0, 0.0],
637        };
638        let transform = GridTransform {
639            origin: DVec3::ZERO,
640            rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
641        };
642        let local = super::world_camera_to_grid_local(&camera, &transform);
643        // World +y == grid-local +x.
644        let approx_eq =
645            |a: [f64; 3], b: [f64; 3]| a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-9);
646        assert!(
647            approx_eq(local.pos, [10.0, 0.0, 0.0]),
648            "pos={:?} expected ~(10, 0, 0)",
649            local.pos
650        );
651        // World +x (right) maps to grid-local -y.
652        assert!(
653            approx_eq(local.right, [0.0, -1.0, 0.0]),
654            "right={:?} expected ~(0, -1, 0)",
655            local.right
656        );
657        // World +z (down) is unchanged — it's the rotation axis.
658        assert!(
659            approx_eq(local.down, [0.0, 0.0, 1.0]),
660            "down={:?} expected ~(0, 0, 1)",
661            local.down
662        );
663        // World +y (forward) maps to grid-local +x.
664        assert!(
665            approx_eq(local.forward, [1.0, 0.0, 0.0]),
666            "forward={:?} expected ~(1, 0, 0)",
667            local.forward
668        );
669    }
670
671    /// Basis orthonormality + handedness both survive the
672    /// inverse-rotation transform. Property: any unit-quaternion
673    /// conjugation preserves the input basis's orthonormality AND
674    /// its handedness (rotations are orientation-preserving).
675    #[test]
676    fn world_camera_to_grid_local_preserves_basis_orthonormality() {
677        use glam::DQuat;
678        // Right-handed voxlap basis (`right × down == forward`):
679        // looking +y, right = -x makes the cross product land on +y.
680        let camera = Camera {
681            pos: [3.0, -5.0, 7.0],
682            right: [-1.0, 0.0, 0.0],
683            down: [0.0, 0.0, 1.0],
684            forward: [0.0, 1.0, 0.0],
685        };
686        let transform = GridTransform {
687            origin: DVec3::new(1.0, 2.0, 3.0),
688            rotation: DQuat::from_axis_angle(glam::DVec3::new(0.3, 0.8, 0.5).normalize(), 0.7),
689        };
690        let local = super::world_camera_to_grid_local(&camera, &transform);
691        let r = DVec3::from_array(local.right);
692        let d = DVec3::from_array(local.down);
693        let f = DVec3::from_array(local.forward);
694        // Norms ≈ 1.
695        for v in [r, d, f] {
696            assert!(
697                (v.length_squared() - 1.0).abs() < 1e-12,
698                "basis vec {v:?} not unit length"
699            );
700        }
701        // Orthogonality.
702        assert!(r.dot(d).abs() < 1e-12, "right·down = {}", r.dot(d));
703        assert!(r.dot(f).abs() < 1e-12, "right·forward = {}", r.dot(f));
704        assert!(d.dot(f).abs() < 1e-12, "down·forward = {}", d.dot(f));
705        // Right-handed: right × down == forward (voxlap convention).
706        let cross = r.cross(d);
707        assert!(
708            (cross - f).length() < 1e-12,
709            "right×down={cross:?} forward={f:?}"
710        );
711    }
712
713    // ---- S5.1: rotated-grid render correctness ----
714
715    /// Build a single-grid scene at the given transform with a
716    /// marker box near one corner of chunk (0, 0, 0). Returns the
717    /// scene and the marker colour. Picking a single chunk + small
718    /// box keeps the test compact while still exercising the gline
719    /// + grouscan path through the rotated frame.
720    fn build_one_grid_marker_scene(transform: GridTransform) -> (Scene, crate::GridId, u32) {
721        let mut scene = Scene::new();
722        let id = scene.add_grid(transform);
723        let grid = scene.grid_mut(id).unwrap();
724        // Bright marker box at chunk-local (40..56, 40..56, 40..56).
725        grid.set_rect(
726            IVec3::new(40, 40, 40),
727            IVec3::new(55, 55, 55),
728            Some(0x80_55_aa_22), // distinctive green
729        );
730        (scene, id, 0x80_55_aa_22)
731    }
732
733    /// Pin S5.1's central equivalence: rotating both the grid and the
734    /// camera by the SAME rotation around the grid's origin must
735    /// leave the rendered framebuffer unchanged — the grid-local
736    /// camera pose collapses to the same values in both scenarios.
737    ///
738    /// We use `DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0)`, the
739    /// 180°-around-Z unit quaternion. This rotation acts on vectors
740    /// as `(x, y, z) → (-x, -y, z)`, which only multiplies f64
741    /// components by 0 or ±1 — bit-exact under glam's standard quat
742    /// conjugation formula. Other angles (e.g. 90°) would introduce
743    /// sub-1e-15 noise from sin/cos, breaking byte-identity at
744    /// chunk / voxel boundaries.
745    #[test]
746    fn s5_1_180deg_z_rotated_grid_byte_identical_to_axis_aligned() {
747        use glam::DQuat;
748        // Right-handed voxlap basis (right × down == forward).
749        let axis_aligned_camera = Camera {
750            pos: [40.0, -20.0, 50.0],
751            right: [-1.0, 0.0, 0.0],
752            down: [0.0, 0.0, 1.0],
753            forward: [0.0, 1.0, 0.0],
754        };
755        // R_z(180°): (x, y, z) → (-x, -y, z).
756        let rotated_camera = Camera {
757            pos: [-40.0, 20.0, 50.0],
758            right: [1.0, 0.0, 0.0],
759            down: [0.0, 0.0, 1.0],
760            forward: [0.0, -1.0, 0.0],
761        };
762        // Sanity: prove the exact-arithmetic rotation lands on the
763        // baseline. If glam ever changes its quat*vec formula in a
764        // way that loses exactness here, the next two assertions
765        // catch it before the framebuffer comparison.
766        let q = DQuat::from_xyzw(0.0, 0.0, 1.0, 0.0);
767        let rot_pos = q * DVec3::from_array(axis_aligned_camera.pos);
768        let rot_fwd = q * DVec3::from_array(axis_aligned_camera.forward);
769        assert_eq!(rot_pos.to_array(), rotated_camera.pos);
770        assert_eq!(rot_fwd.to_array(), rotated_camera.forward);
771
772        let (mut scene_a, _, _) = build_one_grid_marker_scene(GridTransform::identity());
773        let fb_a = render_via_scene(&mut scene_a, &axis_aligned_camera);
774
775        let (mut scene_b, _, _) = build_one_grid_marker_scene(GridTransform {
776            origin: DVec3::ZERO,
777            rotation: q,
778        });
779        let fb_b = render_via_scene(&mut scene_b, &rotated_camera);
780
781        assert_eq!(
782            fb_a, fb_b,
783            "rotating both grid and camera by R about the grid origin must leave the framebuffer unchanged"
784        );
785    }
786
787    /// 45° smoke test: rotated grid renders to something non-trivial
788    /// without panicking. No equivalence assertion (45° quat math is
789    /// approximate at f64 level; that path is exercised structurally,
790    /// not bit-exactly). Camera is placed at a fixed world pose where
791    /// — under the rotation — the marker box stays inside the view
792    /// frustum.
793    #[test]
794    fn s5_1_45deg_z_rotated_grid_renders_marker() {
795        use glam::DQuat;
796        let rotation = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4);
797        let (mut scene, _, marker) = build_one_grid_marker_scene(GridTransform {
798            origin: DVec3::ZERO,
799            rotation,
800        });
801
802        // World position of the marker's centre. Grid-local
803        // (47.5, 47.5, 47.5) → world `rotation * (47.5, 47.5, 47.5)`.
804        // R_z(45°): (47.5, 47.5, 47.5) → (0, 67.18, 47.5) (the x/y
805        // components combine into a single +y vector at √2 * 47.5).
806        let marker_world = rotation * DVec3::new(47.5, 47.5, 47.5);
807        // Camera 80 units south of the marker on the world Y axis,
808        // looking +y at the same z. RH basis.
809        let camera = Camera {
810            pos: [marker_world.x, marker_world.y - 80.0, marker_world.z],
811            right: [-1.0, 0.0, 0.0],
812            down: [0.0, 0.0, 1.0],
813            forward: [0.0, 1.0, 0.0],
814        };
815
816        let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
817        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
818        let outcome = render_scene(
819            &mut fb,
820            &mut zb,
821            XRES as usize,
822            XRES,
823            YRES,
824            &mut pool,
825            &mut scene,
826            &camera,
827            &settings,
828            None,
829        );
830        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
831        let marker_count = fb.iter().filter(|&&p| p == marker).count();
832        assert!(
833            marker_count > 50,
834            "45°-rotated marker box should be visible — got {marker_count} marker pixels"
835        );
836    }
837
838    // ---- S5.2-followup: per-grid render_sky opt-out ----
839
840    /// Two-grid scene where grid B sits behind grid A along +y;
841    /// grid A is opaque only in the centre of the framebuffer, so
842    /// the camera's view through grid A is mostly "ray miss". When
843    /// `A.render_sky = false`, the pixels around A's silhouette
844    /// must remain whatever grid B (or the shared pre-fill)
845    /// painted — NOT A's grid-local sky colour. This pins the
846    /// sentinel-mask path: without it, A's sky would write into
847    /// the composed framebuffer wherever its sky-z happened to win
848    /// the min-z race with B's sky-z.
849    #[test]
850    fn render_sky_false_drops_grid_sky_pixels() {
851        use crate::{GridId, GridTransform};
852
853        // Grid B (far, sky owner) — a wide floor of distinct
854        // colour spanning chunk-local x/y so most rays land on it.
855        let mut scene = Scene::new();
856        let _b_id: GridId = scene.add_grid(GridTransform::at(DVec3::new(0.0, 600.0, 0.0)));
857        // Find grid B's id (HashMap iteration; we only just added
858        // one grid, so its id is whichever the iterator yields).
859        let b_id = scene.grids().next().unwrap().0;
860        scene.grid_mut(b_id).unwrap().set_rect(
861            IVec3::new(0, 0, 100),
862            IVec3::new(127, 127, 110),
863            Some(0x80_22_88_22), // green floor
864        );
865
866        // Grid A (near, sky disabled) — a SMALL marker box that
867        // covers only a fraction of the screen. Most pixels of A's
868        // local render are sky.
869        let a_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
870        scene.grid_mut(a_id).unwrap().set_rect(
871            IVec3::new(60, 60, 60),
872            IVec3::new(67, 67, 67),
873            Some(0x80_aa_22_22), // red cube
874        );
875        scene.grid_mut(a_id).unwrap().render_sky = false;
876
877        let unique_sky: u32 = 0xFF_AB_CD_EF;
878        let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
879        let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
880        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
881        let camera = camera_at([64.0, 0.0, 100.0]);
882        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
883        let outcome = render_scene_composed(
884            &mut fb,
885            &mut zb,
886            XRES as usize,
887            XRES,
888            YRES,
889            &mut pool,
890            &mut scene,
891            &camera,
892            &settings,
893            unique_sky,
894            None,
895        );
896        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
897
898        // The sentinel must never appear in the composed output —
899        // every sentinel pixel must have been masked out before
900        // compose. If any leak through, the test catches it.
901        let leaked = fb
902            .iter()
903            .filter(|&&p| p == super::SKY_MASK_SENTINEL)
904            .count();
905        assert_eq!(
906            leaked, 0,
907            "SKY_MASK_SENTINEL leaked into composed framebuffer ({leaked} pixels)"
908        );
909        // Grid A's hit (red cube) must still render — render_sky=false
910        // only affects sky pixels, not hits.
911        let red_count = fb.iter().filter(|&&p| p == 0x80_aa_22_22).count();
912        assert!(
913            red_count > 0,
914            "red cube from sky-disabled grid A is missing — render_sky=false should only mask sky"
915        );
916        // Grid B's floor must be visible past grid A's silhouette
917        // (the sky-disabled grid doesn't hide B's render).
918        let green_count = fb.iter().filter(|&&p| p == 0x80_22_88_22).count();
919        assert!(
920            green_count > 0,
921            "grid B's floor invisible — grid A's masked sky may have overwritten it"
922        );
923    }
924
925    /// Identity-rotation, single-grid scene with `render_sky = false`
926    /// must produce a sentinel-free framebuffer. Sanity test for the
927    /// trivial 1-grid case (no second grid to compose against).
928    #[test]
929    fn render_sky_false_single_grid_no_sentinel_leak() {
930        let (mut scene, id, _) = build_one_grid_marker_scene(GridTransform::identity());
931        scene.grid_mut(id).unwrap().render_sky = false;
932        let unique_sky: u32 = 0xFF_12_34_56;
933        let (_engine, mut pool, _) = make_composed_pool(CHUNK_SIZE_XY);
934        let mut fb = vec![unique_sky; pixel_count(XRES, YRES)];
935        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
936        let camera = camera_at([64.0, 0.0, 64.0]);
937        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
938        let outcome = render_scene_composed(
939            &mut fb,
940            &mut zb,
941            XRES as usize,
942            XRES,
943            YRES,
944            &mut pool,
945            &mut scene,
946            &camera,
947            &settings,
948            unique_sky,
949            None,
950        );
951        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
952        let leaked = fb
953            .iter()
954            .filter(|&&p| p == super::SKY_MASK_SENTINEL)
955            .count();
956        assert_eq!(leaked, 0, "SKY_MASK_SENTINEL leaked ({leaked} pixels)");
957        // Pixels that would have been the grid's sky now show
958        // through to the pre-fill (unique_sky).
959        let prefill_count = fb.iter().filter(|&&p| p == unique_sky).count();
960        assert!(
961            prefill_count > 0,
962            "no pre-fill pixels survived — render_sky=false should leave non-hit pixels untouched"
963        );
964    }
965
966    #[test]
967    fn render_scene_at_origin_matches_direct_opticast() {
968        // Grid at world origin → grid-local camera == world camera.
969        // Single 1-chunk grid: combined view's bytes are byte-identical
970        // to the chunk's own column data (slng-walk equivalence), so
971        // render_scene must produce the same framebuffer as a direct
972        // opticast on the chunk.
973        let (mut scene, _) = build_one_grid_scene(DVec3::ZERO);
974        let cam = camera_at([64.0, 0.0, 64.0]);
975        let via_scene = render_via_scene(&mut scene, &cam);
976        let via_direct = render_via_direct_opticast(&scene, &cam);
977        assert_eq!(
978            via_scene, via_direct,
979            "render_scene with single 1-chunk grid at origin should match direct opticast"
980        );
981    }
982
983    #[test]
984    fn render_scene_translated_grid_matches_grid_local_opticast() {
985        // Grid at world (1000, 2000, 3000). World camera at
986        // (1064, 2000, 3064) — grid-local (64, 0, 64). render_scene
987        // should produce the same output as a direct opticast call
988        // with grid-local camera.
989        let world_origin = DVec3::new(1000.0, 2000.0, 3000.0);
990        let (mut scene, _) = build_one_grid_scene(world_origin);
991        let world_cam = camera_at([1064.0, 2000.0, 3064.0]);
992        let local_cam = camera_at([64.0, 0.0, 64.0]);
993        let via_scene = render_via_scene(&mut scene, &world_cam);
994        let via_direct = render_via_direct_opticast(&scene, &local_cam);
995        assert_eq!(
996            via_scene, via_direct,
997            "render_scene of translated grid should match opticast with grid-local camera"
998        );
999    }
1000
1001    #[test]
1002    fn empty_scene_returns_empty_outcome() {
1003        let mut scene = Scene::new();
1004        let (_engine, mut pool, mut fb, mut zb) = render_setup(CHUNK_SIZE_XY);
1005        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1006        let outcome = render_scene(
1007            &mut fb,
1008            &mut zb,
1009            XRES as usize,
1010            XRES,
1011            YRES,
1012            &mut pool,
1013            &mut scene,
1014            &camera_at([0.0, 0.0, 0.0]),
1015            &settings,
1016            None,
1017        );
1018        assert_eq!(outcome, RenderOutcome::Empty);
1019    }
1020
1021    // ---- S3.1 / S4.0: render_scene_composed + 2-grid composition ----
1022
1023    /// Build a 2-grid scene with two distinguishable boxes placed
1024    /// side-by-side in world space along the camera's right axis.
1025    /// Each grid holds one chunk (`(0, 0, 0)`) containing a single
1026    /// 16-voxel box with a uniquely-coloured surface so the
1027    /// composited framebuffer is partitionable by colour.
1028    fn build_two_grid_side_by_side() -> (Scene, u32, u32) {
1029        let mut scene = Scene::new();
1030        // Grid 0 at world (0, 200, 0): box centred chunk-local (64, 64, 100).
1031        let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1032        scene.grid_mut(g0).unwrap().set_rect(
1033            IVec3::new(56, 56, 92),
1034            IVec3::new(71, 71, 107),
1035            Some(0x80_88_22_22), // dark red
1036        );
1037        // Grid 1 at world (200, 200, 0): box centred chunk-local (64, 64, 100).
1038        let _g1 = scene.add_grid(GridTransform::at(DVec3::new(200.0, 200.0, 0.0)));
1039        // Borrow-checker dance: re-borrow grid 1 mutably.
1040        let g1_id = scene
1041            .grids()
1042            .filter(|(id, _)| *id != g0)
1043            .map(|(id, _)| id)
1044            .next()
1045            .unwrap();
1046        scene.grid_mut(g1_id).unwrap().set_rect(
1047            IVec3::new(56, 56, 92),
1048            IVec3::new(71, 71, 107),
1049            Some(0x80_22_22_88), // dark blue
1050        );
1051        (scene, 0x80_88_22_22, 0x80_22_22_88)
1052    }
1053
1054    fn make_composed_pool(pool_vsid: u32) -> (Engine, ScratchPool, u32) {
1055        let engine = Engine::new();
1056        let mut pool = ScratchPool::new(XRES, YRES, pool_vsid);
1057        let sky_color = engine.sky_color();
1058        let sky_col_i = i32::from_ne_bytes(sky_color.to_ne_bytes());
1059        pool.set_skycast(sky_col_i, 0);
1060        let fog_col_i = i32::from_ne_bytes(engine.fog_color().to_ne_bytes());
1061        pool.set_fog(fog_col_i, engine.fog_max_scan_dist());
1062        pool.set_treat_z_max_as_air(true);
1063        (engine, pool, sky_color)
1064    }
1065
1066    fn pixel_count(width: u32, height: u32) -> usize {
1067        (width as usize) * (height as usize)
1068    }
1069
1070    #[test]
1071    fn compose_into_takes_smaller_z() {
1072        let mut shared_fb = vec![0xff_ff_ff_ff_u32; 4];
1073        let mut shared_zb = vec![10.0f32; 4];
1074        let temp_fb = [0xaa_aa_aa_aa, 0x11_22_33_44, 0x55_66_77_88, 0xde_ad_be_ef];
1075        let temp_zb = [5.0f32, 20.0, 10.0, f32::INFINITY];
1076        compose_into(&mut shared_fb, &mut shared_zb, &temp_fb, &temp_zb);
1077        // i=0: 5 < 10 → take temp.
1078        assert_eq!(shared_fb[0], 0xaa_aa_aa_aa);
1079        assert_eq!(shared_zb[0], 5.0);
1080        // i=1: 20 > 10 → keep shared.
1081        assert_eq!(shared_fb[1], 0xff_ff_ff_ff);
1082        assert_eq!(shared_zb[1], 10.0);
1083        // i=2: 10 == 10 → keep shared (`<` not `<=`).
1084        assert_eq!(shared_fb[2], 0xff_ff_ff_ff);
1085        // i=3: INFINITY > 10 → keep shared.
1086        assert_eq!(shared_fb[3], 0xff_ff_ff_ff);
1087    }
1088
1089    #[test]
1090    fn render_scene_composed_two_grids_both_visible() {
1091        // Camera positioned to see both grids' boxes. Grid 0's box
1092        // at world (~64, ~264, ~100); grid 1's box at world
1093        // (~264, ~264, ~100). Camera at world (160, 100, 100)
1094        // looking +y centres both in view.
1095        let (mut scene, red, blue) = build_two_grid_side_by_side();
1096        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1097        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1098        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1099
1100        let camera = camera_at([160.0, 100.0, 100.0]);
1101        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1102        let outcome = render_scene_composed(
1103            &mut fb,
1104            &mut zb,
1105            XRES as usize,
1106            XRES,
1107            YRES,
1108            &mut pool,
1109            &mut scene,
1110            &camera,
1111            &settings,
1112            sky_color,
1113            None,
1114        );
1115        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1116
1117        // Both colours should appear somewhere in the framebuffer.
1118        let red_count = fb.iter().filter(|&&p| p == red).count();
1119        let blue_count = fb.iter().filter(|&&p| p == blue).count();
1120        assert!(
1121            red_count > 0,
1122            "no red pixels: grid 0 (red box) not visible after compose"
1123        );
1124        assert!(
1125            blue_count > 0,
1126            "no blue pixels: grid 1 (blue box) not visible after compose"
1127        );
1128    }
1129
1130    #[test]
1131    fn render_scene_composed_grid_a_in_front_of_grid_b() {
1132        // Two grids stacked along +y so grid A (closer) occludes
1133        // grid B (farther). After composition only grid A's colour
1134        // should appear on the overlap.
1135        let mut scene = Scene::new();
1136        let g_a = scene.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1137        scene.grid_mut(g_a).unwrap().set_rect(
1138            IVec3::new(56, 56, 92),
1139            IVec3::new(71, 71, 107),
1140            Some(0x80_aa_00_00), // red
1141        );
1142        let _g_b = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1143        let g_b_id = scene
1144            .grids()
1145            .filter(|(id, _)| *id != g_a)
1146            .map(|(id, _)| id)
1147            .next()
1148            .unwrap();
1149        scene.grid_mut(g_b_id).unwrap().set_rect(
1150            IVec3::new(56, 56, 92),
1151            IVec3::new(71, 71, 107),
1152            Some(0x80_00_00_aa), // blue
1153        );
1154
1155        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1156        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1157        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1158
1159        // Camera at (64, -10, 100) looking +y — both boxes line up
1160        // along the camera's forward axis.
1161        let camera = camera_at([64.0, -10.0, 100.0]);
1162        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1163        let outcome = render_scene_composed(
1164            &mut fb,
1165            &mut zb,
1166            XRES as usize,
1167            XRES,
1168            YRES,
1169            &mut pool,
1170            &mut scene,
1171            &camera,
1172            &settings,
1173            sky_color,
1174            None,
1175        );
1176        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1177
1178        // Red (closer grid) should be visible. Blue (farther grid)
1179        // may peek around the edges but the central pixels should
1180        // be red where both boxes project.
1181        let red_count = fb.iter().filter(|&&p| p == 0x80_aa_00_00).count();
1182        assert!(
1183            red_count > 0,
1184            "expected red pixels (closer box should win z-test)"
1185        );
1186
1187        // Reverse the registration order (force grid B drawn first)
1188        // and verify that's irrelevant — composition is commutative.
1189        let mut scene2 = Scene::new();
1190        let g_b2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1191        scene2.grid_mut(g_b2).unwrap().set_rect(
1192            IVec3::new(56, 56, 92),
1193            IVec3::new(71, 71, 107),
1194            Some(0x80_00_00_aa),
1195        );
1196        let g_a2 = scene2.add_grid(GridTransform::at(DVec3::new(0.0, 50.0, 0.0)));
1197        scene2.grid_mut(g_a2).unwrap().set_rect(
1198            IVec3::new(56, 56, 92),
1199            IVec3::new(71, 71, 107),
1200            Some(0x80_aa_00_00),
1201        );
1202
1203        let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1204        let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1205        let outcome2 = render_scene_composed(
1206            &mut fb2,
1207            &mut zb2,
1208            XRES as usize,
1209            XRES,
1210            YRES,
1211            &mut pool,
1212            &mut scene2,
1213            &camera,
1214            &settings,
1215            sky_color,
1216            None,
1217        );
1218        assert_eq!(outcome2, RenderOutcome::Rendered { grids_drawn: 2 });
1219        assert_eq!(
1220            fb, fb2,
1221            "composition should be order-independent — same scene in different add order should produce identical output"
1222        );
1223    }
1224
1225    // ---- S6.1: Mid-tier mip overrides ----
1226
1227    /// Build a multi-mip-friendly grid: solid floor spanning the
1228    /// whole chunk at z=100..254 + `generate_mips(3)`. This is the
1229    /// same setup `vxl_generate_mips_on_set_voxel_chunk_renders`
1230    /// uses and is known to render at `mip_levels = 3,
1231    /// mip_scan_dist = 32`.
1232    ///
1233    /// Returns `(scene, grid_id)`. The Mid test sets the camera
1234    /// inside the chunk so chunk-local rays reach the floor at
1235    /// short distances; that lets the Mid override use
1236    /// `mip_scan_dist = 16` without busting the ray budget
1237    /// (`mip_scan_dist * 2^(mip_levels-1) = 16 * 4 = 64` covers the
1238    /// distance from camera to floor).
1239    fn build_mip_visible_grid(world_origin: DVec3) -> (Scene, crate::GridId) {
1240        let mut scene = Scene::new();
1241        let id = scene.add_grid(GridTransform::at(world_origin));
1242        let grid = scene.grid_mut(id).unwrap();
1243        // Solid floor across the entire chunk at z=100..254.
1244        grid.set_rect(
1245            IVec3::new(0, 0, 100),
1246            IVec3::new(127, 127, 254),
1247            Some(0x80_88_88_88),
1248        );
1249        // Build the per-chunk mip ladder so `gmipnum` can grow past 1.
1250        grid.chunk_mut(IVec3::ZERO).unwrap().generate_mips(3);
1251        (scene, id)
1252    }
1253
1254    /// FNV-1a 64-bit hash of the framebuffer bytes.
1255    fn fb_hash(fb: &[u32]) -> u64 {
1256        let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1257        for px in fb {
1258            for b in px.to_le_bytes() {
1259                h ^= u64::from(b);
1260                h = h.wrapping_mul(0x0000_0100_0000_01b3);
1261            }
1262        }
1263        h
1264    }
1265
1266    /// Render `scene` via composed path with `mip_levels = 3,
1267    /// mip_scan_dist = 32` — same values the working
1268    /// `vxl_generate_mips_on_set_voxel_chunk_renders` test uses.
1269    /// Returns the framebuffer.
1270    fn render_with_multi_mip(scene: &mut Scene, camera: &Camera) -> Vec<u32> {
1271        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1272        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1273        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1274        let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1275        settings.mip_levels = 3;
1276        settings.mip_scan_dist = 32;
1277        let outcome = render_scene_composed(
1278            &mut fb,
1279            &mut zb,
1280            XRES as usize,
1281            XRES,
1282            YRES,
1283            &mut pool,
1284            scene,
1285            camera,
1286            &settings,
1287            sky_color,
1288            None,
1289        );
1290        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1291        fb
1292    }
1293
1294    /// Mid-tier with explicit mip overrides must produce a different
1295    /// framebuffer than Near-tier rendering of the same scene. Loose
1296    /// test — checks hash inequality and that the Mid render still
1297    /// has some non-sky pixels (sanity that the renderer didn't bail).
1298    #[test]
1299    fn s6_1_mid_overrides_produce_different_framebuffer_than_near() {
1300        // Camera at grid origin + a bit inside the chunk, looking +y.
1301        let camera = camera_at([64.0, 0.0, 64.0]);
1302
1303        // Scene A: default thresholds → Near tier → renderer uses
1304        // base settings (mip_levels=3, mip_scan_dist=32).
1305        let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1306        let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1307
1308        // Scene B: thresholds force Mid tier + mip overrides. We
1309        // cap mip_levels at 1 (no multi-mip) to guarantee a visible
1310        // pixel difference vs Near's mip-1/2 transitions. This is
1311        // the inverse of the typical "Mid = coarser" use case but
1312        // is the most reliable way to demonstrate that the override
1313        // path actually fires; production callers would more
1314        // commonly leave mip_levels at base and lower mip_scan_dist.
1315        let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1316        scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1317            // r_near = 0 → any nonzero distance lands in Mid or Far.
1318            r_near: 0.0,
1319            r_mid: f64::INFINITY,
1320            mid_mip_levels: Some(1),
1321            mid_mip_scan_dist: None,
1322        };
1323        // Sanity: confirm the picker actually returns Mid for this pose.
1324        let lod = scene_b
1325            .grid(b_id)
1326            .unwrap()
1327            .select_lod(DVec3::from_array(camera.pos));
1328        assert_eq!(lod, Lod::Mid, "expected Mid tier for forced thresholds");
1329        let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1330
1331        // Both renders must contain non-sky pixels (the override
1332        // didn't bust the renderer).
1333        let (_engine, _, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1334        let non_sky_near = fb_near.iter().filter(|&&p| p != sky_color).count();
1335        let non_sky_mid = fb_mid.iter().filter(|&&p| p != sky_color).count();
1336        assert!(
1337            non_sky_near > 100,
1338            "Near render too sparse ({non_sky_near})"
1339        );
1340        assert!(non_sky_mid > 100, "Mid render too sparse ({non_sky_mid})");
1341
1342        // Hash must differ — the Mid override changed mip_levels
1343        // from 3 to 1, which changes mip transitions and so changes
1344        // pixels.
1345        let h_near = fb_hash(&fb_near);
1346        let h_mid = fb_hash(&fb_mid);
1347        assert_ne!(
1348            h_near, h_mid,
1349            "Mid tier with mid_mip_levels=Some(1) must differ from Near (h_near={h_near:016x})"
1350        );
1351    }
1352
1353    /// Mid tier with `mid_mip_levels = None` AND
1354    /// `mid_mip_scan_dist = None` must produce a byte-identical
1355    /// framebuffer to Near. This is the graceful-degrade contract
1356    /// — callers can opt into the Mid plumbing without committing
1357    /// to a mip override and stay byte-stable.
1358    #[test]
1359    fn s6_1_mid_without_overrides_byte_identical_to_near() {
1360        let camera = camera_at([64.0, 0.0, 64.0]);
1361
1362        // Scene A: default thresholds → Near.
1363        let (mut scene_a, _) = build_mip_visible_grid(DVec3::ZERO);
1364        let fb_near = render_with_multi_mip(&mut scene_a, &camera);
1365
1366        // Scene B: thresholds force Mid but no mip overrides set.
1367        let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1368        scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1369            r_near: 0.0,
1370            r_mid: f64::INFINITY,
1371            mid_mip_levels: None,
1372            mid_mip_scan_dist: None,
1373        };
1374        let lod = scene_b
1375            .grid(b_id)
1376            .unwrap()
1377            .select_lod(DVec3::from_array(camera.pos));
1378        assert_eq!(lod, Lod::Mid);
1379        let fb_mid = render_with_multi_mip(&mut scene_b, &camera);
1380
1381        // Byte-identical: Mid with no overrides degrades cleanly.
1382        assert_eq!(
1383            fb_near, fb_mid,
1384            "Mid with both overrides=None must byte-match Near"
1385        );
1386    }
1387
1388    /// `mip_levels_override` (the global per-grid cap) must compose
1389    /// with the Mid override: the effective `mip_levels` should be
1390    /// `min(mid_override, global_cap)`. Tests that the ship
1391    /// anti-axis-aligned-beam workaround survives Mid tier.
1392    ///
1393    /// We pin this by checking that a grid with `mip_levels_override
1394    /// = Some(1)` (mip-0 only — ship workaround) AND Mid-tier
1395    /// `mid_mip_levels = Some(4)` renders the same as a grid with
1396    /// just `mip_levels_override = Some(1)` at Near tier. Both
1397    /// resolve to `mip_levels = 1` (no multi-mip), so both must
1398    /// produce the same framebuffer.
1399    #[test]
1400    fn s6_1_global_mip_cap_survives_mid_tier() {
1401        let camera = camera_at([64.0, 0.0, 64.0]);
1402
1403        // Scene A: Near tier + global cap=1.
1404        let (mut scene_a, a_id) = build_mip_visible_grid(DVec3::ZERO);
1405        scene_a.grid_mut(a_id).unwrap().mip_levels_override = Some(1);
1406        let fb_a = render_with_multi_mip(&mut scene_a, &camera);
1407
1408        // Scene B: Mid tier + global cap=1 + Mid override=4. Global
1409        // cap (1) should win since it's the tighter floor on
1410        // `min(global_cap, mid_override)`.
1411        let (mut scene_b, b_id) = build_mip_visible_grid(DVec3::ZERO);
1412        scene_b.grid_mut(b_id).unwrap().mip_levels_override = Some(1);
1413        scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1414            r_near: 0.0,
1415            r_mid: f64::INFINITY,
1416            mid_mip_levels: Some(4),
1417            // mip_scan_dist override left None so we isolate the
1418            // mip_levels resolution path.
1419            mid_mip_scan_dist: None,
1420        };
1421        let fb_b = render_with_multi_mip(&mut scene_b, &camera);
1422
1423        assert_eq!(
1424            fb_a, fb_b,
1425            "global mip_levels_override should clamp Mid override (ship workaround survives Mid tier)"
1426        );
1427    }
1428
1429    // ---- S6.3: Far-tier billboard blit ----
1430
1431    /// Force Far tier via `r_near = 0, r_mid = 0`: any non-zero
1432    /// camera-to-grid distance lands on `Lod::Far`. Renders a small
1433    /// grid at world (0, 200, 0) with default-radius thresholds
1434    /// turned all-Far. The composed framebuffer must contain
1435    /// non-sky pixels from the impostor blit.
1436    #[test]
1437    fn s6_3_far_tier_blits_non_sky_pixels() {
1438        let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1439        scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1440            r_near: 0.0,
1441            r_mid: 0.0,
1442            mid_mip_levels: None,
1443            mid_mip_scan_dist: None,
1444        };
1445
1446        let camera = camera_at([64.0, 0.0, 100.0]);
1447        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1448        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1449        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1450        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1451        let outcome = render_scene_composed(
1452            &mut fb,
1453            &mut zb,
1454            XRES as usize,
1455            XRES,
1456            YRES,
1457            &mut pool,
1458            &mut scene,
1459            &camera,
1460            &settings,
1461            sky_color,
1462            None,
1463        );
1464        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1465
1466        // Sanity: picker actually picked Far.
1467        let lod = scene
1468            .grid(id)
1469            .unwrap()
1470            .select_lod(DVec3::from_array(camera.pos));
1471        assert_eq!(lod, Lod::Far);
1472
1473        // Impostor must paint at least some non-sky pixels.
1474        let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1475        assert!(
1476            non_sky > 0,
1477            "Far-tier render produced no non-sky pixels — billboard blit not firing"
1478        );
1479    }
1480
1481    /// Lazy populate: cache starts `None`, becomes `Some` after the
1482    /// first Far render.
1483    #[test]
1484    fn s6_3_far_render_lazily_populates_cache() {
1485        let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1486        scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1487            r_near: 0.0,
1488            r_mid: 0.0,
1489            mid_mip_levels: None,
1490            mid_mip_scan_dist: None,
1491        };
1492        assert!(scene.grid(id).unwrap().billboards.is_none());
1493
1494        let camera = camera_at([64.0, 0.0, 100.0]);
1495        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1496        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1497        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1498        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1499        let _ = render_scene_composed(
1500            &mut fb,
1501            &mut zb,
1502            XRES as usize,
1503            XRES,
1504            YRES,
1505            &mut pool,
1506            &mut scene,
1507            &camera,
1508            &settings,
1509            sky_color,
1510            None,
1511        );
1512        let cache = scene
1513            .grid(id)
1514            .unwrap()
1515            .billboards
1516            .as_ref()
1517            .expect("Far render should have populated billboards");
1518        assert_eq!(cache.len(), 26);
1519    }
1520
1521    /// Edit invalidates the cache; a subsequent Far render rebuilds.
1522    #[test]
1523    fn s6_3_edit_invalidates_then_far_render_rebuilds() {
1524        let (mut scene, id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1525        scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1526            r_near: 0.0,
1527            r_mid: 0.0,
1528            mid_mip_levels: None,
1529            mid_mip_scan_dist: None,
1530        };
1531        let camera = camera_at([64.0, 0.0, 100.0]);
1532        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1533        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1534
1535        // First Far render → cache built.
1536        let mut fb1 = vec![sky_color; pixel_count(XRES, YRES)];
1537        let mut zb1 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1538        let _ = render_scene_composed(
1539            &mut fb1,
1540            &mut zb1,
1541            XRES as usize,
1542            XRES,
1543            YRES,
1544            &mut pool,
1545            &mut scene,
1546            &camera,
1547            &settings,
1548            sky_color,
1549            None,
1550        );
1551        assert!(scene.grid(id).unwrap().billboards.is_some());
1552
1553        // Edit invalidates.
1554        scene
1555            .grid_mut(id)
1556            .unwrap()
1557            .set_voxel(IVec3::new(70, 70, 70), Some(0x80_aa_aa_22));
1558        assert!(scene.grid(id).unwrap().billboards.is_none());
1559
1560        // Second Far render rebuilds.
1561        let mut fb2 = vec![sky_color; pixel_count(XRES, YRES)];
1562        let mut zb2 = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1563        let _ = render_scene_composed(
1564            &mut fb2,
1565            &mut zb2,
1566            XRES as usize,
1567            XRES,
1568            YRES,
1569            &mut pool,
1570            &mut scene,
1571            &camera,
1572            &settings,
1573            sky_color,
1574            None,
1575        );
1576        assert!(scene.grid(id).unwrap().billboards.is_some());
1577    }
1578
1579    /// Hybrid scene: one Near grid + one Far grid. Both must render
1580    /// visibly; the Far grid via blit, the Near grid via opticast.
1581    /// Sanity check that the two paths cohabit one
1582    /// `render_scene_composed` call.
1583    #[test]
1584    fn s6_3_near_and_far_grids_in_same_scene() {
1585        let mut scene = Scene::new();
1586        // Grid A: stays Near (default thresholds). Solid box at
1587        // world (-30..-20, 190..210, 50..70).
1588        let a_id = scene.add_grid(GridTransform::at(DVec3::new(-100.0, 200.0, 0.0)));
1589        scene.grid_mut(a_id).unwrap().set_rect(
1590            IVec3::new(70, 0, 50),
1591            IVec3::new(85, 15, 70),
1592            Some(0x80_22_88_22), // green
1593        );
1594        // Grid B: forced Far. Box at world (~100, 200, 100).
1595        let b_id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1596        scene.grid_mut(b_id).unwrap().set_rect(
1597            IVec3::new(0, 0, 80),
1598            IVec3::new(20, 20, 110),
1599            Some(0x80_aa_22_22), // red
1600        );
1601        scene.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds {
1602            r_near: 0.0,
1603            r_mid: 0.0,
1604            mid_mip_levels: None,
1605            mid_mip_scan_dist: None,
1606        };
1607
1608        let camera = camera_at([0.0, 0.0, 80.0]);
1609        // Confirm A is Near, B is Far for this pose.
1610        assert_eq!(
1611            scene
1612                .grid(a_id)
1613                .unwrap()
1614                .select_lod(DVec3::from_array(camera.pos)),
1615            Lod::Near
1616        );
1617        assert_eq!(
1618            scene
1619                .grid(b_id)
1620                .unwrap()
1621                .select_lod(DVec3::from_array(camera.pos)),
1622            Lod::Far
1623        );
1624
1625        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1626        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1627        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1628        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1629        let outcome = render_scene_composed(
1630            &mut fb,
1631            &mut zb,
1632            XRES as usize,
1633            XRES,
1634            YRES,
1635            &mut pool,
1636            &mut scene,
1637            &camera,
1638            &settings,
1639            sky_color,
1640            None,
1641        );
1642        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 2 });
1643
1644        // Each grid should contribute visible pixels.
1645        let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1646        assert!(
1647            non_sky > 20,
1648            "hybrid scene produced too few non-sky pixels ({non_sky}); one tier may have failed"
1649        );
1650    }
1651
1652    /// Empty grid at Far tier: skipped silently (no panic, no
1653    /// allocation), `billboards` stays `None`.
1654    #[test]
1655    fn s6_3_empty_grid_at_far_is_skipped() {
1656        let mut scene = Scene::new();
1657        let id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 200.0, 0.0)));
1658        scene.grid_mut(id).unwrap().lod_thresholds = crate::LodThresholds {
1659            r_near: 0.0,
1660            r_mid: 0.0,
1661            mid_mip_levels: None,
1662            mid_mip_scan_dist: None,
1663        };
1664
1665        let camera = camera_at([0.0, 0.0, 100.0]);
1666        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1667        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1668        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1669        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1670        let outcome = render_scene_composed(
1671            &mut fb,
1672            &mut zb,
1673            XRES as usize,
1674            XRES,
1675            YRES,
1676            &mut pool,
1677            &mut scene,
1678            &camera,
1679            &settings,
1680            sky_color,
1681            None,
1682        );
1683        // No grids contributed.
1684        assert_eq!(outcome, RenderOutcome::Empty);
1685        // Cache must NOT have been built for an empty grid.
1686        assert!(scene.grid(id).unwrap().billboards.is_none());
1687        // Framebuffer unchanged.
1688        assert!(fb.iter().all(|&p| p == sky_color));
1689    }
1690
1691    // ---- S6.0: LOD picker wired but every tier falls through to Near ----
1692
1693    /// Threshold-invariance: a grid rendered with the S6 derived
1694    /// thresholds (`from_radius` of the actual bounding sphere) must
1695    /// produce a framebuffer byte-identical to the same grid with
1696    /// default `always_near` thresholds, because S6.0 takes the
1697    /// `Near` arm of the match for all three tiers. This is the
1698    /// regression test for the S6.0 contract.
1699    #[test]
1700    fn render_scene_composed_lod_threshold_invariance() {
1701        // Scene A: default thresholds (always_near).
1702        let (mut scene_a, _a_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1703        let cam = camera_at([64.0, 0.0, 100.0]);
1704        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1705        let mut fb_a = vec![sky_color; pixel_count(XRES, YRES)];
1706        let mut zb_a = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1707        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1708        let outcome_a = render_scene_composed(
1709            &mut fb_a,
1710            &mut zb_a,
1711            XRES as usize,
1712            XRES,
1713            YRES,
1714            &mut pool,
1715            &mut scene_a,
1716            &cam,
1717            &settings,
1718            sky_color,
1719            None,
1720        );
1721        assert_eq!(outcome_a, RenderOutcome::Rendered { grids_drawn: 1 });
1722
1723        // Scene B: thresholds derived from the grid's bounding
1724        // radius. At this camera distance the grid lands on Mid or
1725        // Far; if S6.0 ever stops falling through to Near, this test
1726        // catches the divergence.
1727        let (mut scene_b, b_id) = build_one_grid_scene(DVec3::new(0.0, 200.0, 0.0));
1728        let radius = scene_b.grid(b_id).unwrap().bounding_radius();
1729        assert!(
1730            radius > 0.0,
1731            "bounding_radius should be > 0 for a populated grid"
1732        );
1733        scene_b.grid_mut(b_id).unwrap().lod_thresholds = crate::LodThresholds::from_radius(radius);
1734        // Sanity: the camera is far enough that the picker no longer
1735        // returns Near (otherwise the invariance test would be vacuous).
1736        let lod = scene_b
1737            .grid(b_id)
1738            .unwrap()
1739            .select_lod(DVec3::from_array(cam.pos));
1740        assert_ne!(
1741            lod,
1742            Lod::Near,
1743            "camera should land in Mid or Far for derived thresholds — got {lod:?}",
1744        );
1745
1746        let mut fb_b = vec![sky_color; pixel_count(XRES, YRES)];
1747        let mut zb_b = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1748        let outcome_b = render_scene_composed(
1749            &mut fb_b,
1750            &mut zb_b,
1751            XRES as usize,
1752            XRES,
1753            YRES,
1754            &mut pool,
1755            &mut scene_b,
1756            &cam,
1757            &settings,
1758            sky_color,
1759            None,
1760        );
1761        assert_eq!(outcome_b, RenderOutcome::Rendered { grids_drawn: 1 });
1762
1763        // Byte-identity is the S6.0 contract — Mid/Far still take
1764        // the Near arm.
1765        assert_eq!(
1766            fb_a, fb_b,
1767            "S6.0 framebuffer must be byte-identical regardless of LOD thresholds"
1768        );
1769    }
1770
1771    #[test]
1772    fn render_scene_composed_empty_scene_returns_empty() {
1773        let mut scene = Scene::new();
1774        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1775        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1776        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1777        let camera = camera_at([0.0, 0.0, 0.0]);
1778        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1779        let outcome = render_scene_composed(
1780            &mut fb,
1781            &mut zb,
1782            XRES as usize,
1783            XRES,
1784            YRES,
1785            &mut pool,
1786            &mut scene,
1787            &camera,
1788            &settings,
1789            sky_color,
1790            None,
1791        );
1792        assert_eq!(outcome, RenderOutcome::Empty);
1793        // fb should be unchanged (still all sky).
1794        assert!(fb.iter().all(|&p| p == sky_color));
1795    }
1796
1797    /// FNV-1a 64-bit hash. Same offset/prime as the
1798    /// `roxlap-oracle::fnv1a64` helper used by the wasm-render
1799    /// goldens; pinning a render hash here is the same flavour of
1800    /// regression catch.
1801    fn fnv1a64(data: &[u8]) -> u64 {
1802        let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1803        for &b in data {
1804            h ^= u64::from(b);
1805            h = h.wrapping_mul(0x0000_0100_0000_01b3);
1806        }
1807        h
1808    }
1809
1810    // ---- S4.0 cross-chunk smoke test ----
1811
1812    /// Two-chunk-wide grid: a recognisable shape spans the chunk
1813    /// boundary at `virtual_x = 128`. The render must not have a
1814    /// horizontal seam line at the boundary.
1815    #[test]
1816    fn render_scene_two_chunk_x_grid_no_seam() {
1817        let mut scene = Scene::new();
1818        let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
1819        let g = scene.grid_mut(id).unwrap();
1820        // 100-voxel-tall stripe spanning x=[120..136] across the
1821        // x=128 chunk seam at z=200, y=[60..68]. After bake-free
1822        // render, every column in the stripe paints the same colour
1823        // at the same z; a seam at x=128 would show as missing
1824        // pixels in the column at virtual_x=128 / 129 / ...
1825        g.set_rect(
1826            IVec3::new(120, 60, 200),
1827            IVec3::new(136, 67, 215),
1828            Some(0x80_aa_55_22),
1829        );
1830        // Sanity: ensure both chunks were materialised.
1831        assert_eq!(g.chunk_count(), 2);
1832
1833        // Render with a camera positioned to look at the stripe
1834        // straight on. Stripe at world (120..136, 260..268, 200..215).
1835        // Camera at (128, 100, 207) looking +y centres on it.
1836        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
1837        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1838        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1839        let camera = camera_at([128.0, 100.0, 207.0]);
1840        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1841        let outcome = render_scene_composed(
1842            &mut fb,
1843            &mut zb,
1844            XRES as usize,
1845            XRES,
1846            YRES,
1847            &mut pool,
1848            &mut scene,
1849            &camera,
1850            &settings,
1851            sky_color,
1852            None,
1853        );
1854        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1855
1856        // Stripe colour should appear in roughly the centre of the
1857        // framebuffer. A chunk-edge seam would manifest as a thin
1858        // sky-coloured vertical line splitting the stripe in two.
1859        let stripe = 0x80_aa_55_22;
1860        let stripe_count = fb.iter().filter(|&&p| p == stripe).count();
1861        assert!(
1862            stripe_count > 200,
1863            "stripe rendered too few pixels ({stripe_count}) — chunks may not be stitching"
1864        );
1865
1866        // Walk the centre row left-to-right looking for a sky-pixel
1867        // gap inside a stripe run. A gap 1+ pixels wide flags a
1868        // chunk-edge seam.
1869        let centre_y = (YRES / 2) as usize;
1870        let row_start = centre_y * (XRES as usize);
1871        let row = &fb[row_start..row_start + (XRES as usize)];
1872        let mut in_stripe = false;
1873        let mut seam_gaps = 0usize;
1874        for &px in row {
1875            if px == stripe {
1876                in_stripe = true;
1877            } else if in_stripe && px == sky_color {
1878                // Stripe ended; if we re-enter it on this row that's
1879                // a seam.
1880                if row.iter().skip_while(|&&p| p != px).any(|&p| p == stripe) {
1881                    // Look ahead for any further stripe pixel.
1882                    seam_gaps += 1;
1883                }
1884                in_stripe = false;
1885            }
1886        }
1887        // We allow seam_gaps to count the legitimate "stripe ended,
1888        // didn't restart" transition once; more than that means
1889        // multiple disjoint runs on the row → seam.
1890        assert!(
1891            seam_gaps <= 1,
1892            "centre row has {seam_gaps} disjoint stripe runs — expected 1 (chunk-edge seam suspected)"
1893        );
1894    }
1895
1896    /// Pin the byte-exact FNV-1a64 of a 2-chunk render. Catches
1897    /// any drift in the cross-chunk stitch / opticast path.
1898    /// S4B.5: regression test for the cf-halving + column-step
1899    /// interaction at chunk-vsid (=128) under multi-mip rendering.
1900    /// Prior to the fix in `grouscan.rs:phase_after_delete_kept_presync`
1901    /// the single-chunk column-step recomputed `ixy_sptr_col_idx`
1902    /// from `cy * vsid + cx`, mapping the post-remiporend mip-N
1903    /// sub-table index back into mip-0's range. The next mip
1904    /// transition then underflowed at
1905    /// `state.ixy_sptr_col_idx - mip_base_offsets[old_mip]`.
1906    ///
1907    /// `mip_scan_dist=32` chosen so the 3-mip depth ladder
1908    /// (32→64→128 PREC scan budgets) reaches the floor 36 voxels
1909    /// away at mip-1 or mip-2 — exercising the post-transition
1910    /// rendering path that was broken pre-fix.
1911    #[test]
1912    fn vxl_generate_mips_on_set_voxel_chunk_renders() {
1913        let mut grid = crate::Grid::new(GridTransform::identity());
1914        // Solid floor at z=100..254 across the entire chunk —
1915        // looks like the oracle test's terrain.
1916        grid.set_rect(
1917            IVec3::new(0, 0, 100),
1918            IVec3::new(127, 127, 254),
1919            Some(0x80_88_88_88),
1920        );
1921        let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1922        chunk.generate_mips(3);
1923        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1924        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1925        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1926        let camera = camera_at([64.0, 0.0, 64.0]);
1927        let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1928        settings.mip_levels = 3;
1929        settings.mip_scan_dist = 32;
1930        let grid_view = roxlap_core::GridView::from_single_vxl(&chunk);
1931        let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
1932        let _ = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
1933        drop(rasterizer);
1934        let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1935        assert!(
1936            non_sky > 0,
1937            "Vxl::generate_mips on a set_voxel-built chunk should render to something non-sky (got {non_sky})"
1938        );
1939    }
1940
1941    /// Mip-0 preservation when mips are generated on the combined
1942    /// view but `mip_levels = 1` in the rasterizer's settings.
1943    /// Confirms `generate_mips` only APPENDS data — mip-0
1944    /// prefix is unchanged.
1945    #[test]
1946    fn render_with_mips_present_still_renders_mip0() {
1947        let mut scene = Scene::new();
1948        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
1949        scene.grid_mut(id).unwrap().set_rect(
1950            IVec3::new(40, 40, 40),
1951            IVec3::new(55, 55, 55),
1952            Some(0x80_88_88_88),
1953        );
1954        // S4B.4.a: force mip-1..mip-2 generation on the single
1955        // chunk directly (the Grid's combined-view cache API was
1956        // removed). The chunk's own Vxl::generate_mips builds its
1957        // own mip tables and the renderer happens to render through
1958        // them via Approach B's chunk_at_xy lookup.
1959        {
1960            let grid = scene.grid_mut(id).unwrap();
1961            let chunk = grid.chunks.get_mut(&IVec3::ZERO).unwrap();
1962            chunk.generate_mips(3);
1963        }
1964
1965        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
1966        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
1967        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
1968        let camera = camera_at([64.0, 0.0, 64.0]);
1969        // mip_scan_dist huge → renderer never transitions past mip-0
1970        // so this test pins mip-0 correctness only.
1971        let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
1972        settings.mip_scan_dist = 100_000;
1973        let outcome = render_scene_composed(
1974            &mut fb,
1975            &mut zb,
1976            XRES as usize,
1977            XRES,
1978            YRES,
1979            &mut pool,
1980            &mut scene,
1981            &camera,
1982            &settings,
1983            sky_color,
1984            None,
1985        );
1986        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
1987        let non_sky = fb.iter().filter(|&&p| p != sky_color).count();
1988        assert!(
1989            non_sky > 0,
1990            "render of single-grid scene with mips present rendered all-sky: mip-0 may be corrupted by generate_mips"
1991        );
1992    }
1993
1994    #[test]
1995    fn render_scene_two_chunk_x_grid_hash_is_stable() {
1996        // Frozen 2026-05-10 at S4.0 landing on x86_64.
1997        const GOLDEN: u64 = 0x215e_d66d_7359_4725;
1998        // Same scene shape as `render_scene_two_chunk_x_grid_no_seam`
1999        // — kept distinct so the hash assertion doesn't share its
2000        // setup with the structural seam check.
2001        let mut scene = Scene::new();
2002        let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 200.0, 0.0)));
2003        scene.grid_mut(id).unwrap().set_rect(
2004            IVec3::new(120, 60, 200),
2005            IVec3::new(136, 67, 215),
2006            Some(0x80_aa_55_22),
2007        );
2008        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2009        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2010        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2011        let camera = camera_at([128.0, 100.0, 207.0]);
2012        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2013        let outcome = render_scene_composed(
2014            &mut fb,
2015            &mut zb,
2016            XRES as usize,
2017            XRES,
2018            YRES,
2019            &mut pool,
2020            &mut scene,
2021            &camera,
2022            &settings,
2023            sky_color,
2024            None,
2025        );
2026        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2027
2028        let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2029        let hash = fnv1a64(&bytes);
2030        if GOLDEN == SENTINEL {
2031            // First-run capture mode — print the hash so the
2032            // developer can paste it into GOLDEN above.
2033            eprintln!("render_scene_two_chunk_x_grid_hash_is_stable: capture hash = 0x{hash:016x}");
2034            panic!("GOLDEN is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN above");
2035        }
2036        assert_eq!(
2037            hash, GOLDEN,
2038            "2-chunk render hash drifted: expected 0x{GOLDEN:016x}, got 0x{hash:016x}"
2039        );
2040    }
2041
2042    /// Sentinel for first-run hash capture in
2043    /// [`render_scene_two_chunk_x_grid_hash_is_stable`]. Replace
2044    /// `GOLDEN`'s definition with the printed value once captured.
2045    const SENTINEL: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2046
2047    /// S4B.2.c.3: render a 2-chunk x-stripe scene via Approach B
2048    /// (multi-chunk GridView + direct opticast). Validates the full
2049    /// chain — `Grid::chunk_xy_backing` → `ChunkGrid` →
2050    /// `GridView::from_chunk_grid` → opticast prelude
2051    /// `recompute_camera_chunk` → `camera_chunk_air_gap` lookup →
2052    /// gline's chunk-aware seed → grouscan's chunk-swap column-step.
2053    ///
2054    /// Geometry: a floor in chunk (0, 0) at grid-local
2055    /// `(0..127, 0..127, 200..205)` plus a recognisable box in
2056    /// chunk (1, 0) at `(160..170, 50..60, 150..165)`. Camera in
2057    /// chunk (0, 0) looking +x so rays cross into chunk (1, 0) and
2058    /// must trigger the cross-chunk DDA. (Camera in chunk (1, 0) is
2059    /// blocked by the in_bounds_xy check that still uses the per-
2060    /// chunk vsid; full grid-AABB in_bounds gets revisited later.)
2061    ///
2062    /// Hash-pinned. Non-sky pixel count is the primary correctness
2063    /// signal — if the cross-chunk DDA were broken, only the floor
2064    /// in chunk (0, 0) would render.
2065    #[test]
2066    fn approach_b_renders_two_chunk_x_stripe_via_chunk_grid() {
2067        const SENTINEL_B: u64 = 0xDEAD_BEEF_DEAD_BEEF;
2068        // Frozen 2026-05-11 on x86_64 — Approach B's first
2069        // multi-chunk render (S4B.2.c.3). Refreeze when changing
2070        // the rasterizer; the cross-chunk DDA stays validated by
2071        // the `floor_count` + `box_count` assertions above.
2072        const GOLDEN_B: u64 = 0x5ee1_e81c_66a8_d1f1;
2073
2074        let mut scene = Scene::new();
2075        let id = scene.add_grid(GridTransform::identity());
2076        let g = scene.grid_mut(id).unwrap();
2077        // Floor across chunk (0, 0) so rays looking +x always have
2078        // a hit before they exit the grid AABB.
2079        g.set_rect(
2080            IVec3::new(0, 0, 200),
2081            IVec3::new(127, 127, 205),
2082            Some(0x80_44_44_aa),
2083        );
2084        // Recognisable box deep in chunk (1, 0) — only visible if
2085        // the cross-chunk DDA fires.
2086        g.set_rect(
2087            IVec3::new(160, 50, 150),
2088            IVec3::new(170, 60, 165),
2089            Some(0x80_aa_55_22),
2090        );
2091        assert_eq!(g.chunk_count(), 2);
2092
2093        // Build the multi-chunk GridView.
2094        let backing = g.chunk_xyz_backing().expect("at least one chunk populated");
2095        assert_eq!(backing.chunks_x, 2);
2096        assert_eq!(backing.chunks_y, 1);
2097        assert_eq!(backing.origin_chunk_xy, [0, 0]);
2098        let cg = roxlap_core::ChunkGrid {
2099            chunks: &backing.chunks,
2100            origin_chunk_xy: backing.origin_chunk_xy,
2101            origin_chunk_z: backing.origin_chunk_z,
2102            chunks_x: backing.chunks_x,
2103            chunks_y: backing.chunks_y,
2104            chunks_z: backing.chunks_z,
2105        };
2106        let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2107
2108        // Camera in chunk (0, 0) looking +x toward chunk (1, 0).
2109        // Voxlap z-down basis: right × down == forward.
2110        let camera = Camera {
2111            pos: [10.0, 64.0, 160.0],
2112            right: [0.0, 1.0, 0.0],
2113            down: [0.0, 0.0, 1.0],
2114            forward: [1.0, 0.0, 0.0],
2115        };
2116        let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2117        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2118        let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2119        let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2120        drop(rasterizer);
2121        assert_eq!(outcome, OpticastOutcome::Rendered);
2122
2123        // Hits BOTH the floor (chunk 0, 0) AND the box (chunk 1, 0).
2124        let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2125        let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2126        assert!(
2127            floor_count > 1000,
2128            "floor not visible — only {floor_count} floor pixels (single-chunk path?)"
2129        );
2130        assert!(
2131            box_count > 50,
2132            "box in chunk (1, 0) not visible — only {box_count} box pixels — cross-chunk DDA may have failed to fire"
2133        );
2134
2135        // Hash-pin the output. Refreeze when changing the rasterizer.
2136        let bytes: Vec<u8> = fb.iter().flat_map(|p| p.to_ne_bytes()).collect();
2137        let hash = fnv1a64(&bytes);
2138        if GOLDEN_B == SENTINEL_B {
2139            eprintln!("approach_b_renders_two_chunk_x_stripe_via_chunk_grid: capture hash = 0x{hash:016x}");
2140            panic!(
2141                "GOLDEN_B is the SENTINEL placeholder — paste 0x{hash:016x} into GOLDEN_B above"
2142            );
2143        }
2144        assert_eq!(
2145            hash, GOLDEN_B,
2146            "Approach B 2-chunk render hash drifted: expected 0x{GOLDEN_B:016x}, got 0x{hash:016x}"
2147        );
2148    }
2149
2150    /// S4B.2.d: multi-chunk camera that sits **past** the first
2151    /// chunk's vsid (i.e., inside chunk (1, 0)). Validates that
2152    /// `recompute_in_bounds_xy` against the grid AABB recognises
2153    /// the camera as in-bounds — and that the seed path looks up
2154    /// chunk (1, 0)'s column via `chunk_at_xy(camera_chunk_idx)`
2155    /// instead of returning the OOB-XY bedrock placeholder.
2156    ///
2157    /// Scene shape: a 2-chunk x-stripe with a floor in chunk (1, 0)
2158    /// (where the camera sits) and the recognisable box in chunk
2159    /// (0, 0). The camera looks `-x` so rays cross from chunk
2160    /// (1, 0) back into chunk (0, 0). Tests the OTHER direction of
2161    /// cross-chunk DDA + the in-bounds AABB fix together.
2162    #[test]
2163    fn approach_b_camera_in_chunk_1_0_renders_neighbour() {
2164        let mut scene = Scene::new();
2165        let id = scene.add_grid(GridTransform::identity());
2166        let g = scene.grid_mut(id).unwrap();
2167        // Floor under the camera in chunk (1, 0).
2168        g.set_rect(
2169            IVec3::new(128, 0, 200),
2170            IVec3::new(255, 127, 205),
2171            Some(0x80_44_44_aa),
2172        );
2173        // Box deep in chunk (0, 0) — only visible if the camera in
2174        // chunk (1, 0) is recognised as in-bounds AND the
2175        // cross-chunk DDA fires westward.
2176        g.set_rect(
2177            IVec3::new(20, 50, 150),
2178            IVec3::new(30, 60, 165),
2179            Some(0x80_aa_55_22),
2180        );
2181        assert_eq!(g.chunk_count(), 2);
2182
2183        let backing = g.chunk_xyz_backing().expect("populated");
2184        let cg = roxlap_core::ChunkGrid {
2185            chunks: &backing.chunks,
2186            origin_chunk_xy: backing.origin_chunk_xy,
2187            origin_chunk_z: backing.origin_chunk_z,
2188            chunks_x: backing.chunks_x,
2189            chunks_y: backing.chunks_y,
2190            chunks_z: backing.chunks_z,
2191        };
2192        let grid_view = roxlap_core::GridView::from_chunk_grid(&cg, CHUNK_SIZE_XY);
2193        let (aabb_min, aabb_max) = grid_view.aabb_xy();
2194        assert_eq!(aabb_min, [0, 0]);
2195        assert_eq!(aabb_max, [256, 128]);
2196
2197        // Camera deep in chunk (1, 0): world (200, 64, 160). Past
2198        // the single-chunk vsid=128 OOB cutoff but inside the
2199        // multi-chunk AABB. Look -x toward chunk (0, 0).
2200        let camera = Camera {
2201            pos: [200.0, 64.0, 160.0],
2202            right: [0.0, -1.0, 0.0],
2203            down: [0.0, 0.0, 1.0],
2204            forward: [-1.0, 0.0, 0.0],
2205        };
2206        let (_engine, mut pool, mut fb, mut zb) = render_setup(2 * CHUNK_SIZE_XY);
2207        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2208        let mut rasterizer = ScalarRasterizer::new(&mut fb, &mut zb, XRES as usize, grid_view);
2209        let outcome = core_opticast(&mut rasterizer, &mut pool, &camera, &settings, grid_view);
2210        drop(rasterizer);
2211        assert_eq!(outcome, OpticastOutcome::Rendered);
2212
2213        let floor_count = fb.iter().filter(|&&p| p == 0x80_44_44_aa).count();
2214        let box_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2215        assert!(
2216            floor_count > 1000,
2217            "floor under camera in chunk (1, 0) not visible — only {floor_count} floor pixels — in_bounds_xy fix may not have taken effect"
2218        );
2219        assert!(
2220            box_count > 50,
2221            "box in chunk (0, 0) not visible — only {box_count} box pixels — westward cross-chunk DDA failed"
2222        );
2223    }
2224
2225    /// S4B.6.c: stacked-grid scaffold — camera in chz=1 (= world
2226    /// z=256..511) of a 2-chunk-tall grid should render its own
2227    /// chunk's terrain. Verifies cf seed + slab-byte reads + chunk-
2228    /// XY swaps all use world-z consistently.
2229    ///
2230    /// Cross-chunk look-down (= camera in chz=0 sees terrain in
2231    /// chz=1) needs cf z range extension at air-gap-lookup time;
2232    /// that's a follow-up to S4B.6.c.
2233    #[test]
2234    fn stacked_two_chunk_z_camera_in_chz1_sees_own_chunk_floor() {
2235        let mut scene = Scene::new();
2236        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2237        let g = scene.grid_mut(id).unwrap();
2238        // chz=0: all-air (materialised so chunk_xyz_backing enumerates).
2239        g.ensure_chunk(IVec3::new(0, 0, 0));
2240        // chz=1: floor at local z=50 (= world z=306).
2241        g.set_rect(
2242            IVec3::new(60, 60, 306),
2243            IVec3::new(72, 72, 310),
2244            Some(0x80_33_66_99),
2245        );
2246        assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2247
2248        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2249        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2250        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2251        pool.set_treat_z_max_as_air(true);
2252        // Camera at world (66, 66, 280) — directly above the
2253        // floor at world z=306. Look STRAIGHT DOWN (z increases =
2254        // down in voxlap z-down).
2255        let camera = Camera {
2256            pos: [66.0, 66.0, 280.0],
2257            right: [1.0, 0.0, 0.0],
2258            down: [0.0, 1.0, 0.0],
2259            forward: [0.0, 0.0, 1.0],
2260        };
2261        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2262        let outcome = render_scene_composed(
2263            &mut fb,
2264            &mut zb,
2265            XRES as usize,
2266            XRES,
2267            YRES,
2268            &mut pool,
2269            &mut scene,
2270            &camera,
2271            &settings,
2272            sky_color,
2273            None,
2274        );
2275        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2276        let floor_count = fb.iter().filter(|&&p| p == 0x80_33_66_99).count();
2277        assert!(
2278            floor_count > 100,
2279            "camera at chz=1 with floor in same chunk should see it — got {floor_count} floor pixels"
2280        );
2281    }
2282
2283    /// S4B.6.e: cross-chunk look-down. Camera in chz=0's all-air
2284    /// chunk should see chz=1's floor below it. This was deferred
2285    /// from S4B.6.c because the cf seed's z range capped at the
2286    /// camera-chunk's bedrock (world z=255); S4B.6.e extends the
2287    /// air-gap walk in `camera_chunk_air_gap` to step into the
2288    /// next chunk down when the camera's column is all-air-bedrock,
2289    /// and the rasterizer routes state.column / slab_buf to the
2290    /// chunk holding the real floor via `seed_chunk_z`.
2291    #[test]
2292    fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor() {
2293        let mut scene = Scene::new();
2294        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2295        let g = scene.grid_mut(id).unwrap();
2296        // chz=0: all-air. Materialised so chunk_xyz_backing
2297        // enumerates it.
2298        g.ensure_chunk(IVec3::new(0, 0, 0));
2299        // chz=1: floor at world z=306..310 (= local z=50..54).
2300        g.set_rect(
2301            IVec3::new(60, 60, 306),
2302            IVec3::new(72, 72, 310),
2303            Some(0x80_77_aa_44),
2304        );
2305        assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2306
2307        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2308        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2309        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2310        pool.set_treat_z_max_as_air(true);
2311        // Camera at world (66, 66, 100) — in chz=0's all-air
2312        // chunk. Look STRAIGHT DOWN (z+) toward chz=1's floor at
2313        // world z=306.
2314        let camera = Camera {
2315            pos: [66.0, 66.0, 100.0],
2316            right: [1.0, 0.0, 0.0],
2317            down: [0.0, 1.0, 0.0],
2318            forward: [0.0, 0.0, 1.0],
2319        };
2320        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2321        let outcome = render_scene_composed(
2322            &mut fb,
2323            &mut zb,
2324            XRES as usize,
2325            XRES,
2326            YRES,
2327            &mut pool,
2328            &mut scene,
2329            &camera,
2330            &settings,
2331            sky_color,
2332            None,
2333        );
2334        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2335        let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2336        assert!(
2337            floor_count > 50,
2338            "camera in chz=0 air-gap should see chz=1 floor via cross-chunk look-down — got {floor_count} floor pixels"
2339        );
2340    }
2341
2342    /// S4B.6.l KNOWN LIMITATION (pre-S4B.6.l fix): camera at chz=0
2343    /// with all-air-bedrock at the camera's own XY column
2344    /// (seed_chz=1 via cross-chunk look-down). A DIFFERENT XY column
2345    /// has chz=0 content (= a distant mountain entirely inside
2346    /// chz=0). State.current_chunk_z gets pinned to 1 at seed time
2347    /// and the chunk-XY swap reads chz=1 chunks across the DDA, so
2348    /// the chz=0 mountain is invisible. This test pins the limitation
2349    /// — it currently asserts mountain_count == 0 to fail loudly
2350    /// (= the limitation has been fixed) once cf-splitting at chz
2351    /// boundaries lands.
2352    #[test]
2353    #[ignore = "S4B.6.l: known limitation — needs cf-splitting at chz boundaries"]
2354    fn stacked_chz0_distant_mountain_visible_from_chz0_camera() {
2355        let mut scene = Scene::new();
2356        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2357        let g = scene.grid_mut(id).unwrap();
2358        // chz=0 mountain at a column DISTANT from the camera —
2359        // entirely in chz=0 (world z=100..200), so chz=1 at the
2360        // same XY is all-air-bedrock.
2361        g.set_rect(
2362            IVec3::new(100, 100, 100),
2363            IVec3::new(124, 124, 200),
2364            Some(0x80_aa_55_22), // distinct brown
2365        );
2366        // chz=1 hills filling the floor at world z=336..360 across
2367        // the chunk EXCEPT a hole around the mountain XY (so the
2368        // mountain doesn't sit on a green tower).
2369        g.set_rect(
2370            IVec3::new(0, 0, 336),
2371            IVec3::new(128, 128, 360),
2372            Some(0x80_22_88_44),
2373        );
2374        g.set_rect(IVec3::new(100, 100, 336), IVec3::new(124, 124, 360), None);
2375        // Materialise chz=0 + chz=1 (chz=0 has the mountain; chz=1
2376        // has the hills).
2377        assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2378        assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2379
2380        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2381        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2382        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2383        pool.set_treat_z_max_as_air(true);
2384        // Camera at (40, 40, 60) — chz=0 air, FAR from the mountain
2385        // XY (100..124, 100..124). Yaw=π/4 (look toward +x+y =
2386        // mountain direction), pitch=0.72 rad (≈ 41° down) so the
2387        // ray bisecting the screen aims at the chz=0 mountain centre
2388        // ≈ (112, 112, 150).
2389        let (sy, cy) = (std::f64::consts::FRAC_PI_4).sin_cos();
2390        let (sp, cp) = 0.72_f64.sin_cos();
2391        let camera = Camera {
2392            pos: [40.0, 40.0, 60.0],
2393            right: [-sy, cy, 0.0],
2394            down: [-cy * sp, -sy * sp, cp],
2395            forward: [cy * cp, sy * cp, sp],
2396        };
2397        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2398        let outcome = render_scene_composed(
2399            &mut fb,
2400            &mut zb,
2401            XRES as usize,
2402            XRES,
2403            YRES,
2404            &mut pool,
2405            &mut scene,
2406            &camera,
2407            &settings,
2408            sky_color,
2409            None,
2410        );
2411        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2412        let mountain_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2413        let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2414        eprintln!("chz0-distant-mountain: mountain_chz0={mountain_count} hill_chz1={hill_count}");
2415        // chz=1 hills are reachable via seed-time cross-chunk
2416        // look-down.
2417        assert!(
2418            hill_count > 50,
2419            "expected chz=1 hills via cross-chunk look-down — got {hill_count}"
2420        );
2421        // The proper-fix assertion: chz=0 distant mountain SHOULD be
2422        // visible. Currently fails — pins the limitation.
2423        assert!(
2424            mountain_count > 50,
2425            "expected chz=0 distant mountain visible — got {mountain_count} (S4B.6.l limitation)"
2426        );
2427    }
2428
2429    /// S4B.6.h: mid-render chunk-Z handoff. Camera column has
2430    /// content in chz=0 (= a mountain at the camera's XY) so
2431    /// seed-time cross-chunk look-down does NOT fire — seed_chz=0.
2432    /// As rays DDA across the scene, they visit XY columns where
2433    /// chz=0 is all-air-bedrock. Mid-render handoff should swap
2434    /// state to chz=1's column at those XY positions and reveal
2435    /// hill content sitting under the camera's chz=0 layer.
2436    ///
2437    /// This is the "tall mountains breaching chunk-Z boundary"
2438    /// case the demo aims for.
2439    #[test]
2440    fn mid_render_handoff_reveals_chz1_hills_under_mountain_camera() {
2441        let mut scene = Scene::new();
2442        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2443        let g = scene.grid_mut(id).unwrap();
2444        // chz=0: a small "mountain peak" at the camera's XY.
2445        // Mountain at world z=150..200 — solid block.
2446        g.set_rect(
2447            IVec3::new(60, 60, 150),
2448            IVec3::new(72, 72, 200),
2449            Some(0x80_88_44_22), // brown mountain
2450        );
2451        // chz=1: hills at world z=336..360 across the WHOLE chunk
2452        // (so DDA rays hit them when chz=0 is air).
2453        g.set_rect(
2454            IVec3::new(0, 0, 336),
2455            IVec3::new(128, 128, 360),
2456            Some(0x80_22_88_44), // green hills
2457        );
2458        // Carve a hole in chz=1's hill at the mountain's footprint
2459        // so the mountain doesn't appear to "float" on green.
2460        g.set_rect(IVec3::new(60, 60, 336), IVec3::new(72, 72, 360), None);
2461        assert!(g.chunk(IVec3::new(0, 0, 0)).is_some());
2462        assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2463
2464        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2465        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2466        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2467        pool.set_treat_z_max_as_air(true);
2468        // Camera at world (66, 66, 100) — directly above the
2469        // mountain peak (at z=150). Camera column has the
2470        // mountain in chz=0. Look straight down.
2471        let camera = Camera {
2472            pos: [66.0, 66.0, 100.0],
2473            right: [1.0, 0.0, 0.0],
2474            down: [0.0, 1.0, 0.0],
2475            forward: [0.0, 0.0, 1.0],
2476        };
2477        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2478        let outcome = render_scene_composed(
2479            &mut fb,
2480            &mut zb,
2481            XRES as usize,
2482            XRES,
2483            YRES,
2484            &mut pool,
2485            &mut scene,
2486            &camera,
2487            &settings,
2488            sky_color,
2489            None,
2490        );
2491        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2492        let mountain_count = fb.iter().filter(|&&p| p == 0x80_88_44_22).count();
2493        let hill_count = fb.iter().filter(|&&p| p == 0x80_22_88_44).count();
2494        // Verify the hills render at approximately the correct
2495        // world-z by sampling the z-buffer at hill pixels. Camera
2496        // at z=100 looking straight down; hills at world z=336.
2497        // Expected depth = 236 for directly-below pixels. If
2498        // state.z1 stays stuck at the mountain peak's z=150 the
2499        // hills would render with depth ≈ 50 → orders of magnitude
2500        // off.
2501        let mut hill_depths: Vec<f32> = fb
2502            .iter()
2503            .zip(zb.iter())
2504            .filter_map(|(&p, &d)| if p == 0x80_22_88_44 { Some(d) } else { None })
2505            .collect();
2506        hill_depths.sort_by(|a, b| a.partial_cmp(b).unwrap());
2507        let median_hill_depth = hill_depths[hill_depths.len() / 2];
2508        eprintln!(
2509            "mid-render handoff: mountain={mountain_count} hill={hill_count} median_hill_depth={median_hill_depth:.1}"
2510        );
2511        assert!(
2512            mountain_count > 50,
2513            "should see mountain peak via chz=0 — got {mountain_count} mountain pixels"
2514        );
2515        assert!(
2516            hill_count > 50,
2517            "should see chz=1 hills via mid-render handoff — got {hill_count} hill pixels"
2518        );
2519        assert!(
2520            (median_hill_depth - 236.0).abs() < 80.0,
2521            "hill median depth should be ≈236 (camera→z=336); got {median_hill_depth:.1} — state.z1 may be stale at the mountain peak's z"
2522        );
2523    }
2524
2525    /// S4B.6.g: cross-chunk look-down under multi-mip. Same scene
2526    /// as `stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor` but
2527    /// with `mip_levels=2, mip_scan_dist=16` so the rasterizer
2528    /// transitions to mip-1 well within the chz=1 terrain. Locks in
2529    /// the slab_z_at mip-N offset fix (= `chunk_world_z_base >>
2530    /// gmipcnt`). Pre-fix produced a green / brown "wall in a circle
2531    /// around the camera" because mip-1 rendered the floor at
2532    /// world-z ≈ 178 instead of 306.
2533    #[test]
2534    fn stacked_two_chunk_z_camera_in_chz0_sees_chz1_floor_multi_mip() {
2535        let mut scene = Scene::new();
2536        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2537        let g = scene.grid_mut(id).unwrap();
2538        g.ensure_chunk(IVec3::new(0, 0, 0));
2539        g.set_rect(
2540            IVec3::new(60, 60, 306),
2541            IVec3::new(72, 72, 310),
2542            Some(0x80_77_aa_44),
2543        );
2544        assert!(g.chunk(IVec3::new(0, 0, 1)).is_some());
2545
2546        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2547        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2548        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2549        pool.set_treat_z_max_as_air(true);
2550        let camera = Camera {
2551            pos: [66.0, 66.0, 100.0],
2552            right: [1.0, 0.0, 0.0],
2553            down: [0.0, 1.0, 0.0],
2554            forward: [0.0, 0.0, 1.0],
2555        };
2556        let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2557        settings.mip_levels = 2;
2558        settings.mip_scan_dist = 16;
2559        let outcome = render_scene_composed(
2560            &mut fb,
2561            &mut zb,
2562            XRES as usize,
2563            XRES,
2564            YRES,
2565            &mut pool,
2566            &mut scene,
2567            &camera,
2568            &settings,
2569            sky_color,
2570            None,
2571        );
2572        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2573        let floor_count = fb.iter().filter(|&&p| p == 0x80_77_aa_44).count();
2574        assert!(
2575            floor_count > 50,
2576            "multi-mip cross-chunk look-down should still see chz=1 floor — got {floor_count} floor pixels"
2577        );
2578    }
2579
2580    /// S4B.6.d: 3-chunk-tall stack stresses the widened gylookup
2581    /// (`(chunks_z * 512) >> mip + 4` per mip). Pre-S4B.6.d, gylookup
2582    /// was hardcoded at `(512 >> mip) + 4`, which would OOB or alias
2583    /// for any z > 511. This test renders a floor at world z=562
2584    /// (= chz=2, local z=50) with the camera at world z=540, looking
2585    /// straight down. Multi-mip is on so we exercise the mip slide
2586    /// path in `phase_remiporend` that scales `advance` by chunks_z.
2587    #[test]
2588    fn stacked_three_chunk_z_camera_in_chz2_sees_own_chunk_floor_multi_mip() {
2589        let mut scene = Scene::new();
2590        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2591        let g = scene.grid_mut(id).unwrap();
2592        // Materialise chz=0 + chz=1 so chunk_xyz_backing enumerates
2593        // the full stack.
2594        g.ensure_chunk(IVec3::new(0, 0, 0));
2595        g.ensure_chunk(IVec3::new(0, 0, 1));
2596        // chz=2: floor at world z=562..566 (= local z=50..54).
2597        g.set_rect(
2598            IVec3::new(60, 60, 562),
2599            IVec3::new(72, 72, 566),
2600            Some(0x80_aa_55_22),
2601        );
2602        assert!(g.chunk(IVec3::new(0, 0, 2)).is_some());
2603
2604        let (_engine, mut pool, sky_color) = make_composed_pool(2 * CHUNK_SIZE_XY);
2605        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2606        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2607        pool.set_treat_z_max_as_air(true);
2608        let camera = Camera {
2609            pos: [66.0, 66.0, 540.0],
2610            right: [1.0, 0.0, 0.0],
2611            down: [0.0, 1.0, 0.0],
2612            forward: [0.0, 0.0, 1.0],
2613        };
2614        // Multi-mip on to exercise the gylookup-slide path.
2615        let mut settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2616        settings.mip_levels = 2;
2617        settings.mip_scan_dist = 16;
2618        let outcome = render_scene_composed(
2619            &mut fb,
2620            &mut zb,
2621            XRES as usize,
2622            XRES,
2623            YRES,
2624            &mut pool,
2625            &mut scene,
2626            &camera,
2627            &settings,
2628            sky_color,
2629            None,
2630        );
2631        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2632        let floor_count = fb.iter().filter(|&&p| p == 0x80_aa_55_22).count();
2633        assert!(
2634            floor_count > 100,
2635            "camera at chz=2 with floor in same chunk should see it — got {floor_count} floor pixels"
2636        );
2637    }
2638
2639    // ---- S7.4: render integration with streaming ----
2640
2641    /// Floor-stamping generator for S7.4 render tests. Produces a
2642    /// 10-voxel-thick floor at the bottom of every chunk it
2643    /// generates (chunk-local `z = 230..239`, all xy). Visible as
2644    /// a green stripe along the bottom of the framebuffer when
2645    /// the camera looks +y across populated chunks.
2646    #[derive(Debug)]
2647    struct FloorGenerator;
2648
2649    impl crate::ChunkGenerator for FloorGenerator {
2650        fn generate(&self, _chunk_idx: IVec3) -> roxlap_formats::vxl::Vxl {
2651            // Lean on `Grid::ensure_chunk` for the empty-chunk
2652            // builder, then carve a floor via `set_rect`. Detach
2653            // the chunk from the temporary grid and return it.
2654            let mut tmp = crate::Grid::new(GridTransform::identity());
2655            tmp.ensure_chunk(IVec3::ZERO);
2656            let mut vxl = tmp.chunks.remove(&IVec3::ZERO).unwrap();
2657            #[allow(clippy::cast_possible_wrap)]
2658            roxlap_formats::edit::set_rect(
2659                &mut vxl,
2660                glam::IVec3::new(0, 0, 230).into(),
2661                glam::IVec3::new((CHUNK_SIZE_XY - 1) as i32, (CHUNK_SIZE_XY - 1) as i32, 239)
2662                    .into(),
2663                Some(0x80_22_aa_22),
2664            );
2665            vxl
2666        }
2667    }
2668
2669    #[test]
2670    fn render_scene_composed_unpumped_streaming_grid_renders_all_sky() {
2671        // S7.4(a): a grid with a generator + active stream radius
2672        // but no pump_streaming call has zero chunks. The render
2673        // walks the grid (chunk_xyz_backing returns None for an
2674        // empty chunk map → grid is skipped), framebuffer stays
2675        // sky.
2676        use std::sync::Arc;
2677        let mut scene = Scene::new();
2678        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2679        let g = scene.grid_mut(id).unwrap();
2680        g.set_generator(Some(Arc::new(FloorGenerator)));
2681        g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2682        assert!(g.chunks.is_empty(), "no pump yet → no chunks");
2683
2684        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2685        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2686        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2687        // Camera at (64, -100, 200) looking +y so it would see
2688        // chunks ahead once they exist.
2689        let camera = camera_at([64.0, -100.0, 200.0]);
2690        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2691        let _ = render_scene_composed(
2692            &mut fb,
2693            &mut zb,
2694            XRES as usize,
2695            XRES,
2696            YRES,
2697            &mut pool,
2698            &mut scene,
2699            &camera,
2700            &settings,
2701            sky_color,
2702            None,
2703        );
2704        // Empty grid path skips opticast → framebuffer untouched.
2705        assert!(
2706            fb.iter().all(|&p| p == sky_color),
2707            "unpumped streaming grid must render as all sky"
2708        );
2709    }
2710
2711    #[test]
2712    fn render_scene_composed_picks_up_streamed_chunks_after_sync_pump() {
2713        // S7.4(a): once the streaming pump installs chunks, the
2714        // next render shows them. Using pump_streaming_sync for
2715        // deterministic timing — pump_streaming (async) lands
2716        // the same way modulo a frame of latency.
2717        use std::sync::Arc;
2718        let mut scene = Scene::new();
2719        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2720        let g = scene.grid_mut(id).unwrap();
2721        g.set_generator(Some(Arc::new(FloorGenerator)));
2722        // Cover chunks ahead of the camera (y=0, y=128, y=256).
2723        g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2724
2725        // Render BEFORE pump: zero floor pixels.
2726        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2727        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2728        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2729        let camera = camera_at([64.0, -100.0, 200.0]);
2730        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2731        let _ = render_scene_composed(
2732            &mut fb,
2733            &mut zb,
2734            XRES as usize,
2735            XRES,
2736            YRES,
2737            &mut pool,
2738            &mut scene,
2739            &camera,
2740            &settings,
2741            sky_color,
2742            None,
2743        );
2744        let pre_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2745        assert_eq!(pre_floor, 0, "pre-pump frame has no streamed chunks");
2746
2747        // Pump synchronously — `world_pos` matches the camera so
2748        // chunks ahead of it (within r_active = 300) stream in.
2749        scene.pump_streaming_sync(DVec3::new(64.0, -100.0, 200.0));
2750        let g = scene.grid(id).unwrap();
2751        assert!(
2752            !g.chunks.is_empty(),
2753            "pump should have streamed at least one chunk"
2754        );
2755
2756        // Render AFTER pump: the floor should now be visible. Reset
2757        // the framebuffer to sky first.
2758        fb.iter_mut().for_each(|p| *p = sky_color);
2759        zb.iter_mut().for_each(|z| *z = f32::INFINITY);
2760        let outcome = render_scene_composed(
2761            &mut fb,
2762            &mut zb,
2763            XRES as usize,
2764            XRES,
2765            YRES,
2766            &mut pool,
2767            &mut scene,
2768            &camera,
2769            &settings,
2770            sky_color,
2771            None,
2772        );
2773        assert_eq!(outcome, RenderOutcome::Rendered { grids_drawn: 1 });
2774        let post_floor = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2775        assert!(
2776            post_floor > 100,
2777            "post-pump frame should show the streamed floor — got {post_floor} green pixels"
2778        );
2779    }
2780
2781    #[test]
2782    fn render_scene_composed_partial_streaming_renders_pending_chunks_as_air() {
2783        // S7.4(a): mixed state — some r_active chunks are
2784        // materialised, others are still pending (not in
2785        // `chunks`). The render must treat pending chunks as
2786        // implicit-air. Verified by stamping one chunk via the
2787        // generator + skipping the others, then confirming the
2788        // framebuffer has fewer floor pixels than the
2789        // fully-pumped baseline.
2790        use std::sync::Arc;
2791        let mut scene = Scene::new();
2792        let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
2793        let g = scene.grid_mut(id).unwrap();
2794        g.set_generator(Some(Arc::new(FloorGenerator)));
2795        // r_active must be set so the later pump_streaming_sync
2796        // sanity-check actually streams more chunks in. Kept at
2797        // 300 so the resulting chunks_z stack is ≤ 3 — opticast's
2798        // `gylookup` table multiplies `chunks_z * 512 * PREC` and
2799        // overflows i32 at chunks_z ≥ 4 (pre-existing limit, not
2800        // S7.4 scope to fix).
2801        g.stream_radius = crate::StreamRadius::new(300.0, 600.0);
2802
2803        // Materialise ONLY chunk (0, 0, 0) manually via the
2804        // sync helper — leave (0, 1, 0), (0, 2, 0) absent.
2805        let installed = g.ensure_chunk_generated(IVec3::ZERO);
2806        assert!(installed, "manual install of one chunk");
2807        assert_eq!(g.chunks.len(), 1);
2808        // Make sure (0, 1, 0), (0, 2, 0) are NOT present.
2809        assert!(g.chunk(IVec3::new(0, 1, 0)).is_none());
2810        assert!(g.chunk(IVec3::new(0, 2, 0)).is_none());
2811
2812        let (_engine, mut pool, sky_color) = make_composed_pool(CHUNK_SIZE_XY);
2813        let mut fb = vec![sky_color; pixel_count(XRES, YRES)];
2814        let mut zb = vec![f32::INFINITY; pixel_count(XRES, YRES)];
2815        // Camera inside chunk (0, 0, 0); looking +y means the
2816        // floor of (0, 0, 0) gets rendered until the ray walks
2817        // off the chunk into implicit-air space at y=128. No
2818        // floor pixels past that distance.
2819        let camera = camera_at([64.0, 32.0, 200.0]);
2820        let settings = OpticastSettings::for_oracle_framebuffer(XRES, YRES);
2821        let _ = render_scene_composed(
2822            &mut fb,
2823            &mut zb,
2824            XRES as usize,
2825            XRES,
2826            YRES,
2827            &mut pool,
2828            &mut scene,
2829            &camera,
2830            &settings,
2831            sky_color,
2832            None,
2833        );
2834        let floor_pixels = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2835        // Visible floor inside chunk (0,0,0); pending neighbours
2836        // contribute nothing. The number isn't pinned exactly —
2837        // it just needs to be non-zero (we have content) and
2838        // less than what a fully-streamed scene would produce.
2839        assert!(
2840            floor_pixels > 0,
2841            "should see at least some floor from the loaded chunk"
2842        );
2843        // Sanity: stream the missing chunks; verify the floor
2844        // pixel count goes up.
2845        scene.pump_streaming_sync(DVec3::new(64.0, 32.0, 200.0));
2846        assert!(scene.grid(id).unwrap().chunk_count() >= 2);
2847        fb.iter_mut().for_each(|p| *p = sky_color);
2848        zb.iter_mut().for_each(|z| *z = f32::INFINITY);
2849        let _ = render_scene_composed(
2850            &mut fb,
2851            &mut zb,
2852            XRES as usize,
2853            XRES,
2854            YRES,
2855            &mut pool,
2856            &mut scene,
2857            &camera,
2858            &settings,
2859            sky_color,
2860            None,
2861        );
2862        let floor_pixels_full = fb.iter().filter(|&&p| p == 0x80_22_aa_22).count();
2863        assert!(
2864            floor_pixels_full > floor_pixels,
2865            "fully-streamed scene should show more floor than partial: \
2866             partial={floor_pixels} full={floor_pixels_full}"
2867        );
2868    }
2869}