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