Skip to main content

roxlap_core/
dda.rs

1//! Per-pixel 3D-DDA + brickmap CPU renderer (Substage DDA).
2//!
3//! This is the clean-room replacement for the voxlap-derived
4//! column-coherent opticast pipeline (`opticast` + `grouscan` +
5//! `scan_loops`). Every pixel casts one independent ray, so none of
6//! the column/row-coherence stitching artifacts of the 2.5D voxlap
7//! renderer can occur (silhouette notch, floor hairlines, axis-aligned
8//! mip beams, cross-chunk virtual-column complexity). See
9//! `PORTING-DDA.md` for the full stage plan.
10//!
11//! **Stage status — DDA.6 (per-grid distance mip) + DDA.7 (tile
12//! parallelism).** Each pixel casts one ray over the grid's full voxel
13//! box ([`GridView::voxel_bounds`], spanning every chunk in XY **and**
14//! Z) via a 3D-DDA (Amanatides–Woo). A uniform render mip (chosen per
15//! grid by LOD distance, clamped by [`effective_mip`] to a level every
16//! chunk has built) coarsens the cell size to `2^mip` mip-0 voxels and
17//! samples mip-`mip` data — the ray stays in mip-0 units so depth and
18//! fog are exact. `BrickMaps` (one occupancy map per populated chunk,
19//! at the render mip) are built once per frame and shared immutably; a
20//! `Sampler` resolves each cell to its chunk
21//! ([`GridView::chunk_at_xyz`]) and brick-gates the
22//! [`GridView::surface_color_mip`] slab walk, caching the current chunk
23//! so air costs an O(1) bit test. [`render_dda_parallel`] splits the
24//! frame into disjoint rayon bands — bit-identical to sequential since
25//! pixels are independent. Hits are shaded by baked brightness
26//! (`shade`) + [`DdaEnv::side_shades`] face tint, fogged toward
27//! [`DdaEnv::fog_color`] (`apply_fog`); misses sample the
28//! [`DdaEnv::sky`] panorama (`sample_sky`) or keep the solid pre-fill.
29//!
30//! Buffer conventions match the rest of the engine so this backend is
31//! colour is packed `0x80RRGGBB`; depth is perpendicular distance from
32//! the camera with **smaller = closer** (so the scene compositor's
33//! min-z merge works directly on the z-buffer this writes).
34
35use std::collections::HashMap;
36
37use rayon::prelude::*;
38
39use crate::camera_math::{self, CameraState};
40use crate::grid_view::GridView;
41use crate::opticast::OpticastSettings;
42use crate::raster_target::RasterTarget;
43use crate::sky::Sky;
44use crate::Camera;
45use roxlap_formats::material::{material_for_color, Material, MaterialTable};
46use roxlap_formats::Rgb;
47
48/// Per-frame environment for DDA shading (Substage DDA.5): a textured
49/// sky panorama, distance fog, and per-face side shading.
50///
51/// [`DdaEnv::default`] disables all three — flat baked-brightness hits
52/// and a caller-pre-filled solid sky — so the brickmap/dense equivalence
53/// tests run against an unchanged pipeline.
54#[derive(Clone, Copy)]
55pub struct DdaEnv<'a> {
56    /// Textured sky sampled per-ray-direction on a miss. `None` leaves
57    /// the destination untouched (caller's solid sky pre-fill shows).
58    pub sky: Option<&'a Sky>,
59    /// Fog target colour (`0x__RRGGBB`); hits blend toward it with
60    /// distance. Typically the sky colour so terrain fades into the sky.
61    pub fog_color: u32,
62    /// Depth at which fog is fully opaque. `<= 0` disables fog.
63    pub fog_max_dist: f32,
64    /// Per-face brightness reduction `[x-, x+, y-, y+, z-, z+]`, applied
65    /// to the hit face (voxlap `setsideshades`). All-zero = off.
66    pub side_shades: [i8; 6],
67    /// TV: global voxel-material palette (id → opacity + blend mode). `None`
68    /// keeps terrain fully opaque (the first-hit path, bit-identical).
69    pub materials: Option<&'a MaterialTable>,
70    /// TV: terrain colour→material map (`(rgb, material_id)`). A hit voxel's
71    /// colour is looked up here for its material. **Empty** (the default) ⇒
72    /// every voxel is opaque, so the march returns the first hit unchanged.
73    pub terrain_materials: &'a [(Rgb, u8)],
74    /// CPU.1 — dynamic lighting (stage DL on the CPU): sun + point lights +
75    /// stylized cel/ramp, evaluated flat per voxel. Disabled by default ⇒ the
76    /// hit uses the baked-byte `shade` path, byte-identical to pre-DL. Lights
77    /// here are already in the grid's **local** frame (the scene renderer
78    /// transforms them per grid). Shadows: see [`Self::world_shadow`].
79    pub lights: CpuLights<'a>,
80    /// XS.1 — when set, shadow rays test the **whole scene** (all grids +
81    /// sprites) via this world-space occluder + the current grid's
82    /// local→world transform, instead of the single-grid `SamplerShadow`.
83    /// `None` ⇒ single-grid shadows (the direct `render_dda` path / tests).
84    pub world_shadow: Option<WorldShadowCtx<'a>>,
85}
86
87/// CPU.1 — one point light in a grid's local frame for the CPU renderer.
88#[derive(Clone, Copy)]
89pub struct CpuPointLight {
90    /// Grid-local position (world/voxel units).
91    pub pos: [f32; 3],
92    /// Linear RGB, 0..1.
93    pub color: [f32; 3],
94    /// Scalar multiplier on `color` — the light's contribution is
95    /// `albedo · color · intensity · N·L · falloff` per channel.
96    /// `1.0` is nominal; values above it over-drive (the sum clamps at
97    /// pack time).
98    pub intensity: f32,
99    /// Hard cutoff distance (world/voxel units).
100    pub radius: f32,
101    /// CPU.2 — whether this light casts a hard shadow (a shadow ray
102    /// marches to the light through the grid's voxels). Mirrors the
103    /// GPU's per-light `casts_shadow`; the renderer applies the same
104    /// caster cap before building the CPU rig.
105    pub casts_shadow: bool,
106    /// SL — spot (cone) axis: grid-local unit direction the light shines
107    /// **along**. Ignored for a pure point light (see [`Self::cos_outer`]).
108    pub spot_dir: [f32; 3],
109    /// SL — cosine of the inner cone half-angle (full brightness within it).
110    pub cos_inner: f32,
111    /// SL — cosine of the outer cone half-angle (zero past it; soft
112    /// `smoothstep` between the two). `-1.0` (a 180° cone) ⇒ a pure point
113    /// light: the cone mask is skipped entirely and the light is omnidirectional.
114    pub cos_outer: f32,
115}
116
117/// CPU.1 — the per-frame dynamic-light environment for one grid (grid-local).
118/// Mirror of the GPU `shade_lit` inputs. `enabled == false` (the default)
119/// keeps the baked-byte path. CPU.2 adds hard voxel shadows (sun + flagged
120/// point lights) via a per-(voxel,face) shadow march; `shadow_strength == 0`
121/// (the [`Default`]) leaves the lighting diffuse-only.
122#[derive(Clone, Copy, Default)]
123pub struct CpuLights<'a> {
124    /// Whether dynamic lighting is active this frame (else the baked path).
125    pub enabled: bool,
126    /// Whether the sun is present.
127    pub sun: bool,
128    /// Grid-local unit direction **to** the sun.
129    pub sun_dir: [f32; 3],
130    /// Sun colour, linear RGB 0..1.
131    pub sun_color: [f32; 3],
132    /// Scalar multiplier on `sun_color` (the sun term is
133    /// `albedo · sun_color · sun_intensity · key`). `1.0` is nominal.
134    pub sun_intensity: f32,
135    /// CPU.2 — whether the sun casts a hard shadow.
136    pub sun_casts_shadow: bool,
137    /// Grid-local point lights.
138    pub points: &'a [CpuPointLight],
139    /// Ambient multiplier on the baked byte (smooth mode's fill).
140    pub ambient: [f32; 3],
141    /// Cel band count: 0 = smooth, ≥1 = quantize + gradient-map (stylized).
142    pub bands: u32,
143    /// Stylized ramp's cool unlit-end tint (used when `bands > 0`).
144    pub shadow_tint: [f32; 3],
145    /// CPU.2 — fraction of a caster's light removed where a shadow ray is
146    /// occluded (`0` ⇒ shadows off, `1` ⇒ full black). A shadowed sample
147    /// keeps `1 - shadow_strength` of that caster.
148    pub shadow_strength: f32,
149    /// CPU.2 — shadow-ray origin bias along the surface normal, voxel
150    /// units (kills self-shadow acne). ~1.5 is a good default.
151    pub shadow_bias: f32,
152    /// CPU.2 — sun shadow-ray length cap, voxel units (point-light rays
153    /// stop at the light instead).
154    pub shadow_max_dist: f32,
155}
156
157impl Default for DdaEnv<'_> {
158    fn default() -> Self {
159        Self {
160            sky: None,
161            fog_color: 0,
162            fog_max_dist: 0.0,
163            side_shades: [0; 6],
164            materials: None,
165            terrain_materials: &[],
166            lights: CpuLights::default(),
167            world_shadow: None,
168        }
169    }
170}
171
172/// Per-pixel output target for the DDA renderer.
173///
174/// Abstracts "where does a ray hit go" so the traversal core stays
175/// free of framebuffer mechanics. The production impl is
176/// [`RasterSink`] (raw fb/zb pointers); tests use a recording sink.
177/// Only *hits* are reported — misses (sky) leave the destination
178/// untouched, matching the caller-pre-fills-sky convention.
179pub trait PixelSink {
180    /// Record a ray hit at framebuffer index `idx` (`py * pitch + px`)
181    /// with packed ARGB `color` and perpendicular `dist` (smaller =
182    /// closer).
183    fn put(&mut self, idx: usize, color: u32, dist: f32);
184}
185
186/// [`PixelSink`] over a borrowed `(framebuffer, zbuffer)` pair.
187///
188/// Wraps a [`RasterTarget`] so the DDA path writes through the same
189/// raw-pointer mechanism the scalar rasterizer uses — which keeps the
190/// door open for the same strip/tile-disjoint parallel writes in
191/// DDA.7.
192pub struct RasterSink<'a> {
193    target: RasterTarget<'a>,
194    len: usize,
195}
196
197impl<'a> RasterSink<'a> {
198    /// Build a sink from exclusive framebuffer + zbuffer borrows.
199    /// Both slices must have the same length (the pixel count).
200    #[must_use]
201    pub fn new(framebuffer: &'a mut [u32], zbuffer: &'a mut [f32]) -> Self {
202        debug_assert_eq!(framebuffer.len(), zbuffer.len());
203        let len = framebuffer.len();
204        Self {
205            target: RasterTarget::new(framebuffer, zbuffer),
206            len,
207        }
208    }
209}
210
211impl PixelSink for RasterSink<'_> {
212    fn put(&mut self, idx: usize, color: u32, dist: f32) {
213        if idx < self.len {
214            // SAFETY: bounds checked above; single-threaded writer in
215            // DDA.0 so the disjoint-write invariant holds trivially.
216            unsafe {
217                self.target.write_color(idx, color);
218                self.target.write_depth(idx, dist);
219            }
220        }
221    }
222}
223
224/// A resolved ray hit: surface colour + perpendicular distance.
225#[derive(Debug, Clone, Copy)]
226struct Hit {
227    color: u32,
228    dist: f32,
229}
230
231/// Test-only per-thread traversal counters for the perf bench.
232#[cfg(test)]
233pub(crate) mod prof {
234    use std::cell::Cell;
235    thread_local! {
236        pub static CELLS: Cell<u64> = const { Cell::new(0) };
237        pub static BRICKS: Cell<u64> = const { Cell::new(0) };
238        pub static SURF: Cell<u64> = const { Cell::new(0) };
239    }
240    pub fn reset() {
241        CELLS.with(|x| x.set(0));
242        BRICKS.with(|x| x.set(0));
243        SURF.with(|x| x.set(0));
244    }
245    pub fn read() -> (u64, u64, u64) {
246        (
247            CELLS.with(Cell::get),
248            BRICKS.with(Cell::get),
249            SURF.with(Cell::get),
250        )
251    }
252}
253
254/// Apply the voxel's baked directional brightness (Substage DDA.5).
255///
256/// Voxlap (and the GPU marcher, `grid_dda.wgsl`) store per-voxel
257/// brightness in the colour's high byte on a `0..128` scale — `0x80`
258/// is full brightness — written by `Grid::bake_lightmode` (estnorm
259/// directional shading). The shaded channel is `c · a / 128`, so the
260/// DDA matches the GPU look; an unbaked / full-bright voxel (`a =
261/// 0x80`) passes through unchanged. Output alpha is normalised to
262/// `0x80` (the standard "lit" flag; the present blit ignores it).
263///
264/// The renderer only *reads* the baked byte — it computes no normals
265/// itself, so per-impact relight is free (re-bake the chunk and the
266/// byte updates). The estnorm bake that produces the byte is the
267/// voxlap-derived piece slated for a clean-room rewrite in DDA.10.
268///
269/// `bright_sub` is the per-face `side_shades` reduction (DDA.5): voxlap
270/// subtracts it from the brightness byte before the multiply, so a
271/// shaded face is uniformly darker. `0` = no side shading.
272#[inline]
273pub(crate) fn shade(color: u32, bright_sub: u32) -> u32 {
274    let a = ((color >> 24) & 0xff).saturating_sub(bright_sub);
275    let ch = |shift: u32| -> u32 { ((((color >> shift) & 0xff) * a) >> 7).min(255) };
276    0x8000_0000 | (ch(16) << 16) | (ch(8) << 8) | ch(0)
277}
278
279/// EV.1 — shade an **emissive** voxel: the baked brightness byte, the
280/// per-face side shade and the dynamic rig are all ignored; the albedo
281/// is scaled by `(128 + (emissive >> 1)) / 128` on the same `>> 7`
282/// fixed-point ladder as [`shade`] — ~1.0× at `emissive = 0..=1` up to
283/// ~2.0× over-bright (channel-clamped) at `255`. Fog is applied by the
284/// caller as for any other hit.
285#[inline]
286pub(crate) fn emissive_shade(color: u32, emissive: u8) -> u32 {
287    let a = 128 + u32::from(emissive >> 1);
288    let ch = |shift: u32| -> u32 { ((((color >> shift) & 0xff) * a) >> 7).min(255) };
289    0x8000_0000 | (ch(16) << 16) | (ch(8) << 8) | ch(0)
290}
291
292// CPU.1 — cel quantization: snap a 0..1 factor to `bands + 1` levels.
293#[inline]
294fn cel_band(x: f32, bands: u32) -> f32 {
295    let b = bands as f32;
296    ((x * b).round() / b).clamp(0.0, 1.0)
297}
298
299// CPU.1 — point-light distance falloff (mirror of the GPU's): smooth
300// quadratic from 1 at the light to 0 at `radius`, hard-cut beyond.
301#[inline]
302fn point_falloff(d: f32, radius: f32) -> f32 {
303    let x = (1.0 - d / radius).clamp(0.0, 1.0);
304    x * x
305}
306
307// SL — Hermite `smoothstep` (mirror of WGSL's), with a defined hard-edge case:
308// when `edge0 == edge1` WGSL is undefined, so we step at the shared threshold.
309#[inline]
310fn smoothstep_scalar(edge0: f32, edge1: f32, x: f32) -> f32 {
311    if edge1 <= edge0 {
312        return if x < edge0 { 0.0 } else { 1.0 };
313    }
314    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
315    t * t * (3.0 - 2.0 * t)
316}
317
318// SL — spot (cone) angular mask (mirror of the shaders' `spot_cone`). `ldir` is
319// the unit direction from the surface TO the light; `axis` the cone axis (the
320// way the light shines). Returns 1.0 for a pure point light (`cos_outer <=
321// -0.999`, the 180° degenerate); else a soft `smoothstep` from 0 at the outer
322// half-angle to 1 at the inner (hard step when the two coincide).
323#[inline]
324fn spot_cone(ldir: [f32; 3], axis: [f32; 3], cos_inner: f32, cos_outer: f32) -> f32 {
325    if cos_outer <= -0.999 {
326        return 1.0;
327    }
328    let cd = -dot3(ldir, axis);
329    smoothstep_scalar(cos_outer, cos_inner, cd)
330}
331
332// CPU.1 — face normal (grid-local) from the crossed axis + step: points back
333// toward the incoming ray. `axis == 3` (entry voxel, no face) falls back to up
334// (-z, voxlap z-down).
335#[inline]
336fn face_normal_cpu(axis: usize, step: [i32; 3]) -> [f32; 3] {
337    let mut n = [0.0f32; 3];
338    if axis < 3 {
339        n[axis] = -(step[axis] as f32);
340    } else {
341        n[2] = -1.0;
342    }
343    n
344}
345
346#[inline]
347fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
348    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
349}
350
351/// CPU.2 — a hard-shadow occlusion test for the dynamic-lighting shade.
352/// `occluded(origin, dir, max_t)` returns `true` if a solid voxel blocks
353/// the segment from `origin` (grid-local, already biased off the surface)
354/// in unit direction `dir` within `max_t` voxel units. Terrain hits pass
355/// a [`SamplerShadow`] (marches the current grid only) or a
356/// [`WorldShadow`] (cross-grid + sprites, XS.1/XS.2); sprites that don't
357/// cast/receive shadows pass `None`.
358pub(crate) trait ShadowTester {
359    fn occluded(&mut self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool;
360}
361
362/// XS.1 — a **world-space** occlusion oracle over the whole scene (all grids,
363/// and sprites in XS.2). Implemented in `roxlap-scene` (it needs the grid /
364/// sprite stores); the CPU DDA reaches it through [`DdaEnv::world_shadow`] so
365/// shadow rays cross grid + object boundaries instead of stopping at the
366/// current grid. `occluded_world(origin, dir, max_t)` is in **world** voxel
367/// units: `true` iff any solid voxel anywhere blocks the segment.
368///
369/// SC.2 — `origin` is **f64**: a scaled scene puts a fine grid's voxels at
370/// large world coordinates, and each occluder re-projects the ray into its
371/// own voxel frame (dividing by that grid's `vws`), so the world origin must
372/// keep full precision through the lift. `dir` stays f32 (a unit-ish
373/// direction); implementations narrow to f32 only at their voxel-frame entry.
374///
375/// `Sync` because [`DdaEnv`] (which borrows it) is shared across the
376/// rayon strip workers in [`render_dda_parallel`]; the occluder is a
377/// read-only borrow of the scene, so this holds.
378pub trait WorldOccluder: Sync {
379    /// `true` iff any solid voxel in the scene blocks the segment from
380    /// world-space `origin` (f64) in unit direction `dir` within `max_t`
381    /// world voxel units.
382    fn occluded_world(&self, origin: [f64; 3], dir: [f32; 3], max_t: f32) -> bool;
383}
384
385/// XS.1 — per-grid context for a cross-scene shadow query: the scene-wide
386/// [`WorldOccluder`] plus the **current grid's** local→world transform, so a
387/// grid-local shadow ray (the frame `shade_dynamic` works in) can be lifted
388/// to world space before the scene-wide test. `cols[i]` is the world-space
389/// image of grid-local axis `i` (the grid rotation's columns); `origin` is the
390/// grid's world origin.
391#[derive(Clone, Copy)]
392pub struct WorldShadowCtx<'a> {
393    /// The scene-wide occlusion oracle every lifted shadow ray is
394    /// tested against.
395    pub occluder: &'a dyn WorldOccluder,
396    /// The current grid's world-space origin — the translation part of
397    /// its local→world transform (added to rotated positions;
398    /// directions skip it). SC.2 — **f64**: kept full-precision so a
399    /// scaled grid's large world coordinates survive the lift (see
400    /// [`WorldOccluder`]).
401    pub origin: [f64; 3],
402    /// Columns of the grid's local→world rotation: `cols[i]` is the
403    /// world-space image of grid-local axis `i`. Kept **orthonormal** (pure
404    /// rotation) — the grid's voxel scale rides in `voxel_world_size`, not
405    /// folded into the columns.
406    pub cols: [[f32; 3]; 3],
407    /// SC.2 — the caster grid's `voxel_world_size` (world units per voxel).
408    /// The shade works in the grid's voxel frame, so lifting a shadow ray to
409    /// world scales the local **position** and **direction** by this (a
410    /// voxel offset becomes `vws` world units). `1.0` for an unscaled grid
411    /// (byte-identical to XS.1) and for already-world-space sprites.
412    pub voxel_world_size: f32,
413}
414
415impl<'a> WorldShadowCtx<'a> {
416    /// Identity transform — for shading already in world space (sprites): the
417    /// grid-local ray IS the world ray.
418    #[must_use]
419    pub fn identity(occluder: &'a dyn WorldOccluder) -> Self {
420        Self {
421            occluder,
422            origin: [0.0; 3],
423            cols: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
424            voxel_world_size: 1.0,
425        }
426    }
427}
428
429/// XS.2 — a [`WorldOccluder`] that ORs two others (e.g. the grid occluder +
430/// the sprite occluder), so a single shadow query covers both. `true` if
431/// either blocks the ray.
432pub struct CompositeOccluder<'a> {
433    /// First occluder — queried first (short-circuits `b` on a hit).
434    pub a: &'a dyn WorldOccluder,
435    /// Second occluder — queried only when `a` reports no hit.
436    pub b: &'a dyn WorldOccluder,
437}
438
439impl WorldOccluder for CompositeOccluder<'_> {
440    fn occluded_world(&self, origin: [f64; 3], dir: [f32; 3], max_t: f32) -> bool {
441        self.a.occluded_world(origin, dir, max_t) || self.b.occluded_world(origin, dir, max_t)
442    }
443}
444
445/// XS.1 — [`ShadowTester`] that lifts a grid-local shadow ray to world space
446/// (via [`WorldShadowCtx`]) and queries the scene-wide [`WorldOccluder`], so
447/// occlusion crosses grid + sprite boundaries. Sprites (already world-space)
448/// use an identity [`WorldShadowCtx`] (see [`WorldShadowCtx::identity`]).
449pub(crate) struct WorldShadow<'a> {
450    pub ctx: WorldShadowCtx<'a>,
451}
452
453impl ShadowTester for WorldShadow<'_> {
454    fn occluded(&mut self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool {
455        let c = &self.ctx.cols;
456        // SC.2 — the shade works in the caster grid's VOXEL frame. Lift to
457        // world: a voxel offset becomes `vws` world units, so the local
458        // position scales by vws before rotate+translate, and the ray
459        // direction scales by vws too (the world segment then covers
460        // `t · vws` world units — `max_t` is a caster-voxel distance). The
461        // occluder side divides back out by its own vws, so a grid shadowing
462        // ITSELF is scale-invariant and vws==1.0 is byte-identical to XS.1.
463        let s = self.ctx.voxel_world_size;
464        let l = [origin[0] * s, origin[1] * s, origin[2] * s];
465        // world = grid_origin + R · (local · vws) (R columns = `cols`). The
466        // rotated-local term is small (voxel offset), so it's summed in f32;
467        // the large `origin` stays f64 so `wo` keeps full world precision.
468        let wo = [
469            self.ctx.origin[0] + f64::from(c[0][0] * l[0] + c[1][0] * l[1] + c[2][0] * l[2]),
470            self.ctx.origin[1] + f64::from(c[0][1] * l[0] + c[1][1] * l[1] + c[2][1] * l[2]),
471            self.ctx.origin[2] + f64::from(c[0][2] * l[0] + c[1][2] * l[1] + c[2][2] * l[2]),
472        ];
473        // dir rotates then scales by vws (skips the origin translation).
474        let wd = [
475            (c[0][0] * dir[0] + c[1][0] * dir[1] + c[2][0] * dir[2]) * s,
476            (c[0][1] * dir[0] + c[1][1] * dir[1] + c[2][1] * dir[2]) * s,
477            (c[0][2] * dir[0] + c[1][2] * dir[1] + c[2][2] * dir[2]) * s,
478        ];
479        self.ctx.occluder.occluded_world(wo, wd, max_t)
480    }
481}
482
483/// CPU.1 — dynamic-lighting shade for a terrain voxel (the CPU mirror of the
484/// GPU `shade_lit`): raw albedo × (ambient/AO + sun + point lights), evaluated
485/// **flat per voxel** (at the voxel centre, so a whole face reads one tone —
486/// the retro look). `bands > 0` quantizes (cel) and gradient-maps the sun key
487/// from `shadow_tint` (cool) to the sun colour (warm). **No shadows.** Returns
488/// a packed `0x80RRGGBB` colour (same convention as [`shade`]).
489fn shade_lit_cpu(
490    color: u32,
491    bright_sub: u32,
492    axis: usize,
493    step: [i32; 3],
494    cellc: [i32; 3],
495    cell_size: f32,
496    l: &CpuLights<'_>,
497    shadow: Option<&mut dyn ShadowTester>,
498) -> u32 {
499    let a_b = ((color >> 24) & 0xff).saturating_sub(bright_sub);
500    let ao = a_b as f32 / 128.0;
501    let albedo = [
502        ((color >> 16) & 0xff) as f32 / 255.0,
503        ((color >> 8) & 0xff) as f32 / 255.0,
504        (color & 0xff) as f32 / 255.0,
505    ];
506    let n = face_normal_cpu(axis, step);
507    // Voxel centre (grid-local) — flat per-voxel sample point.
508    let center = [
509        (cellc[0] as f32 + 0.5) * cell_size,
510        (cellc[1] as f32 + 0.5) * cell_size,
511        (cellc[2] as f32 + 0.5) * cell_size,
512    ];
513    shade_dynamic(albedo, ao, n, center, l, shadow)
514}
515
516/// CPU.1/DL.7 — the shared dynamic-lighting core (terrain + sprites): raw
517/// `albedo` × (ambient/AO + sun + point lights), sampled **flat per voxel**
518/// at `sample` with surface normal `n`. `bands > 0` quantizes (cel) and
519/// gradient-maps the sun key from `shadow_tint` (cool) to the sun colour
520/// (warm). **No shadows** (GPU-only). Returns a packed `0x80RRGGBB` colour.
521pub(crate) fn shade_dynamic(
522    albedo: [f32; 3],
523    ao: f32,
524    n: [f32; 3],
525    sample: [f32; 3],
526    l: &CpuLights<'_>,
527    shadow: Option<&mut dyn ShadowTester>,
528) -> u32 {
529    let styled = l.bands > 0;
530    // CPU.2 — shadow ray origin: bias off the surface along the normal to
531    // avoid self-shadow acne (shared by every caster). Light kept in
532    // shadow = `1 - shadow_strength` (1.0 ⇒ shadows effectively off).
533    let mut shadow = shadow;
534    let shadow_origin = [
535        sample[0] + n[0] * l.shadow_bias,
536        sample[1] + n[1] * l.shadow_bias,
537        sample[2] + n[2] * l.shadow_bias,
538    ];
539    let in_shadow = 1.0 - l.shadow_strength;
540
541    // Sun key (0..1): N·L × shadow factor.
542    let sun_key = if l.sun {
543        let ndl = dot3(n, l.sun_dir).max(0.0);
544        if ndl > 0.0 && l.sun_casts_shadow {
545            let occ = shadow
546                .as_deref_mut()
547                .is_some_and(|s| s.occluded(shadow_origin, l.sun_dir, l.shadow_max_dist));
548            if occ {
549                ndl * in_shadow
550            } else {
551                ndl
552            }
553        } else {
554            ndl
555        }
556    } else {
557        0.0
558    };
559
560    // Base term: ambient + sun. Smooth = additive; stylized = gradient map.
561    let mut lit = if styled {
562        let key = cel_band(sun_key, l.bands);
563        let m = |i: usize| {
564            let warm = l.sun_color[i] * l.sun_intensity;
565            (l.shadow_tint[i] + (warm - l.shadow_tint[i]) * key) * ao
566        };
567        [albedo[0] * m(0), albedo[1] * m(1), albedo[2] * m(2)]
568    } else {
569        let base = |i: usize| {
570            albedo[i] * l.ambient[i] * ao + albedo[i] * l.sun_color[i] * l.sun_intensity * sun_key
571        };
572        [base(0), base(1), base(2)]
573    };
574
575    // Point lights (flat per voxel). CPU.2 — a flagged caster's shadow ray
576    // marches to the light; an occluded sample keeps `in_shadow` of it.
577    for p in l.points {
578        let d3 = [
579            p.pos[0] - sample[0],
580            p.pos[1] - sample[1],
581            p.pos[2] - sample[2],
582        ];
583        // PF.7 (C4) — squared-distance reject first: the sqrt only runs
584        // for lights actually within radius (same thresholds squared).
585        let d2 = d3[0] * d3[0] + d3[1] * d3[1] + d3[2] * d3[2];
586        if d2 < p.radius * p.radius && d2 > 1e-8 {
587            let dist = d2.sqrt();
588            let inv = 1.0 / dist;
589            let ldir = [d3[0] * inv, d3[1] * inv, d3[2] * inv];
590            let ndl = dot3(n, ldir).max(0.0);
591            // SL — spot cone mask (1.0 for a pure point light). Computed
592            // before the shadow march so an off-cone spot skips it entirely.
593            let cone = spot_cone(ldir, p.spot_dir, p.cos_inner, p.cos_outer);
594            if ndl > 0.0 && cone > 0.0 {
595                // Shadow ray marches from the surface to the light (`dist`).
596                let sh = if p.casts_shadow
597                    && shadow
598                        .as_deref_mut()
599                        .is_some_and(|s| s.occluded(shadow_origin, ldir, dist))
600                {
601                    in_shadow
602                } else {
603                    1.0
604                };
605                let mut f = ndl * point_falloff(dist, p.radius) * cone * sh;
606                if styled {
607                    f = cel_band(f, l.bands);
608                }
609                for i in 0..3 {
610                    lit[i] += albedo[i] * p.color[i] * p.intensity * f;
611                }
612            }
613        }
614    }
615
616    let pack = |v: f32| -> u32 { (v.clamp(0.0, 1.0) * 255.0) as u32 };
617    0x8000_0000 | (pack(lit[0]) << 16) | (pack(lit[1]) << 8) | pack(lit[2])
618}
619
620/// Blend `color` toward `env.fog_color` by perpendicular `depth`
621/// (linear, fully fogged at `env.fog_max_dist`). No-op when fog is
622/// disabled (`fog_max_dist <= 0`).
623#[inline]
624fn apply_fog(color: u32, depth: f32, env: &DdaEnv<'_>) -> u32 {
625    if env.fog_max_dist <= 0.0 {
626        return color;
627    }
628    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
629    let f = ((depth / env.fog_max_dist).clamp(0.0, 1.0) * 256.0) as u32; // 0..256
630    let g = 256 - f;
631    let fog = env.fog_color;
632    let mix = |shift: u32| -> u32 {
633        let src = (color >> shift) & 0xff;
634        let dst = (fog >> shift) & 0xff;
635        ((src * g + dst * f) >> 8).min(255)
636    };
637    0x8000_0000 | (mix(16) << 16) | (mix(8) << 8) | mix(0)
638}
639
640/// Composite premultiplied `accum` (+ remaining `trans`) over a packed
641/// background colour → packed `0x80RRGGBB`.
642#[inline]
643fn composite_over(accum: [f32; 3], trans: f32, bg: u32) -> u32 {
644    let b = rgb_to_f32(bg);
645    f32_to_rgb([
646        accum[0] + trans * b[0],
647        accum[1] + trans * b[1],
648        accum[2] + trans * b[2],
649    ])
650}
651
652/// Finalize a translucent terrain ray that exited the grid (sky). Returns
653/// `None` when nothing was accumulated (the opaque first-hit path — the
654/// caller's sky handling stands, bit-identical), else the accumulated
655/// layers composited over the sky at `dist`.
656#[inline]
657fn finalize_exit(
658    touched: bool,
659    accum: [f32; 3],
660    trans: f32,
661    env: &DdaEnv<'_>,
662    dir: [f32; 3],
663    dist: f32,
664) -> Option<Hit> {
665    if !touched {
666        return None;
667    }
668    let bg = match env.sky {
669        Some(s) => sample_sky(s, dir),
670        None => 0x8000_0000 | (env.fog_color & 0x00ff_ffff),
671    };
672    Some(Hit {
673        color: composite_over(accum, trans, bg),
674        dist,
675    })
676}
677
678/// Unpack `0x__RRGGBB` to `0..1` float channels (RGB; the high byte is
679/// dropped — it has already been folded into the colour by `shade`/`fog`).
680#[inline]
681#[allow(clippy::cast_precision_loss)]
682fn rgb_to_f32(c: u32) -> [f32; 3] {
683    [
684        ((c >> 16) & 0xff) as f32 / 255.0,
685        ((c >> 8) & 0xff) as f32 / 255.0,
686        (c & 0xff) as f32 / 255.0,
687    ]
688}
689
690/// Repack `0..1` float channels (clamped) into `0x80RRGGBB`.
691#[inline]
692#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
693fn f32_to_rgb(c: [f32; 3]) -> u32 {
694    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
695    0x8000_0000 | (q(c[0]) << 16) | (q(c[1]) << 8) | q(c[2])
696}
697
698/// Sample the sky panorama in ray direction `dir` (need not be
699/// normalised), returning a packed `0x80RRGGBB` colour.
700///
701/// Clean-room equirectangular mapping (not voxlap's `lng`/`lat` asm
702/// search): the texture's x axis is elevation (`asin` of the vertical
703/// component), the y axis is azimuth (`atan2` around the vertical). A
704/// `ysiz == 1` panorama (e.g. [`Sky::blue_gradient`]) is a pure
705/// horizon→zenith gradient.
706#[allow(
707    clippy::cast_possible_truncation,
708    clippy::cast_sign_loss,
709    clippy::cast_precision_loss
710)]
711fn sample_sky(sky: &Sky, dir: [f32; 3]) -> u32 {
712    let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
713    if len < 1e-9 {
714        return 0x8000_0000;
715    }
716    let d = [dir[0] / len, dir[1] / len, dir[2] / len];
717    let xsiz_full = sky.lat.len().max(1) as i32; // original column count
718    let pi = std::f32::consts::PI;
719    // Elevation → x, matching the GPU `sky_color` (scene_dda.wgsl): z is
720    // down, so `acos(-z)` is 0 at the zenith (looking up) and π at the nadir
721    // (looking down); `/π` puts the zenith at x=0 and the nadir at x=xsiz.
722    let elev01 = (-d[2]).clamp(-1.0, 1.0).acos() / pi; // 0 (up) .. 1 (down)
723    let x = (elev01 * xsiz_full as f32) as i32;
724    let x = x.clamp(0, xsiz_full - 1);
725    // Azimuth → y (wrapped).
726    let y = if sky.ysiz <= 1 {
727        0
728    } else {
729        let az = d[1].atan2(d[0]); // -pi..pi
730        let yf = ((az / (pi * 2.0)) + 0.5) * sky.ysiz as f32;
731        (yf as i32).rem_euclid(sky.ysiz)
732    };
733    let idx = (y * xsiz_full + x) as usize;
734    let px = sky.pixels.get(idx).copied().unwrap_or(0) as u32;
735    0x8000_0000 | (px & 0x00ff_ffff)
736}
737
738/// Fill the panorama [`Sky`] into every **background** pixel — one whose
739/// z-buffer entry is still `+∞` (no grid/terrain hit). The per-grid DDA only
740/// samples the sky inside each grid's screen rect (and only its sky-owning
741/// grid); pixels outside any grid — most of a sprite/effect-only view, or the
742/// margins around a small world grid — would otherwise keep the caller's flat
743/// clear colour. This paints the real panorama there while leaving terrain
744/// (finite z) and composited translucent pixels untouched. The z-buffer is
745/// not modified. `cam`/`settings` are the same per-frame projection the
746/// renderer used.
747#[allow(clippy::cast_possible_truncation)]
748pub fn render_sky_fill(
749    fb: &mut [u32],
750    zb: &[f32],
751    pitch_pixels: usize,
752    width: u32,
753    height: u32,
754    cam: &CameraState,
755    settings: &OpticastSettings,
756    sky: &Sky,
757) {
758    // PF.7 (C6) — rayon rows: this was a full-frame single-threaded pass
759    // with an `acos` + `atan2` per background pixel (an entire serial
760    // frame on sky-dominant views). Rows are disjoint (`by_ref` chunks of
761    // the framebuffer); `zb` is read-only. Bit-identical.
762    fb.par_chunks_mut(pitch_pixels)
763        .take(height as usize)
764        .enumerate()
765        .for_each(|(py, frow)| {
766            let row = py * pitch_pixels;
767            #[allow(clippy::cast_possible_truncation)]
768            let py = py as u32;
769            for px in 0..width {
770                let idx = row + px as usize;
771                if zb[idx].is_finite() {
772                    continue; // a grid/terrain hit owns this pixel
773                }
774                let (_origin, dir) = pixel_ray(cam, settings, px, py);
775                frow[px as usize] = sample_sky(sky, dir);
776            }
777        });
778}
779
780/// World-space ray for screen pixel `(px, py)` under opticast's
781/// pinhole: origin is the camera position, direction is
782/// `(px - hx)·right + (py - hy)·down + hz·forward`.
783///
784/// This is the exact ray `camera_math::derive` bakes into its corner
785/// vectors (`corn[0]` is `pixel (0, 0)`'s direction), so the DDA
786/// renderer samples the same rays the voxlap path's frustum is built
787/// from. The direction is **not** normalised — callers that need a
788/// unit ray (and a true Euclidean distance) normalise themselves;
789/// DDA.1 will track perpendicular distance via the forward-projection
790/// instead, matching the engine's z-buffer convention.
791#[must_use]
792pub fn pixel_ray(
793    cs: &CameraState,
794    settings: &OpticastSettings,
795    px: u32,
796    py: u32,
797) -> ([f32; 3], [f32; 3]) {
798    // u32 → f32 is exact for any realistic screen coordinate.
799    #[allow(clippy::cast_precision_loss)]
800    let sx = px as f32 - settings.hx;
801    #[allow(clippy::cast_precision_loss)]
802    let sy = py as f32 - settings.hy;
803    let dir = [
804        sx * cs.right[0] + sy * cs.down[0] + settings.hz * cs.forward[0],
805        sx * cs.right[1] + sy * cs.down[1] + settings.hz * cs.forward[1],
806        sx * cs.right[2] + sy * cs.down[2] + settings.hz * cs.forward[2],
807    ];
808    (cs.pos, dir)
809}
810
811/// Ray ↔ axis-aligned box `[lo, hi]` slab test. Returns the
812/// `(t_enter, t_exit)` parameter interval along `dir` (already clamped
813/// so `t_enter >= 0`, i.e. a camera inside the box starts at `t = 0`),
814/// or `None` if the ray misses the box. `dir` need not be normalised —
815/// `t` is in units of `|dir|`.
816pub(crate) fn intersect_aabb(
817    o: [f32; 3],
818    dir: [f32; 3],
819    lo: [f32; 3],
820    hi: [f32; 3],
821) -> Option<(f32, f32)> {
822    let mut t0 = 0.0f32;
823    let mut t1 = f32::INFINITY;
824    for a in 0..3 {
825        if dir[a].abs() < 1e-9 {
826            // Ray parallel to this slab — must already be inside it.
827            if o[a] < lo[a] || o[a] > hi[a] {
828                return None;
829            }
830        } else {
831            let inv = 1.0 / dir[a];
832            let mut ta = (lo[a] - o[a]) * inv;
833            let mut tb = (hi[a] - o[a]) * inv;
834            if ta > tb {
835                core::mem::swap(&mut ta, &mut tb);
836            }
837            t0 = t0.max(ta);
838            t1 = t1.min(tb);
839            if t0 > t1 {
840                return None;
841            }
842        }
843    }
844    Some((t0, t1))
845}
846
847/// Brick edge length in voxels — one occupancy bit per `BRICK³` block.
848const BRICK: i32 = 8;
849
850/// Per-chunk brick occupancy map for two-level DDA empty-space skip
851/// (Substage DDA.3).
852///
853/// One bit per `BRICK³` block of the active chunk, set iff any voxel in
854/// the block is solid. The ray steps the coarse brick grid (8× longer
855/// strides) and only descends into a per-voxel walk inside occupied
856/// bricks, so a ray through open air crosses ~`length / 8` empty bricks
857/// instead of `length` air voxels — each of which would otherwise walk
858/// the column slab chain via `surface_color`.
859///
860/// Built per frame from a [`GridView`] in [`render_dda`]. A persistent
861/// per-chunk cache with edit-driven invalidation (locked decision #2 in
862/// `PORTING-DDA.md`) is a later perf refinement.
863#[derive(Debug)]
864pub(crate) struct BrickMap {
865    /// Brick counts along x / y / z (one entry per `BRICK³` cells).
866    nb: [i32; 3],
867    /// Brick occupancy bitset; brick `(bx, by, bz)` is bit
868    /// `(bz * nb[1] + by) * nb[0] + bx`.
869    bits: Vec<u64>,
870    /// Super-brick counts (one entry per `BRICK³` *bricks* = `SUPER³`
871    /// cells), `ceil(nb / BRICK)`.
872    ns: [i32; 3],
873    /// Super-brick occupancy (DDA.7 perf): a coarse level so a ray
874    /// through open air above the terrain skips `SUPER` cells per outer
875    /// step instead of `BRICK`. A super-brick is set iff any child brick
876    /// is set.
877    super_bits: Vec<u64>,
878}
879
880/// Super-brick edge in cells (`BRICK` bricks per axis).
881const SUPER: i32 = BRICK * BRICK;
882
883impl BrickMap {
884    /// Scan every mip-`mip` column of `grid`, building brick + super-
885    /// brick occupancy. `mip` must be `< grid.mip_count()`.
886    #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
887    fn build(grid: &GridView<'_>, mip: u32) -> Self {
888        let vsid_m = (grid.vsid >> mip).max(1) as i32;
889        let z_m = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1) as i32;
890        let nb = [
891            (vsid_m + BRICK - 1) / BRICK,
892            (vsid_m + BRICK - 1) / BRICK,
893            (z_m + BRICK - 1) / BRICK,
894        ];
895        let ns = [
896            (nb[0] + BRICK - 1) / BRICK,
897            (nb[1] + BRICK - 1) / BRICK,
898            (nb[2] + BRICK - 1) / BRICK,
899        ];
900        let count = (nb[0] * nb[1] * nb[2]) as usize;
901        let scount = (ns[0] * ns[1] * ns[2]) as usize;
902        let mut bits = vec![0u64; count.div_ceil(64)];
903        let mut super_bits = vec![0u64; scount.div_ceil(64)];
904        for y in 0..vsid_m {
905            for x in 0..vsid_m {
906                let (bx, by) = (x / BRICK, y / BRICK);
907                grid.for_each_run_mip(x as u32, y as u32, mip, |top, bot| {
908                    for bz in (top / BRICK)..=((bot - 1) / BRICK) {
909                        let idx = ((bz * nb[1] + by) * nb[0] + bx) as usize;
910                        bits[idx / 64] |= 1u64 << (idx % 64);
911                        let sidx =
912                            (((bz / BRICK) * ns[1] + by / BRICK) * ns[0] + bx / BRICK) as usize;
913                        super_bits[sidx / 64] |= 1u64 << (sidx % 64);
914                    }
915                });
916            }
917        }
918        Self {
919            nb,
920            bits,
921            ns,
922            super_bits,
923        }
924    }
925
926    /// Whether brick `b` is in range and holds any solid voxel.
927    #[inline]
928    #[allow(clippy::cast_sign_loss)]
929    fn occupied(&self, b: [i32; 3]) -> bool {
930        if b[0] < 0
931            || b[0] >= self.nb[0]
932            || b[1] < 0
933            || b[1] >= self.nb[1]
934            || b[2] < 0
935            || b[2] >= self.nb[2]
936        {
937            return false;
938        }
939        let idx = ((b[2] * self.nb[1] + b[1]) * self.nb[0] + b[0]) as usize;
940        (self.bits[idx / 64] >> (idx % 64)) & 1 != 0
941    }
942
943    /// Whether super-brick `s` is in range and holds any solid voxel.
944    #[inline]
945    #[allow(clippy::cast_sign_loss)]
946    fn occupied_super(&self, s: [i32; 3]) -> bool {
947        if s[0] < 0
948            || s[0] >= self.ns[0]
949            || s[1] < 0
950            || s[1] >= self.ns[1]
951            || s[2] < 0
952            || s[2] >= self.ns[2]
953        {
954            return false;
955        }
956        let idx = ((s[2] * self.ns[1] + s[1]) * self.ns[0] + s[0]) as usize;
957        (self.super_bits[idx / 64] >> (idx % 64)) & 1 != 0
958    }
959}
960
961/// Per-axis 3D-DDA stepping state for a cell size of `cell` voxels.
962/// `t_max[a]` is the ray parameter at which the next `a`-boundary is
963/// crossed; `t_delta[a]` is the parameter increment per cell. An
964/// axis-parallel component gets `t_max = t_delta = +inf` so it's never
965/// chosen as the stepping axis.
966pub(crate) fn dda_setup(
967    origin: [f32; 3],
968    dir: [f32; 3],
969    cell: [i32; 3],
970    cell_size: f32,
971) -> ([i32; 3], [f32; 3], [f32; 3]) {
972    let mut step = [0i32; 3];
973    let mut t_max = [f32::INFINITY; 3];
974    let mut t_delta = [f32::INFINITY; 3];
975    for a in 0..3 {
976        if dir[a] > 1e-9 {
977            step[a] = 1;
978            #[allow(clippy::cast_precision_loss)]
979            let boundary = (cell[a] + 1) as f32 * cell_size;
980            t_max[a] = (boundary - origin[a]) / dir[a];
981            t_delta[a] = cell_size / dir[a];
982        } else if dir[a] < -1e-9 {
983            step[a] = -1;
984            #[allow(clippy::cast_precision_loss)]
985            let boundary = cell[a] as f32 * cell_size;
986            t_max[a] = (boundary - origin[a]) / dir[a];
987            t_delta[a] = -cell_size / dir[a];
988        }
989    }
990    (step, t_max, t_delta)
991}
992
993/// Index of the axis with the smallest `t_max` (the next boundary the
994/// ray crosses).
995#[inline]
996pub(crate) fn min_axis(t_max: [f32; 3]) -> usize {
997    if t_max[0] <= t_max[1] && t_max[0] <= t_max[2] {
998        0
999    } else if t_max[1] <= t_max[2] {
1000        1
1001    } else {
1002        2
1003    }
1004}
1005
1006/// Persistent, cross-frame brick occupancy cache (Substage DDA.7
1007/// perf). Keyed by `(chunk x, y, z, mip)` with the chunk's edit
1008/// `version`; an entry is reused until its chunk's version changes, so a
1009/// static / streamed-once world pays **zero** brick-build cost after the
1010/// first frame (the per-frame rebuild was the dominant DDA cost).
1011///
1012/// Owned by the caller across frames (the scene's `Grid`), populated
1013/// single-threaded via [`Self::ensure`], then borrowed immutably by the
1014/// parallel render bands.
1015#[derive(Debug, Default)]
1016pub struct BrickCache {
1017    maps: HashMap<(i32, i32, i32, u32), (u64, BrickMap)>,
1018}
1019
1020impl BrickCache {
1021    /// An empty cache (same as [`Default`]). Entries appear via
1022    /// [`ensure`](Self::ensure) as chunks are first rendered.
1023    #[must_use]
1024    pub fn new() -> Self {
1025        Self::default()
1026    }
1027
1028    /// Ensure a current mip-`mip` brick map exists for `chunk` (built
1029    /// from `view`); rebuilds only when the cached `version` differs.
1030    pub fn ensure(&mut self, chunk: [i32; 3], mip: u32, version: u64, view: &GridView<'_>) {
1031        let key = (chunk[0], chunk[1], chunk[2], mip);
1032        let stale = self.maps.get(&key).is_none_or(|(v, _)| *v != version);
1033        if stale {
1034            self.maps.insert(key, (version, BrickMap::build(view, mip)));
1035        }
1036    }
1037
1038    #[inline]
1039    fn get(&self, chunk: [i32; 3], mip: u32) -> Option<&BrickMap> {
1040        self.maps
1041            .get(&(chunk[0], chunk[1], chunk[2], mip))
1042            .map(|(_, m)| m)
1043    }
1044
1045    /// Drop cached entries whose chunk fails `keep` — bounds memory as
1046    /// streaming evicts chunks. Called once per frame by the scene.
1047    pub fn retain_chunks(&mut self, keep: impl Fn([i32; 3]) -> bool) {
1048        self.maps.retain(|k, _| keep([k.0, k.1, k.2]));
1049    }
1050
1051    /// PF.9 — occupancy of the `BRICK`³ block containing chunk-local
1052    /// mip-`mip` cell `cell` of `chunk`. `None` when no map is cached for
1053    /// that (chunk, mip) — the caller must fall back to dense stepping.
1054    /// `Some(false)` guarantees the whole 8³ block holds no solid voxel,
1055    /// so an external shadow march may skip it wholesale.
1056    #[must_use]
1057    pub fn brick_occupied_at(&self, chunk: [i32; 3], mip: u32, cell: [i32; 3]) -> Option<bool> {
1058        self.get(chunk, mip)
1059            .map(|m| m.occupied([cell[0] >> 3, cell[1] >> 3, cell[2] >> 3]))
1060    }
1061
1062    /// PF.9 — like [`Self::brick_occupied_at`] for the `SUPER`³ (64³ at
1063    /// mip 0) super-brick level.
1064    #[must_use]
1065    pub fn super_occupied_at(&self, chunk: [i32; 3], mip: u32, cell: [i32; 3]) -> Option<bool> {
1066        self.get(chunk, mip)
1067            .map(|m| m.occupied_super([cell[0] >> 6, cell[1] >> 6, cell[2] >> 6]))
1068    }
1069}
1070
1071/// Build a throwaway [`BrickCache`] covering every populated chunk of
1072/// `grid` at the effective mip — for the sequential [`render_dda`] /
1073/// tests, where no persistent cache is threaded in. Returns
1074/// `(cache, effective_mip)`.
1075#[allow(clippy::cast_possible_wrap)]
1076fn local_cache(grid: &GridView<'_>, requested_mip: u32) -> (BrickCache, u32) {
1077    let mip = effective_mip(grid, requested_mip);
1078    let mut cache = BrickCache::new();
1079    if let Some(cg) = grid.chunk_grid {
1080        for dz in 0..cg.chunks_z as i32 {
1081            for dy in 0..cg.chunks_y as i32 {
1082                for dx in 0..cg.chunks_x as i32 {
1083                    let slot = ((dz * cg.chunks_y as i32 + dy) * cg.chunks_x as i32 + dx) as usize;
1084                    if let Some(Some(view)) = cg.chunks.get(slot) {
1085                        let ch = [
1086                            cg.origin_chunk_xy[0] + dx,
1087                            cg.origin_chunk_xy[1] + dy,
1088                            cg.origin_chunk_z + dz,
1089                        ];
1090                        cache.ensure(ch, mip, 0, view);
1091                    }
1092                }
1093            }
1094        }
1095    } else {
1096        cache.ensure([0, 0, 0], mip, 0, grid);
1097    }
1098    (cache, mip)
1099}
1100
1101/// Clamp a requested render mip to one every populated chunk actually
1102/// has built — so the uniform-mip traversal never under-samples a chunk
1103/// that lacks the requested level (which would punch holes). `0` short-
1104/// circuits (always available).
1105#[must_use]
1106pub fn effective_mip(grid: &GridView<'_>, requested: u32) -> u32 {
1107    if requested == 0 {
1108        return 0;
1109    }
1110    let mut m = requested;
1111    if let Some(cg) = grid.chunk_grid {
1112        for c in cg.chunks.iter().flatten() {
1113            m = m.min(c.mip_count().saturating_sub(1));
1114        }
1115    } else {
1116        m = m.min(grid.mip_count().saturating_sub(1));
1117    }
1118    m
1119}
1120
1121/// Cross-chunk voxel sampler (Substage DDA.4 / DDA.7).
1122///
1123/// Resolves a grid-local voxel coordinate to the chunk that owns it
1124/// (via [`GridView::chunk_at_xyz`]) and answers the DDA's per-voxel hit
1125/// query — brick-gated [`GridView::surface_color`]. It borrows the
1126/// shared immutable [`BrickMaps`] and caches the **current chunk**
1127/// (`cur_*`: view + brick-map reference): a ray usually stays in one
1128/// chunk for many voxels, so the per-voxel cost is a single index
1129/// compare + an O(1) brick bit test — no hashing, no mutation. Holding
1130/// only shared borrows, a `Sampler` is cheap to spin up per render band.
1131///
1132/// Single-chunk grids are the degenerate case: every voxel maps to
1133/// chunk `[0, 0, 0]` (= the view itself).
1134struct Sampler<'a> {
1135    grid: GridView<'a>,
1136    bricks: &'a BrickCache,
1137    /// Effective render mip (DDA.6). Traversal cells are mip-`mip`
1138    /// cells; sampling reads mip-`mip` data.
1139    mip: u32,
1140    /// Chunk size in mip-`mip` cells is a power of two; store it as
1141    /// `log2` (shift) + `size - 1` (mask) so [`Self::locate`] splits a
1142    /// cell into `(chunk, in-chunk)` with a shift + an `&` per axis
1143    /// instead of a signed `div_euclid` — the dominant per-cell cost.
1144    /// Arithmetic `>>` floors toward -∞ (= `div_euclid` for a positive
1145    /// power-of-two divisor) and `& mask` gives the non-negative
1146    /// remainder (= `rem_euclid`) even for negative cells (two's
1147    /// complement), so results are identical to the division form.
1148    xy_shift: u32,
1149    xy_mask: i32,
1150    z_shift: u32,
1151    z_mask: i32,
1152    cur_ch: [i32; 3],
1153    cur_view: Option<GridView<'a>>,
1154    cur_brick: Option<&'a BrickMap>,
1155    has_cur: bool,
1156}
1157
1158impl<'a> Sampler<'a> {
1159    fn new(grid: GridView<'a>, bricks: &'a BrickCache, mip: u32) -> Self {
1160        let cs_xy = (grid.chunk_size_xy >> mip).max(1);
1161        let cs_z = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1);
1162        debug_assert!(
1163            cs_xy.is_power_of_two() && cs_z.is_power_of_two(),
1164            "chunk dims must be powers of two for the shift/mask split"
1165        );
1166        #[allow(clippy::cast_possible_wrap)]
1167        Self {
1168            grid,
1169            bricks,
1170            mip,
1171            xy_shift: cs_xy.trailing_zeros(),
1172            xy_mask: cs_xy as i32 - 1,
1173            z_shift: cs_z.trailing_zeros(),
1174            z_mask: cs_z as i32 - 1,
1175            cur_ch: [0; 3],
1176            cur_view: None,
1177            cur_brick: None,
1178            has_cur: false,
1179        }
1180    }
1181
1182    /// Refresh the current-chunk cache (view + brick map) for `ch`.
1183    fn select_chunk(&mut self, ch: [i32; 3]) {
1184        if self.has_cur && self.cur_ch == ch {
1185            return;
1186        }
1187        self.cur_view = self.grid.chunk_at_xyz(ch);
1188        self.cur_brick = self.bricks.get(ch, self.mip);
1189        self.cur_ch = ch;
1190        self.has_cur = true;
1191    }
1192
1193    /// Split a grid-local **mip-`mip` cell** index into `(chunk index,
1194    /// in-chunk mip-cell)` via shift + mask (see field docs). Chunk
1195    /// indices are mip-independent; only the per-chunk resolution
1196    /// shrinks with mip.
1197    #[allow(clippy::cast_sign_loss)]
1198    fn locate(&self, c: [i32; 3]) -> ([i32; 3], [u32; 3]) {
1199        let ch = [
1200            c[0] >> self.xy_shift,
1201            c[1] >> self.xy_shift,
1202            c[2] >> self.z_shift,
1203        ];
1204        let loc = [
1205            (c[0] & self.xy_mask) as u32,
1206            (c[1] & self.xy_mask) as u32,
1207            (c[2] & self.z_mask) as u32,
1208        ];
1209        (ch, loc)
1210    }
1211
1212    /// Hit colour for grid-local mip-cell `c`, or `None` for air / empty
1213    /// chunk / uncoloured bedrock. Brick-gated so air inside a populated
1214    /// chunk costs only a bit test, not a slab walk.
1215    #[allow(clippy::cast_possible_wrap)]
1216    fn hit(&mut self, c: [i32; 3]) -> Option<u32> {
1217        #[cfg(test)]
1218        prof::SURF.with(|x| x.set(x.get() + 1));
1219        let (ch, loc) = self.locate(c);
1220        self.select_chunk(ch);
1221        let occupied = self.cur_brick.is_some_and(|bm| {
1222            bm.occupied([
1223                loc[0] as i32 / BRICK,
1224                loc[1] as i32 / BRICK,
1225                loc[2] as i32 / BRICK,
1226            ])
1227        });
1228        if !occupied {
1229            return None;
1230        }
1231        self.cur_view?
1232            .surface_color_mip(loc[0], loc[1], loc[2], self.mip)
1233            .map(|c| c.0)
1234    }
1235
1236    /// Chunk size in mip-cells along XY / Z (always a power of two).
1237    #[inline]
1238    fn cells_per_chunk_xy(&self) -> i32 {
1239        1 << self.xy_shift
1240    }
1241    #[inline]
1242    fn cells_per_chunk_z(&self) -> i32 {
1243        1 << self.z_shift
1244    }
1245
1246    /// Whether the brick at brick-index `brick` (in `BRICK`-mip-cell
1247    /// units) holds any solid voxel. Used by the outer brick-DDA to skip
1248    /// empty space `BRICK` cells at a time. Assumes bricks nest within
1249    /// chunks (caller gates on [`Self::cells_per_chunk_xy`]`>= BRICK`).
1250    #[allow(clippy::cast_sign_loss)]
1251    fn brick_occupied(&mut self, brick: [i32; 3]) -> bool {
1252        // First mip-cell of the brick (BRICK = 8 → `<< 3`).
1253        let c0 = [brick[0] << 3, brick[1] << 3, brick[2] << 3];
1254        let ch = [
1255            c0[0] >> self.xy_shift,
1256            c0[1] >> self.xy_shift,
1257            c0[2] >> self.z_shift,
1258        ];
1259        self.select_chunk(ch);
1260        self.cur_brick.is_some_and(|bm| {
1261            bm.occupied([
1262                (c0[0] & self.xy_mask) >> 3,
1263                (c0[1] & self.xy_mask) >> 3,
1264                (c0[2] & self.z_mask) >> 3,
1265            ])
1266        })
1267    }
1268
1269    /// Whether the super-brick at super-index `s` (in `SUPER`-mip-cell
1270    /// units) holds any solid voxel. Outer-most empty-space skip (steps
1271    /// `SUPER` cells). Assumes super-bricks nest in chunks (caller gates
1272    /// on `cells_per_chunk >= SUPER`).
1273    #[allow(clippy::cast_sign_loss)]
1274    fn super_occupied(&mut self, s: [i32; 3]) -> bool {
1275        // First mip-cell of the super-brick (SUPER = 64 → `<< 6`).
1276        let c0 = [s[0] << 6, s[1] << 6, s[2] << 6];
1277        let ch = [
1278            c0[0] >> self.xy_shift,
1279            c0[1] >> self.xy_shift,
1280            c0[2] >> self.z_shift,
1281        ];
1282        self.select_chunk(ch);
1283        self.cur_brick.is_some_and(|bm| {
1284            bm.occupied_super([
1285                (c0[0] & self.xy_mask) >> 6,
1286                (c0[1] & self.xy_mask) >> 6,
1287                (c0[2] & self.z_mask) >> 6,
1288            ])
1289        })
1290    }
1291}
1292
1293/// CPU.2 — safety cap on a shadow ray's voxel steps (the `shadow_max_dist`
1294/// / light-distance bound is the real limit; this only backstops a
1295/// degenerate ray). Mirrors the GPU `shadow_max_steps`.
1296const SHADOW_MAX_STEPS: u32 = 1024;
1297
1298/// CPU.2 — [`ShadowTester`] backed by the render [`Sampler`]: a hard-shadow
1299/// occlusion march over the grid's mip-`mip` voxels. The march reuses the
1300/// same `sampler.hit()` occupancy the primary ray uses (so a shadow ray is
1301/// blocked by the same surfaces the camera sees) and the same `[lo_c, hi_c)`
1302/// voxel-box bounds, stepping a standard 3D-DDA until it hits a solid cell
1303/// (occluded), leaves the box / exceeds `max_t` (lit), or hits the step cap.
1304struct SamplerShadow<'s, 'a> {
1305    sampler: &'s mut Sampler<'a>,
1306    cell_size: f32,
1307    lo_c: [i32; 3],
1308    hi_c: [i32; 3],
1309}
1310
1311impl ShadowTester for SamplerShadow<'_, '_> {
1312    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
1313    fn occluded(&mut self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool {
1314        let cs = self.cell_size;
1315        // PF.9 (C3) — the shadow march gets the primary ray's empty-space
1316        // skip: fast-forward across empty super-bricks / bricks (skipping
1317        // an EMPTY box can never hide an occluder). The step budget is
1318        // consumed in Manhattan cell distance — exactly what the dense
1319        // walk would have spent crossing the same span — so the
1320        // `SHADOW_MAX_STEPS` truncation fires at the identical point and
1321        // hit/no-hit stays bit-compatible with the pre-skip march.
1322        let has_super =
1323            self.sampler.cells_per_chunk_xy() >= SUPER && self.sampler.cells_per_chunk_z() >= SUPER;
1324        let has_brick =
1325            self.sampler.cells_per_chunk_xy() >= BRICK && self.sampler.cells_per_chunk_z() >= BRICK;
1326        let mut cellc = [
1327            (origin[0] / cs).floor() as i32,
1328            (origin[1] / cs).floor() as i32,
1329            (origin[2] / cs).floor() as i32,
1330        ];
1331        let (step, mut t_max, t_delta) = dda_setup(origin, dir, cellc, cs);
1332        let inv = [
1333            if step[0] != 0 { 1.0 / dir[0] } else { 0.0 },
1334            if step[1] != 0 { 1.0 / dir[1] } else { 0.0 },
1335            if step[2] != 0 { 1.0 / dir[2] } else { 0.0 },
1336        ];
1337        let mut t_curr = 0.0f32;
1338        let mut used = 0u32;
1339        while used < SHADOW_MAX_STEPS {
1340            if cellc[0] < self.lo_c[0]
1341                || cellc[0] >= self.hi_c[0]
1342                || cellc[1] < self.lo_c[1]
1343                || cellc[1] >= self.hi_c[1]
1344                || cellc[2] < self.lo_c[2]
1345                || cellc[2] >= self.hi_c[2]
1346            {
1347                return false; // left the voxel box → no occluder ahead
1348            }
1349            if t_curr > max_t {
1350                return false; // past the cap / the light → unshadowed
1351            }
1352            // Empty-space skip (mirrors `cell_walk_skip`'s landing logic:
1353            // the exit axis is pinned to the integer boundary cell so the
1354            // next box's entry cell is always visited densely).
1355            let skip_shift = if has_super
1356                && !self
1357                    .sampler
1358                    .super_occupied([cellc[0] >> 6, cellc[1] >> 6, cellc[2] >> 6])
1359            {
1360                Some(6u32)
1361            } else if has_brick
1362                && !self
1363                    .sampler
1364                    .brick_occupied([cellc[0] >> 3, cellc[1] >> 3, cellc[2] >> 3])
1365            {
1366                Some(3u32)
1367            } else {
1368                None
1369            };
1370            if let Some(sh) = skip_shift {
1371                let mut best_t = f32::INFINITY;
1372                let mut best_axis = 3usize;
1373                let mut plane = [0i32; 3];
1374                for a in 0..3 {
1375                    if step[a] == 0 {
1376                        continue;
1377                    }
1378                    let idx = cellc[a] >> sh;
1379                    plane[a] = if step[a] > 0 {
1380                        (idx + 1) << sh
1381                    } else {
1382                        idx << sh
1383                    };
1384                    let tb = (plane[a] as f32 * cs - origin[a]) * inv[a];
1385                    if tb < best_t {
1386                        best_t = tb;
1387                        best_axis = a;
1388                    }
1389                }
1390                if best_axis == 3 {
1391                    return false;
1392                }
1393                let pb = [
1394                    origin[0] + dir[0] * (best_t + 1e-4),
1395                    origin[1] + dir[1] * (best_t + 1e-4),
1396                    origin[2] + dir[2] * (best_t + 1e-4),
1397                ];
1398                let mut nc = [
1399                    (pb[0] / cs).floor() as i32,
1400                    (pb[1] / cs).floor() as i32,
1401                    (pb[2] / cs).floor() as i32,
1402                ];
1403                nc[best_axis] = if step[best_axis] > 0 {
1404                    plane[best_axis]
1405                } else {
1406                    plane[best_axis] - 1
1407                };
1408                // Budget: the dense walk would have spent one step per
1409                // cell (Manhattan distance). If it runs out inside the
1410                // empty box the dense walk would have returned `false`
1411                // there — nothing solid to find inside it.
1412                let crossed =
1413                    cellc[0].abs_diff(nc[0]) + cellc[1].abs_diff(nc[1]) + cellc[2].abs_diff(nc[2]);
1414                if used.saturating_add(crossed) >= SHADOW_MAX_STEPS {
1415                    return false;
1416                }
1417                used += crossed;
1418                cellc = nc;
1419                for a in 0..3 {
1420                    if step[a] > 0 {
1421                        t_max[a] = ((cellc[a] + 1) as f32 * cs - origin[a]) * inv[a];
1422                    } else if step[a] < 0 {
1423                        t_max[a] = (cellc[a] as f32 * cs - origin[a]) * inv[a];
1424                    }
1425                }
1426                t_curr = best_t.max(t_curr);
1427                continue;
1428            }
1429            if self.sampler.hit(cellc).is_some() {
1430                return true; // a surface blocks the ray
1431            }
1432            let axis = min_axis(t_max);
1433            t_curr = t_max[axis];
1434            cellc[axis] += step[axis];
1435            t_max[axis] += t_delta[axis];
1436            used += 1;
1437        }
1438        false
1439    }
1440}
1441
1442/// Walk mip-cells along the ray within `[lo_c, hi_c)` and return the
1443/// first solid hit, with leak-free empty-space skipping (DDA.7 redux).
1444///
1445/// **Why one continuous DDA, not nested level-walks.** The previous
1446/// design ran an outer brick/super DDA that *jumped* whole bricks and
1447/// only descended into occupied ones. Stepping a coarse cell at a time
1448/// lets the ray slip diagonally **past an occupied coarse cell it only
1449/// touches at a shared edge/corner** — a leak that showed as bright
1450/// sky seams across thin diagonal walls (the cave-demo report). Here a
1451/// *single* cell-granularity DDA carries the exact `(cellc, t_max)`
1452/// state for the whole ray; it only ever **fast-forwards across an
1453/// empty super-brick / brick**, where skipping cannot miss anything.
1454/// The exit axis lands on the integer box-boundary cell (no float
1455/// re-floor on the critical axis), so the entry cell of the next —
1456/// possibly occupied — box is always visited densely. Result: hits are
1457/// bit-identical to the dense per-cell reference, with the empty-space
1458/// speed-up retained.
1459///
1460/// `cell_size` is the mip-cell edge in mip-0 voxels (`1 << mip`);
1461/// `fwd_dot = dir·forward` → perpendicular depth.
1462#[allow(
1463    clippy::too_many_arguments,
1464    clippy::cast_possible_truncation,
1465    clippy::cast_sign_loss,
1466    clippy::cast_precision_loss
1467)]
1468fn cell_walk_skip(
1469    origin: [f32; 3],
1470    dir: [f32; 3],
1471    fwd_dot: f32,
1472    sampler: &mut Sampler<'_>,
1473    lo_c: [i32; 3],
1474    hi_c: [i32; 3],
1475    cell_size: f32,
1476    t_enter: f32,
1477    t_exit: f32,
1478    max_dist: f32,
1479    env: &DdaEnv<'_>,
1480) -> Option<Hit> {
1481    let has_super = sampler.cells_per_chunk_xy() >= SUPER && sampler.cells_per_chunk_z() >= SUPER;
1482    let has_brick = sampler.cells_per_chunk_xy() >= BRICK && sampler.cells_per_chunk_z() >= BRICK;
1483
1484    let start = t_enter + 1e-4;
1485    let p = [
1486        origin[0] + dir[0] * start,
1487        origin[1] + dir[1] * start,
1488        origin[2] + dir[2] * start,
1489    ];
1490    let mut cellc = [
1491        ((p[0] / cell_size).floor() as i32).clamp(lo_c[0], hi_c[0] - 1),
1492        ((p[1] / cell_size).floor() as i32).clamp(lo_c[1], hi_c[1] - 1),
1493        ((p[2] / cell_size).floor() as i32).clamp(lo_c[2], hi_c[2] - 1),
1494    ];
1495    let (step, mut t_max, t_delta) = dda_setup(origin, dir, cellc, cell_size);
1496    // Reciprocal direction → the per-skip box-boundary t and the t_max
1497    // refresh use multiplies instead of divisions (the dominant skip
1498    // cost). `0.0` where `step == 0` (that axis' t_max stays +∞).
1499    let inv = [
1500        if step[0] != 0 { 1.0 / dir[0] } else { 0.0 },
1501        if step[1] != 0 { 1.0 / dir[1] } else { 0.0 },
1502        if step[2] != 0 { 1.0 / dir[2] } else { 0.0 },
1503    ];
1504    let mut t_curr = t_enter;
1505    let mut last_axis = 3usize;
1506    // World ray length per ray-parameter unit; divided by `cell_size` it turns
1507    // a cell's `t` span into its path length in voxel units (Volumetric weight).
1508    let dir_len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
1509    // PF.7 (C4) — hoisted out of the hit block: "does anything cast a
1510    // shadow?" is ray-invariant, and the O(lights) `any()` scan ran per
1511    // hit (per translucent layer on glass/water rays).
1512    let shadow_casts = env.lights.enabled
1513        && env.lights.shadow_strength > 0.0
1514        && (env.lights.sun_casts_shadow || env.lights.points.iter().any(|p| p.casts_shadow));
1515
1516    // TV: front-to-back translucent accumulation. While no translucent voxel
1517    // is hit (`touched` stays false) every return is unchanged — the opaque
1518    // world renders bit-identically. `prev_*` drive per-span compositing (one
1519    // alpha layer per contiguous solid run or material change).
1520    let mut accum = [0.0f32; 3];
1521    let mut trans = 1.0f32;
1522    let mut touched = false;
1523    let mut prev_solid = false;
1524    let mut prev_mat = 0u8;
1525
1526    // Each iteration either advances ≥1 cell (dense) or ≥1 box (skip),
1527    // so the total cell span bounds the loop.
1528    let span = (hi_c[0] - lo_c[0]) + (hi_c[1] - lo_c[1]) + (hi_c[2] - lo_c[2]);
1529    let max_steps = span.max(0) as usize + 16;
1530    for _ in 0..max_steps {
1531        if cellc[0] < lo_c[0]
1532            || cellc[0] >= hi_c[0]
1533            || cellc[1] < lo_c[1]
1534            || cellc[1] >= hi_c[1]
1535            || cellc[2] < lo_c[2]
1536            || cellc[2] >= hi_c[2]
1537        {
1538            return finalize_exit(touched, accum, trans, env, dir, max_dist);
1539        }
1540        let depth = t_curr * fwd_dot;
1541        if depth > max_dist || t_curr > t_exit {
1542            return finalize_exit(touched, accum, trans, env, dir, max_dist);
1543        }
1544        // Fog is fully opaque at `fog_max_dist`: nothing beyond is
1545        // visible, so stop the ray there and return the fog colour
1546        // rather than traversing (and skip/step-counting) to the far box
1547        // wall. Both correct and the dominant perf win for foggy worlds —
1548        // it caps every ray's length at the fog distance.
1549        if env.fog_max_dist > 0.0 && depth >= env.fog_max_dist {
1550            let fog = 0x8000_0000 | (env.fog_color & 0x00ff_ffff);
1551            let color = if touched {
1552                composite_over(accum, trans, fog)
1553            } else {
1554                fog
1555            };
1556            return Some(Hit {
1557                color,
1558                dist: env.fog_max_dist,
1559            });
1560        }
1561
1562        // Empty-space skip: a whole empty super-brick, else an empty
1563        // brick. Skipping only empty boxes can never miss a surface.
1564        let skip_shift = if has_super
1565            && !sampler.super_occupied([cellc[0] >> 6, cellc[1] >> 6, cellc[2] >> 6])
1566        {
1567            Some(6u32)
1568        } else if has_brick
1569            && !sampler.brick_occupied([cellc[0] >> 3, cellc[1] >> 3, cellc[2] >> 3])
1570        {
1571            Some(3u32)
1572        } else {
1573            None
1574        };
1575        if let Some(sh) = skip_shift {
1576            #[cfg(test)]
1577            prof::BRICKS.with(|x| x.set(x.get() + 1));
1578            // Nearest box boundary along the ray (in cell units).
1579            let mut best_t = f32::INFINITY;
1580            let mut best_axis = 3usize;
1581            let mut plane = [0i32; 3];
1582            for a in 0..3 {
1583                if step[a] == 0 {
1584                    continue;
1585                }
1586                let idx = cellc[a] >> sh;
1587                plane[a] = if step[a] > 0 {
1588                    (idx + 1) << sh
1589                } else {
1590                    idx << sh
1591                };
1592                let tb = (plane[a] as f32 * cell_size - origin[a]) * inv[a];
1593                if tb < best_t {
1594                    best_t = tb;
1595                    best_axis = a;
1596                }
1597            }
1598            if best_axis == 3 {
1599                return finalize_exit(touched, accum, trans, env, dir, max_dist);
1600            }
1601            // Land just across the boundary; pin the exit axis to the
1602            // integer boundary cell so float error can't skip the next
1603            // box's entry cell. Other axes haven't crossed their box
1604            // boundary (best_t is the min), so the point's floor is safe.
1605            let pb = [
1606                origin[0] + dir[0] * (best_t + 1e-4),
1607                origin[1] + dir[1] * (best_t + 1e-4),
1608                origin[2] + dir[2] * (best_t + 1e-4),
1609            ];
1610            let mut nc = [
1611                (pb[0] / cell_size).floor() as i32,
1612                (pb[1] / cell_size).floor() as i32,
1613                (pb[2] / cell_size).floor() as i32,
1614            ];
1615            nc[best_axis] = if step[best_axis] > 0 {
1616                plane[best_axis]
1617            } else {
1618                plane[best_axis] - 1
1619            };
1620            // The skip crossed a box boundary; if that takes the ray out
1621            // of the grid box it has exited (sky) — return rather than
1622            // clamping back in-bounds, which would spin at the edge.
1623            if nc[0] < lo_c[0]
1624                || nc[0] >= hi_c[0]
1625                || nc[1] < lo_c[1]
1626                || nc[1] >= hi_c[1]
1627                || nc[2] < lo_c[2]
1628                || nc[2] >= hi_c[2]
1629            {
1630                return finalize_exit(touched, accum, trans, env, dir, max_dist);
1631            }
1632            cellc = nc;
1633            // Refresh t_max for the new cell (dir unchanged → t_delta and
1634            // step constant; axes with step==0 keep their +∞).
1635            for a in 0..3 {
1636                if step[a] > 0 {
1637                    t_max[a] = ((cellc[a] + 1) as f32 * cell_size - origin[a]) * inv[a];
1638                } else if step[a] < 0 {
1639                    t_max[a] = (cellc[a] as f32 * cell_size - origin[a]) * inv[a];
1640                }
1641            }
1642            t_curr = best_t.max(t_curr);
1643            last_axis = best_axis;
1644            prev_solid = false; // skipped empty space → next hit starts a run
1645            continue;
1646        }
1647
1648        // Occupied brick: dense per-cell surface test.
1649        #[cfg(test)]
1650        prof::CELLS.with(|x| x.set(x.get() + 1));
1651        if let Some(color) = sampler.hit(cellc) {
1652            let bright_sub = side_shade_sub(env, last_axis, step);
1653            // PF.7 (C4) — one colour→material scan per hit: resolve the id
1654            // and the material together. EV.1 hoisted this above the shade
1655            // so the emissive branch can bypass lighting entirely; with no
1656            // terrain material map the result is `OPAQUE` (emissive 0) and
1657            // every path below is bit-identical to the pre-material code.
1658            let (m, mat_id) = match env.materials {
1659                Some(table) if !env.terrain_materials.is_empty() => {
1660                    let id = material_for_color(env.terrain_materials, color);
1661                    (table.get(id), id)
1662                }
1663                _ => (Material::OPAQUE, 0),
1664            };
1665            // CPU.1 — dynamic lighting (flat per voxel) when a rig is active;
1666            // else the baked-byte `shade` path (byte-identical). CPU.2 — a
1667            // sun/point shadow march reuses this same `sampler` (occupancy +
1668            // box bounds); only built when a caster is actually flagged so
1669            // the no-shadow rig stays march-free. EV.1 — an emissive
1670            // material outranks both: full-bright albedo, no face shade,
1671            // no rig, no shadow march.
1672            let shaded = if m.emissive > 0 {
1673                emissive_shade(color, m.emissive)
1674            } else if env.lights.enabled {
1675                let casts = shadow_casts;
1676                // Pick the shadow oracle: the scene-wide one (cross-grid +
1677                // sprites, XS.1) when present, else the single-grid Sampler;
1678                // `None` when no caster is flagged, so the rig stays
1679                // march-free. The two testers live in branch-local slots so
1680                // exactly one is borrowed for the `shade_lit_cpu` call.
1681                let mut world_sh;
1682                let mut sampler_sh;
1683                let tester: Option<&mut dyn ShadowTester> = if !casts {
1684                    None
1685                } else if let Some(ctx) = env.world_shadow {
1686                    world_sh = WorldShadow { ctx };
1687                    Some(&mut world_sh)
1688                } else {
1689                    sampler_sh = SamplerShadow {
1690                        sampler: &mut *sampler,
1691                        cell_size,
1692                        lo_c,
1693                        hi_c,
1694                    };
1695                    Some(&mut sampler_sh)
1696                };
1697                shade_lit_cpu(
1698                    color,
1699                    bright_sub,
1700                    last_axis,
1701                    step,
1702                    cellc,
1703                    cell_size,
1704                    &env.lights,
1705                    tester,
1706                )
1707            } else {
1708                shade(color, bright_sub)
1709            };
1710            let lit = apply_fog(shaded, depth.max(0.0), env);
1711            if m.is_opaque() {
1712                // Opaque surface: the background. Return the first hit verbatim
1713                // when nothing translucent preceded it (bit-identical), else
1714                // composite the accumulated layers over it.
1715                let color = if touched {
1716                    composite_over(accum, trans, lit)
1717                } else {
1718                    lit
1719                };
1720                return Some(Hit {
1721                    color,
1722                    dist: depth.max(0.0),
1723                });
1724            }
1725            let a = f32::from(m.alpha) / 255.0;
1726            if matches!(m.mode, roxlap_formats::material::BlendMode::Volumetric) {
1727                // Per-cell Beer–Lambert: opacity weighted by the ray's path
1728                // length through this voxel (so a filled volume thickens
1729                // smoothly with depth, a sliver contributes ≈0). Occludes.
1730                let t_exit = t_max[min_axis(t_max)];
1731                let seg_len = (t_exit - t_curr).max(0.0) * dir_len / cell_size;
1732                let eff_a = 1.0 - (1.0 - a).powf(seg_len);
1733                let c = rgb_to_f32(lit);
1734                accum[0] += trans * eff_a * c[0];
1735                accum[1] += trans * eff_a * c[1];
1736                accum[2] += trans * eff_a * c[2];
1737                trans *= 1.0 - eff_a;
1738                touched = true;
1739                prev_mat = mat_id;
1740                if trans < 1.0 / 256.0 {
1741                    return Some(Hit {
1742                        color: f32_to_rgb(accum),
1743                        dist: depth.max(0.0),
1744                    });
1745                }
1746            } else if !prev_solid || mat_id != prev_mat {
1747                // AlphaBlend / Additive: one alpha layer per solid-run entry or
1748                // material change (per-span — avoids the voxel-grid striping
1749                // through a thick glass/water slab; thickness-independent).
1750                let c = rgb_to_f32(lit);
1751                accum[0] += trans * a * c[0];
1752                accum[1] += trans * a * c[1];
1753                accum[2] += trans * a * c[2];
1754                if !matches!(m.mode, roxlap_formats::material::BlendMode::Additive) {
1755                    trans *= 1.0 - a; // AlphaBlend occludes; Additive does not.
1756                }
1757                touched = true;
1758                prev_mat = mat_id;
1759                if trans < 1.0 / 256.0 {
1760                    return Some(Hit {
1761                        color: f32_to_rgb(accum),
1762                        dist: depth.max(0.0),
1763                    });
1764                }
1765            }
1766            prev_solid = true;
1767        } else {
1768            prev_solid = false;
1769        }
1770        let axis = min_axis(t_max);
1771        last_axis = axis;
1772        t_curr = t_max[axis];
1773        cellc[axis] += step[axis];
1774        t_max[axis] += t_delta[axis];
1775    }
1776    None
1777}
1778
1779/// Per-face brightness reduction for the hit face. `axis` is the axis
1780/// the ray crossed to enter the hit voxel (`3` = entry voxel, no face);
1781/// `step[axis]` gives the crossing direction. Maps to the
1782/// `[x-, x+, y-, y+, z-, z+]` `side_shades` entry of the face the ray
1783/// looks at (a `+step` crossing enters through the low / `-` face).
1784#[inline]
1785fn side_shade_sub(env: &DdaEnv<'_>, axis: usize, step: [i32; 3]) -> u32 {
1786    if axis >= 3 {
1787        return 0;
1788    }
1789    let face = axis * 2 + usize::from(step[axis] < 0);
1790    env.side_shades[face].max(0) as u32
1791}
1792
1793/// Cast one ray into the grid and return the first solid hit.
1794///
1795/// **DDA.4:** cross-chunk per-pixel 3D-DDA over the grid's full voxel
1796/// box ([`GridView::voxel_bounds`], spanning every chunk in XY **and**
1797/// Z). The [`Sampler`] resolves each stepped voxel to its chunk and
1798/// brick-gates the slab walk. Cross-chunk look-down (the case the
1799/// voxlap renderer needed the whole virtual-column stack for) falls out
1800/// of the box simply spanning `chunks_z` along Z.
1801fn cast_ray(
1802    origin: [f32; 3],
1803    dir: [f32; 3],
1804    forward: [f32; 3],
1805    sampler: &mut Sampler<'_>,
1806    settings: &OpticastSettings,
1807    env: &DdaEnv<'_>,
1808) -> Option<Hit> {
1809    let (lo_i, hi_i) = sampler.grid.voxel_bounds();
1810    #[allow(clippy::cast_precision_loss)]
1811    let lo_f = [lo_i[0] as f32, lo_i[1] as f32, lo_i[2] as f32];
1812    #[allow(clippy::cast_precision_loss)]
1813    let hi_f = [hi_i[0] as f32, hi_i[1] as f32, hi_i[2] as f32];
1814    let (t_enter, t_exit) = intersect_aabb(origin, dir, lo_f, hi_f)?;
1815    let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
1816    #[allow(clippy::cast_precision_loss)]
1817    let max_dist = settings.max_scan_dist.max(1) as f32;
1818    let cell = 1i32 << sampler.mip;
1819    let cell_size = cell as f32;
1820    let lo_c = [
1821        lo_i[0].div_euclid(cell),
1822        lo_i[1].div_euclid(cell),
1823        lo_i[2].div_euclid(cell),
1824    ];
1825    let hi_c = [
1826        hi_i[0].div_euclid(cell),
1827        hi_i[1].div_euclid(cell),
1828        hi_i[2].div_euclid(cell),
1829    ];
1830    cell_walk_skip(
1831        origin, dir, fwd_dot, sampler, lo_c, hi_c, cell_size, t_enter, t_exit, max_dist, env,
1832    )
1833}
1834
1835/// Render one grid into `sink` with per-pixel 3D-DDA.
1836///
1837/// `camera` is the grid-local pose, `settings`
1838/// ([`OpticastSettings`]) carries the projection + viewport (including
1839/// the `y_start..y_end` strip bound), and `grid` is the per-frame
1840/// [`GridView`] borrow. `pitch_pixels` is the framebuffer
1841/// row stride in pixels (matches `ScalarRasterizer::new`'s argument).
1842///
1843/// On a miss, a textured sky ([`DdaEnv::sky`]) is sampled per ray
1844/// direction and written at `+inf` depth; with no textured sky the miss
1845/// writes nothing, so the caller's solid sky pre-fill shows (the
1846/// `render_scene_composed` path pre-fills it).
1847pub fn render_dda(
1848    camera: &Camera,
1849    settings: &OpticastSettings,
1850    grid: GridView<'_>,
1851    pitch_pixels: usize,
1852    env: &DdaEnv<'_>,
1853    mip: u32,
1854    sink: &mut impl PixelSink,
1855) {
1856    let cs = camera_math::derive(
1857        camera,
1858        settings.xres,
1859        settings.yres,
1860        settings.hx,
1861        settings.hy,
1862        settings.hz,
1863    );
1864
1865    // Sequential path builds a throwaway per-call cache (tests / single
1866    // grid). The parallel path takes a persistent cross-frame cache.
1867    let (cache, mip) = local_cache(&grid, mip);
1868    let mut sampler = Sampler::new(grid, &cache, mip);
1869
1870    for py in settings.y_start..settings.y_end {
1871        let row = py as usize * pitch_pixels;
1872        for px in settings.x_start..settings.x_end {
1873            if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py) {
1874                sink.put(row + px as usize, color, dist);
1875            }
1876        }
1877    }
1878}
1879
1880/// Resolve one pixel: a shaded + fogged hit colour, a sampled textured
1881/// sky on a miss, or `None` (miss with no textured sky → caller's
1882/// pre-fill stands). Shared by the sequential ([`render_dda`]) and
1883/// parallel ([`render_dda_parallel`]) drivers.
1884#[inline]
1885fn pixel_result(
1886    cs: &CameraState,
1887    settings: &OpticastSettings,
1888    sampler: &mut Sampler<'_>,
1889    env: &DdaEnv<'_>,
1890    px: u32,
1891    py: u32,
1892) -> Option<(u32, f32)> {
1893    let (origin, dir) = pixel_ray(cs, settings, px, py);
1894    if let Some(hit) = cast_ray(origin, dir, cs.forward, sampler, settings, env) {
1895        Some((hit.color, hit.dist))
1896    } else {
1897        env.sky.map(|sky| (sample_sky(sky, dir), f32::INFINITY))
1898    }
1899}
1900
1901/// Tile-parallel [`render_dda`] writing straight into `(fb, zb)`.
1902///
1903/// DDA pixels are independent, so the framebuffer splits into disjoint
1904/// horizontal bands rendered concurrently (rayon) — **bit-identical**
1905/// to the sequential render regardless of thread count, unlike voxlap's
1906/// per-strip discretisation. Each band spins up its own lightweight
1907/// `Sampler` over the shared, immutable `cache`.
1908///
1909/// `cache` must already hold current brick maps for every chunk at
1910/// `mip` (populate via [`BrickCache::ensure`]); `mip` is the effective
1911/// render mip ([`effective_mip`]). `(fb, zb)` use the standard
1912/// conventions (`0x80RRGGBB`; z = perp distance, smaller = closer); a
1913/// miss writes nothing unless [`DdaEnv::sky`] is set. `pitch_pixels` is
1914/// the row stride.
1915#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
1916pub fn render_dda_parallel(
1917    camera: &Camera,
1918    settings: &OpticastSettings,
1919    grid: GridView<'_>,
1920    fb: &mut [u32],
1921    zb: &mut [f32],
1922    pitch_pixels: usize,
1923    env: &DdaEnv<'_>,
1924    cache: &BrickCache,
1925    mip: u32,
1926) {
1927    debug_assert_eq!(fb.len(), zb.len());
1928    let (y0, y1) = (settings.y_start, settings.y_end);
1929    if y1 <= y0 {
1930        return;
1931    }
1932    let cs = camera_math::derive(
1933        camera,
1934        settings.xres,
1935        settings.yres,
1936        settings.hx,
1937        settings.hy,
1938        settings.hz,
1939    );
1940    let target = RasterTarget::new(fb, zb);
1941
1942    // PF.7 (C5) — small fixed bands + rayon work-stealing instead of one
1943    // equal band per thread: sky-heavy rows finish instantly while
1944    // horizon/terrain rows dominate, so an equal split left threads idle
1945    // for the tail of every frame. 8 rows amortises the per-band
1946    // `Sampler` construction while staying fine-grained enough to
1947    // balance. Bit-identical (pixels are independent; rows disjoint).
1948    let band = 8u32;
1949    let bands: Vec<(u32, u32)> = (y0..y1)
1950        .step_by(band as usize)
1951        .map(|s| (s, (s + band).min(y1)))
1952        .collect();
1953
1954    bands.par_iter().for_each(|&(by0, by1)| {
1955        let mut sampler = Sampler::new(grid, cache, mip);
1956        for py in by0..by1 {
1957            let row = py as usize * pitch_pixels;
1958            for px in settings.x_start..settings.x_end {
1959                if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py)
1960                {
1961                    let idx = row + px as usize;
1962                    // SAFETY: bands cover disjoint row ranges, so writes
1963                    // never alias across threads; `idx` is in-bounds for
1964                    // a `pitch * height`-sized buffer.
1965                    unsafe {
1966                        target.write_color(idx, color);
1967                        target.write_depth(idx, dist);
1968                    }
1969                }
1970            }
1971        }
1972    });
1973}
1974
1975/// Dense per-voxel reference cast for a **single-chunk** grid: walks
1976/// every voxel of `[0, vsid)² × [0, CHUNK_SIZE_Z)` calling
1977/// [`GridView::surface_color`] directly — no brick gate, no chunk
1978/// resolution. The equivalence oracle the brickmap + sampler
1979/// [`cast_ray`] is checked against in tests.
1980#[cfg(test)]
1981#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1982fn cast_ray_reference(
1983    origin: [f32; 3],
1984    dir: [f32; 3],
1985    forward: [f32; 3],
1986    grid: &GridView<'_>,
1987    settings: &OpticastSettings,
1988) -> Option<Hit> {
1989    let nx = grid.vsid as f32;
1990    let nz = f32::from(u16::try_from(crate::grid_view::CHUNK_SIZE_Z).unwrap_or(256));
1991    #[allow(clippy::cast_possible_wrap)]
1992    let n_i = [
1993        grid.vsid as i32,
1994        grid.vsid as i32,
1995        crate::grid_view::CHUNK_SIZE_Z as i32,
1996    ];
1997    let (t_enter, t_exit) = intersect_aabb(origin, dir, [0.0; 3], [nx, nx, nz])?;
1998    let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
1999    let max_dist = settings.max_scan_dist.max(1) as f32;
2000
2001    let start = t_enter + 1e-4;
2002    let p = [
2003        origin[0] + dir[0] * start,
2004        origin[1] + dir[1] * start,
2005        origin[2] + dir[2] * start,
2006    ];
2007    let mut voxel = [
2008        (p[0].floor() as i32).clamp(0, n_i[0] - 1),
2009        (p[1].floor() as i32).clamp(0, n_i[1] - 1),
2010        (p[2].floor() as i32).clamp(0, n_i[2] - 1),
2011    ];
2012    let (step, mut t_max, t_delta) = dda_setup(origin, dir, voxel, 1.0);
2013    let mut t_curr = t_enter;
2014    let max_steps = (n_i[0] + n_i[1] + n_i[2]) as usize + 8;
2015    for _ in 0..max_steps {
2016        if voxel[0] < 0
2017            || voxel[0] >= n_i[0]
2018            || voxel[1] < 0
2019            || voxel[1] >= n_i[1]
2020            || voxel[2] < 0
2021            || voxel[2] >= n_i[2]
2022        {
2023            return None;
2024        }
2025        let depth = t_curr * fwd_dot;
2026        if depth > max_dist || t_curr > t_exit {
2027            return None;
2028        }
2029        #[allow(clippy::cast_sign_loss)]
2030        if let Some(color) = grid.surface_color(voxel[0] as u32, voxel[1] as u32, voxel[2] as u32) {
2031            return Some(Hit {
2032                color: shade(color.0, 0),
2033                dist: depth.max(0.0),
2034            });
2035        }
2036        let axis = min_axis(t_max);
2037        t_curr = t_max[axis];
2038        voxel[axis] += step[axis];
2039        t_max[axis] += t_delta[axis];
2040    }
2041    None
2042}
2043
2044#[cfg(test)]
2045mod tests {
2046    use super::*;
2047    use roxlap_formats::VoxColor;
2048
2049    // CPU.1 — luminance of a packed colour's low-24-bit RGB.
2050    fn lum(p: u32) -> u32 {
2051        (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff)
2052    }
2053
2054    #[test]
2055    fn cel_band_quantizes_and_collapses() {
2056        // Two distinct factors round to the same band at bands=2.
2057        assert_eq!(cel_band(0.8, 2), cel_band(0.9, 2));
2058        assert!((cel_band(0.8, 2) - 1.0).abs() < 1e-6);
2059        // ...but a low factor lands on a different band.
2060        assert_ne!(cel_band(0.3, 2), cel_band(0.8, 2));
2061    }
2062
2063    #[test]
2064    fn shade_lit_cpu_sun_lights_by_facing() {
2065        // Grey voxel (brightness 0x80 = full ambient). Floor top face: hit via
2066        // a +z step (axis 2) ⇒ normal points up (-z).
2067        let color = 0x80_80_80_80;
2068        let step = [0, 0, 1];
2069        let base = CpuLights {
2070            enabled: true,
2071            sun: true,
2072            sun_color: [1.0; 3],
2073            sun_intensity: 1.0,
2074            ambient: [0.2; 3],
2075            ..CpuLights::default()
2076        };
2077        let facing = CpuLights {
2078            sun_dir: [0.0, 0.0, -1.0],
2079            ..base
2080        }; // toward sun = up
2081        let back = CpuLights {
2082            sun_dir: [0.0, 0.0, 1.0],
2083            ..base
2084        }; // sun below the face
2085        let lit = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &facing, None);
2086        let dark = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &back, None);
2087        assert!(
2088            lum(lit) > lum(dark),
2089            "sun facing the surface must brighten it: {lit:#08x} vs {dark:#08x}",
2090        );
2091    }
2092
2093    #[test]
2094    fn shade_dynamic_spot_cone_masks_off_axis() {
2095        // Surface at the origin, up-facing normal (-z, voxlap z-down); a light
2096        // 10 units "above" it (at -z). No ambient/AO ⇒ only the light shows.
2097        let albedo = [0.5, 0.5, 0.5];
2098        let n = [0.0, 0.0, -1.0];
2099        let sample = [0.0, 0.0, 0.0];
2100        let inner = 10.0f32.to_radians().cos();
2101        let outer = 15.0f32.to_radians().cos();
2102        let shade = |spot_dir: [f32; 3], cos_inner: f32, cos_outer: f32| {
2103            let pts = [CpuPointLight {
2104                pos: [0.0, 0.0, -10.0],
2105                color: [1.0; 3],
2106                intensity: 1.0,
2107                radius: 64.0,
2108                casts_shadow: false,
2109                spot_dir,
2110                cos_inner,
2111                cos_outer,
2112            }];
2113            let l = CpuLights {
2114                enabled: true,
2115                ambient: [0.0; 3],
2116                points: &pts,
2117                ..CpuLights::default()
2118            };
2119            shade_dynamic(albedo, 0.0, n, sample, &l, None)
2120        };
2121        // A pure point light (cos_outer = -1) ignores the axis entirely.
2122        let point = shade([0.0, 0.0, 1.0], -1.0, -1.0);
2123        // A spot whose axis shines straight down onto the surface (on-axis).
2124        let on_axis = shade([0.0, 0.0, 1.0], inner, outer);
2125        // Same spot aimed sideways ⇒ the surface is outside the cone.
2126        let off_axis = shade([1.0, 0.0, 0.0], inner, outer);
2127
2128        // On-axis (cd == 1) is fully inside the cone ⇒ identical to a point.
2129        assert_eq!(
2130            on_axis, point,
2131            "on-axis spot must equal the point light: {on_axis:#08x} vs {point:#08x}",
2132        );
2133        // Off-axis is masked to zero ⇒ only the (zero) ambient remains.
2134        assert!(
2135            lum(on_axis) > lum(off_axis),
2136            "off-axis spot must be darker: {on_axis:#08x} vs {off_axis:#08x}",
2137        );
2138        assert_eq!(lum(off_axis), 0, "off-cone spot contributes nothing");
2139    }
2140
2141    #[test]
2142    fn shade_lit_cpu_cel_terraces_sun() {
2143        // Two sun elevations with distinct N·L (0.8 / 0.9) collapse to one
2144        // band at bands=2 ⇒ identical stylized colour; smooth (bands=0) differs.
2145        let color = 0x80_80_80_80;
2146        let step = [0, 0, 1];
2147        let mk = |zc: f32, bands: u32| {
2148            let n = (1.0f32 - zc * zc).sqrt();
2149            CpuLights {
2150                enabled: true,
2151                sun: true,
2152                sun_dir: [n, 0.0, -zc], // ndl on the up face = zc
2153                sun_color: [1.0; 3],
2154                sun_intensity: 1.0,
2155                ambient: [0.1; 3],
2156                bands,
2157                ..CpuLights::default()
2158            }
2159        };
2160        let smooth_a = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.8, 0), None);
2161        let smooth_b = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.9, 0), None);
2162        assert_ne!(smooth_a, smooth_b, "smooth diffuse must vary with N·L");
2163        let cel_a = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.8, 2), None);
2164        let cel_b = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.9, 2), None);
2165        assert_eq!(
2166            cel_a, cel_b,
2167            "cel banding must terrace both N·L to one level"
2168        );
2169    }
2170
2171    /// CPU.2 — the shadow application math (independent of the march): an
2172    /// occluded sun-lit sample keeps only `1 - shadow_strength` of the sun
2173    /// key, and `shadow_strength == 0` makes shadows invisible.
2174    #[test]
2175    fn shade_dynamic_sun_shadow_darkens() {
2176        struct Mock(bool);
2177        impl ShadowTester for Mock {
2178            fn occluded(&mut self, _: [f32; 3], _: [f32; 3], _: f32) -> bool {
2179                self.0
2180            }
2181        }
2182        let l = CpuLights {
2183            enabled: true,
2184            sun: true,
2185            sun_dir: [0.0, 0.0, -1.0], // up = toward the sun
2186            sun_color: [1.0; 3],
2187            sun_intensity: 1.0,
2188            sun_casts_shadow: true,
2189            ambient: [0.2; 3],
2190            shadow_strength: 0.7,
2191            shadow_bias: 1.5,
2192            shadow_max_dist: 64.0,
2193            ..CpuLights::default()
2194        };
2195        let albedo = [0.8; 3];
2196        let n = [0.0, 0.0, -1.0]; // up face, faces the sun
2197        let s = [0.5, 0.5, 0.5];
2198        let lit = shade_dynamic(albedo, 1.0, n, s, &l, Some(&mut Mock(false)));
2199        let shadowed = shade_dynamic(albedo, 1.0, n, s, &l, Some(&mut Mock(true)));
2200        assert!(
2201            lum(shadowed) < lum(lit),
2202            "an occluded sun face must darken: shadowed={shadowed:#08x} lit={lit:#08x}",
2203        );
2204        // strength 0 ⇒ no visible shadow even when occluded.
2205        let l0 = CpuLights {
2206            shadow_strength: 0.0,
2207            ..l
2208        };
2209        assert_eq!(
2210            shade_dynamic(albedo, 1.0, n, s, &l0, Some(&mut Mock(true))),
2211            shade_dynamic(albedo, 1.0, n, s, &l0, Some(&mut Mock(false))),
2212            "shadow_strength 0 ⇒ shadows invisible",
2213        );
2214    }
2215
2216    /// CPU.2 — the actual [`SamplerShadow`] march casts a sun shadow through
2217    /// the grid: a wall on a floor, lit by a grazing sun, darkens the floor
2218    /// in the wall's shadow. Total scene luminance with shadows enabled is
2219    /// strictly less than with them off (shadows only ever subtract), and
2220    /// the gap is non-trivial (a real shadow, not FP noise).
2221    #[test]
2222    fn sampler_shadow_march_casts_sun_shadow() {
2223        // Floor at z>=60; a thin wall at x==32 rising from the floor (z 30..60).
2224        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, _y, z| {
2225            if z >= 60 {
2226                Some(VoxColor(0x80_80_80_80)) // floor
2227            } else if x == 32 && (30..60).contains(&z) {
2228                Some(VoxColor(0x80_70_70_70)) // wall (distinct so it's not a dead branch)
2229            } else {
2230                None
2231            }
2232        });
2233        let grid = GridView::from_single_vxl(&vxl);
2234        // Straight-down camera over the floor (voxlap z-down: forward = +z).
2235        let cam = Camera {
2236            pos: [32.0, 32.0, 6.0],
2237            right: [1.0, 0.0, 0.0],
2238            down: [0.0, 1.0, 0.0],
2239            forward: [0.0, 0.0, 1.0],
2240        };
2241        // Sun grazing from +x and above ⇒ the wall shadows the floor at x<32.
2242        let inv = 1.0f32 / 2.0f32.sqrt();
2243        let base = CpuLights {
2244            enabled: true,
2245            sun: true,
2246            sun_dir: [inv, 0.0, -inv],
2247            sun_color: [1.0; 3],
2248            sun_intensity: 1.0,
2249            ambient: [0.25; 3],
2250            shadow_strength: 0.8,
2251            shadow_bias: 1.5,
2252            shadow_max_dist: 128.0,
2253            ..CpuLights::default()
2254        };
2255        let (w, h) = (96u32, 96u32);
2256        let lit_env = DdaEnv {
2257            lights: CpuLights {
2258                sun_casts_shadow: false,
2259                ..base
2260            },
2261            ..DdaEnv::default()
2262        };
2263        let shadow_env = DdaEnv {
2264            lights: CpuLights {
2265                sun_casts_shadow: true,
2266                ..base
2267            },
2268            ..DdaEnv::default()
2269        };
2270        let (fb_lit, _) = render_brickmap_env(grid, &cam, w, h, &lit_env);
2271        let (fb_sh, _) = render_brickmap_env(grid, &cam, w, h, &shadow_env);
2272        let sum: fn(&[u32]) -> u64 = |fb| fb.iter().map(|&p| u64::from(lum(p))).sum();
2273        let lit_sum = sum(&fb_lit);
2274        let sh_sum = sum(&fb_sh);
2275        assert!(
2276            sh_sum < lit_sum,
2277            "the wall's shadow must darken the floor: shadow_sum={sh_sum} lit_sum={lit_sum}",
2278        );
2279        // Non-trivial: at least a few % of the lit total was removed.
2280        assert!(
2281            (lit_sum - sh_sum) * 50 > lit_sum,
2282            "shadow should remove >2% of total luminance: lit={lit_sum} shadow={sh_sum}",
2283        );
2284    }
2285
2286    /// Recording sink: collects `(idx, color, dist)` puts for tests.
2287    #[derive(Default)]
2288    struct Recorder {
2289        puts: Vec<(usize, u32, f32)>,
2290    }
2291    impl PixelSink for Recorder {
2292        fn put(&mut self, idx: usize, color: u32, dist: f32) {
2293            self.puts.push((idx, color, dist));
2294        }
2295    }
2296
2297    fn oracle_camera() -> Camera {
2298        // Identity-basis camera at origin: ray math is integer-exact.
2299        Camera {
2300            pos: [0.0, 0.0, 0.0],
2301            right: [1.0, 0.0, 0.0],
2302            down: [0.0, 0.0, 1.0],
2303            forward: [0.0, 1.0, 0.0],
2304        }
2305    }
2306
2307    /// Render `grid` from `camera` into a `w × h` framebuffer and
2308    /// return the per-pixel hit mask (`true` where a ray hit a voxel).
2309    fn render_mask(grid: GridView<'_>, camera: &Camera, w: u32, h: u32) -> Vec<bool> {
2310        let n = (w as usize) * (h as usize);
2311        let mut fb = vec![0u32; n]; // sky sentinel = 0
2312        let mut zb = vec![f32::INFINITY; n];
2313        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2314        {
2315            let mut sink = RasterSink::new(&mut fb, &mut zb);
2316            render_dda(
2317                camera,
2318                &settings,
2319                grid,
2320                w as usize,
2321                &DdaEnv::default(),
2322                0,
2323                &mut sink,
2324            );
2325        }
2326        fb.iter().map(|&c| c != 0).collect()
2327    }
2328
2329    /// A silhouette is "row-convex" if every framebuffer row's hit
2330    /// pixels form a single contiguous run (no interior gap). The
2331    /// voxlap silhouette notch is exactly such an interior gap, so this
2332    /// is the headline DDA.1 acceptance check.
2333    fn rows_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
2334        let w = w as usize;
2335        for y in 0..h as usize {
2336            let row = &mask[y * w..(y + 1) * w];
2337            let first = row.iter().position(|&b| b);
2338            let last = row.iter().rposition(|&b| b);
2339            if let (Some(f), Some(l)) = (first, last) {
2340                if row[f..=l].iter().any(|&b| !b) {
2341                    return false;
2342                }
2343            }
2344        }
2345        true
2346    }
2347
2348    /// Same contiguity check down each column.
2349    fn cols_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
2350        let w = w as usize;
2351        let h = h as usize;
2352        for x in 0..w {
2353            let col: Vec<bool> = (0..h).map(|y| mask[y * w + x]).collect();
2354            let first = col.iter().position(|&b| b);
2355            let last = col.iter().rposition(|&b| b);
2356            if let (Some(f), Some(l)) = (first, last) {
2357                if col[f..=l].iter().any(|&b| !b) {
2358                    return false;
2359                }
2360            }
2361        }
2362        true
2363    }
2364
2365    /// The principal-point pixel `(hx, hy)` looks straight down the
2366    /// forward axis, scaled by `hz`.
2367    #[test]
2368    fn center_pixel_ray_is_forward() {
2369        let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
2370        let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
2371        // hx = hy = 320 / 240 → use the exact principal point.
2372        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2373        let (origin, dir) = pixel_ray(&cs, &settings, settings.hx as u32, settings.hy as u32);
2374        assert_eq!(origin, [0.0, 0.0, 0.0]);
2375        // hz·forward = 320·[0,1,0].
2376        assert_eq!(
2377            dir.map(f32::to_bits),
2378            [0.0f32, 320.0, 0.0].map(f32::to_bits)
2379        );
2380    }
2381
2382    /// Pixel `(0, 0)`'s ray equals `camera_math`'s `corn[0]` — proving
2383    /// the DDA renderer samples the same rays the voxlap frustum is
2384    /// built from.
2385    #[test]
2386    fn corner_pixel_ray_matches_camera_corn0() {
2387        let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
2388        let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
2389        let (_origin, dir) = pixel_ray(&cs, &settings, 0, 0);
2390        assert_eq!(dir.map(f32::to_bits), cs.corn[0].map(f32::to_bits));
2391    }
2392
2393    /// The renderer's independent slab decoder
2394    /// ([`GridView::voxel_color`]) must agree with the reference
2395    /// [`roxlap_formats::vxl::Vxl::voxel_color`] for every cell —
2396    /// including a column with an air gap, which exercises the
2397    /// ceiling-colour-list branch.
2398    #[test]
2399    fn gridview_voxel_color_matches_reference() {
2400        // Two solid runs per column separated by air → ceiling list.
2401        let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |x, _, z| {
2402            let lo = (10..=12).contains(&z);
2403            let hi = (40..=42).contains(&z);
2404            (lo || hi).then_some(VoxColor(0x80_10_20_30 + x))
2405        });
2406        let grid = GridView::from_single_vxl(&vxl);
2407        for x in 0..8 {
2408            for y in 0..8 {
2409                for z in 0..64 {
2410                    assert_eq!(
2411                        grid.voxel_color(x, y, z),
2412                        vxl.voxel_color(x, y, z),
2413                        "mismatch at ({x},{y},{z})"
2414                    );
2415                }
2416            }
2417        }
2418    }
2419
2420    /// An all-air grid produces no hits (every ray misses).
2421    #[test]
2422    fn empty_grid_no_hits() {
2423        let vxl = roxlap_formats::vxl::Vxl::empty(64);
2424        let grid = GridView::from_single_vxl(&vxl);
2425        let settings = OpticastSettings::for_oracle_framebuffer(64, 48);
2426        let mut rec = Recorder::default();
2427        render_dda(
2428            &oracle_camera(),
2429            &settings,
2430            grid,
2431            64,
2432            &DdaEnv::default(),
2433            0,
2434            &mut rec,
2435        );
2436        assert!(rec.puts.is_empty(), "all-air grid must produce no hits");
2437    }
2438
2439    /// Camera above a solid floor, looking straight down: every ray
2440    /// hits, the recovered colour is the floor colour, and the centre
2441    /// pixel's depth ≈ the camera's height above the floor.
2442    #[test]
2443    fn floor_seen_from_above() {
2444        const FLOOR_Z: u32 = 40;
2445        const FLOOR_COL: VoxColor = VoxColor(0x80_30_60_90);
2446        let vxl =
2447            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= FLOOR_Z).then_some(FLOOR_COL));
2448        let grid = GridView::from_single_vxl(&vxl);
2449
2450        // Eye above the floor (z is down), looking down (+z).
2451        let cam = Camera {
2452            pos: [16.0, 16.0, 10.0],
2453            right: [1.0, 0.0, 0.0],
2454            down: [0.0, 1.0, 0.0],
2455            forward: [0.0, 0.0, 1.0],
2456        };
2457        let settings = OpticastSettings::for_oracle_framebuffer(48, 48);
2458        let mut rec = Recorder::default();
2459        render_dda(&cam, &settings, grid, 48, &DdaEnv::default(), 0, &mut rec);
2460
2461        assert!(!rec.puts.is_empty(), "floor must be visible");
2462        // Centre pixel looks straight down → depth ≈ FLOOR_Z - eye_z.
2463        let centre = 24usize * 48 + 24;
2464        let hit = rec
2465            .puts
2466            .iter()
2467            .find(|(idx, _, _)| *idx == centre)
2468            .expect("centre ray must hit the floor");
2469        assert_eq!(hit.1 & 0x00ff_ffff, FLOOR_COL.0 & 0x00ff_ffff);
2470        let expected = (FLOOR_Z as f32) - 10.0;
2471        assert!(
2472            (hit.2 - expected).abs() < 1.5,
2473            "centre depth {} not ≈ {}",
2474            hit.2,
2475            expected
2476        );
2477    }
2478
2479    /// DDA.2: a camera looking at the horizon splits the frame into
2480    /// sky (upward rays miss → no write) and floor (downward rays hit).
2481    /// The top of the frame must be mostly sky, the bottom mostly
2482    /// floor.
2483    #[test]
2484    fn horizon_splits_sky_and_floor() {
2485        const FLOOR_Z: u32 = 40;
2486        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
2487            (z >= FLOOR_Z).then_some(VoxColor(0x80_44_66_88))
2488        });
2489        let grid = GridView::from_single_vxl(&vxl);
2490
2491        // At z=30 (above the z=40 floor), looking +y horizontally,
2492        // down = +z. Upward rays (low py) escape through the box top
2493        // (z=0) → sky; downward rays (high py) strike the floor.
2494        let cam = Camera {
2495            pos: [32.0, 4.0, 30.0],
2496            right: [-1.0, 0.0, 0.0],
2497            down: [0.0, 0.0, 1.0],
2498            forward: [0.0, 1.0, 0.0],
2499        };
2500        let (w, h) = (64u32, 64u32);
2501        let mask = render_mask(grid, &cam, w, h);
2502
2503        let count_band = |y0: usize, y1: usize| -> usize {
2504            (y0 * w as usize..y1 * w as usize)
2505                .filter(|&i| mask[i])
2506                .count()
2507        };
2508        let top = count_band(0, h as usize / 4);
2509        let bottom = count_band(3 * h as usize / 4, h as usize);
2510        assert!(mask.iter().any(|&b| b), "floor must be visible");
2511        assert!(mask.iter().any(|&b| !b), "sky must be visible");
2512        assert!(
2513            bottom > top,
2514            "bottom band ({bottom}) should hit more floor than top band ({top})"
2515        );
2516    }
2517
2518    /// Render `grid` from `camera` with the dense reference cast (no
2519    /// brickmap), returning `(colour, depth)` buffers.
2520    fn render_reference(
2521        grid: GridView<'_>,
2522        camera: &Camera,
2523        w: u32,
2524        h: u32,
2525    ) -> (Vec<u32>, Vec<f32>) {
2526        let n = (w as usize) * (h as usize);
2527        let mut fb = vec![0u32; n];
2528        let mut zb = vec![f32::INFINITY; n];
2529        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2530        let cs = camera_math::derive(camera, w, h, settings.hx, settings.hy, settings.hz);
2531        for py in 0..h {
2532            for px in 0..w {
2533                let (o, d) = pixel_ray(&cs, &settings, px, py);
2534                if let Some(hit) = cast_ray_reference(o, d, cs.forward, &grid, &settings) {
2535                    let i = (py * w + px) as usize;
2536                    fb[i] = hit.color;
2537                    zb[i] = hit.dist;
2538                }
2539            }
2540        }
2541        (fb, zb)
2542    }
2543
2544    /// Render `grid` from `camera` via the production brickmap path.
2545    fn render_brickmap(
2546        grid: GridView<'_>,
2547        camera: &Camera,
2548        w: u32,
2549        h: u32,
2550    ) -> (Vec<u32>, Vec<f32>) {
2551        render_brickmap_env(grid, camera, w, h, &DdaEnv::default())
2552    }
2553
2554    /// As [`render_brickmap`] but with an explicit [`DdaEnv`] (fog /
2555    /// textured sky / side shades).
2556    fn render_brickmap_env(
2557        grid: GridView<'_>,
2558        camera: &Camera,
2559        w: u32,
2560        h: u32,
2561        env: &DdaEnv<'_>,
2562    ) -> (Vec<u32>, Vec<f32>) {
2563        let n = (w as usize) * (h as usize);
2564        let mut fb = vec![0u32; n];
2565        let mut zb = vec![f32::INFINITY; n];
2566        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2567        {
2568            let mut sink = RasterSink::new(&mut fb, &mut zb);
2569            render_dda(camera, &settings, grid, w as usize, env, 0, &mut sink);
2570        }
2571        (fb, zb)
2572    }
2573
2574    /// Regression for the cave-demo "bright sky seams" report: the
2575    /// empty-space-skip walk must not leak past an occupied box the ray
2576    /// only grazes at a shared edge/corner. A 1-voxel-thick diagonal
2577    /// wall (`x+y==64`, voxels edge-connected) with air on both sides is
2578    /// the canonical case. The production skip walk must hit exactly the
2579    /// same pixels as the dense per-cell reference — zero divergence.
2580    #[test]
2581    fn no_sky_leak_through_diagonal_wall() {
2582        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
2583            ((x + y == 64) && (2..62).contains(&z)).then_some(VoxColor(0x80_40_80_60))
2584        });
2585        let grid = GridView::from_single_vxl(&vxl);
2586        let (w, h) = (160u32, 160u32);
2587        let c = [10.0, 10.0, 32.0];
2588        let poses = [
2589            Camera::from_yaw_pitch(c, 0.785, 0.0),
2590            Camera::from_yaw_pitch(c, 0.6, 0.1),
2591            Camera::from_yaw_pitch(c, 0.95, -0.1),
2592            Camera::from_yaw_pitch(c, 0.785, 0.3),
2593            Camera::from_yaw_pitch(c, 0.5, 0.0),
2594        ];
2595        for (i, cam) in poses.iter().enumerate() {
2596            let (fb_b, _) = render_brickmap(grid, cam, w, h);
2597            let (fb_r, _) = render_reference(grid, cam, w, h);
2598            let leak = (0..(w * h) as usize)
2599                .filter(|&k| (fb_b[k] != 0) != (fb_r[k] != 0))
2600                .count();
2601            assert_eq!(leak, 0, "pose {i}: {leak} px diverge from dense reference");
2602        }
2603    }
2604
2605    /// TV terrain transparency: a glass-coloured voxel slab in front of an
2606    /// opaque floor. With no terrain material map the glass is an opaque first
2607    /// hit; with the map it becomes translucent and the floor tints through.
2608    #[test]
2609    fn terrain_glass_tints_floor_behind() {
2610        let glass = VoxColor(0x80_40_C0_E0); // cyan
2611        let floor = VoxColor(0x80_C0_40_40); // red
2612        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
2613            if z == 4 {
2614                Some(glass)
2615            } else if z >= 10 {
2616                Some(floor)
2617            } else {
2618                None
2619            }
2620        });
2621        let grid = GridView::from_single_vxl(&vxl);
2622        // Camera above the grid looking straight down (+z), centred.
2623        let cam = Camera {
2624            pos: [8.0, 8.0, 0.0],
2625            right: [1.0, 0.0, 0.0],
2626            down: [0.0, 1.0, 0.0],
2627            forward: [0.0, 0.0, 1.0],
2628        };
2629        let (w, h) = (32u32, 32u32);
2630        let centre = (h / 2 * w + w / 2) as usize;
2631
2632        // Opaque: the glass voxel stops the ray (no terrain materials).
2633        let (fb_op, _) = render_brickmap(grid, &cam, w, h);
2634        assert_eq!(
2635            fb_op[centre] & 0x00ff_ffff,
2636            0x0040_C0E0,
2637            "opaque glass first-hit"
2638        );
2639
2640        // Translucent: glass colour → material 1 (alpha-blend).
2641        let mut table = MaterialTable::new();
2642        table.set(1, Material::alpha_blend(128));
2643        let env = DdaEnv {
2644            materials: Some(&table),
2645            terrain_materials: &[(glass.rgb_part(), 1)],
2646            lights: CpuLights::default(),
2647            ..DdaEnv::default()
2648        };
2649        let (fb_tr, _) = render_brickmap_env(grid, &cam, w, h, &env);
2650        assert_ne!(
2651            fb_tr[centre], fb_op[centre],
2652            "glass should composite over the floor, not stay opaque"
2653        );
2654        let r_op = (fb_op[centre] >> 16) & 0xff; // glass red ≈ 0x40
2655        let r_tr = (fb_tr[centre] >> 16) & 0xff; // + floor red bleeds in
2656        assert!(
2657            r_tr > r_op,
2658            "floor red tints through the glass (op={r_op:02x} tr={r_tr:02x})"
2659        );
2660    }
2661
2662    /// EV.1 — an emissive terrain voxel renders at over-bright albedo,
2663    /// ignoring the baked brightness byte, per-face side shades and the
2664    /// dynamic light rig.
2665    #[test]
2666    fn terrain_emissive_ignores_lighting() {
2667        let crystal = VoxColor(0x40_20_60_80); // deliberately DIM baked byte 0x40
2668        let vxl =
2669            roxlap_formats::vxl::Vxl::from_dense(
2670                16,
2671                |_, _, z| if z >= 4 { Some(crystal) } else { None },
2672            );
2673        let grid = GridView::from_single_vxl(&vxl);
2674        let cam = Camera {
2675            pos: [8.0, 8.0, 0.0],
2676            right: [1.0, 0.0, 0.0],
2677            down: [0.0, 1.0, 0.0],
2678            forward: [0.0, 0.0, 1.0],
2679        };
2680        let (w, h) = (32u32, 32u32);
2681        let centre = (h / 2 * w + w / 2) as usize;
2682
2683        // Control: the baked 0x40 byte halves the albedo.
2684        let (fb_dim, _) = render_brickmap(grid, &cam, w, h);
2685        assert_eq!(
2686            fb_dim[centre] & 0x00ff_ffff,
2687            0x0010_3040,
2688            "baked byte 0x40 = albedo/2"
2689        );
2690
2691        // Emissive: glow(255) ⇒ ×255/128 over-bright, per-channel clamp.
2692        let mut table = MaterialTable::new();
2693        table.set(1, Material::glow(255));
2694        let base = DdaEnv {
2695            materials: Some(&table),
2696            terrain_materials: &[(crystal.rgb_part(), 1)],
2697            ..DdaEnv::default()
2698        };
2699        let (fb_em, _) = render_brickmap_env(grid, &cam, w, h, &base);
2700        assert_eq!(
2701            fb_em[centre] & 0x00ff_ffff,
2702            0x003f_bfff,
2703            "glow(255) ≈ 2× albedo (0x20,0x60,0x80 → 0x3f,0xbf,0xff)"
2704        );
2705
2706        // Max side shades: a normal voxel darkens, an emissive one must not.
2707        let shaded_env = DdaEnv {
2708            side_shades: [64; 6],
2709            ..DdaEnv::default()
2710        };
2711        let (fb_ss_plain, _) = render_brickmap_env(grid, &cam, w, h, &shaded_env);
2712        assert_ne!(
2713            fb_ss_plain[centre], fb_dim[centre],
2714            "control: side shades darken a non-emissive voxel"
2715        );
2716        let em_ss = DdaEnv {
2717            side_shades: [64; 6],
2718            ..base
2719        };
2720        let (fb_em_ss, _) = render_brickmap_env(grid, &cam, w, h, &em_ss);
2721        assert_eq!(
2722            fb_em_ss[centre], fb_em[centre],
2723            "side shades must not touch an emissive voxel"
2724        );
2725
2726        // Dynamic rig active (no sun, zero ambient ⇒ a normal voxel goes
2727        // black): the emissive voxel is rig-independent.
2728        let em_rig = DdaEnv {
2729            lights: CpuLights {
2730                enabled: true,
2731                ..CpuLights::default()
2732            },
2733            ..base
2734        };
2735        let (fb_em_rig, _) = render_brickmap_env(grid, &cam, w, h, &em_rig);
2736        assert_eq!(
2737            fb_em_rig[centre], fb_em[centre],
2738            "the dynamic rig must not touch an emissive voxel"
2739        );
2740    }
2741
2742    /// TV terrain Volumetric: a **filled** grey smoke volume over a red floor.
2743    /// Beer–Lambert opacity grows with the ray's path length, so a deeper smoke
2744    /// column shows more of its own colour (green channel rises toward the
2745    /// smoke grey) — thickness-dependent, unlike per-span AlphaBlend.
2746    #[test]
2747    fn terrain_volumetric_thickness_deepens_opacity() {
2748        let smoke = VoxColor(0x80_90_90_90); // grey
2749        let floor = VoxColor(0x80_C0_20_20); // red (low green)
2750                                             // Centre green channel for a smoke column `depth` voxels deep (filled),
2751                                             // floor at z>=12, camera looking straight down.
2752        let green_at = |depth: u32| -> u32 {
2753            let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
2754                if (4..4 + depth).contains(&z) {
2755                    Some(smoke)
2756                } else if z >= 12 {
2757                    Some(floor)
2758                } else {
2759                    None
2760                }
2761            });
2762            let grid = GridView::from_single_vxl(&vxl);
2763            let cam = Camera {
2764                pos: [8.0, 8.0, 0.0],
2765                right: [1.0, 0.0, 0.0],
2766                down: [0.0, 1.0, 0.0],
2767                forward: [0.0, 0.0, 1.0],
2768            };
2769            let (w, h) = (32u32, 32u32);
2770            let mut table = MaterialTable::new();
2771            table.set(1, Material::volumetric(80));
2772            let env = DdaEnv {
2773                materials: Some(&table),
2774                terrain_materials: &[(smoke.rgb_part(), 1)],
2775                lights: CpuLights::default(),
2776                ..DdaEnv::default()
2777            };
2778            let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
2779            (fb[(h / 2 * w + w / 2) as usize] >> 8) & 0xff
2780        };
2781        let shallow = green_at(1);
2782        let deep = green_at(7);
2783        assert!(
2784            deep > shallow,
2785            "deeper Volumetric smoke shows more of its grey (deep g={deep:02x} > shallow g={shallow:02x})"
2786        );
2787    }
2788
2789    /// DDA.5: distance fog blends a hit toward the fog colour. A far
2790    /// floor pixel is closer to the fog colour than a near one.
2791    #[test]
2792    fn distance_fog_blends_toward_fog_color() {
2793        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
2794            (z >= 40).then_some(VoxColor(0x80_FF_FF_FF))
2795        });
2796        let grid = GridView::from_single_vxl(&vxl);
2797        let cam = Camera {
2798            pos: [32.0, 2.0, 38.0],
2799            right: [1.0, 0.0, 0.0],
2800            down: [0.0, 0.0, 1.0],
2801            forward: [0.0, 1.0, 0.0],
2802        };
2803        let env = DdaEnv {
2804            sky: None,
2805            fog_color: 0x00_00_00_00, // black fog → distance darkens
2806            fog_max_dist: 64.0,
2807            side_shades: [0; 6],
2808            materials: None,
2809            terrain_materials: &[],
2810            lights: CpuLights::default(),
2811            world_shadow: None,
2812        };
2813        let (w, h) = (64u32, 64u32);
2814        let (fog, _) = render_brickmap_env(grid, &cam, w, h, &env);
2815        let (nofog, zb) = render_brickmap(grid, &cam, w, h);
2816        let (idx, depth) = zb.iter().enumerate().filter(|(_, z)| z.is_finite()).fold(
2817            (0usize, 0.0f32),
2818            |acc, (i, &z)| {
2819                if z > acc.1 {
2820                    (i, z)
2821                } else {
2822                    acc
2823                }
2824            },
2825        );
2826        assert!(depth > 20.0, "need a deep pixel to test fog (got {depth})");
2827        let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
2828        assert!(
2829            lum(fog[idx]) < lum(nofog[idx]),
2830            "fogged pixel {:08x} not darker than {:08x}",
2831            fog[idx],
2832            nofog[idx]
2833        );
2834    }
2835
2836    /// DDA.5: with a textured sky, miss pixels are filled from the sky
2837    /// panorama (direction-dependent) instead of left at the pre-fill.
2838    #[test]
2839    fn textured_sky_fills_misses() {
2840        let sky = crate::sky::Sky::blue_gradient();
2841        let vxl = roxlap_formats::vxl::Vxl::empty(32); // all air → all miss
2842        let grid = GridView::from_single_vxl(&vxl);
2843        let env = DdaEnv {
2844            sky: Some(&sky),
2845            fog_color: 0,
2846            fog_max_dist: 0.0,
2847            side_shades: [0; 6],
2848            materials: None,
2849            terrain_materials: &[],
2850            lights: CpuLights::default(),
2851            world_shadow: None,
2852        };
2853        let cam = Camera::from_yaw_pitch([16.0, 16.0, 128.0], 0.3, -0.4);
2854        let (w, h) = (48u32, 48u32);
2855        let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
2856        assert!(fb.iter().all(|&c| c >> 24 == 0x80), "all misses sky-filled");
2857        let top = fb[0];
2858        let bottom = fb[(h - 1) as usize * w as usize];
2859        assert_ne!(top, bottom, "sky gradient should vary with elevation");
2860    }
2861
2862    /// Sky elevation orientation matches the GPU `sky_color` (acos(-z)/π):
2863    /// looking **up** (−z) samples panorama column 0 (zenith), looking
2864    /// **down** (+z) samples the last column (nadir). Regression for the
2865    /// CPU up/down inversion.
2866    #[test]
2867    fn sky_elevation_zenith_at_column_zero() {
2868        let mut pixels = vec![0i32; 8];
2869        pixels[0] = 0x0011_1111; // zenith marker
2870        pixels[7] = 0x0099_9999; // nadir marker
2871        let sky = crate::sky::Sky::from_pixels(pixels, 8, 1);
2872        let up = sample_sky(&sky, [0.0, 0.0, -1.0]); // −z is up
2873        let down = sample_sky(&sky, [0.0, 0.0, 1.0]); // +z is down
2874        assert_eq!(
2875            up & 0x00ff_ffff,
2876            0x0011_1111,
2877            "looking up → column 0 (zenith)"
2878        );
2879        assert_eq!(
2880            down & 0x00ff_ffff,
2881            0x0099_9999,
2882            "looking down → last column (nadir)"
2883        );
2884    }
2885
2886    /// `render_sky_fill` paints the panorama for a **gridless** view — the
2887    /// same per-pixel sky sample the miss-ray path uses, with no grid present
2888    /// (the CPU empty-scene background, matching the GPU).
2889    #[test]
2890    fn sky_fill_paints_panorama_gridless() {
2891        let sky = crate::sky::Sky::blue_gradient();
2892        let cam = Camera::from_yaw_pitch([0.0, 0.0, 0.0], 0.3, -0.4);
2893        let (w, h) = (48u32, 48u32);
2894        let cs = crate::camera_math::derive(&cam, w, h, 24.0, 24.0, 24.0);
2895        let settings = crate::opticast::OpticastSettings::for_oracle_framebuffer(w, h);
2896        let mut fb = vec![0u32; (w * h) as usize];
2897        // All-background z-buffer (+∞) → every pixel gets the sky.
2898        let zb = vec![f32::INFINITY; (w * h) as usize];
2899        render_sky_fill(&mut fb, &zb, w as usize, w, h, &cs, &settings, &sky);
2900        assert!(
2901            fb.iter().all(|&c| c >> 24 == 0x80),
2902            "every pixel sky-filled with the brightness byte set"
2903        );
2904        let top = fb[0];
2905        let bottom = fb[(h - 1) as usize * w as usize];
2906        assert_ne!(top, bottom, "sky gradient should vary with elevation");
2907        // A finite-z (terrain) pixel is left untouched.
2908        let mut fb2 = vec![0x1234_5678u32; (w * h) as usize];
2909        let mut zb2 = vec![f32::INFINITY; (w * h) as usize];
2910        zb2[0] = 10.0; // pretend a terrain hit at pixel 0
2911        render_sky_fill(&mut fb2, &zb2, w as usize, w, h, &cs, &settings, &sky);
2912        assert_eq!(fb2[0], 0x1234_5678, "finite-z pixel is not overwritten");
2913    }
2914
2915    /// DDA.5: side shading darkens the hit face by its `side_shades`
2916    /// entry. A top-facing floor (ray crosses +z to enter) gets the
2917    /// `z-` face reduction (index 4).
2918    #[test]
2919    fn side_shades_darken_hit_face() {
2920        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
2921            (z >= 8).then_some(VoxColor(0x80_FF_FF_FF))
2922        });
2923        let grid = GridView::from_single_vxl(&vxl);
2924        let cam = Camera {
2925            pos: [8.0, 8.0, 2.0],
2926            right: [1.0, 0.0, 0.0],
2927            down: [0.0, 1.0, 0.0],
2928            forward: [0.0, 0.0, 1.0],
2929        };
2930        let centre = 16 * 32 + 16;
2931        let (plain, _) = render_brickmap(grid, &cam, 32, 32);
2932        let env = DdaEnv {
2933            sky: None,
2934            fog_color: 0,
2935            fog_max_dist: 0.0,
2936            side_shades: [0, 0, 0, 0, 0x40, 0],
2937            materials: None,
2938            terrain_materials: &[],
2939            lights: CpuLights::default(),
2940            world_shadow: None,
2941        };
2942        let (shaded, _) = render_brickmap_env(grid, &cam, 32, 32, &env);
2943        let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
2944        assert!(
2945            lum(shaded[centre]) < lum(plain[centre]),
2946            "side-shaded face {:08x} not darker than {:08x}",
2947            shaded[centre],
2948            plain[centre]
2949        );
2950    }
2951
2952    /// The two-level brick-skip cast closely approximates the dense
2953    /// per-voxel reference. The outer brick DDA re-seeds the inner cell
2954    /// walk at each occupied brick, so a few silhouette-boundary pixels
2955    /// jitter by one voxel (different hit cell → different colour/depth)
2956    /// — visually invisible, and the gain is ~`BRICK`× fewer air steps.
2957    /// Assert the divergence is tiny: coverage (hit/sky mask) is nearly
2958    /// identical and only a small fraction of pixels differ. (The
2959    /// thread-invariance guarantee is the separate, exact
2960    /// `parallel_matches_sequential`.)
2961    #[test]
2962    fn brickmap_approximates_dense_reference() {
2963        // Rolling heightmap + a floating block (air above and below).
2964        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
2965            let surf = 30 + ((x / 5 + y / 7) % 11);
2966            let ground = z >= surf;
2967            let block = (20..=24).contains(&z) && (10..20).contains(&x) && (40..50).contains(&y);
2968            (ground || block).then_some(VoxColor(0x80_30_50_70 + (x ^ y) % 0x40))
2969        });
2970        let grid = GridView::from_single_vxl(&vxl);
2971
2972        let (w, h) = (80u32, 80u32);
2973        let poses = [
2974            Camera::orbit(0.6, 0.5, 90.0, [32.0, 32.0, 40.0]),
2975            Camera::orbit(2.1, 0.2, 70.0, [32.0, 32.0, 35.0]),
2976            Camera::orbit(-1.0, 0.9, 120.0, [32.0, 32.0, 45.0]),
2977        ];
2978        let n = (w * h) as usize;
2979        for (i, cam) in poses.iter().enumerate() {
2980            let (fb_b, zb_b) = render_brickmap(grid, cam, w, h);
2981            let (fb_r, _zb_r) = render_reference(grid, cam, w, h);
2982            // Coverage (hit vs sky) must match almost exactly.
2983            let cov_b = fb_b.iter().filter(|&&c| c != 0).count();
2984            let cov_r = fb_r.iter().filter(|&&c| c != 0).count();
2985            assert!(cov_b > 200, "pose {i} rendered ~empty (cov {cov_b})");
2986            let cov_diff = cov_b.abs_diff(cov_r);
2987            assert!(
2988                cov_diff * 100 <= n, // < 1 % of pixels flip hit↔sky
2989                "pose {i} coverage diverged: brick {cov_b} vs dense {cov_r}"
2990            );
2991            // Colour diffs (boundary-voxel jitter) must be a small slice.
2992            let diffs = fb_b.iter().zip(&fb_r).filter(|(a, b)| a != b).count();
2993            assert!(
2994                diffs * 100 <= n * 3, // < 3 % of pixels differ
2995                "pose {i} too many pixel diffs vs dense: {diffs}/{n}"
2996            );
2997            // Depth must be sane (finite where hit), not wildly off.
2998            for k in 0..n {
2999                if fb_b[k] != 0 {
3000                    assert!(zb_b[k].is_finite(), "pose {i} px {k} non-finite depth");
3001                }
3002            }
3003        }
3004    }
3005
3006    /// DDA.5: a voxel's baked brightness byte darkens its colour. A
3007    /// half-bright voxel (`a = 0x40`) renders at roughly half RGB; a
3008    /// full-bright one (`a = 0x80`) is unchanged.
3009    #[test]
3010    fn baked_brightness_darkens_color() {
3011        // Half brightness: alpha 0x40 (64/128). White RGB → ~mid grey.
3012        let dim = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3013            (z >= 8).then_some(VoxColor(0x40_FF_FF_FF))
3014        });
3015        let grid = GridView::from_single_vxl(&dim);
3016        let cam = Camera {
3017            pos: [8.0, 8.0, 2.0],
3018            right: [1.0, 0.0, 0.0],
3019            down: [0.0, 1.0, 0.0],
3020            forward: [0.0, 0.0, 1.0],
3021        };
3022        let (fb, _) = render_brickmap(grid, &cam, 32, 32);
3023        let centre = 16 * 32 + 16;
3024        // 0xFF * 64 >> 7 = 127 per channel; alpha normalised to 0x80.
3025        assert_eq!(fb[centre], 0x80_7F_7F_7F, "got {:08x}", fb[centre]);
3026
3027        // Full brightness passes RGB through unchanged.
3028        let full = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3029            (z >= 8).then_some(VoxColor(0x80_FF_FF_FF))
3030        });
3031        let gridf = GridView::from_single_vxl(&full);
3032        let (fbf, _) = render_brickmap(gridf, &cam, 32, 32);
3033        assert_eq!(fbf[centre], 0x80_FF_FF_FF, "got {:08x}", fbf[centre]);
3034    }
3035
3036    /// DDA.4 headline gate: cross-chunk look-down. A camera in an
3037    /// all-air upper chunk (chz=0) looking straight down must see the
3038    /// floor in the *lower* stacked chunk (chz=1), through the chunk-Z
3039    /// boundary. This is exactly the case the voxlap renderer needed the
3040    /// whole virtual-column stack (S4B.6.j / VC) for; the DDA gets it
3041    /// for free from the outer box spanning `chunks_z`.
3042    #[test]
3043    fn cross_chunk_lookdown_sees_lower_stacked_floor() {
3044        const FLOOR_LOCAL_Z: u32 = 40;
3045        const FLOOR_COL: VoxColor = VoxColor(0x80_22_88_44);
3046        let upper = roxlap_formats::vxl::Vxl::empty(32); // all air + bedrock
3047        let lower = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
3048            (z >= FLOOR_LOCAL_Z).then_some(FLOOR_COL)
3049        });
3050        let v_up = GridView::from_single_vxl(&upper);
3051        let v_lo = GridView::from_single_vxl(&lower);
3052        // Z-stack: index (dz*chunks_y+dy)*chunks_x+dx → [upper, lower].
3053        let chunks = [Some(v_up), Some(v_lo)];
3054        let cg = crate::ChunkGrid {
3055            chunks: &chunks,
3056            origin_chunk_xy: [0, 0],
3057            origin_chunk_z: 0,
3058            chunks_x: 1,
3059            chunks_y: 1,
3060            chunks_z: 2,
3061        };
3062        let grid = GridView::from_chunk_grid(&cg, 32);
3063
3064        // Camera in the upper chunk (world z=100), looking straight down.
3065        let cam = Camera {
3066            pos: [16.0, 16.0, 100.0],
3067            right: [1.0, 0.0, 0.0],
3068            down: [0.0, 1.0, 0.0],
3069            forward: [0.0, 0.0, 1.0],
3070        };
3071        let (w, h) = (48u32, 48u32);
3072        let (fb, zb) = render_brickmap(grid, &cam, w, h);
3073        let centre = 24 * 48 + 24;
3074        assert!(
3075            fb[centre] & 0x00ff_ffff == FLOOR_COL.0 & 0x00ff_ffff,
3076            "centre ray must reach the lower-chunk floor (got {:08x})",
3077            fb[centre]
3078        );
3079        // Floor world-z = 256 + 40 = 296; camera z = 100 → depth ≈ 196.
3080        let expected = 296.0 - 100.0;
3081        assert!(
3082            (zb[centre] - expected).abs() < 2.0,
3083            "look-down depth {} not ≈ {expected}",
3084            zb[centre]
3085        );
3086    }
3087
3088    /// DDA.4: a floor spanning two side-by-side chunks (chunks_x=2)
3089    /// renders continuously across the chunk-XY seam — hits on both
3090    /// sides, no gap column.
3091    #[test]
3092    fn cross_chunk_xy_floor_is_seamless() {
3093        let mk = || {
3094            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
3095                (z >= 20).then_some(VoxColor(0x80_50_50_50))
3096            })
3097        };
3098        let (c0, c1) = (mk(), mk());
3099        let v0 = GridView::from_single_vxl(&c0);
3100        let v1 = GridView::from_single_vxl(&c1);
3101        let chunks = [Some(v0), Some(v1)];
3102        let cg = crate::ChunkGrid {
3103            chunks: &chunks,
3104            origin_chunk_xy: [0, 0],
3105            origin_chunk_z: 0,
3106            chunks_x: 2,
3107            chunks_y: 1,
3108            chunks_z: 1,
3109        };
3110        let grid = GridView::from_chunk_grid(&cg, 32);
3111
3112        // High above the seam (x=32), looking straight down.
3113        let cam = Camera {
3114            pos: [32.0, 16.0, 4.0],
3115            right: [1.0, 0.0, 0.0],
3116            down: [0.0, 1.0, 0.0],
3117            forward: [0.0, 0.0, 1.0],
3118        };
3119        let (w, h) = (64u32, 64u32);
3120        let mask = render_mask(grid, &cam, w, h);
3121        // Both the left chunk (screen left) and right chunk (screen
3122        // right) must show floor on the centre row.
3123        let row = (h / 2) as usize * w as usize;
3124        let left = (0..w as usize / 2).filter(|&x| mask[row + x]).count();
3125        let right = (w as usize / 2..w as usize)
3126            .filter(|&x| mask[row + x])
3127            .count();
3128        assert!(
3129            left > 5 && right > 5,
3130            "seam not continuous: left={left} right={right}"
3131        );
3132    }
3133
3134    /// Render `grid` from `camera` at render `mip` and return the hit
3135    /// mask.
3136    fn render_mask_mip(grid: GridView<'_>, camera: &Camera, w: u32, h: u32, mip: u32) -> Vec<bool> {
3137        let n = (w as usize) * (h as usize);
3138        let mut fb = vec![0u32; n];
3139        let mut zb = vec![f32::INFINITY; n];
3140        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
3141        {
3142            let mut sink = RasterSink::new(&mut fb, &mut zb);
3143            render_dda(
3144                camera,
3145                &settings,
3146                grid,
3147                w as usize,
3148                &DdaEnv::default(),
3149                mip,
3150                &mut sink,
3151            );
3152        }
3153        fb.iter().map(|&c| c != 0).collect()
3154    }
3155
3156    /// DDA.6: rendering a mip-built grid at a coarse mip stays complete
3157    /// (hole-free silhouette) with roughly the same screen coverage as
3158    /// mip 0 — LOD coarsens detail, it doesn't punch holes or shrink the
3159    /// shape. (DDA has no axis-aligned mip beam — the artifact is
3160    /// structurally impossible with honest per-cell traversal.)
3161    #[test]
3162    fn mip_render_is_coarse_but_complete() {
3163        let mut vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
3164            let surf = 24 + ((x / 3 + y / 5) % 17);
3165            (z >= surf).then_some(VoxColor(0x80_50_70_90))
3166        });
3167        vxl.generate_mips(4);
3168        assert!(vxl.mip_count() >= 3, "need mips built for this test");
3169        let grid = GridView::from_single_vxl(&vxl);
3170        let (w, h) = (96u32, 96u32);
3171        let cam = Camera::orbit(0.7, 0.6, 110.0, [32.0, 32.0, 36.0]);
3172
3173        let m0 = render_mask_mip(grid, &cam, w, h, 0);
3174        let m2 = render_mask_mip(grid, &cam, w, h, 2);
3175
3176        let c0 = m0.iter().filter(|&&b| b).count();
3177        let c2 = m2.iter().filter(|&&b| b).count();
3178        assert!(c0 > 200 && c2 > 200, "both mips visible (c0={c0} c2={c2})");
3179        // Coverage within ~30 % — a coarse-mip silhouette closely tracks
3180        // the fine one (LOD coarsens detail, it doesn't lose the shape).
3181        // (Terrain silhouettes are non-convex — sky shows through
3182        // valleys — so a hole-free invariant doesn't apply here; that's
3183        // the convex single-voxel test's job.)
3184        let ratio = c2 as f32 / c0 as f32;
3185        assert!(
3186            (0.7..1.4).contains(&ratio),
3187            "mip-2 coverage {c2} vs mip-0 {c0} (ratio {ratio:.2}) diverged"
3188        );
3189    }
3190
3191    /// Headless perf bench (run: `cargo test -p roxlap-core --release
3192    /// dda::tests::bench_terrain -- --ignored --nocapture`). Single-
3193    /// thread `render_dda` over a hilly chunk at a horizon pose; prints
3194    /// ms/frame + per-frame traversal counters (cells / bricks /
3195    /// surface_color calls) to locate the bottleneck.
3196    #[test]
3197    #[ignore = "perf benchmark — run explicitly with --ignored"]
3198    fn bench_terrain() {
3199        use std::time::Instant;
3200        // Multi-chunk grid like the demo: NC×NC chunks of 128, hills.
3201        const NC: i32 = 6;
3202        let cs = crate::grid_view::CHUNK_SIZE_Z; // 256, but vsid is 128
3203        let _ = cs;
3204        let mut vxls: Vec<roxlap_formats::vxl::Vxl> = Vec::new();
3205        for cy in 0..NC {
3206            for cx in 0..NC {
3207                let (ox, oy) = (cx * 128, cy * 128);
3208                let mut v = roxlap_formats::vxl::Vxl::from_dense(128, |x, y, z| {
3209                    let (gx, gy) = (ox + x as i32, oy + y as i32);
3210                    let surf = 90 + ((gx / 7 + gy / 9).rem_euclid(40)) + ((gx / 23).rem_euclid(20));
3211                    (z as i32 >= surf).then_some(VoxColor(0x80_50_70_90 + (x ^ y) % 0x30))
3212                });
3213                v.generate_mips(4);
3214                vxls.push(v);
3215            }
3216        }
3217        let views: Vec<Option<GridView>> = vxls
3218            .iter()
3219            .map(|v| Some(GridView::from_single_vxl(v)))
3220            .collect();
3221        let cg = crate::ChunkGrid {
3222            chunks: &views,
3223            origin_chunk_xy: [0, 0],
3224            origin_chunk_z: 0,
3225            chunks_x: NC as u32,
3226            chunks_y: NC as u32,
3227            chunks_z: 1,
3228        };
3229        let grid = GridView::from_chunk_grid(&cg, 128);
3230
3231        let (w, h) = (960u32, 600u32);
3232        let mut settings = OpticastSettings::for_oracle_framebuffer(w, h);
3233        settings.max_scan_dist = 512;
3234        let n = (w * h) as usize;
3235        let mut fb = vec![0u32; n];
3236        let mut zb = vec![f32::INFINITY; n];
3237        let centre = [f64::from(NC * 128) / 2.0, f64::from(NC * 128) / 2.0, 60.0];
3238
3239        // Two poses: eye-level toward horizon (long rays) + looking down
3240        // at nearby terrain (short rays, demo-typical).
3241        let poses = [
3242            (
3243                "horizon",
3244                Camera::from_yaw_pitch([20.0, 20.0, 40.0], 0.6, 0.15),
3245            ),
3246            ("down", Camera::orbit(0.7, 1.0, 130.0, centre)),
3247        ];
3248        for (name, cam) in poses {
3249            {
3250                let mut sink = RasterSink::new(&mut fb, &mut zb);
3251                prof::reset();
3252                render_dda(
3253                    &cam,
3254                    &settings,
3255                    grid,
3256                    w as usize,
3257                    &DdaEnv::default(),
3258                    0,
3259                    &mut sink,
3260                );
3261            }
3262            let (cells, bricks, surf) = prof::read();
3263            let iters = 6;
3264            let t0 = Instant::now();
3265            for _ in 0..iters {
3266                let mut sink = RasterSink::new(&mut fb, &mut zb);
3267                render_dda(
3268                    &cam,
3269                    &settings,
3270                    grid,
3271                    w as usize,
3272                    &DdaEnv::default(),
3273                    0,
3274                    &mut sink,
3275                );
3276            }
3277            let ms = t0.elapsed().as_secs_f64() * 1000.0 / f64::from(iters);
3278            let hits = fb.iter().filter(|&&c| c != 0).count();
3279            eprintln!(
3280                "[{name}] {w}x{h} 1-thread: {ms:.1} ms | hits={hits}/{n} | per-px: cells={:.1} bricks={:.1} surf={:.1}",
3281                cells as f64 / n as f64,
3282                bricks as f64 / n as f64,
3283                surf as f64 / n as f64,
3284            );
3285        }
3286    }
3287
3288    /// DDA.7: the tile-parallel driver is bit-identical to the
3289    /// sequential one — DDA pixels are independent, so banding can't
3290    /// change a pixel.
3291    #[test]
3292    fn parallel_matches_sequential() {
3293        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
3294            let surf = 28 + ((x / 4 + y / 6) % 13);
3295            (z >= surf).then_some(VoxColor(0x80_40_60_80 + (x ^ y) % 0x30))
3296        });
3297        let grid = GridView::from_single_vxl(&vxl);
3298        let (w, h) = (96u32, 96u32);
3299        let cam = Camera::orbit(0.8, 0.55, 100.0, [32.0, 32.0, 40.0]);
3300        let env = DdaEnv {
3301            sky: None,
3302            fog_color: 0x00_20_30_40,
3303            fog_max_dist: 120.0,
3304            side_shades: [0, 0, 0, 0, 0x30, 0x10],
3305            materials: None,
3306            terrain_materials: &[],
3307            lights: CpuLights::default(),
3308            world_shadow: None,
3309        };
3310
3311        let (seq_fb, seq_zb) = render_brickmap_env(grid, &cam, w, h, &env);
3312
3313        let n = (w * h) as usize;
3314        let mut par_fb = vec![0u32; n];
3315        let mut par_zb = vec![f32::INFINITY; n];
3316        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
3317        let (cache, mip) = local_cache(&grid, 0);
3318        render_dda_parallel(
3319            &cam,
3320            &settings,
3321            grid,
3322            &mut par_fb,
3323            &mut par_zb,
3324            w as usize,
3325            &env,
3326            &cache,
3327            mip,
3328        );
3329        assert!(par_fb == seq_fb, "parallel colour differs from sequential");
3330        assert!(
3331            par_zb
3332                .iter()
3333                .zip(&seq_zb)
3334                .all(|(a, b)| a.to_bits() == b.to_bits()),
3335            "parallel depth differs from sequential"
3336        );
3337    }
3338
3339    /// DDA.2 correctness: a heightmap column's interior is solid even
3340    /// though voxlap only stores a colour for its surface. `voxel_color`
3341    /// returns `None` for an interior voxel, but `surface_color` must
3342    /// return the run's surface colour — otherwise oblique rays striking
3343    /// a cliff *side* would pass straight through (see-through terrain).
3344    #[test]
3345    fn cliff_side_is_solid_not_see_through() {
3346        const TOP_Z: u32 = 50;
3347        const COL: VoxColor = VoxColor(0x80_77_88_99);
3348        let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |_, _, z| (z >= TOP_Z).then_some(COL));
3349        let grid = GridView::from_single_vxl(&vxl);
3350
3351        // Surface voxel: coloured directly.
3352        assert_eq!(grid.voxel_color(4, 4, TOP_Z), Some(COL));
3353        // Interior voxel: voxlap stores no colour …
3354        assert_eq!(grid.voxel_color(4, 4, 150), None);
3355        // … but it is solid, and surface_color bleeds the run-top colour
3356        // down the cliff face → a real hit, not see-through.
3357        assert_eq!(grid.surface_color(4, 4, 150), Some(COL));
3358        // Bedrock-style air above the surface stays air.
3359        assert_eq!(grid.surface_color(4, 4, 10), None);
3360    }
3361
3362    /// DDA.2: a camera embedded in solid material hits its own voxel
3363    /// immediately — every ray reports a hit (no skip / no garbage).
3364    #[test]
3365    fn camera_inside_solid_hits_everywhere() {
3366        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, _| Some(VoxColor(0x80_55_55_55)));
3367        let grid = GridView::from_single_vxl(&vxl);
3368        let cam = Camera {
3369            pos: [8.0, 8.0, 128.0],
3370            right: [1.0, 0.0, 0.0],
3371            down: [0.0, 1.0, 0.0],
3372            forward: [0.0, 0.0, 1.0],
3373        };
3374        let (w, h) = (32u32, 32u32);
3375        let mask = render_mask(grid, &cam, w, h);
3376        assert!(
3377            mask.iter().all(|&b| b),
3378            "every ray must hit when the camera is inside solid"
3379        );
3380    }
3381
3382    /// Headline DDA.1 gate: a single solid voxel viewed obliquely
3383    /// projects to a convex silhouette with **no interior holes** —
3384    /// the artifact class (`tiny_grid_1x1x1` silhouette notch) the
3385    /// voxlap renderer cannot avoid. DDA casts independent per-pixel
3386    /// rays, so the silhouette is hole-free by construction.
3387    #[test]
3388    fn single_voxel_silhouette_has_no_notch() {
3389        const C: VoxColor = VoxColor(0x80_FF_80_40);
3390        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |x, y, z| {
3391            (x == 8 && y == 8 && z == 8).then_some(C)
3392        });
3393        let grid = GridView::from_single_vxl(&vxl);
3394
3395        // Orbit the voxel centre obliquely so all three faces show and
3396        // the silhouette is a sizeable hexagon (dist 4 → ~12 px wide).
3397        let cam = Camera::orbit(0.7, 0.6, 4.0, [8.5, 8.5, 8.5]);
3398        let (w, h) = (96u32, 96u32);
3399        let mask = render_mask(grid, &cam, w, h);
3400
3401        let hits = mask.iter().filter(|&&b| b).count();
3402        assert!(
3403            hits > 30,
3404            "silhouette too small to be meaningful: {hits} px"
3405        );
3406        assert!(
3407            rows_have_no_holes(&mask, w, h),
3408            "row-interior gap in single-voxel silhouette (notch)"
3409        );
3410        assert!(
3411            cols_have_no_holes(&mask, w, h),
3412            "column-interior gap in single-voxel silhouette (notch)"
3413        );
3414    }
3415}