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}
74
75impl Default for DdaEnv<'_> {
76    fn default() -> Self {
77        Self {
78            sky: None,
79            fog_color: 0,
80            fog_max_dist: 0.0,
81            side_shades: [0; 6],
82            materials: None,
83            terrain_materials: &[],
84        }
85    }
86}
87
88/// Per-pixel output target for the DDA renderer.
89///
90/// Abstracts "where does a ray hit go" so the traversal core stays
91/// free of framebuffer mechanics. The production impl is
92/// [`RasterSink`] (raw fb/zb pointers); tests use a recording sink.
93/// Only *hits* are reported — misses (sky) leave the destination
94/// untouched, matching the caller-pre-fills-sky convention.
95pub trait PixelSink {
96    /// Record a ray hit at framebuffer index `idx` (`py * pitch + px`)
97    /// with packed ARGB `color` and perpendicular `dist` (smaller =
98    /// closer).
99    fn put(&mut self, idx: usize, color: u32, dist: f32);
100}
101
102/// [`PixelSink`] over a borrowed `(framebuffer, zbuffer)` pair.
103///
104/// Wraps a [`RasterTarget`] so the DDA path writes through the same
105/// raw-pointer mechanism the scalar rasterizer uses — which keeps the
106/// door open for the same strip/tile-disjoint parallel writes in
107/// DDA.7.
108pub struct RasterSink<'a> {
109    target: RasterTarget<'a>,
110    len: usize,
111}
112
113impl<'a> RasterSink<'a> {
114    /// Build a sink from exclusive framebuffer + zbuffer borrows.
115    /// Both slices must have the same length (the pixel count).
116    #[must_use]
117    pub fn new(framebuffer: &'a mut [u32], zbuffer: &'a mut [f32]) -> Self {
118        debug_assert_eq!(framebuffer.len(), zbuffer.len());
119        let len = framebuffer.len();
120        Self {
121            target: RasterTarget::new(framebuffer, zbuffer),
122            len,
123        }
124    }
125}
126
127impl PixelSink for RasterSink<'_> {
128    fn put(&mut self, idx: usize, color: u32, dist: f32) {
129        if idx < self.len {
130            // SAFETY: bounds checked above; single-threaded writer in
131            // DDA.0 so the disjoint-write invariant holds trivially.
132            unsafe {
133                self.target.write_color(idx, color);
134                self.target.write_depth(idx, dist);
135            }
136        }
137    }
138}
139
140/// A resolved ray hit: surface colour + perpendicular distance.
141#[derive(Debug, Clone, Copy)]
142struct Hit {
143    color: u32,
144    dist: f32,
145}
146
147/// Test-only per-thread traversal counters for the perf bench.
148#[cfg(test)]
149pub(crate) mod prof {
150    use std::cell::Cell;
151    thread_local! {
152        pub static CELLS: Cell<u64> = const { Cell::new(0) };
153        pub static BRICKS: Cell<u64> = const { Cell::new(0) };
154        pub static SURF: Cell<u64> = const { Cell::new(0) };
155    }
156    pub fn reset() {
157        CELLS.with(|x| x.set(0));
158        BRICKS.with(|x| x.set(0));
159        SURF.with(|x| x.set(0));
160    }
161    pub fn read() -> (u64, u64, u64) {
162        (
163            CELLS.with(Cell::get),
164            BRICKS.with(Cell::get),
165            SURF.with(Cell::get),
166        )
167    }
168}
169
170/// Apply the voxel's baked directional brightness (Substage DDA.5).
171///
172/// Voxlap (and the GPU marcher, `grid_dda.wgsl`) store per-voxel
173/// brightness in the colour's high byte on a `0..128` scale — `0x80`
174/// is full brightness — written by `Grid::bake_lightmode` (estnorm
175/// directional shading). The shaded channel is `c · a / 128`, so the
176/// DDA matches the GPU look; an unbaked / full-bright voxel (`a =
177/// 0x80`) passes through unchanged. Output alpha is normalised to
178/// `0x80` (the standard "lit" flag; the present blit ignores it).
179///
180/// The renderer only *reads* the baked byte — it computes no normals
181/// itself, so per-impact relight is free (re-bake the chunk and the
182/// byte updates). The estnorm bake that produces the byte is the
183/// voxlap-derived piece slated for a clean-room rewrite in DDA.10.
184///
185/// `bright_sub` is the per-face `side_shades` reduction (DDA.5): voxlap
186/// subtracts it from the brightness byte before the multiply, so a
187/// shaded face is uniformly darker. `0` = no side shading.
188#[inline]
189pub(crate) fn shade(color: u32, bright_sub: u32) -> u32 {
190    let a = ((color >> 24) & 0xff).saturating_sub(bright_sub);
191    let ch = |shift: u32| -> u32 { ((((color >> shift) & 0xff) * a) >> 7).min(255) };
192    0x8000_0000 | (ch(16) << 16) | (ch(8) << 8) | ch(0)
193}
194
195/// Blend `color` toward `env.fog_color` by perpendicular `depth`
196/// (linear, fully fogged at `env.fog_max_dist`). No-op when fog is
197/// disabled (`fog_max_dist <= 0`).
198#[inline]
199fn apply_fog(color: u32, depth: f32, env: &DdaEnv<'_>) -> u32 {
200    if env.fog_max_dist <= 0.0 {
201        return color;
202    }
203    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
204    let f = ((depth / env.fog_max_dist).clamp(0.0, 1.0) * 256.0) as u32; // 0..256
205    let g = 256 - f;
206    let fog = env.fog_color;
207    let mix = |shift: u32| -> u32 {
208        let src = (color >> shift) & 0xff;
209        let dst = (fog >> shift) & 0xff;
210        ((src * g + dst * f) >> 8).min(255)
211    };
212    0x8000_0000 | (mix(16) << 16) | (mix(8) << 8) | mix(0)
213}
214
215/// TV: resolve a terrain voxel's [`Material`] from its colour via the env's
216/// colour→material map + palette. Returns [`Material::OPAQUE`] when no
217/// material table is set, the map is empty, or the colour is unmapped — so
218/// the march stays on the opaque first-hit path.
219#[inline]
220fn terrain_material(env: &DdaEnv<'_>, color: u32) -> Material {
221    match env.materials {
222        Some(table) if !env.terrain_materials.is_empty() => {
223            table.get(material_for_color(env.terrain_materials, color))
224        }
225        _ => Material::OPAQUE,
226    }
227}
228
229/// Composite premultiplied `accum` (+ remaining `trans`) over a packed
230/// background colour → packed `0x80RRGGBB`.
231#[inline]
232fn composite_over(accum: [f32; 3], trans: f32, bg: u32) -> u32 {
233    let b = rgb_to_f32(bg);
234    f32_to_rgb([
235        accum[0] + trans * b[0],
236        accum[1] + trans * b[1],
237        accum[2] + trans * b[2],
238    ])
239}
240
241/// Finalize a translucent terrain ray that exited the grid (sky). Returns
242/// `None` when nothing was accumulated (the opaque first-hit path — the
243/// caller's sky handling stands, bit-identical), else the accumulated
244/// layers composited over the sky at `dist`.
245#[inline]
246fn finalize_exit(
247    touched: bool,
248    accum: [f32; 3],
249    trans: f32,
250    env: &DdaEnv<'_>,
251    dir: [f32; 3],
252    dist: f32,
253) -> Option<Hit> {
254    if !touched {
255        return None;
256    }
257    let bg = match env.sky {
258        Some(s) => sample_sky(s, dir),
259        None => 0x8000_0000 | (env.fog_color & 0x00ff_ffff),
260    };
261    Some(Hit {
262        color: composite_over(accum, trans, bg),
263        dist,
264    })
265}
266
267/// Unpack `0x__RRGGBB` to `0..1` float channels (RGB; the high byte is
268/// dropped — it has already been folded into the colour by `shade`/`fog`).
269#[inline]
270#[allow(clippy::cast_precision_loss)]
271fn rgb_to_f32(c: u32) -> [f32; 3] {
272    [
273        ((c >> 16) & 0xff) as f32 / 255.0,
274        ((c >> 8) & 0xff) as f32 / 255.0,
275        (c & 0xff) as f32 / 255.0,
276    ]
277}
278
279/// Repack `0..1` float channels (clamped) into `0x80RRGGBB`.
280#[inline]
281#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
282fn f32_to_rgb(c: [f32; 3]) -> u32 {
283    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
284    0x8000_0000 | (q(c[0]) << 16) | (q(c[1]) << 8) | q(c[2])
285}
286
287/// Sample the sky panorama in ray direction `dir` (need not be
288/// normalised), returning a packed `0x80RRGGBB` colour.
289///
290/// Clean-room equirectangular mapping (not voxlap's `lng`/`lat` asm
291/// search): the texture's x axis is elevation (`asin` of the vertical
292/// component), the y axis is azimuth (`atan2` around the vertical). A
293/// `ysiz == 1` panorama (e.g. [`Sky::blue_gradient`]) is a pure
294/// horizon→zenith gradient.
295#[allow(
296    clippy::cast_possible_truncation,
297    clippy::cast_sign_loss,
298    clippy::cast_precision_loss
299)]
300fn sample_sky(sky: &Sky, dir: [f32; 3]) -> u32 {
301    let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
302    if len < 1e-9 {
303        return 0x8000_0000;
304    }
305    let d = [dir[0] / len, dir[1] / len, dir[2] / len];
306    let xsiz_full = sky.lat.len().max(1) as i32; // original column count
307    let pi = std::f32::consts::PI;
308    // Elevation → x, matching the GPU `sky_color` (scene_dda.wgsl): z is
309    // down, so `acos(-z)` is 0 at the zenith (looking up) and π at the nadir
310    // (looking down); `/π` puts the zenith at x=0 and the nadir at x=xsiz.
311    let elev01 = (-d[2]).clamp(-1.0, 1.0).acos() / pi; // 0 (up) .. 1 (down)
312    let x = (elev01 * xsiz_full as f32) as i32;
313    let x = x.clamp(0, xsiz_full - 1);
314    // Azimuth → y (wrapped).
315    let y = if sky.ysiz <= 1 {
316        0
317    } else {
318        let az = d[1].atan2(d[0]); // -pi..pi
319        let yf = ((az / (pi * 2.0)) + 0.5) * sky.ysiz as f32;
320        (yf as i32).rem_euclid(sky.ysiz)
321    };
322    let idx = (y * xsiz_full + x) as usize;
323    let px = sky.pixels.get(idx).copied().unwrap_or(0) as u32;
324    0x8000_0000 | (px & 0x00ff_ffff)
325}
326
327/// Fill the panorama [`Sky`] into every **background** pixel — one whose
328/// z-buffer entry is still `+∞` (no grid/terrain hit). The per-grid DDA only
329/// samples the sky inside each grid's screen rect (and only its sky-owning
330/// grid); pixels outside any grid — most of a sprite/effect-only view, or the
331/// margins around a small world grid — would otherwise keep the caller's flat
332/// clear colour. This paints the real panorama there while leaving terrain
333/// (finite z) and composited translucent pixels untouched. The z-buffer is
334/// not modified. `cam`/`settings` are the same per-frame projection the
335/// renderer used.
336#[allow(clippy::cast_possible_truncation)]
337pub fn render_sky_fill(
338    fb: &mut [u32],
339    zb: &[f32],
340    pitch_pixels: usize,
341    width: u32,
342    height: u32,
343    cam: &CameraState,
344    settings: &OpticastSettings,
345    sky: &Sky,
346) {
347    for py in 0..height {
348        let row = py as usize * pitch_pixels;
349        for px in 0..width {
350            let idx = row + px as usize;
351            if zb[idx].is_finite() {
352                continue; // a grid/terrain hit owns this pixel
353            }
354            let (_origin, dir) = pixel_ray(cam, settings, px, py);
355            fb[idx] = sample_sky(sky, dir);
356        }
357    }
358}
359
360/// World-space ray for screen pixel `(px, py)` under opticast's
361/// pinhole: origin is the camera position, direction is
362/// `(px - hx)·right + (py - hy)·down + hz·forward`.
363///
364/// This is the exact ray `camera_math::derive` bakes into its corner
365/// vectors (`corn[0]` is `pixel (0, 0)`'s direction), so the DDA
366/// renderer samples the same rays the voxlap path's frustum is built
367/// from. The direction is **not** normalised — callers that need a
368/// unit ray (and a true Euclidean distance) normalise themselves;
369/// DDA.1 will track perpendicular distance via the forward-projection
370/// instead, matching the engine's z-buffer convention.
371#[must_use]
372pub fn pixel_ray(
373    cs: &CameraState,
374    settings: &OpticastSettings,
375    px: u32,
376    py: u32,
377) -> ([f32; 3], [f32; 3]) {
378    // u32 → f32 is exact for any realistic screen coordinate.
379    #[allow(clippy::cast_precision_loss)]
380    let sx = px as f32 - settings.hx;
381    #[allow(clippy::cast_precision_loss)]
382    let sy = py as f32 - settings.hy;
383    let dir = [
384        sx * cs.right[0] + sy * cs.down[0] + settings.hz * cs.forward[0],
385        sx * cs.right[1] + sy * cs.down[1] + settings.hz * cs.forward[1],
386        sx * cs.right[2] + sy * cs.down[2] + settings.hz * cs.forward[2],
387    ];
388    (cs.pos, dir)
389}
390
391/// Ray ↔ axis-aligned box `[lo, hi]` slab test. Returns the
392/// `(t_enter, t_exit)` parameter interval along `dir` (already clamped
393/// so `t_enter >= 0`, i.e. a camera inside the box starts at `t = 0`),
394/// or `None` if the ray misses the box. `dir` need not be normalised —
395/// `t` is in units of `|dir|`.
396pub(crate) fn intersect_aabb(
397    o: [f32; 3],
398    dir: [f32; 3],
399    lo: [f32; 3],
400    hi: [f32; 3],
401) -> Option<(f32, f32)> {
402    let mut t0 = 0.0f32;
403    let mut t1 = f32::INFINITY;
404    for a in 0..3 {
405        if dir[a].abs() < 1e-9 {
406            // Ray parallel to this slab — must already be inside it.
407            if o[a] < lo[a] || o[a] > hi[a] {
408                return None;
409            }
410        } else {
411            let inv = 1.0 / dir[a];
412            let mut ta = (lo[a] - o[a]) * inv;
413            let mut tb = (hi[a] - o[a]) * inv;
414            if ta > tb {
415                core::mem::swap(&mut ta, &mut tb);
416            }
417            t0 = t0.max(ta);
418            t1 = t1.min(tb);
419            if t0 > t1 {
420                return None;
421            }
422        }
423    }
424    Some((t0, t1))
425}
426
427/// Brick edge length in voxels — one occupancy bit per `BRICK³` block.
428const BRICK: i32 = 8;
429
430/// Per-chunk brick occupancy map for two-level DDA empty-space skip
431/// (Substage DDA.3).
432///
433/// One bit per `BRICK³` block of the active chunk, set iff any voxel in
434/// the block is solid. The ray steps the coarse brick grid (8× longer
435/// strides) and only descends into a per-voxel walk inside occupied
436/// bricks, so a ray through open air crosses ~`length / 8` empty bricks
437/// instead of `length` air voxels — each of which would otherwise walk
438/// the column slab chain via `surface_color`.
439///
440/// Built per frame from a [`GridView`] in [`render_dda`]. A persistent
441/// per-chunk cache with edit-driven invalidation (locked decision #2 in
442/// `PORTING-DDA.md`) is a later perf refinement.
443#[derive(Debug)]
444pub(crate) struct BrickMap {
445    /// Brick counts along x / y / z (one entry per `BRICK³` cells).
446    nb: [i32; 3],
447    /// Brick occupancy bitset; brick `(bx, by, bz)` is bit
448    /// `(bz * nb[1] + by) * nb[0] + bx`.
449    bits: Vec<u64>,
450    /// Super-brick counts (one entry per `BRICK³` *bricks* = `SUPER³`
451    /// cells), `ceil(nb / BRICK)`.
452    ns: [i32; 3],
453    /// Super-brick occupancy (DDA.7 perf): a coarse level so a ray
454    /// through open air above the terrain skips `SUPER` cells per outer
455    /// step instead of `BRICK`. A super-brick is set iff any child brick
456    /// is set.
457    super_bits: Vec<u64>,
458}
459
460/// Super-brick edge in cells (`BRICK` bricks per axis).
461const SUPER: i32 = BRICK * BRICK;
462
463impl BrickMap {
464    /// Scan every mip-`mip` column of `grid`, building brick + super-
465    /// brick occupancy. `mip` must be `< grid.mip_count()`.
466    #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
467    fn build(grid: &GridView<'_>, mip: u32) -> Self {
468        let vsid_m = (grid.vsid >> mip).max(1) as i32;
469        let z_m = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1) as i32;
470        let nb = [
471            (vsid_m + BRICK - 1) / BRICK,
472            (vsid_m + BRICK - 1) / BRICK,
473            (z_m + BRICK - 1) / BRICK,
474        ];
475        let ns = [
476            (nb[0] + BRICK - 1) / BRICK,
477            (nb[1] + BRICK - 1) / BRICK,
478            (nb[2] + BRICK - 1) / BRICK,
479        ];
480        let count = (nb[0] * nb[1] * nb[2]) as usize;
481        let scount = (ns[0] * ns[1] * ns[2]) as usize;
482        let mut bits = vec![0u64; count.div_ceil(64)];
483        let mut super_bits = vec![0u64; scount.div_ceil(64)];
484        for y in 0..vsid_m {
485            for x in 0..vsid_m {
486                let (bx, by) = (x / BRICK, y / BRICK);
487                grid.for_each_run_mip(x as u32, y as u32, mip, |top, bot| {
488                    for bz in (top / BRICK)..=((bot - 1) / BRICK) {
489                        let idx = ((bz * nb[1] + by) * nb[0] + bx) as usize;
490                        bits[idx / 64] |= 1u64 << (idx % 64);
491                        let sidx =
492                            (((bz / BRICK) * ns[1] + by / BRICK) * ns[0] + bx / BRICK) as usize;
493                        super_bits[sidx / 64] |= 1u64 << (sidx % 64);
494                    }
495                });
496            }
497        }
498        Self {
499            nb,
500            bits,
501            ns,
502            super_bits,
503        }
504    }
505
506    /// Whether brick `b` is in range and holds any solid voxel.
507    #[inline]
508    #[allow(clippy::cast_sign_loss)]
509    fn occupied(&self, b: [i32; 3]) -> bool {
510        if b[0] < 0
511            || b[0] >= self.nb[0]
512            || b[1] < 0
513            || b[1] >= self.nb[1]
514            || b[2] < 0
515            || b[2] >= self.nb[2]
516        {
517            return false;
518        }
519        let idx = ((b[2] * self.nb[1] + b[1]) * self.nb[0] + b[0]) as usize;
520        (self.bits[idx / 64] >> (idx % 64)) & 1 != 0
521    }
522
523    /// Whether super-brick `s` is in range and holds any solid voxel.
524    #[inline]
525    #[allow(clippy::cast_sign_loss)]
526    fn occupied_super(&self, s: [i32; 3]) -> bool {
527        if s[0] < 0
528            || s[0] >= self.ns[0]
529            || s[1] < 0
530            || s[1] >= self.ns[1]
531            || s[2] < 0
532            || s[2] >= self.ns[2]
533        {
534            return false;
535        }
536        let idx = ((s[2] * self.ns[1] + s[1]) * self.ns[0] + s[0]) as usize;
537        (self.super_bits[idx / 64] >> (idx % 64)) & 1 != 0
538    }
539}
540
541/// Per-axis 3D-DDA stepping state for a cell size of `cell` voxels.
542/// `t_max[a]` is the ray parameter at which the next `a`-boundary is
543/// crossed; `t_delta[a]` is the parameter increment per cell. An
544/// axis-parallel component gets `t_max = t_delta = +inf` so it's never
545/// chosen as the stepping axis.
546pub(crate) fn dda_setup(
547    origin: [f32; 3],
548    dir: [f32; 3],
549    cell: [i32; 3],
550    cell_size: f32,
551) -> ([i32; 3], [f32; 3], [f32; 3]) {
552    let mut step = [0i32; 3];
553    let mut t_max = [f32::INFINITY; 3];
554    let mut t_delta = [f32::INFINITY; 3];
555    for a in 0..3 {
556        if dir[a] > 1e-9 {
557            step[a] = 1;
558            #[allow(clippy::cast_precision_loss)]
559            let boundary = (cell[a] + 1) as f32 * cell_size;
560            t_max[a] = (boundary - origin[a]) / dir[a];
561            t_delta[a] = cell_size / dir[a];
562        } else if dir[a] < -1e-9 {
563            step[a] = -1;
564            #[allow(clippy::cast_precision_loss)]
565            let boundary = cell[a] as f32 * cell_size;
566            t_max[a] = (boundary - origin[a]) / dir[a];
567            t_delta[a] = -cell_size / dir[a];
568        }
569    }
570    (step, t_max, t_delta)
571}
572
573/// Index of the axis with the smallest `t_max` (the next boundary the
574/// ray crosses).
575#[inline]
576pub(crate) fn min_axis(t_max: [f32; 3]) -> usize {
577    if t_max[0] <= t_max[1] && t_max[0] <= t_max[2] {
578        0
579    } else if t_max[1] <= t_max[2] {
580        1
581    } else {
582        2
583    }
584}
585
586/// Persistent, cross-frame brick occupancy cache (Substage DDA.7
587/// perf). Keyed by `(chunk x, y, z, mip)` with the chunk's edit
588/// `version`; an entry is reused until its chunk's version changes, so a
589/// static / streamed-once world pays **zero** brick-build cost after the
590/// first frame (the per-frame rebuild was the dominant DDA cost).
591///
592/// Owned by the caller across frames (the scene's `Grid`), populated
593/// single-threaded via [`Self::ensure`], then borrowed immutably by the
594/// parallel render bands.
595#[derive(Debug, Default)]
596pub struct BrickCache {
597    maps: HashMap<(i32, i32, i32, u32), (u64, BrickMap)>,
598}
599
600impl BrickCache {
601    #[must_use]
602    pub fn new() -> Self {
603        Self::default()
604    }
605
606    /// Ensure a current mip-`mip` brick map exists for `chunk` (built
607    /// from `view`); rebuilds only when the cached `version` differs.
608    pub fn ensure(&mut self, chunk: [i32; 3], mip: u32, version: u64, view: &GridView<'_>) {
609        let key = (chunk[0], chunk[1], chunk[2], mip);
610        let stale = self.maps.get(&key).map_or(true, |(v, _)| *v != version);
611        if stale {
612            self.maps.insert(key, (version, BrickMap::build(view, mip)));
613        }
614    }
615
616    #[inline]
617    fn get(&self, chunk: [i32; 3], mip: u32) -> Option<&BrickMap> {
618        self.maps
619            .get(&(chunk[0], chunk[1], chunk[2], mip))
620            .map(|(_, m)| m)
621    }
622
623    /// Drop cached entries whose chunk fails `keep` — bounds memory as
624    /// streaming evicts chunks. Called once per frame by the scene.
625    pub fn retain_chunks(&mut self, keep: impl Fn([i32; 3]) -> bool) {
626        self.maps.retain(|k, _| keep([k.0, k.1, k.2]));
627    }
628}
629
630/// Build a throwaway [`BrickCache`] covering every populated chunk of
631/// `grid` at the effective mip — for the sequential [`render_dda`] /
632/// tests, where no persistent cache is threaded in. Returns
633/// `(cache, effective_mip)`.
634#[allow(clippy::cast_possible_wrap)]
635fn local_cache(grid: &GridView<'_>, requested_mip: u32) -> (BrickCache, u32) {
636    let mip = effective_mip(grid, requested_mip);
637    let mut cache = BrickCache::new();
638    if let Some(cg) = grid.chunk_grid {
639        for dz in 0..cg.chunks_z as i32 {
640            for dy in 0..cg.chunks_y as i32 {
641                for dx in 0..cg.chunks_x as i32 {
642                    let slot = ((dz * cg.chunks_y as i32 + dy) * cg.chunks_x as i32 + dx) as usize;
643                    if let Some(Some(view)) = cg.chunks.get(slot) {
644                        let ch = [
645                            cg.origin_chunk_xy[0] + dx,
646                            cg.origin_chunk_xy[1] + dy,
647                            cg.origin_chunk_z + dz,
648                        ];
649                        cache.ensure(ch, mip, 0, view);
650                    }
651                }
652            }
653        }
654    } else {
655        cache.ensure([0, 0, 0], mip, 0, grid);
656    }
657    (cache, mip)
658}
659
660/// Clamp a requested render mip to one every populated chunk actually
661/// has built — so the uniform-mip traversal never under-samples a chunk
662/// that lacks the requested level (which would punch holes). `0` short-
663/// circuits (always available).
664#[must_use]
665pub fn effective_mip(grid: &GridView<'_>, requested: u32) -> u32 {
666    if requested == 0 {
667        return 0;
668    }
669    let mut m = requested;
670    if let Some(cg) = grid.chunk_grid {
671        for c in cg.chunks.iter().flatten() {
672            m = m.min(c.mip_count().saturating_sub(1));
673        }
674    } else {
675        m = m.min(grid.mip_count().saturating_sub(1));
676    }
677    m
678}
679
680/// Cross-chunk voxel sampler (Substage DDA.4 / DDA.7).
681///
682/// Resolves a grid-local voxel coordinate to the chunk that owns it
683/// (via [`GridView::chunk_at_xyz`]) and answers the DDA's per-voxel hit
684/// query — brick-gated [`GridView::surface_color`]. It borrows the
685/// shared immutable [`BrickMaps`] and caches the **current chunk**
686/// (`cur_*`: view + brick-map reference): a ray usually stays in one
687/// chunk for many voxels, so the per-voxel cost is a single index
688/// compare + an O(1) brick bit test — no hashing, no mutation. Holding
689/// only shared borrows, a `Sampler` is cheap to spin up per render band.
690///
691/// Single-chunk grids are the degenerate case: every voxel maps to
692/// chunk `[0, 0, 0]` (= the view itself).
693struct Sampler<'a> {
694    grid: GridView<'a>,
695    bricks: &'a BrickCache,
696    /// Effective render mip (DDA.6). Traversal cells are mip-`mip`
697    /// cells; sampling reads mip-`mip` data.
698    mip: u32,
699    /// Chunk size in mip-`mip` cells is a power of two; store it as
700    /// `log2` (shift) + `size - 1` (mask) so [`Self::locate`] splits a
701    /// cell into `(chunk, in-chunk)` with a shift + an `&` per axis
702    /// instead of a signed `div_euclid` — the dominant per-cell cost.
703    /// Arithmetic `>>` floors toward -∞ (= `div_euclid` for a positive
704    /// power-of-two divisor) and `& mask` gives the non-negative
705    /// remainder (= `rem_euclid`) even for negative cells (two's
706    /// complement), so results are identical to the division form.
707    xy_shift: u32,
708    xy_mask: i32,
709    z_shift: u32,
710    z_mask: i32,
711    cur_ch: [i32; 3],
712    cur_view: Option<GridView<'a>>,
713    cur_brick: Option<&'a BrickMap>,
714    has_cur: bool,
715}
716
717impl<'a> Sampler<'a> {
718    fn new(grid: GridView<'a>, bricks: &'a BrickCache, mip: u32) -> Self {
719        let cs_xy = (grid.chunk_size_xy >> mip).max(1);
720        let cs_z = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1);
721        debug_assert!(
722            cs_xy.is_power_of_two() && cs_z.is_power_of_two(),
723            "chunk dims must be powers of two for the shift/mask split"
724        );
725        #[allow(clippy::cast_possible_wrap)]
726        Self {
727            grid,
728            bricks,
729            mip,
730            xy_shift: cs_xy.trailing_zeros(),
731            xy_mask: cs_xy as i32 - 1,
732            z_shift: cs_z.trailing_zeros(),
733            z_mask: cs_z as i32 - 1,
734            cur_ch: [0; 3],
735            cur_view: None,
736            cur_brick: None,
737            has_cur: false,
738        }
739    }
740
741    /// Refresh the current-chunk cache (view + brick map) for `ch`.
742    fn select_chunk(&mut self, ch: [i32; 3]) {
743        if self.has_cur && self.cur_ch == ch {
744            return;
745        }
746        self.cur_view = self.grid.chunk_at_xyz(ch);
747        self.cur_brick = self.bricks.get(ch, self.mip);
748        self.cur_ch = ch;
749        self.has_cur = true;
750    }
751
752    /// Split a grid-local **mip-`mip` cell** index into `(chunk index,
753    /// in-chunk mip-cell)` via shift + mask (see field docs). Chunk
754    /// indices are mip-independent; only the per-chunk resolution
755    /// shrinks with mip.
756    #[allow(clippy::cast_sign_loss)]
757    fn locate(&self, c: [i32; 3]) -> ([i32; 3], [u32; 3]) {
758        let ch = [
759            c[0] >> self.xy_shift,
760            c[1] >> self.xy_shift,
761            c[2] >> self.z_shift,
762        ];
763        let loc = [
764            (c[0] & self.xy_mask) as u32,
765            (c[1] & self.xy_mask) as u32,
766            (c[2] & self.z_mask) as u32,
767        ];
768        (ch, loc)
769    }
770
771    /// Hit colour for grid-local mip-cell `c`, or `None` for air / empty
772    /// chunk / uncoloured bedrock. Brick-gated so air inside a populated
773    /// chunk costs only a bit test, not a slab walk.
774    #[allow(clippy::cast_possible_wrap)]
775    fn hit(&mut self, c: [i32; 3]) -> Option<u32> {
776        #[cfg(test)]
777        prof::SURF.with(|x| x.set(x.get() + 1));
778        let (ch, loc) = self.locate(c);
779        self.select_chunk(ch);
780        let occupied = self.cur_brick.is_some_and(|bm| {
781            bm.occupied([
782                loc[0] as i32 / BRICK,
783                loc[1] as i32 / BRICK,
784                loc[2] as i32 / BRICK,
785            ])
786        });
787        if !occupied {
788            return None;
789        }
790        self.cur_view?
791            .surface_color_mip(loc[0], loc[1], loc[2], self.mip)
792    }
793
794    /// Chunk size in mip-cells along XY / Z (always a power of two).
795    #[inline]
796    fn cells_per_chunk_xy(&self) -> i32 {
797        1 << self.xy_shift
798    }
799    #[inline]
800    fn cells_per_chunk_z(&self) -> i32 {
801        1 << self.z_shift
802    }
803
804    /// Whether the brick at brick-index `brick` (in `BRICK`-mip-cell
805    /// units) holds any solid voxel. Used by the outer brick-DDA to skip
806    /// empty space `BRICK` cells at a time. Assumes bricks nest within
807    /// chunks (caller gates on [`Self::cells_per_chunk_xy`]`>= BRICK`).
808    #[allow(clippy::cast_sign_loss)]
809    fn brick_occupied(&mut self, brick: [i32; 3]) -> bool {
810        // First mip-cell of the brick (BRICK = 8 → `<< 3`).
811        let c0 = [brick[0] << 3, brick[1] << 3, brick[2] << 3];
812        let ch = [
813            c0[0] >> self.xy_shift,
814            c0[1] >> self.xy_shift,
815            c0[2] >> self.z_shift,
816        ];
817        self.select_chunk(ch);
818        self.cur_brick.is_some_and(|bm| {
819            bm.occupied([
820                (c0[0] & self.xy_mask) >> 3,
821                (c0[1] & self.xy_mask) >> 3,
822                (c0[2] & self.z_mask) >> 3,
823            ])
824        })
825    }
826
827    /// Whether the super-brick at super-index `s` (in `SUPER`-mip-cell
828    /// units) holds any solid voxel. Outer-most empty-space skip (steps
829    /// `SUPER` cells). Assumes super-bricks nest in chunks (caller gates
830    /// on `cells_per_chunk >= SUPER`).
831    #[allow(clippy::cast_sign_loss)]
832    fn super_occupied(&mut self, s: [i32; 3]) -> bool {
833        // First mip-cell of the super-brick (SUPER = 64 → `<< 6`).
834        let c0 = [s[0] << 6, s[1] << 6, s[2] << 6];
835        let ch = [
836            c0[0] >> self.xy_shift,
837            c0[1] >> self.xy_shift,
838            c0[2] >> self.z_shift,
839        ];
840        self.select_chunk(ch);
841        self.cur_brick.is_some_and(|bm| {
842            bm.occupied_super([
843                (c0[0] & self.xy_mask) >> 6,
844                (c0[1] & self.xy_mask) >> 6,
845                (c0[2] & self.z_mask) >> 6,
846            ])
847        })
848    }
849}
850
851/// Walk mip-cells along the ray within `[lo_c, hi_c)` and return the
852/// first solid hit, with leak-free empty-space skipping (DDA.7 redux).
853///
854/// **Why one continuous DDA, not nested level-walks.** The previous
855/// design ran an outer brick/super DDA that *jumped* whole bricks and
856/// only descended into occupied ones. Stepping a coarse cell at a time
857/// lets the ray slip diagonally **past an occupied coarse cell it only
858/// touches at a shared edge/corner** — a leak that showed as bright
859/// sky seams across thin diagonal walls (the cave-demo report). Here a
860/// *single* cell-granularity DDA carries the exact `(cellc, t_max)`
861/// state for the whole ray; it only ever **fast-forwards across an
862/// empty super-brick / brick**, where skipping cannot miss anything.
863/// The exit axis lands on the integer box-boundary cell (no float
864/// re-floor on the critical axis), so the entry cell of the next —
865/// possibly occupied — box is always visited densely. Result: hits are
866/// bit-identical to the dense per-cell reference, with the empty-space
867/// speed-up retained.
868///
869/// `cell_size` is the mip-cell edge in mip-0 voxels (`1 << mip`);
870/// `fwd_dot = dir·forward` → perpendicular depth.
871#[allow(
872    clippy::too_many_arguments,
873    clippy::cast_possible_truncation,
874    clippy::cast_sign_loss,
875    clippy::cast_precision_loss
876)]
877fn cell_walk_skip(
878    origin: [f32; 3],
879    dir: [f32; 3],
880    fwd_dot: f32,
881    sampler: &mut Sampler<'_>,
882    lo_c: [i32; 3],
883    hi_c: [i32; 3],
884    cell_size: f32,
885    t_enter: f32,
886    t_exit: f32,
887    max_dist: f32,
888    env: &DdaEnv<'_>,
889) -> Option<Hit> {
890    let has_super = sampler.cells_per_chunk_xy() >= SUPER && sampler.cells_per_chunk_z() >= SUPER;
891    let has_brick = sampler.cells_per_chunk_xy() >= BRICK && sampler.cells_per_chunk_z() >= BRICK;
892
893    let start = t_enter + 1e-4;
894    let p = [
895        origin[0] + dir[0] * start,
896        origin[1] + dir[1] * start,
897        origin[2] + dir[2] * start,
898    ];
899    let mut cellc = [
900        ((p[0] / cell_size).floor() as i32).clamp(lo_c[0], hi_c[0] - 1),
901        ((p[1] / cell_size).floor() as i32).clamp(lo_c[1], hi_c[1] - 1),
902        ((p[2] / cell_size).floor() as i32).clamp(lo_c[2], hi_c[2] - 1),
903    ];
904    let (step, mut t_max, t_delta) = dda_setup(origin, dir, cellc, cell_size);
905    // Reciprocal direction → the per-skip box-boundary t and the t_max
906    // refresh use multiplies instead of divisions (the dominant skip
907    // cost). `0.0` where `step == 0` (that axis' t_max stays +∞).
908    let inv = [
909        if step[0] != 0 { 1.0 / dir[0] } else { 0.0 },
910        if step[1] != 0 { 1.0 / dir[1] } else { 0.0 },
911        if step[2] != 0 { 1.0 / dir[2] } else { 0.0 },
912    ];
913    let mut t_curr = t_enter;
914    let mut last_axis = 3usize;
915    // World ray length per ray-parameter unit; divided by `cell_size` it turns
916    // a cell's `t` span into its path length in voxel units (Volumetric weight).
917    let dir_len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
918
919    // TV: front-to-back translucent accumulation. While no translucent voxel
920    // is hit (`touched` stays false) every return is unchanged — the opaque
921    // world renders bit-identically. `prev_*` drive per-span compositing (one
922    // alpha layer per contiguous solid run or material change).
923    let mut accum = [0.0f32; 3];
924    let mut trans = 1.0f32;
925    let mut touched = false;
926    let mut prev_solid = false;
927    let mut prev_mat = 0u8;
928
929    // Each iteration either advances ≥1 cell (dense) or ≥1 box (skip),
930    // so the total cell span bounds the loop.
931    let span = (hi_c[0] - lo_c[0]) + (hi_c[1] - lo_c[1]) + (hi_c[2] - lo_c[2]);
932    let max_steps = span.max(0) as usize + 16;
933    for _ in 0..max_steps {
934        if cellc[0] < lo_c[0]
935            || cellc[0] >= hi_c[0]
936            || cellc[1] < lo_c[1]
937            || cellc[1] >= hi_c[1]
938            || cellc[2] < lo_c[2]
939            || cellc[2] >= hi_c[2]
940        {
941            return finalize_exit(touched, accum, trans, env, dir, max_dist);
942        }
943        let depth = t_curr * fwd_dot;
944        if depth > max_dist || t_curr > t_exit {
945            return finalize_exit(touched, accum, trans, env, dir, max_dist);
946        }
947        // Fog is fully opaque at `fog_max_dist`: nothing beyond is
948        // visible, so stop the ray there and return the fog colour
949        // rather than traversing (and skip/step-counting) to the far box
950        // wall. Both correct and the dominant perf win for foggy worlds —
951        // it caps every ray's length at the fog distance.
952        if env.fog_max_dist > 0.0 && depth >= env.fog_max_dist {
953            let fog = 0x8000_0000 | (env.fog_color & 0x00ff_ffff);
954            let color = if touched {
955                composite_over(accum, trans, fog)
956            } else {
957                fog
958            };
959            return Some(Hit {
960                color,
961                dist: env.fog_max_dist,
962            });
963        }
964
965        // Empty-space skip: a whole empty super-brick, else an empty
966        // brick. Skipping only empty boxes can never miss a surface.
967        let skip_shift = if has_super
968            && !sampler.super_occupied([cellc[0] >> 6, cellc[1] >> 6, cellc[2] >> 6])
969        {
970            Some(6u32)
971        } else if has_brick
972            && !sampler.brick_occupied([cellc[0] >> 3, cellc[1] >> 3, cellc[2] >> 3])
973        {
974            Some(3u32)
975        } else {
976            None
977        };
978        if let Some(sh) = skip_shift {
979            #[cfg(test)]
980            prof::BRICKS.with(|x| x.set(x.get() + 1));
981            // Nearest box boundary along the ray (in cell units).
982            let mut best_t = f32::INFINITY;
983            let mut best_axis = 3usize;
984            let mut plane = [0i32; 3];
985            for a in 0..3 {
986                if step[a] == 0 {
987                    continue;
988                }
989                let idx = cellc[a] >> sh;
990                plane[a] = if step[a] > 0 {
991                    (idx + 1) << sh
992                } else {
993                    idx << sh
994                };
995                let tb = (plane[a] as f32 * cell_size - origin[a]) * inv[a];
996                if tb < best_t {
997                    best_t = tb;
998                    best_axis = a;
999                }
1000            }
1001            if best_axis == 3 {
1002                return finalize_exit(touched, accum, trans, env, dir, max_dist);
1003            }
1004            // Land just across the boundary; pin the exit axis to the
1005            // integer boundary cell so float error can't skip the next
1006            // box's entry cell. Other axes haven't crossed their box
1007            // boundary (best_t is the min), so the point's floor is safe.
1008            let pb = [
1009                origin[0] + dir[0] * (best_t + 1e-4),
1010                origin[1] + dir[1] * (best_t + 1e-4),
1011                origin[2] + dir[2] * (best_t + 1e-4),
1012            ];
1013            let mut nc = [
1014                (pb[0] / cell_size).floor() as i32,
1015                (pb[1] / cell_size).floor() as i32,
1016                (pb[2] / cell_size).floor() as i32,
1017            ];
1018            nc[best_axis] = if step[best_axis] > 0 {
1019                plane[best_axis]
1020            } else {
1021                plane[best_axis] - 1
1022            };
1023            // The skip crossed a box boundary; if that takes the ray out
1024            // of the grid box it has exited (sky) — return rather than
1025            // clamping back in-bounds, which would spin at the edge.
1026            if nc[0] < lo_c[0]
1027                || nc[0] >= hi_c[0]
1028                || nc[1] < lo_c[1]
1029                || nc[1] >= hi_c[1]
1030                || nc[2] < lo_c[2]
1031                || nc[2] >= hi_c[2]
1032            {
1033                return finalize_exit(touched, accum, trans, env, dir, max_dist);
1034            }
1035            cellc = nc;
1036            // Refresh t_max for the new cell (dir unchanged → t_delta and
1037            // step constant; axes with step==0 keep their +∞).
1038            for a in 0..3 {
1039                if step[a] > 0 {
1040                    t_max[a] = ((cellc[a] + 1) as f32 * cell_size - origin[a]) * inv[a];
1041                } else if step[a] < 0 {
1042                    t_max[a] = (cellc[a] as f32 * cell_size - origin[a]) * inv[a];
1043                }
1044            }
1045            t_curr = best_t.max(t_curr);
1046            last_axis = best_axis;
1047            prev_solid = false; // skipped empty space → next hit starts a run
1048            continue;
1049        }
1050
1051        // Occupied brick: dense per-cell surface test.
1052        #[cfg(test)]
1053        prof::CELLS.with(|x| x.set(x.get() + 1));
1054        if let Some(color) = sampler.hit(cellc) {
1055            let bright_sub = side_shade_sub(env, last_axis, step);
1056            let lit = apply_fog(shade(color, bright_sub), depth.max(0.0), env);
1057            let m = terrain_material(env, color);
1058            if m.is_opaque() {
1059                // Opaque surface: the background. Return the first hit verbatim
1060                // when nothing translucent preceded it (bit-identical), else
1061                // composite the accumulated layers over it.
1062                let color = if touched {
1063                    composite_over(accum, trans, lit)
1064                } else {
1065                    lit
1066                };
1067                return Some(Hit {
1068                    color,
1069                    dist: depth.max(0.0),
1070                });
1071            }
1072            let mat_id = material_for_color(env.terrain_materials, color);
1073            let a = f32::from(m.alpha) / 255.0;
1074            if matches!(m.mode, roxlap_formats::material::BlendMode::Volumetric) {
1075                // Per-cell Beer–Lambert: opacity weighted by the ray's path
1076                // length through this voxel (so a filled volume thickens
1077                // smoothly with depth, a sliver contributes ≈0). Occludes.
1078                let t_exit = t_max[min_axis(t_max)];
1079                let seg_len = (t_exit - t_curr).max(0.0) * dir_len / cell_size;
1080                let eff_a = 1.0 - (1.0 - a).powf(seg_len);
1081                let c = rgb_to_f32(lit);
1082                accum[0] += trans * eff_a * c[0];
1083                accum[1] += trans * eff_a * c[1];
1084                accum[2] += trans * eff_a * c[2];
1085                trans *= 1.0 - eff_a;
1086                touched = true;
1087                prev_mat = mat_id;
1088                if trans < 1.0 / 256.0 {
1089                    return Some(Hit {
1090                        color: f32_to_rgb(accum),
1091                        dist: depth.max(0.0),
1092                    });
1093                }
1094            } else if !prev_solid || mat_id != prev_mat {
1095                // AlphaBlend / Additive: one alpha layer per solid-run entry or
1096                // material change (per-span — avoids the voxel-grid striping
1097                // through a thick glass/water slab; thickness-independent).
1098                let c = rgb_to_f32(lit);
1099                accum[0] += trans * a * c[0];
1100                accum[1] += trans * a * c[1];
1101                accum[2] += trans * a * c[2];
1102                if !matches!(m.mode, roxlap_formats::material::BlendMode::Additive) {
1103                    trans *= 1.0 - a; // AlphaBlend occludes; Additive does not.
1104                }
1105                touched = true;
1106                prev_mat = mat_id;
1107                if trans < 1.0 / 256.0 {
1108                    return Some(Hit {
1109                        color: f32_to_rgb(accum),
1110                        dist: depth.max(0.0),
1111                    });
1112                }
1113            }
1114            prev_solid = true;
1115        } else {
1116            prev_solid = false;
1117        }
1118        let axis = min_axis(t_max);
1119        last_axis = axis;
1120        t_curr = t_max[axis];
1121        cellc[axis] += step[axis];
1122        t_max[axis] += t_delta[axis];
1123    }
1124    None
1125}
1126
1127/// Per-face brightness reduction for the hit face. `axis` is the axis
1128/// the ray crossed to enter the hit voxel (`3` = entry voxel, no face);
1129/// `step[axis]` gives the crossing direction. Maps to the
1130/// `[x-, x+, y-, y+, z-, z+]` `side_shades` entry of the face the ray
1131/// looks at (a `+step` crossing enters through the low / `-` face).
1132#[inline]
1133fn side_shade_sub(env: &DdaEnv<'_>, axis: usize, step: [i32; 3]) -> u32 {
1134    if axis >= 3 {
1135        return 0;
1136    }
1137    let face = axis * 2 + usize::from(step[axis] < 0);
1138    env.side_shades[face].max(0) as u32
1139}
1140
1141/// Cast one ray into the grid and return the first solid hit.
1142///
1143/// **DDA.4:** cross-chunk per-pixel 3D-DDA over the grid's full voxel
1144/// box ([`GridView::voxel_bounds`], spanning every chunk in XY **and**
1145/// Z). The [`Sampler`] resolves each stepped voxel to its chunk and
1146/// brick-gates the slab walk. Cross-chunk look-down (the case the
1147/// voxlap renderer needed the whole virtual-column stack for) falls out
1148/// of the box simply spanning `chunks_z` along Z.
1149fn cast_ray(
1150    origin: [f32; 3],
1151    dir: [f32; 3],
1152    forward: [f32; 3],
1153    sampler: &mut Sampler<'_>,
1154    settings: &OpticastSettings,
1155    env: &DdaEnv<'_>,
1156) -> Option<Hit> {
1157    let (lo_i, hi_i) = sampler.grid.voxel_bounds();
1158    #[allow(clippy::cast_precision_loss)]
1159    let lo_f = [lo_i[0] as f32, lo_i[1] as f32, lo_i[2] as f32];
1160    #[allow(clippy::cast_precision_loss)]
1161    let hi_f = [hi_i[0] as f32, hi_i[1] as f32, hi_i[2] as f32];
1162    let (t_enter, t_exit) = intersect_aabb(origin, dir, lo_f, hi_f)?;
1163    let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
1164    #[allow(clippy::cast_precision_loss)]
1165    let max_dist = settings.max_scan_dist.max(1) as f32;
1166    let cell = 1i32 << sampler.mip;
1167    let cell_size = cell as f32;
1168    let lo_c = [
1169        lo_i[0].div_euclid(cell),
1170        lo_i[1].div_euclid(cell),
1171        lo_i[2].div_euclid(cell),
1172    ];
1173    let hi_c = [
1174        hi_i[0].div_euclid(cell),
1175        hi_i[1].div_euclid(cell),
1176        hi_i[2].div_euclid(cell),
1177    ];
1178    cell_walk_skip(
1179        origin, dir, fwd_dot, sampler, lo_c, hi_c, cell_size, t_enter, t_exit, max_dist, env,
1180    )
1181}
1182
1183/// Render one grid into `sink` with per-pixel 3D-DDA.
1184///
1185/// `camera` is the grid-local pose, `settings`
1186/// ([`OpticastSettings`]) carries the projection + viewport (including
1187/// the `y_start..y_end` strip bound), and `grid` is the per-frame
1188/// [`GridView`] borrow. `pitch_pixels` is the framebuffer
1189/// row stride in pixels (matches `ScalarRasterizer::new`'s argument).
1190///
1191/// On a miss, a textured sky ([`DdaEnv::sky`]) is sampled per ray
1192/// direction and written at `+inf` depth; with no textured sky the miss
1193/// writes nothing, so the caller's solid sky pre-fill shows (the
1194/// `render_scene_composed` path pre-fills it).
1195pub fn render_dda(
1196    camera: &Camera,
1197    settings: &OpticastSettings,
1198    grid: GridView<'_>,
1199    pitch_pixels: usize,
1200    env: &DdaEnv<'_>,
1201    mip: u32,
1202    sink: &mut impl PixelSink,
1203) {
1204    let cs = camera_math::derive(
1205        camera,
1206        settings.xres,
1207        settings.yres,
1208        settings.hx,
1209        settings.hy,
1210        settings.hz,
1211    );
1212
1213    // Sequential path builds a throwaway per-call cache (tests / single
1214    // grid). The parallel path takes a persistent cross-frame cache.
1215    let (cache, mip) = local_cache(&grid, mip);
1216    let mut sampler = Sampler::new(grid, &cache, mip);
1217
1218    for py in settings.y_start..settings.y_end {
1219        let row = py as usize * pitch_pixels;
1220        for px in 0..settings.xres {
1221            if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py) {
1222                sink.put(row + px as usize, color, dist);
1223            }
1224        }
1225    }
1226}
1227
1228/// Resolve one pixel: a shaded + fogged hit colour, a sampled textured
1229/// sky on a miss, or `None` (miss with no textured sky → caller's
1230/// pre-fill stands). Shared by the sequential ([`render_dda`]) and
1231/// parallel ([`render_dda_parallel`]) drivers.
1232#[inline]
1233fn pixel_result(
1234    cs: &CameraState,
1235    settings: &OpticastSettings,
1236    sampler: &mut Sampler<'_>,
1237    env: &DdaEnv<'_>,
1238    px: u32,
1239    py: u32,
1240) -> Option<(u32, f32)> {
1241    let (origin, dir) = pixel_ray(cs, settings, px, py);
1242    if let Some(hit) = cast_ray(origin, dir, cs.forward, sampler, settings, env) {
1243        Some((hit.color, hit.dist))
1244    } else {
1245        env.sky.map(|sky| (sample_sky(sky, dir), f32::INFINITY))
1246    }
1247}
1248
1249/// Tile-parallel [`render_dda`] writing straight into `(fb, zb)`.
1250///
1251/// DDA pixels are independent, so the framebuffer splits into disjoint
1252/// horizontal bands rendered concurrently (rayon) — **bit-identical**
1253/// to the sequential render regardless of thread count, unlike voxlap's
1254/// per-strip discretisation. Each band spins up its own lightweight
1255/// [`Sampler`] over the shared, immutable `cache`.
1256///
1257/// `cache` must already hold current brick maps for every chunk at
1258/// `mip` (populate via [`BrickCache::ensure`]); `mip` is the effective
1259/// render mip ([`effective_mip`]). `(fb, zb)` use the standard
1260/// conventions (`0x80RRGGBB`; z = perp distance, smaller = closer); a
1261/// miss writes nothing unless [`DdaEnv::sky`] is set. `pitch_pixels` is
1262/// the row stride.
1263#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
1264pub fn render_dda_parallel(
1265    camera: &Camera,
1266    settings: &OpticastSettings,
1267    grid: GridView<'_>,
1268    fb: &mut [u32],
1269    zb: &mut [f32],
1270    pitch_pixels: usize,
1271    env: &DdaEnv<'_>,
1272    cache: &BrickCache,
1273    mip: u32,
1274) {
1275    debug_assert_eq!(fb.len(), zb.len());
1276    let (y0, y1) = (settings.y_start, settings.y_end);
1277    if y1 <= y0 {
1278        return;
1279    }
1280    let cs = camera_math::derive(
1281        camera,
1282        settings.xres,
1283        settings.yres,
1284        settings.hx,
1285        settings.hy,
1286        settings.hz,
1287    );
1288    let target = RasterTarget::new(fb, zb);
1289
1290    // Split the y-range into ~one band per worker thread.
1291    let nthreads = rayon::current_num_threads().max(1);
1292    let rows = (y1 - y0) as usize;
1293    let band = rows.div_ceil(nthreads).max(1) as u32;
1294    let bands: Vec<(u32, u32)> = (y0..y1)
1295        .step_by(band as usize)
1296        .map(|s| (s, (s + band).min(y1)))
1297        .collect();
1298
1299    bands.par_iter().for_each(|&(by0, by1)| {
1300        let mut sampler = Sampler::new(grid, cache, mip);
1301        for py in by0..by1 {
1302            let row = py as usize * pitch_pixels;
1303            for px in 0..settings.xres {
1304                if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py)
1305                {
1306                    let idx = row + px as usize;
1307                    // SAFETY: bands cover disjoint row ranges, so writes
1308                    // never alias across threads; `idx` is in-bounds for
1309                    // a `pitch * height`-sized buffer.
1310                    unsafe {
1311                        target.write_color(idx, color);
1312                        target.write_depth(idx, dist);
1313                    }
1314                }
1315            }
1316        }
1317    });
1318}
1319
1320/// Dense per-voxel reference cast for a **single-chunk** grid: walks
1321/// every voxel of `[0, vsid)² × [0, CHUNK_SIZE_Z)` calling
1322/// [`GridView::surface_color`] directly — no brick gate, no chunk
1323/// resolution. The equivalence oracle the brickmap + sampler
1324/// [`cast_ray`] is checked against in tests.
1325#[cfg(test)]
1326#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1327fn cast_ray_reference(
1328    origin: [f32; 3],
1329    dir: [f32; 3],
1330    forward: [f32; 3],
1331    grid: &GridView<'_>,
1332    settings: &OpticastSettings,
1333) -> Option<Hit> {
1334    let nx = grid.vsid as f32;
1335    let nz = f32::from(u16::try_from(crate::grid_view::CHUNK_SIZE_Z).unwrap_or(256));
1336    #[allow(clippy::cast_possible_wrap)]
1337    let n_i = [
1338        grid.vsid as i32,
1339        grid.vsid as i32,
1340        crate::grid_view::CHUNK_SIZE_Z as i32,
1341    ];
1342    let (t_enter, t_exit) = intersect_aabb(origin, dir, [0.0; 3], [nx, nx, nz])?;
1343    let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
1344    let max_dist = settings.max_scan_dist.max(1) as f32;
1345
1346    let start = t_enter + 1e-4;
1347    let p = [
1348        origin[0] + dir[0] * start,
1349        origin[1] + dir[1] * start,
1350        origin[2] + dir[2] * start,
1351    ];
1352    let mut voxel = [
1353        (p[0].floor() as i32).clamp(0, n_i[0] - 1),
1354        (p[1].floor() as i32).clamp(0, n_i[1] - 1),
1355        (p[2].floor() as i32).clamp(0, n_i[2] - 1),
1356    ];
1357    let (step, mut t_max, t_delta) = dda_setup(origin, dir, voxel, 1.0);
1358    let mut t_curr = t_enter;
1359    let max_steps = (n_i[0] + n_i[1] + n_i[2]) as usize + 8;
1360    for _ in 0..max_steps {
1361        if voxel[0] < 0
1362            || voxel[0] >= n_i[0]
1363            || voxel[1] < 0
1364            || voxel[1] >= n_i[1]
1365            || voxel[2] < 0
1366            || voxel[2] >= n_i[2]
1367        {
1368            return None;
1369        }
1370        let depth = t_curr * fwd_dot;
1371        if depth > max_dist || t_curr > t_exit {
1372            return None;
1373        }
1374        #[allow(clippy::cast_sign_loss)]
1375        if let Some(color) = grid.surface_color(voxel[0] as u32, voxel[1] as u32, voxel[2] as u32) {
1376            return Some(Hit {
1377                color: shade(color, 0),
1378                dist: depth.max(0.0),
1379            });
1380        }
1381        let axis = min_axis(t_max);
1382        t_curr = t_max[axis];
1383        voxel[axis] += step[axis];
1384        t_max[axis] += t_delta[axis];
1385    }
1386    None
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391    use super::*;
1392
1393    /// Recording sink: collects `(idx, color, dist)` puts for tests.
1394    #[derive(Default)]
1395    struct Recorder {
1396        puts: Vec<(usize, u32, f32)>,
1397    }
1398    impl PixelSink for Recorder {
1399        fn put(&mut self, idx: usize, color: u32, dist: f32) {
1400            self.puts.push((idx, color, dist));
1401        }
1402    }
1403
1404    fn oracle_camera() -> Camera {
1405        // Identity-basis camera at origin: ray math is integer-exact.
1406        Camera {
1407            pos: [0.0, 0.0, 0.0],
1408            right: [1.0, 0.0, 0.0],
1409            down: [0.0, 0.0, 1.0],
1410            forward: [0.0, 1.0, 0.0],
1411        }
1412    }
1413
1414    /// Render `grid` from `camera` into a `w × h` framebuffer and
1415    /// return the per-pixel hit mask (`true` where a ray hit a voxel).
1416    fn render_mask(grid: GridView<'_>, camera: &Camera, w: u32, h: u32) -> Vec<bool> {
1417        let n = (w as usize) * (h as usize);
1418        let mut fb = vec![0u32; n]; // sky sentinel = 0
1419        let mut zb = vec![f32::INFINITY; n];
1420        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
1421        {
1422            let mut sink = RasterSink::new(&mut fb, &mut zb);
1423            render_dda(
1424                camera,
1425                &settings,
1426                grid,
1427                w as usize,
1428                &DdaEnv::default(),
1429                0,
1430                &mut sink,
1431            );
1432        }
1433        fb.iter().map(|&c| c != 0).collect()
1434    }
1435
1436    /// A silhouette is "row-convex" if every framebuffer row's hit
1437    /// pixels form a single contiguous run (no interior gap). The
1438    /// voxlap silhouette notch is exactly such an interior gap, so this
1439    /// is the headline DDA.1 acceptance check.
1440    fn rows_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
1441        let w = w as usize;
1442        for y in 0..h as usize {
1443            let row = &mask[y * w..(y + 1) * w];
1444            let first = row.iter().position(|&b| b);
1445            let last = row.iter().rposition(|&b| b);
1446            if let (Some(f), Some(l)) = (first, last) {
1447                if row[f..=l].iter().any(|&b| !b) {
1448                    return false;
1449                }
1450            }
1451        }
1452        true
1453    }
1454
1455    /// Same contiguity check down each column.
1456    fn cols_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
1457        let w = w as usize;
1458        let h = h as usize;
1459        for x in 0..w {
1460            let col: Vec<bool> = (0..h).map(|y| mask[y * w + x]).collect();
1461            let first = col.iter().position(|&b| b);
1462            let last = col.iter().rposition(|&b| b);
1463            if let (Some(f), Some(l)) = (first, last) {
1464                if col[f..=l].iter().any(|&b| !b) {
1465                    return false;
1466                }
1467            }
1468        }
1469        true
1470    }
1471
1472    /// The principal-point pixel `(hx, hy)` looks straight down the
1473    /// forward axis, scaled by `hz`.
1474    #[test]
1475    fn center_pixel_ray_is_forward() {
1476        let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
1477        let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
1478        // hx = hy = 320 / 240 → use the exact principal point.
1479        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1480        let (origin, dir) = pixel_ray(&cs, &settings, settings.hx as u32, settings.hy as u32);
1481        assert_eq!(origin, [0.0, 0.0, 0.0]);
1482        // hz·forward = 320·[0,1,0].
1483        assert_eq!(
1484            dir.map(f32::to_bits),
1485            [0.0f32, 320.0, 0.0].map(f32::to_bits)
1486        );
1487    }
1488
1489    /// Pixel `(0, 0)`'s ray equals `camera_math`'s `corn[0]` — proving
1490    /// the DDA renderer samples the same rays the voxlap frustum is
1491    /// built from.
1492    #[test]
1493    fn corner_pixel_ray_matches_camera_corn0() {
1494        let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
1495        let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
1496        let (_origin, dir) = pixel_ray(&cs, &settings, 0, 0);
1497        assert_eq!(dir.map(f32::to_bits), cs.corn[0].map(f32::to_bits));
1498    }
1499
1500    /// The renderer's independent slab decoder
1501    /// ([`GridView::voxel_color`]) must agree with the reference
1502    /// [`roxlap_formats::vxl::Vxl::voxel_color`] for every cell —
1503    /// including a column with an air gap, which exercises the
1504    /// ceiling-colour-list branch.
1505    #[test]
1506    fn gridview_voxel_color_matches_reference() {
1507        // Two solid runs per column separated by air → ceiling list.
1508        let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |x, _, z| {
1509            let lo = (10..=12).contains(&z);
1510            let hi = (40..=42).contains(&z);
1511            (lo || hi).then_some(0x80_10_20_30 + x)
1512        });
1513        let grid = GridView::from_single_vxl(&vxl);
1514        for x in 0..8 {
1515            for y in 0..8 {
1516                for z in 0..64 {
1517                    assert_eq!(
1518                        grid.voxel_color(x, y, z),
1519                        vxl.voxel_color(x, y, z),
1520                        "mismatch at ({x},{y},{z})"
1521                    );
1522                }
1523            }
1524        }
1525    }
1526
1527    /// An all-air grid produces no hits (every ray misses).
1528    #[test]
1529    fn empty_grid_no_hits() {
1530        let vxl = roxlap_formats::vxl::Vxl::empty(64);
1531        let grid = GridView::from_single_vxl(&vxl);
1532        let settings = OpticastSettings::for_oracle_framebuffer(64, 48);
1533        let mut rec = Recorder::default();
1534        render_dda(
1535            &oracle_camera(),
1536            &settings,
1537            grid,
1538            64,
1539            &DdaEnv::default(),
1540            0,
1541            &mut rec,
1542        );
1543        assert!(rec.puts.is_empty(), "all-air grid must produce no hits");
1544    }
1545
1546    /// Camera above a solid floor, looking straight down: every ray
1547    /// hits, the recovered colour is the floor colour, and the centre
1548    /// pixel's depth ≈ the camera's height above the floor.
1549    #[test]
1550    fn floor_seen_from_above() {
1551        const FLOOR_Z: u32 = 40;
1552        const FLOOR_COL: u32 = 0x80_30_60_90;
1553        let vxl =
1554            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= FLOOR_Z).then_some(FLOOR_COL));
1555        let grid = GridView::from_single_vxl(&vxl);
1556
1557        // Eye above the floor (z is down), looking down (+z).
1558        let cam = Camera {
1559            pos: [16.0, 16.0, 10.0],
1560            right: [1.0, 0.0, 0.0],
1561            down: [0.0, 1.0, 0.0],
1562            forward: [0.0, 0.0, 1.0],
1563        };
1564        let settings = OpticastSettings::for_oracle_framebuffer(48, 48);
1565        let mut rec = Recorder::default();
1566        render_dda(&cam, &settings, grid, 48, &DdaEnv::default(), 0, &mut rec);
1567
1568        assert!(!rec.puts.is_empty(), "floor must be visible");
1569        // Centre pixel looks straight down → depth ≈ FLOOR_Z - eye_z.
1570        let centre = 24usize * 48 + 24;
1571        let hit = rec
1572            .puts
1573            .iter()
1574            .find(|(idx, _, _)| *idx == centre)
1575            .expect("centre ray must hit the floor");
1576        assert_eq!(hit.1 & 0x00ff_ffff, FLOOR_COL & 0x00ff_ffff);
1577        let expected = (FLOOR_Z as f32) - 10.0;
1578        assert!(
1579            (hit.2 - expected).abs() < 1.5,
1580            "centre depth {} not ≈ {}",
1581            hit.2,
1582            expected
1583        );
1584    }
1585
1586    /// DDA.2: a camera looking at the horizon splits the frame into
1587    /// sky (upward rays miss → no write) and floor (downward rays hit).
1588    /// The top of the frame must be mostly sky, the bottom mostly
1589    /// floor.
1590    #[test]
1591    fn horizon_splits_sky_and_floor() {
1592        const FLOOR_Z: u32 = 40;
1593        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
1594            (z >= FLOOR_Z).then_some(0x80_44_66_88)
1595        });
1596        let grid = GridView::from_single_vxl(&vxl);
1597
1598        // At z=30 (above the z=40 floor), looking +y horizontally,
1599        // down = +z. Upward rays (low py) escape through the box top
1600        // (z=0) → sky; downward rays (high py) strike the floor.
1601        let cam = Camera {
1602            pos: [32.0, 4.0, 30.0],
1603            right: [-1.0, 0.0, 0.0],
1604            down: [0.0, 0.0, 1.0],
1605            forward: [0.0, 1.0, 0.0],
1606        };
1607        let (w, h) = (64u32, 64u32);
1608        let mask = render_mask(grid, &cam, w, h);
1609
1610        let count_band = |y0: usize, y1: usize| -> usize {
1611            (y0 * w as usize..y1 * w as usize)
1612                .filter(|&i| mask[i])
1613                .count()
1614        };
1615        let top = count_band(0, h as usize / 4);
1616        let bottom = count_band(3 * h as usize / 4, h as usize);
1617        assert!(mask.iter().any(|&b| b), "floor must be visible");
1618        assert!(mask.iter().any(|&b| !b), "sky must be visible");
1619        assert!(
1620            bottom > top,
1621            "bottom band ({bottom}) should hit more floor than top band ({top})"
1622        );
1623    }
1624
1625    /// Render `grid` from `camera` with the dense reference cast (no
1626    /// brickmap), returning `(colour, depth)` buffers.
1627    fn render_reference(
1628        grid: GridView<'_>,
1629        camera: &Camera,
1630        w: u32,
1631        h: u32,
1632    ) -> (Vec<u32>, Vec<f32>) {
1633        let n = (w as usize) * (h as usize);
1634        let mut fb = vec![0u32; n];
1635        let mut zb = vec![f32::INFINITY; n];
1636        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
1637        let cs = camera_math::derive(camera, w, h, settings.hx, settings.hy, settings.hz);
1638        for py in 0..h {
1639            for px in 0..w {
1640                let (o, d) = pixel_ray(&cs, &settings, px, py);
1641                if let Some(hit) = cast_ray_reference(o, d, cs.forward, &grid, &settings) {
1642                    let i = (py * w + px) as usize;
1643                    fb[i] = hit.color;
1644                    zb[i] = hit.dist;
1645                }
1646            }
1647        }
1648        (fb, zb)
1649    }
1650
1651    /// Render `grid` from `camera` via the production brickmap path.
1652    fn render_brickmap(
1653        grid: GridView<'_>,
1654        camera: &Camera,
1655        w: u32,
1656        h: u32,
1657    ) -> (Vec<u32>, Vec<f32>) {
1658        render_brickmap_env(grid, camera, w, h, &DdaEnv::default())
1659    }
1660
1661    /// As [`render_brickmap`] but with an explicit [`DdaEnv`] (fog /
1662    /// textured sky / side shades).
1663    fn render_brickmap_env(
1664        grid: GridView<'_>,
1665        camera: &Camera,
1666        w: u32,
1667        h: u32,
1668        env: &DdaEnv<'_>,
1669    ) -> (Vec<u32>, Vec<f32>) {
1670        let n = (w as usize) * (h as usize);
1671        let mut fb = vec![0u32; n];
1672        let mut zb = vec![f32::INFINITY; n];
1673        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
1674        {
1675            let mut sink = RasterSink::new(&mut fb, &mut zb);
1676            render_dda(camera, &settings, grid, w as usize, env, 0, &mut sink);
1677        }
1678        (fb, zb)
1679    }
1680
1681    /// Regression for the cave-demo "bright sky seams" report: the
1682    /// empty-space-skip walk must not leak past an occupied box the ray
1683    /// only grazes at a shared edge/corner. A 1-voxel-thick diagonal
1684    /// wall (`x+y==64`, voxels edge-connected) with air on both sides is
1685    /// the canonical case. The production skip walk must hit exactly the
1686    /// same pixels as the dense per-cell reference — zero divergence.
1687    #[test]
1688    fn no_sky_leak_through_diagonal_wall() {
1689        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
1690            ((x + y == 64) && (2..62).contains(&z)).then_some(0x80_40_80_60)
1691        });
1692        let grid = GridView::from_single_vxl(&vxl);
1693        let (w, h) = (160u32, 160u32);
1694        let c = [10.0, 10.0, 32.0];
1695        let poses = [
1696            Camera::from_yaw_pitch(c, 0.785, 0.0),
1697            Camera::from_yaw_pitch(c, 0.6, 0.1),
1698            Camera::from_yaw_pitch(c, 0.95, -0.1),
1699            Camera::from_yaw_pitch(c, 0.785, 0.3),
1700            Camera::from_yaw_pitch(c, 0.5, 0.0),
1701        ];
1702        for (i, cam) in poses.iter().enumerate() {
1703            let (fb_b, _) = render_brickmap(grid, cam, w, h);
1704            let (fb_r, _) = render_reference(grid, cam, w, h);
1705            let leak = (0..(w * h) as usize)
1706                .filter(|&k| (fb_b[k] != 0) != (fb_r[k] != 0))
1707                .count();
1708            assert_eq!(leak, 0, "pose {i}: {leak} px diverge from dense reference");
1709        }
1710    }
1711
1712    /// TV terrain transparency: a glass-coloured voxel slab in front of an
1713    /// opaque floor. With no terrain material map the glass is an opaque first
1714    /// hit; with the map it becomes translucent and the floor tints through.
1715    #[test]
1716    fn terrain_glass_tints_floor_behind() {
1717        let glass = 0x80_40_C0_E0; // cyan
1718        let floor = 0x80_C0_40_40; // red
1719        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
1720            if z == 4 {
1721                Some(glass)
1722            } else if z >= 10 {
1723                Some(floor)
1724            } else {
1725                None
1726            }
1727        });
1728        let grid = GridView::from_single_vxl(&vxl);
1729        // Camera above the grid looking straight down (+z), centred.
1730        let cam = Camera {
1731            pos: [8.0, 8.0, 0.0],
1732            right: [1.0, 0.0, 0.0],
1733            down: [0.0, 1.0, 0.0],
1734            forward: [0.0, 0.0, 1.0],
1735        };
1736        let (w, h) = (32u32, 32u32);
1737        let centre = (h / 2 * w + w / 2) as usize;
1738
1739        // Opaque: the glass voxel stops the ray (no terrain materials).
1740        let (fb_op, _) = render_brickmap(grid, &cam, w, h);
1741        assert_eq!(
1742            fb_op[centre] & 0x00ff_ffff,
1743            0x0040_C0E0,
1744            "opaque glass first-hit"
1745        );
1746
1747        // Translucent: glass colour → material 1 (alpha-blend).
1748        let mut table = MaterialTable::new();
1749        table.set(1, Material::alpha_blend(128));
1750        let env = DdaEnv {
1751            materials: Some(&table),
1752            terrain_materials: &[(glass & 0x00ff_ffff, 1)],
1753            ..DdaEnv::default()
1754        };
1755        let (fb_tr, _) = render_brickmap_env(grid, &cam, w, h, &env);
1756        assert_ne!(
1757            fb_tr[centre], fb_op[centre],
1758            "glass should composite over the floor, not stay opaque"
1759        );
1760        let r_op = (fb_op[centre] >> 16) & 0xff; // glass red ≈ 0x40
1761        let r_tr = (fb_tr[centre] >> 16) & 0xff; // + floor red bleeds in
1762        assert!(
1763            r_tr > r_op,
1764            "floor red tints through the glass (op={r_op:02x} tr={r_tr:02x})"
1765        );
1766    }
1767
1768    /// TV terrain Volumetric: a **filled** grey smoke volume over a red floor.
1769    /// Beer–Lambert opacity grows with the ray's path length, so a deeper smoke
1770    /// column shows more of its own colour (green channel rises toward the
1771    /// smoke grey) — thickness-dependent, unlike per-span AlphaBlend.
1772    #[test]
1773    fn terrain_volumetric_thickness_deepens_opacity() {
1774        let smoke = 0x80_90_90_90; // grey
1775        let floor = 0x80_C0_20_20; // red (low green)
1776                                   // Centre green channel for a smoke column `depth` voxels deep (filled),
1777                                   // floor at z>=12, camera looking straight down.
1778        let green_at = |depth: u32| -> u32 {
1779            let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
1780                if (4..4 + depth).contains(&z) {
1781                    Some(smoke)
1782                } else if z >= 12 {
1783                    Some(floor)
1784                } else {
1785                    None
1786                }
1787            });
1788            let grid = GridView::from_single_vxl(&vxl);
1789            let cam = Camera {
1790                pos: [8.0, 8.0, 0.0],
1791                right: [1.0, 0.0, 0.0],
1792                down: [0.0, 1.0, 0.0],
1793                forward: [0.0, 0.0, 1.0],
1794            };
1795            let (w, h) = (32u32, 32u32);
1796            let mut table = MaterialTable::new();
1797            table.set(1, Material::volumetric(80));
1798            let env = DdaEnv {
1799                materials: Some(&table),
1800                terrain_materials: &[(smoke & 0x00ff_ffff, 1)],
1801                ..DdaEnv::default()
1802            };
1803            let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
1804            (fb[(h / 2 * w + w / 2) as usize] >> 8) & 0xff
1805        };
1806        let shallow = green_at(1);
1807        let deep = green_at(7);
1808        assert!(
1809            deep > shallow,
1810            "deeper Volumetric smoke shows more of its grey (deep g={deep:02x} > shallow g={shallow:02x})"
1811        );
1812    }
1813
1814    /// DDA.5: distance fog blends a hit toward the fog colour. A far
1815    /// floor pixel is closer to the fog colour than a near one.
1816    #[test]
1817    fn distance_fog_blends_toward_fog_color() {
1818        let vxl =
1819            roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| (z >= 40).then_some(0x80_FF_FF_FF));
1820        let grid = GridView::from_single_vxl(&vxl);
1821        let cam = Camera {
1822            pos: [32.0, 2.0, 38.0],
1823            right: [1.0, 0.0, 0.0],
1824            down: [0.0, 0.0, 1.0],
1825            forward: [0.0, 1.0, 0.0],
1826        };
1827        let env = DdaEnv {
1828            sky: None,
1829            fog_color: 0x00_00_00_00, // black fog → distance darkens
1830            fog_max_dist: 64.0,
1831            side_shades: [0; 6],
1832            materials: None,
1833            terrain_materials: &[],
1834        };
1835        let (w, h) = (64u32, 64u32);
1836        let (fog, _) = render_brickmap_env(grid, &cam, w, h, &env);
1837        let (nofog, zb) = render_brickmap(grid, &cam, w, h);
1838        let (idx, depth) = zb.iter().enumerate().filter(|(_, z)| z.is_finite()).fold(
1839            (0usize, 0.0f32),
1840            |acc, (i, &z)| {
1841                if z > acc.1 {
1842                    (i, z)
1843                } else {
1844                    acc
1845                }
1846            },
1847        );
1848        assert!(depth > 20.0, "need a deep pixel to test fog (got {depth})");
1849        let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
1850        assert!(
1851            lum(fog[idx]) < lum(nofog[idx]),
1852            "fogged pixel {:08x} not darker than {:08x}",
1853            fog[idx],
1854            nofog[idx]
1855        );
1856    }
1857
1858    /// DDA.5: with a textured sky, miss pixels are filled from the sky
1859    /// panorama (direction-dependent) instead of left at the pre-fill.
1860    #[test]
1861    fn textured_sky_fills_misses() {
1862        let sky = crate::sky::Sky::blue_gradient();
1863        let vxl = roxlap_formats::vxl::Vxl::empty(32); // all air → all miss
1864        let grid = GridView::from_single_vxl(&vxl);
1865        let env = DdaEnv {
1866            sky: Some(&sky),
1867            fog_color: 0,
1868            fog_max_dist: 0.0,
1869            side_shades: [0; 6],
1870            materials: None,
1871            terrain_materials: &[],
1872        };
1873        let cam = Camera::from_yaw_pitch([16.0, 16.0, 128.0], 0.3, -0.4);
1874        let (w, h) = (48u32, 48u32);
1875        let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
1876        assert!(fb.iter().all(|&c| c >> 24 == 0x80), "all misses sky-filled");
1877        let top = fb[0];
1878        let bottom = fb[(h - 1) as usize * w as usize];
1879        assert_ne!(top, bottom, "sky gradient should vary with elevation");
1880    }
1881
1882    /// Sky elevation orientation matches the GPU `sky_color` (acos(-z)/π):
1883    /// looking **up** (−z) samples panorama column 0 (zenith), looking
1884    /// **down** (+z) samples the last column (nadir). Regression for the
1885    /// CPU up/down inversion.
1886    #[test]
1887    fn sky_elevation_zenith_at_column_zero() {
1888        let mut pixels = vec![0i32; 8];
1889        pixels[0] = 0x0011_1111; // zenith marker
1890        pixels[7] = 0x0099_9999; // nadir marker
1891        let sky = crate::sky::Sky::from_pixels(pixels, 8, 1);
1892        let up = sample_sky(&sky, [0.0, 0.0, -1.0]); // −z is up
1893        let down = sample_sky(&sky, [0.0, 0.0, 1.0]); // +z is down
1894        assert_eq!(
1895            up & 0x00ff_ffff,
1896            0x0011_1111,
1897            "looking up → column 0 (zenith)"
1898        );
1899        assert_eq!(
1900            down & 0x00ff_ffff,
1901            0x0099_9999,
1902            "looking down → last column (nadir)"
1903        );
1904    }
1905
1906    /// `render_sky_fill` paints the panorama for a **gridless** view — the
1907    /// same per-pixel sky sample the miss-ray path uses, with no grid present
1908    /// (the CPU empty-scene background, matching the GPU).
1909    #[test]
1910    fn sky_fill_paints_panorama_gridless() {
1911        let sky = crate::sky::Sky::blue_gradient();
1912        let cam = Camera::from_yaw_pitch([0.0, 0.0, 0.0], 0.3, -0.4);
1913        let (w, h) = (48u32, 48u32);
1914        let cs = crate::camera_math::derive(&cam, w, h, 24.0, 24.0, 24.0);
1915        let settings = crate::opticast::OpticastSettings::for_oracle_framebuffer(w, h);
1916        let mut fb = vec![0u32; (w * h) as usize];
1917        // All-background z-buffer (+∞) → every pixel gets the sky.
1918        let zb = vec![f32::INFINITY; (w * h) as usize];
1919        render_sky_fill(&mut fb, &zb, w as usize, w, h, &cs, &settings, &sky);
1920        assert!(
1921            fb.iter().all(|&c| c >> 24 == 0x80),
1922            "every pixel sky-filled with the brightness byte set"
1923        );
1924        let top = fb[0];
1925        let bottom = fb[(h - 1) as usize * w as usize];
1926        assert_ne!(top, bottom, "sky gradient should vary with elevation");
1927        // A finite-z (terrain) pixel is left untouched.
1928        let mut fb2 = vec![0x1234_5678u32; (w * h) as usize];
1929        let mut zb2 = vec![f32::INFINITY; (w * h) as usize];
1930        zb2[0] = 10.0; // pretend a terrain hit at pixel 0
1931        render_sky_fill(&mut fb2, &zb2, w as usize, w, h, &cs, &settings, &sky);
1932        assert_eq!(fb2[0], 0x1234_5678, "finite-z pixel is not overwritten");
1933    }
1934
1935    /// DDA.5: side shading darkens the hit face by its `side_shades`
1936    /// entry. A top-facing floor (ray crosses +z to enter) gets the
1937    /// `z-` face reduction (index 4).
1938    #[test]
1939    fn side_shades_darken_hit_face() {
1940        let vxl =
1941            roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| (z >= 8).then_some(0x80_FF_FF_FF));
1942        let grid = GridView::from_single_vxl(&vxl);
1943        let cam = Camera {
1944            pos: [8.0, 8.0, 2.0],
1945            right: [1.0, 0.0, 0.0],
1946            down: [0.0, 1.0, 0.0],
1947            forward: [0.0, 0.0, 1.0],
1948        };
1949        let centre = 16 * 32 + 16;
1950        let (plain, _) = render_brickmap(grid, &cam, 32, 32);
1951        let env = DdaEnv {
1952            sky: None,
1953            fog_color: 0,
1954            fog_max_dist: 0.0,
1955            side_shades: [0, 0, 0, 0, 0x40, 0],
1956            materials: None,
1957            terrain_materials: &[],
1958        };
1959        let (shaded, _) = render_brickmap_env(grid, &cam, 32, 32, &env);
1960        let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
1961        assert!(
1962            lum(shaded[centre]) < lum(plain[centre]),
1963            "side-shaded face {:08x} not darker than {:08x}",
1964            shaded[centre],
1965            plain[centre]
1966        );
1967    }
1968
1969    /// The two-level brick-skip cast closely approximates the dense
1970    /// per-voxel reference. The outer brick DDA re-seeds the inner cell
1971    /// walk at each occupied brick, so a few silhouette-boundary pixels
1972    /// jitter by one voxel (different hit cell → different colour/depth)
1973    /// — visually invisible, and the gain is ~`BRICK`× fewer air steps.
1974    /// Assert the divergence is tiny: coverage (hit/sky mask) is nearly
1975    /// identical and only a small fraction of pixels differ. (The
1976    /// thread-invariance guarantee is the separate, exact
1977    /// `parallel_matches_sequential`.)
1978    #[test]
1979    fn brickmap_approximates_dense_reference() {
1980        // Rolling heightmap + a floating block (air above and below).
1981        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
1982            let surf = 30 + ((x / 5 + y / 7) % 11);
1983            let ground = z >= surf;
1984            let block = (20..=24).contains(&z) && (10..20).contains(&x) && (40..50).contains(&y);
1985            (ground || block).then_some(0x80_30_50_70 + (x ^ y) % 0x40)
1986        });
1987        let grid = GridView::from_single_vxl(&vxl);
1988
1989        let (w, h) = (80u32, 80u32);
1990        let poses = [
1991            Camera::orbit(0.6, 0.5, 90.0, [32.0, 32.0, 40.0]),
1992            Camera::orbit(2.1, 0.2, 70.0, [32.0, 32.0, 35.0]),
1993            Camera::orbit(-1.0, 0.9, 120.0, [32.0, 32.0, 45.0]),
1994        ];
1995        let n = (w * h) as usize;
1996        for (i, cam) in poses.iter().enumerate() {
1997            let (fb_b, zb_b) = render_brickmap(grid, cam, w, h);
1998            let (fb_r, _zb_r) = render_reference(grid, cam, w, h);
1999            // Coverage (hit vs sky) must match almost exactly.
2000            let cov_b = fb_b.iter().filter(|&&c| c != 0).count();
2001            let cov_r = fb_r.iter().filter(|&&c| c != 0).count();
2002            assert!(cov_b > 200, "pose {i} rendered ~empty (cov {cov_b})");
2003            let cov_diff = cov_b.abs_diff(cov_r);
2004            assert!(
2005                cov_diff * 100 <= n, // < 1 % of pixels flip hit↔sky
2006                "pose {i} coverage diverged: brick {cov_b} vs dense {cov_r}"
2007            );
2008            // Colour diffs (boundary-voxel jitter) must be a small slice.
2009            let diffs = fb_b.iter().zip(&fb_r).filter(|(a, b)| a != b).count();
2010            assert!(
2011                diffs * 100 <= n * 3, // < 3 % of pixels differ
2012                "pose {i} too many pixel diffs vs dense: {diffs}/{n}"
2013            );
2014            // Depth must be sane (finite where hit), not wildly off.
2015            for k in 0..n {
2016                if fb_b[k] != 0 {
2017                    assert!(zb_b[k].is_finite(), "pose {i} px {k} non-finite depth");
2018                }
2019            }
2020        }
2021    }
2022
2023    /// DDA.5: a voxel's baked brightness byte darkens its colour. A
2024    /// half-bright voxel (`a = 0x40`) renders at roughly half RGB; a
2025    /// full-bright one (`a = 0x80`) is unchanged.
2026    #[test]
2027    fn baked_brightness_darkens_color() {
2028        // Half brightness: alpha 0x40 (64/128). White RGB → ~mid grey.
2029        let dim =
2030            roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| (z >= 8).then_some(0x40_FF_FF_FF));
2031        let grid = GridView::from_single_vxl(&dim);
2032        let cam = Camera {
2033            pos: [8.0, 8.0, 2.0],
2034            right: [1.0, 0.0, 0.0],
2035            down: [0.0, 1.0, 0.0],
2036            forward: [0.0, 0.0, 1.0],
2037        };
2038        let (fb, _) = render_brickmap(grid, &cam, 32, 32);
2039        let centre = 16 * 32 + 16;
2040        // 0xFF * 64 >> 7 = 127 per channel; alpha normalised to 0x80.
2041        assert_eq!(fb[centre], 0x80_7F_7F_7F, "got {:08x}", fb[centre]);
2042
2043        // Full brightness passes RGB through unchanged.
2044        let full =
2045            roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| (z >= 8).then_some(0x80_FF_FF_FF));
2046        let gridf = GridView::from_single_vxl(&full);
2047        let (fbf, _) = render_brickmap(gridf, &cam, 32, 32);
2048        assert_eq!(fbf[centre], 0x80_FF_FF_FF, "got {:08x}", fbf[centre]);
2049    }
2050
2051    /// DDA.4 headline gate: cross-chunk look-down. A camera in an
2052    /// all-air upper chunk (chz=0) looking straight down must see the
2053    /// floor in the *lower* stacked chunk (chz=1), through the chunk-Z
2054    /// boundary. This is exactly the case the voxlap renderer needed the
2055    /// whole virtual-column stack (S4B.6.j / VC) for; the DDA gets it
2056    /// for free from the outer box spanning `chunks_z`.
2057    #[test]
2058    fn cross_chunk_lookdown_sees_lower_stacked_floor() {
2059        const FLOOR_LOCAL_Z: u32 = 40;
2060        const FLOOR_COL: u32 = 0x80_22_88_44;
2061        let upper = roxlap_formats::vxl::Vxl::empty(32); // all air + bedrock
2062        let lower = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
2063            (z >= FLOOR_LOCAL_Z).then_some(FLOOR_COL)
2064        });
2065        let v_up = GridView::from_single_vxl(&upper);
2066        let v_lo = GridView::from_single_vxl(&lower);
2067        // Z-stack: index (dz*chunks_y+dy)*chunks_x+dx → [upper, lower].
2068        let chunks = [Some(v_up), Some(v_lo)];
2069        let cg = crate::ChunkGrid {
2070            chunks: &chunks,
2071            origin_chunk_xy: [0, 0],
2072            origin_chunk_z: 0,
2073            chunks_x: 1,
2074            chunks_y: 1,
2075            chunks_z: 2,
2076        };
2077        let grid = GridView::from_chunk_grid(&cg, 32);
2078
2079        // Camera in the upper chunk (world z=100), looking straight down.
2080        let cam = Camera {
2081            pos: [16.0, 16.0, 100.0],
2082            right: [1.0, 0.0, 0.0],
2083            down: [0.0, 1.0, 0.0],
2084            forward: [0.0, 0.0, 1.0],
2085        };
2086        let (w, h) = (48u32, 48u32);
2087        let (fb, zb) = render_brickmap(grid, &cam, w, h);
2088        let centre = 24 * 48 + 24;
2089        assert!(
2090            fb[centre] & 0x00ff_ffff == FLOOR_COL & 0x00ff_ffff,
2091            "centre ray must reach the lower-chunk floor (got {:08x})",
2092            fb[centre]
2093        );
2094        // Floor world-z = 256 + 40 = 296; camera z = 100 → depth ≈ 196.
2095        let expected = 296.0 - 100.0;
2096        assert!(
2097            (zb[centre] - expected).abs() < 2.0,
2098            "look-down depth {} not ≈ {expected}",
2099            zb[centre]
2100        );
2101    }
2102
2103    /// DDA.4: a floor spanning two side-by-side chunks (chunks_x=2)
2104    /// renders continuously across the chunk-XY seam — hits on both
2105    /// sides, no gap column.
2106    #[test]
2107    fn cross_chunk_xy_floor_is_seamless() {
2108        let mk = || {
2109            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= 20).then_some(0x80_50_50_50))
2110        };
2111        let (c0, c1) = (mk(), mk());
2112        let v0 = GridView::from_single_vxl(&c0);
2113        let v1 = GridView::from_single_vxl(&c1);
2114        let chunks = [Some(v0), Some(v1)];
2115        let cg = crate::ChunkGrid {
2116            chunks: &chunks,
2117            origin_chunk_xy: [0, 0],
2118            origin_chunk_z: 0,
2119            chunks_x: 2,
2120            chunks_y: 1,
2121            chunks_z: 1,
2122        };
2123        let grid = GridView::from_chunk_grid(&cg, 32);
2124
2125        // High above the seam (x=32), looking straight down.
2126        let cam = Camera {
2127            pos: [32.0, 16.0, 4.0],
2128            right: [1.0, 0.0, 0.0],
2129            down: [0.0, 1.0, 0.0],
2130            forward: [0.0, 0.0, 1.0],
2131        };
2132        let (w, h) = (64u32, 64u32);
2133        let mask = render_mask(grid, &cam, w, h);
2134        // Both the left chunk (screen left) and right chunk (screen
2135        // right) must show floor on the centre row.
2136        let row = (h / 2) as usize * w as usize;
2137        let left = (0..w as usize / 2).filter(|&x| mask[row + x]).count();
2138        let right = (w as usize / 2..w as usize)
2139            .filter(|&x| mask[row + x])
2140            .count();
2141        assert!(
2142            left > 5 && right > 5,
2143            "seam not continuous: left={left} right={right}"
2144        );
2145    }
2146
2147    /// Render `grid` from `camera` at render `mip` and return the hit
2148    /// mask.
2149    fn render_mask_mip(grid: GridView<'_>, camera: &Camera, w: u32, h: u32, mip: u32) -> Vec<bool> {
2150        let n = (w as usize) * (h as usize);
2151        let mut fb = vec![0u32; n];
2152        let mut zb = vec![f32::INFINITY; n];
2153        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2154        {
2155            let mut sink = RasterSink::new(&mut fb, &mut zb);
2156            render_dda(
2157                camera,
2158                &settings,
2159                grid,
2160                w as usize,
2161                &DdaEnv::default(),
2162                mip,
2163                &mut sink,
2164            );
2165        }
2166        fb.iter().map(|&c| c != 0).collect()
2167    }
2168
2169    /// DDA.6: rendering a mip-built grid at a coarse mip stays complete
2170    /// (hole-free silhouette) with roughly the same screen coverage as
2171    /// mip 0 — LOD coarsens detail, it doesn't punch holes or shrink the
2172    /// shape. (DDA has no axis-aligned mip beam — the artifact is
2173    /// structurally impossible with honest per-cell traversal.)
2174    #[test]
2175    fn mip_render_is_coarse_but_complete() {
2176        let mut vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
2177            let surf = 24 + ((x / 3 + y / 5) % 17);
2178            (z >= surf).then_some(0x80_50_70_90)
2179        });
2180        vxl.generate_mips(4);
2181        assert!(vxl.mip_count() >= 3, "need mips built for this test");
2182        let grid = GridView::from_single_vxl(&vxl);
2183        let (w, h) = (96u32, 96u32);
2184        let cam = Camera::orbit(0.7, 0.6, 110.0, [32.0, 32.0, 36.0]);
2185
2186        let m0 = render_mask_mip(grid, &cam, w, h, 0);
2187        let m2 = render_mask_mip(grid, &cam, w, h, 2);
2188
2189        let c0 = m0.iter().filter(|&&b| b).count();
2190        let c2 = m2.iter().filter(|&&b| b).count();
2191        assert!(c0 > 200 && c2 > 200, "both mips visible (c0={c0} c2={c2})");
2192        // Coverage within ~30 % — a coarse-mip silhouette closely tracks
2193        // the fine one (LOD coarsens detail, it doesn't lose the shape).
2194        // (Terrain silhouettes are non-convex — sky shows through
2195        // valleys — so a hole-free invariant doesn't apply here; that's
2196        // the convex single-voxel test's job.)
2197        let ratio = c2 as f32 / c0 as f32;
2198        assert!(
2199            (0.7..1.4).contains(&ratio),
2200            "mip-2 coverage {c2} vs mip-0 {c0} (ratio {ratio:.2}) diverged"
2201        );
2202    }
2203
2204    /// Headless perf bench (run: `cargo test -p roxlap-core --release
2205    /// dda::tests::bench_terrain -- --ignored --nocapture`). Single-
2206    /// thread `render_dda` over a hilly chunk at a horizon pose; prints
2207    /// ms/frame + per-frame traversal counters (cells / bricks /
2208    /// surface_color calls) to locate the bottleneck.
2209    #[test]
2210    #[ignore = "perf benchmark — run explicitly with --ignored"]
2211    fn bench_terrain() {
2212        use std::time::Instant;
2213        // Multi-chunk grid like the demo: NC×NC chunks of 128, hills.
2214        const NC: i32 = 6;
2215        let cs = crate::grid_view::CHUNK_SIZE_Z; // 256, but vsid is 128
2216        let _ = cs;
2217        let mut vxls: Vec<roxlap_formats::vxl::Vxl> = Vec::new();
2218        for cy in 0..NC {
2219            for cx in 0..NC {
2220                let (ox, oy) = (cx * 128, cy * 128);
2221                let mut v = roxlap_formats::vxl::Vxl::from_dense(128, |x, y, z| {
2222                    let (gx, gy) = (ox + x as i32, oy + y as i32);
2223                    let surf = 90 + ((gx / 7 + gy / 9).rem_euclid(40)) + ((gx / 23).rem_euclid(20));
2224                    (z as i32 >= surf).then_some(0x80_50_70_90 + (x ^ y) % 0x30)
2225                });
2226                v.generate_mips(4);
2227                vxls.push(v);
2228            }
2229        }
2230        let views: Vec<Option<GridView>> = vxls
2231            .iter()
2232            .map(|v| Some(GridView::from_single_vxl(v)))
2233            .collect();
2234        let cg = crate::ChunkGrid {
2235            chunks: &views,
2236            origin_chunk_xy: [0, 0],
2237            origin_chunk_z: 0,
2238            chunks_x: NC as u32,
2239            chunks_y: NC as u32,
2240            chunks_z: 1,
2241        };
2242        let grid = GridView::from_chunk_grid(&cg, 128);
2243
2244        let (w, h) = (960u32, 600u32);
2245        let mut settings = OpticastSettings::for_oracle_framebuffer(w, h);
2246        settings.max_scan_dist = 512;
2247        let n = (w * h) as usize;
2248        let mut fb = vec![0u32; n];
2249        let mut zb = vec![f32::INFINITY; n];
2250        let centre = [f64::from(NC * 128) / 2.0, f64::from(NC * 128) / 2.0, 60.0];
2251
2252        // Two poses: eye-level toward horizon (long rays) + looking down
2253        // at nearby terrain (short rays, demo-typical).
2254        let poses = [
2255            (
2256                "horizon",
2257                Camera::from_yaw_pitch([20.0, 20.0, 40.0], 0.6, 0.15),
2258            ),
2259            ("down", Camera::orbit(0.7, 1.0, 130.0, centre)),
2260        ];
2261        for (name, cam) in poses {
2262            {
2263                let mut sink = RasterSink::new(&mut fb, &mut zb);
2264                prof::reset();
2265                render_dda(
2266                    &cam,
2267                    &settings,
2268                    grid,
2269                    w as usize,
2270                    &DdaEnv::default(),
2271                    0,
2272                    &mut sink,
2273                );
2274            }
2275            let (cells, bricks, surf) = prof::read();
2276            let iters = 6;
2277            let t0 = Instant::now();
2278            for _ in 0..iters {
2279                let mut sink = RasterSink::new(&mut fb, &mut zb);
2280                render_dda(
2281                    &cam,
2282                    &settings,
2283                    grid,
2284                    w as usize,
2285                    &DdaEnv::default(),
2286                    0,
2287                    &mut sink,
2288                );
2289            }
2290            let ms = t0.elapsed().as_secs_f64() * 1000.0 / f64::from(iters);
2291            let hits = fb.iter().filter(|&&c| c != 0).count();
2292            eprintln!(
2293                "[{name}] {w}x{h} 1-thread: {ms:.1} ms | hits={hits}/{n} | per-px: cells={:.1} bricks={:.1} surf={:.1}",
2294                cells as f64 / n as f64,
2295                bricks as f64 / n as f64,
2296                surf as f64 / n as f64,
2297            );
2298        }
2299    }
2300
2301    /// DDA.7: the tile-parallel driver is bit-identical to the
2302    /// sequential one — DDA pixels are independent, so banding can't
2303    /// change a pixel.
2304    #[test]
2305    fn parallel_matches_sequential() {
2306        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
2307            let surf = 28 + ((x / 4 + y / 6) % 13);
2308            (z >= surf).then_some(0x80_40_60_80 + (x ^ y) % 0x30)
2309        });
2310        let grid = GridView::from_single_vxl(&vxl);
2311        let (w, h) = (96u32, 96u32);
2312        let cam = Camera::orbit(0.8, 0.55, 100.0, [32.0, 32.0, 40.0]);
2313        let env = DdaEnv {
2314            sky: None,
2315            fog_color: 0x00_20_30_40,
2316            fog_max_dist: 120.0,
2317            side_shades: [0, 0, 0, 0, 0x30, 0x10],
2318            materials: None,
2319            terrain_materials: &[],
2320        };
2321
2322        let (seq_fb, seq_zb) = render_brickmap_env(grid, &cam, w, h, &env);
2323
2324        let n = (w * h) as usize;
2325        let mut par_fb = vec![0u32; n];
2326        let mut par_zb = vec![f32::INFINITY; n];
2327        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2328        let (cache, mip) = local_cache(&grid, 0);
2329        render_dda_parallel(
2330            &cam,
2331            &settings,
2332            grid,
2333            &mut par_fb,
2334            &mut par_zb,
2335            w as usize,
2336            &env,
2337            &cache,
2338            mip,
2339        );
2340        assert!(par_fb == seq_fb, "parallel colour differs from sequential");
2341        assert!(
2342            par_zb
2343                .iter()
2344                .zip(&seq_zb)
2345                .all(|(a, b)| a.to_bits() == b.to_bits()),
2346            "parallel depth differs from sequential"
2347        );
2348    }
2349
2350    /// DDA.2 correctness: a heightmap column's interior is solid even
2351    /// though voxlap only stores a colour for its surface. `voxel_color`
2352    /// returns `None` for an interior voxel, but `surface_color` must
2353    /// return the run's surface colour — otherwise oblique rays striking
2354    /// a cliff *side* would pass straight through (see-through terrain).
2355    #[test]
2356    fn cliff_side_is_solid_not_see_through() {
2357        const TOP_Z: u32 = 50;
2358        const COL: u32 = 0x80_77_88_99;
2359        let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |_, _, z| (z >= TOP_Z).then_some(COL));
2360        let grid = GridView::from_single_vxl(&vxl);
2361
2362        // Surface voxel: coloured directly.
2363        assert_eq!(grid.voxel_color(4, 4, TOP_Z), Some(COL));
2364        // Interior voxel: voxlap stores no colour …
2365        assert_eq!(grid.voxel_color(4, 4, 150), None);
2366        // … but it is solid, and surface_color bleeds the run-top colour
2367        // down the cliff face → a real hit, not see-through.
2368        assert_eq!(grid.surface_color(4, 4, 150), Some(COL));
2369        // Bedrock-style air above the surface stays air.
2370        assert_eq!(grid.surface_color(4, 4, 10), None);
2371    }
2372
2373    /// DDA.2: a camera embedded in solid material hits its own voxel
2374    /// immediately — every ray reports a hit (no skip / no garbage).
2375    #[test]
2376    fn camera_inside_solid_hits_everywhere() {
2377        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, _| Some(0x80_55_55_55));
2378        let grid = GridView::from_single_vxl(&vxl);
2379        let cam = Camera {
2380            pos: [8.0, 8.0, 128.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 (w, h) = (32u32, 32u32);
2386        let mask = render_mask(grid, &cam, w, h);
2387        assert!(
2388            mask.iter().all(|&b| b),
2389            "every ray must hit when the camera is inside solid"
2390        );
2391    }
2392
2393    /// Headline DDA.1 gate: a single solid voxel viewed obliquely
2394    /// projects to a convex silhouette with **no interior holes** —
2395    /// the artifact class (`tiny_grid_1x1x1` silhouette notch) the
2396    /// voxlap renderer cannot avoid. DDA casts independent per-pixel
2397    /// rays, so the silhouette is hole-free by construction.
2398    #[test]
2399    fn single_voxel_silhouette_has_no_notch() {
2400        const C: u32 = 0x80_FF_80_40;
2401        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |x, y, z| {
2402            (x == 8 && y == 8 && z == 8).then_some(C)
2403        });
2404        let grid = GridView::from_single_vxl(&vxl);
2405
2406        // Orbit the voxel centre obliquely so all three faces show and
2407        // the silhouette is a sizeable hexagon (dist 4 → ~12 px wide).
2408        let cam = Camera::orbit(0.7, 0.6, 4.0, [8.5, 8.5, 8.5]);
2409        let (w, h) = (96u32, 96u32);
2410        let mask = render_mask(grid, &cam, w, h);
2411
2412        let hits = mask.iter().filter(|&&b| b).count();
2413        assert!(
2414            hits > 30,
2415            "silhouette too small to be meaningful: {hits} px"
2416        );
2417        assert!(
2418            rows_have_no_holes(&mask, w, h),
2419            "row-interior gap in single-voxel silhouette (notch)"
2420        );
2421        assert!(
2422            cols_have_no_holes(&mask, w, h),
2423            "column-interior gap in single-voxel silhouette (notch)"
2424        );
2425    }
2426}