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