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