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