Skip to main content

roxlap_scene/
render.rs

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