Skip to main content

roxlap_scene/
render.rs

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