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