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