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