Skip to main content

roxlap_core/
dda.rs

1//! Per-pixel 3D-DDA + brickmap CPU renderer (Substage DDA).
2//!
3//! This is the clean-room replacement for the voxlap-derived
4//! column-coherent opticast pipeline (`opticast` + `grouscan` +
5//! `scan_loops`). Every pixel casts one independent ray, so none of
6//! the column/row-coherence stitching artifacts of the 2.5D voxlap
7//! renderer can occur (silhouette notch, floor hairlines, axis-aligned
8//! mip beams, cross-chunk virtual-column complexity). See
9//! `PORTING-DDA.md` for the full stage plan.
10//!
11//! **Stage status — DDA.6 (per-grid distance mip) + DDA.7 (tile
12//! parallelism).** Each pixel casts one ray over the grid's full voxel
13//! box ([`GridView::voxel_bounds`], spanning every chunk in XY **and**
14//! Z) via a 3D-DDA (Amanatides–Woo). A uniform render mip (chosen per
15//! grid by LOD distance, clamped by [`effective_mip`] to a level every
16//! chunk has built) coarsens the cell size to `2^mip` mip-0 voxels and
17//! samples mip-`mip` data — the ray stays in mip-0 units so depth and
18//! fog are exact. `BrickMaps` (one occupancy map per populated chunk,
19//! at the render mip) are built once per frame and shared immutably; a
20//! `Sampler` resolves each cell to its chunk
21//! ([`GridView::chunk_at_xyz`]) and brick-gates the
22//! [`GridView::surface_color_mip`] slab walk, caching the current chunk
23//! so air costs an O(1) bit test. [`render_dda_parallel`] splits the
24//! frame into disjoint rayon bands — bit-identical to sequential since
25//! pixels are independent. Hits are shaded by baked brightness
26//! (`shade`) + [`DdaEnv::side_shades`] face tint, fogged toward
27//! [`DdaEnv::fog_color`] (`apply_fog`); misses sample the
28//! [`DdaEnv::sky`] panorama (`sample_sky`) or keep the solid pre-fill.
29//!
30//! Buffer conventions match the rest of the engine so this backend is
31//! colour is packed `0x80RRGGBB`; depth is perpendicular distance from
32//! the camera with **smaller = closer** (so the scene compositor's
33//! min-z merge works directly on the z-buffer this writes).
34
35use std::collections::HashMap;
36
37use rayon::prelude::*;
38
39use crate::camera_math::{self, CameraState};
40use crate::grid_view::GridView;
41use crate::opticast::OpticastSettings;
42use crate::raster_target::RasterTarget;
43use crate::sky::Sky;
44use crate::Camera;
45use roxlap_formats::material::{material_for_color, Material, MaterialTable};
46use roxlap_formats::Rgb;
47
48/// Per-frame environment for DDA shading (Substage DDA.5): a textured
49/// sky panorama, distance fog, and per-face side shading.
50///
51/// [`DdaEnv::default`] disables all three — flat baked-brightness hits
52/// and a caller-pre-filled solid sky — so the brickmap/dense equivalence
53/// tests run against an unchanged pipeline.
54#[derive(Clone, Copy)]
55pub struct DdaEnv<'a> {
56    /// Textured sky sampled per-ray-direction on a miss. `None` leaves
57    /// the destination untouched (caller's solid sky pre-fill shows).
58    pub sky: Option<&'a Sky>,
59    /// Fog target colour (`0x__RRGGBB`); hits blend toward it with
60    /// distance. Typically the sky colour so terrain fades into the sky.
61    pub fog_color: u32,
62    /// Depth at which fog is fully opaque. `<= 0` disables fog.
63    pub fog_max_dist: f32,
64    /// Per-face brightness reduction `[x-, x+, y-, y+, z-, z+]`, applied
65    /// to the hit face (voxlap `setsideshades`). All-zero = off.
66    pub side_shades: [i8; 6],
67    /// TV: global voxel-material palette (id → opacity + blend mode). `None`
68    /// keeps terrain fully opaque (the first-hit path, bit-identical).
69    pub materials: Option<&'a MaterialTable>,
70    /// TV: terrain colour→material map (`(rgb, material_id)`). A hit voxel's
71    /// colour is looked up here for its material. **Empty** (the default) ⇒
72    /// every voxel is opaque, so the march returns the first hit unchanged.
73    pub terrain_materials: &'a [(Rgb, u8)],
74    /// CPU.1 — dynamic lighting (stage DL on the CPU): sun + point lights +
75    /// stylized cel/ramp, evaluated flat per voxel. Disabled by default ⇒ the
76    /// hit uses the baked-byte `shade` path, byte-identical to pre-DL. Lights
77    /// here are already in the grid's **local** frame (the scene renderer
78    /// transforms them per grid). Shadows: see [`Self::world_shadow`].
79    pub lights: CpuLights<'a>,
80    /// XS.1 — when set, shadow rays test the **whole scene** (all grids +
81    /// sprites) via this world-space occluder + the current grid's
82    /// local→world transform, instead of the single-grid `SamplerShadow`.
83    /// `None` ⇒ single-grid shadows (the direct `render_dda` path / tests).
84    pub world_shadow: Option<WorldShadowCtx<'a>>,
85    /// CA.0 — per-grid cutaway clip (stage CA), grid-local **absolute**
86    /// voxel z (the same space as [`GridView::voxel_bounds`], spanning
87    /// stacked chz chunks; z-down). `Some(z)` makes the renderer treat
88    /// every cell with `z < z_clip` as air — the isometric "deck view"
89    /// cut; `z_clip` itself is the first visible layer, whose top face
90    /// renders as the cut surface (colour = the existing
91    /// [`GridView::surface_color_mip`] run-top fallback). Render-only:
92    /// simulation / collision / audio are untouched. `None` (the
93    /// default) disables clipping — byte-identical to pre-CA renders.
94    pub z_clip: Option<i32>,
95    /// OC.0 — per-grid derived view cutout (stage OC): the screen-space
96    /// "keyhole" through front walls for third-person play. Unlike
97    /// [`Self::z_clip`] (grid state, "world as if removed") this is
98    /// transient per-frame VIEW state: primary rays only — shadow
99    /// marches, occluders, collision and gameplay raycasts never see
100    /// it. `None` (the default) is byte-identical to pre-OC renders.
101    pub cutout: Option<CpuCutout>,
102    /// FW.2 — per-hit fog-of-war styling (stage FW). When set, each
103    /// solid hit is classified by grid-local voxel into a
104    /// [`FowVerdict`]: `Hide` cells read as air (the observer never saw
105    /// them — the marcher continues, like [`Self::cutout`]); `Show`
106    /// cells are dimmed / desaturated and may suppress the dynamic-light
107    /// rig (memory renders the frozen BAKED look). Applied to the
108    /// fog-of-war *twin* grid only (the scene renderer sets it per grid).
109    /// `None` (the default) is byte-identical to pre-FW renders. See
110    /// [`FowStyler`].
111    pub fow: Option<&'a dyn FowStyler>,
112}
113
114/// FW.2 — how one hit cell is shown, from its fog-of-war state.
115#[derive(Clone, Copy, Debug, PartialEq)]
116pub enum FowVerdict {
117    /// The observer never saw this cell — treat it as air (the marcher
118    /// continues past it, so within-chunk unseen geometry the twin
119    /// copied for a neighbour cell stays hidden).
120    Hide,
121    /// The observer knows this cell — shade and show it.
122    Show {
123        /// Whether the dynamic-light rig runs. `false` = the frozen
124        /// BAKED look only (memory / heard: a live torch must not
125        /// relight a remembered room).
126        dynamic: bool,
127        /// Brightness multiplier `0..=1` (the smooth FOV-edge / memory
128        /// taper). `1.0` = full.
129        dim: f32,
130        /// Desaturation `0..=1`, lerp of RGB toward luma (cold memory).
131        /// `0.0` = full colour.
132        desaturate: f32,
133    },
134}
135
136impl FowVerdict {
137    /// `Show { dynamic: true, dim: 1, desaturate: 0 }` — a fully-visible
138    /// cell rendered exactly as with no fog (the byte-identity anchor).
139    pub const LIVE: Self = Self::Show {
140        dynamic: true,
141        dim: 1.0,
142        desaturate: 0.0,
143    };
144}
145
146/// FW.2 — per-cell fog-of-war classifier the DDA consults at each hit.
147/// Implemented in `roxlap-scene` over a `FogOfWar` mask + the render
148/// styling; kept as a trait here so `roxlap-core` needs no dependency
149/// on the scene graph (mirrors how [`CpuLights`] borrows scene-built
150/// data). Called once per resolved hit (mip-0 grid-local voxel coords).
151///
152/// `Send + Sync`: the per-tile DDA render fans out over rayon, so the
153/// borrowed styler is shared across worker threads (read-only).
154pub trait FowStyler: Send + Sync {
155    /// Verdict for a hit at grid-local **mip-0** voxel `(x, y, z)`.
156    fn verdict(&self, x: i32, y: i32, z: i32) -> FowVerdict;
157}
158
159/// FW.2 — apply a [`FowVerdict::Show`]'s dim + desaturate to a shaded
160/// ARGB colour (high byte preserved). Identity when `dim == 1` and
161/// `desaturate == 0`, so a fully-visible cell is bit-exact.
162#[must_use]
163pub fn fow_style(color: u32, dim: f32, desaturate: f32) -> u32 {
164    if dim == 1.0 && desaturate == 0.0 {
165        return color;
166    }
167    let hi = color & 0xff00_0000;
168    let r = ((color >> 16) & 0xff) as f32;
169    let g = ((color >> 8) & 0xff) as f32;
170    let b = (color & 0xff) as f32;
171    let luma = 0.299 * r + 0.587 * g + 0.114 * b;
172    let mix = |c: f32| ((c + (luma - c) * desaturate) * dim).clamp(0.0, 255.0) as u32;
173    hi | (mix(r) << 16) | (mix(g) << 8) | mix(b)
174}
175
176/// OC.0 — the view cutout as one grid's render pass consumes it, fully
177/// derived by the facade each frame (stage OC; see
178/// `FrameParams::view_cutout` in `roxlap-render`). A cell is hidden
179/// iff ALL of: its CENTRE lies inside the view cone around the
180/// eye→focus axis (the cone tapers across the feather band), its
181/// eye distance is under the nearest character-column point's minus
182/// [`margin`](Self::margin), and its grid-local z is above the focus
183/// plane (`z < focus_z`, z-down) — walls between the camera and the
184/// focus cut, the floor in front of the focus stays.
185///
186/// **Per-CELL, not per-pixel** (the OC.3 visual-pass round-2
187/// redesign): the original rule gated each RAY by a screen-space
188/// window, so a cell straddling the window edge rendered for the
189/// outside pixels only — sub-cell fragments that read as ragged teeth
190/// wherever the boundary crossed a floor or wall at a shallow angle.
191/// Classifying whole cells by their centres makes the cut edge
192/// cube-granular and spatially coherent, exactly like the CA clip's
193/// edges.
194#[derive(Clone, Copy, Debug, PartialEq)]
195pub struct CpuCutout {
196    /// The focus point in this grid's local frame, **voxel units**
197    /// (the same space the ray origin lives in).
198    pub focus_local: [f32; 3],
199    /// Tangent of the cone's OUTER half-angle (nothing is cut outside
200    /// it). The facade derives it from the keyhole's pixel radius and
201    /// the frame's focal length (`radius_px / hz`), so it still means
202    /// "a screen circle of ~radius_px around the projected focus".
203    pub tan_outer: f32,
204    /// Tangent of the full-reveal INNER half-angle
205    /// (`(radius − feather)/hz`): between the two the reveal distance
206    /// tapers linearly to zero, closing the keyhole into a smooth
207    /// funnel. `== tan_outer` gives a hard edge.
208    pub tan_inner: f32,
209    /// How far short of the character COLUMN the reveal stops, in this
210    /// grid's **voxel units** (world units `/vws`). The reveal surface
211    /// hugs the column — the vertical segment at the focus xy between
212    /// the focus plane ([`Self::focus_z`], the feet) and its mirror
213    /// above the focus (the head): a cell is hidden while its eye
214    /// distance is under the eye distance of the NEAREST column point
215    /// at the cell's own height, minus this margin. A fixed
216    /// eye-distance sphere through the focus (the previous rule) kept
217    /// waist-down stumps of any obstacle the character stood right
218    /// behind — below-chest cells in front sit FARTHER from an
219    /// elevated eye than the chest itself.
220    pub margin: f32,
221    /// Grid-local **absolute** voxel z of the focus plane (mip-0,
222    /// z-down, spanning stacked chz chunks — the same space as
223    /// [`Self::z_clip`](DdaEnv::z_clip)); cells with `z < focus_z`
224    /// qualify for hiding. At mip N the compare uses `focus_z >> mip`
225    /// (arithmetic shift = floor), the CA.1 formula. This is the
226    /// FIRST, cheap gate in the hit branch: `i32::MIN`-like sentinels
227    /// keep the disabled cost at one never-true compare.
228    pub focus_z: i32,
229}
230
231/// CPU.1 — one point light in a grid's local frame for the CPU renderer.
232#[derive(Clone, Copy)]
233pub struct CpuPointLight {
234    /// Grid-local position (world/voxel units).
235    pub pos: [f32; 3],
236    /// Linear RGB, 0..1.
237    pub color: [f32; 3],
238    /// Scalar multiplier on `color` — the light's contribution is
239    /// `albedo · color · intensity · N·L · falloff` per channel.
240    /// `1.0` is nominal; values above it over-drive (the sum clamps at
241    /// pack time).
242    pub intensity: f32,
243    /// Hard cutoff distance (world/voxel units).
244    pub radius: f32,
245    /// CPU.2 — whether this light casts a hard shadow (a shadow ray
246    /// marches to the light through the grid's voxels). Mirrors the
247    /// GPU's per-light `casts_shadow`; the renderer applies the same
248    /// caster cap before building the CPU rig.
249    pub casts_shadow: bool,
250    /// SL — spot (cone) axis: grid-local unit direction the light shines
251    /// **along**. Ignored for a pure point light (see [`Self::cos_outer`]).
252    pub spot_dir: [f32; 3],
253    /// SL — cosine of the inner cone half-angle (full brightness within it).
254    pub cos_inner: f32,
255    /// SL — cosine of the outer cone half-angle (zero past it; soft
256    /// `smoothstep` between the two). `-1.0` (a 180° cone) ⇒ a pure point
257    /// light: the cone mask is skipped entirely and the light is omnidirectional.
258    pub cos_outer: f32,
259}
260
261/// CPU.1 — the per-frame dynamic-light environment for one grid (grid-local).
262/// Mirror of the GPU `shade_lit` inputs. `enabled == false` (the default)
263/// keeps the baked-byte path. CPU.2 adds hard voxel shadows (sun + flagged
264/// point lights) via a per-(voxel,face) shadow march; `shadow_strength == 0`
265/// (the [`Default`]) leaves the lighting diffuse-only.
266#[derive(Clone, Copy, Default)]
267pub struct CpuLights<'a> {
268    /// Whether dynamic lighting is active this frame (else the baked path).
269    pub enabled: bool,
270    /// Whether the sun is present.
271    pub sun: bool,
272    /// Grid-local unit direction **to** the sun.
273    pub sun_dir: [f32; 3],
274    /// Sun colour, linear RGB 0..1.
275    pub sun_color: [f32; 3],
276    /// Scalar multiplier on `sun_color` (the sun term is
277    /// `albedo · sun_color · sun_intensity · key`). `1.0` is nominal.
278    pub sun_intensity: f32,
279    /// CPU.2 — whether the sun casts a hard shadow.
280    pub sun_casts_shadow: bool,
281    /// Grid-local point lights.
282    pub points: &'a [CpuPointLight],
283    /// Ambient multiplier on the baked byte (smooth mode's fill).
284    pub ambient: [f32; 3],
285    /// Cel band count: 0 = smooth, ≥1 = quantize + gradient-map (stylized).
286    pub bands: u32,
287    /// Stylized ramp's cool unlit-end tint (used when `bands > 0`).
288    pub shadow_tint: [f32; 3],
289    /// CPU.2 — fraction of a caster's light removed where a shadow ray is
290    /// occluded (`0` ⇒ shadows off, `1` ⇒ full black). A shadowed sample
291    /// keeps `1 - shadow_strength` of that caster.
292    pub shadow_strength: f32,
293    /// CPU.2 — shadow-ray origin bias along the surface normal, voxel
294    /// units (kills self-shadow acne). ~1.5 is a good default.
295    pub shadow_bias: f32,
296    /// CPU.2 — sun shadow-ray length cap, voxel units (point-light rays
297    /// stop at the light instead).
298    pub shadow_max_dist: f32,
299}
300
301impl Default for DdaEnv<'_> {
302    fn default() -> Self {
303        Self {
304            sky: None,
305            fog_color: 0,
306            fog_max_dist: 0.0,
307            side_shades: [0; 6],
308            materials: None,
309            terrain_materials: &[],
310            lights: CpuLights::default(),
311            world_shadow: None,
312            z_clip: None,
313            cutout: None,
314            fow: None,
315        }
316    }
317}
318
319/// Per-pixel output target for the DDA renderer.
320///
321/// Abstracts "where does a ray hit go" so the traversal core stays
322/// free of framebuffer mechanics. The production impl is
323/// [`RasterSink`] (raw fb/zb pointers); tests use a recording sink.
324/// Only *hits* are reported — misses (sky) leave the destination
325/// untouched, matching the caller-pre-fills-sky convention.
326pub trait PixelSink {
327    /// Record a ray hit at framebuffer index `idx` (`py * pitch + px`)
328    /// with packed ARGB `color` and perpendicular `dist` (smaller =
329    /// closer).
330    fn put(&mut self, idx: usize, color: u32, dist: f32);
331}
332
333/// [`PixelSink`] over a borrowed `(framebuffer, zbuffer)` pair.
334///
335/// Wraps a [`RasterTarget`] so the DDA path writes through the same
336/// raw-pointer mechanism the scalar rasterizer uses — which keeps the
337/// door open for the same strip/tile-disjoint parallel writes in
338/// DDA.7.
339pub struct RasterSink<'a> {
340    target: RasterTarget<'a>,
341    len: usize,
342}
343
344impl<'a> RasterSink<'a> {
345    /// Build a sink from exclusive framebuffer + zbuffer borrows.
346    /// Both slices must have the same length (the pixel count).
347    #[must_use]
348    pub fn new(framebuffer: &'a mut [u32], zbuffer: &'a mut [f32]) -> Self {
349        debug_assert_eq!(framebuffer.len(), zbuffer.len());
350        let len = framebuffer.len();
351        Self {
352            target: RasterTarget::new(framebuffer, zbuffer),
353            len,
354        }
355    }
356}
357
358impl PixelSink for RasterSink<'_> {
359    fn put(&mut self, idx: usize, color: u32, dist: f32) {
360        if idx < self.len {
361            // SAFETY: bounds checked above; single-threaded writer in
362            // DDA.0 so the disjoint-write invariant holds trivially.
363            unsafe {
364                self.target.write_color(idx, color);
365                self.target.write_depth(idx, dist);
366            }
367        }
368    }
369}
370
371/// A resolved ray hit: surface colour + perpendicular distance.
372#[derive(Debug, Clone, Copy)]
373struct Hit {
374    color: u32,
375    dist: f32,
376}
377
378/// Test-only per-thread traversal counters for the perf bench.
379#[cfg(test)]
380pub(crate) mod prof {
381    use std::cell::Cell;
382    thread_local! {
383        pub static CELLS: Cell<u64> = const { Cell::new(0) };
384        pub static BRICKS: Cell<u64> = const { Cell::new(0) };
385        pub static SURF: Cell<u64> = const { Cell::new(0) };
386    }
387    pub fn reset() {
388        CELLS.with(|x| x.set(0));
389        BRICKS.with(|x| x.set(0));
390        SURF.with(|x| x.set(0));
391    }
392    pub fn read() -> (u64, u64, u64) {
393        (
394            CELLS.with(Cell::get),
395            BRICKS.with(Cell::get),
396            SURF.with(Cell::get),
397        )
398    }
399}
400
401/// Apply the voxel's baked directional brightness (Substage DDA.5).
402///
403/// Voxlap (and the GPU marcher, `grid_dda.wgsl`) store per-voxel
404/// brightness in the colour's high byte on a `0..128` scale — `0x80`
405/// is full brightness — written by `Grid::bake_lightmode` (estnorm
406/// directional shading). The shaded channel is `c · a / 128`, so the
407/// DDA matches the GPU look; an unbaked / full-bright voxel (`a =
408/// 0x80`) passes through unchanged. Output alpha is normalised to
409/// `0x80` (the standard "lit" flag; the present blit ignores it).
410///
411/// The renderer only *reads* the baked byte — it computes no normals
412/// itself, so per-impact relight is free (re-bake the chunk and the
413/// byte updates). The estnorm bake that produces the byte is the
414/// voxlap-derived piece slated for a clean-room rewrite in DDA.10.
415///
416/// `bright_sub` is the per-face `side_shades` reduction (DDA.5): voxlap
417/// subtracts it from the brightness byte before the multiply, so a
418/// shaded face is uniformly darker. `0` = no side shading.
419#[inline]
420pub(crate) fn shade(color: u32, bright_sub: u32) -> u32 {
421    let a = ((color >> 24) & 0xff).saturating_sub(bright_sub);
422    let ch = |shift: u32| -> u32 { ((((color >> shift) & 0xff) * a) >> 7).min(255) };
423    0x8000_0000 | (ch(16) << 16) | (ch(8) << 8) | ch(0)
424}
425
426/// EV.1 — shade an **emissive** voxel: the baked brightness byte, the
427/// per-face side shade and the dynamic rig are all ignored; the albedo
428/// is scaled by `(128 + (emissive >> 1)) / 128` on the same `>> 7`
429/// fixed-point ladder as [`shade`] — ~1.0× at `emissive = 0..=1` up to
430/// ~2.0× over-bright (channel-clamped) at `255`. Fog is applied by the
431/// caller as for any other hit.
432#[inline]
433pub(crate) fn emissive_shade(color: u32, emissive: u8) -> u32 {
434    let a = 128 + u32::from(emissive >> 1);
435    let ch = |shift: u32| -> u32 { ((((color >> shift) & 0xff) * a) >> 7).min(255) };
436    0x8000_0000 | (ch(16) << 16) | (ch(8) << 8) | ch(0)
437}
438
439// CPU.1 — cel quantization: snap a 0..1 factor to `bands + 1` levels.
440#[inline]
441fn cel_band(x: f32, bands: u32) -> f32 {
442    let b = bands as f32;
443    ((x * b).round() / b).clamp(0.0, 1.0)
444}
445
446// CPU.1 — point-light distance falloff (mirror of the GPU's): smooth
447// quadratic from 1 at the light to 0 at `radius`, hard-cut beyond.
448#[inline]
449fn point_falloff(d: f32, radius: f32) -> f32 {
450    let x = (1.0 - d / radius).clamp(0.0, 1.0);
451    x * x
452}
453
454// SL — Hermite `smoothstep` (mirror of WGSL's), with a defined hard-edge case:
455// when `edge0 == edge1` WGSL is undefined, so we step at the shared threshold.
456#[inline]
457fn smoothstep_scalar(edge0: f32, edge1: f32, x: f32) -> f32 {
458    if edge1 <= edge0 {
459        return if x < edge0 { 0.0 } else { 1.0 };
460    }
461    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
462    t * t * (3.0 - 2.0 * t)
463}
464
465// SL — spot (cone) angular mask (mirror of the shaders' `spot_cone`). `ldir` is
466// the unit direction from the surface TO the light; `axis` the cone axis (the
467// way the light shines). Returns 1.0 for a pure point light (`cos_outer <=
468// -0.999`, the 180° degenerate); else a soft `smoothstep` from 0 at the outer
469// half-angle to 1 at the inner (hard step when the two coincide).
470#[inline]
471fn spot_cone(ldir: [f32; 3], axis: [f32; 3], cos_inner: f32, cos_outer: f32) -> f32 {
472    if cos_outer <= -0.999 {
473        return 1.0;
474    }
475    let cd = -dot3(ldir, axis);
476    smoothstep_scalar(cos_outer, cos_inner, cd)
477}
478
479// CPU.1 — face normal (grid-local) from the crossed axis + step: points back
480// toward the incoming ray. `axis == 3` (entry voxel, no face) falls back to up
481// (-z, voxlap z-down).
482#[inline]
483fn face_normal_cpu(axis: usize, step: [i32; 3]) -> [f32; 3] {
484    let mut n = [0.0f32; 3];
485    if axis < 3 {
486        n[axis] = -(step[axis] as f32);
487    } else {
488        n[2] = -1.0;
489    }
490    n
491}
492
493#[inline]
494fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
495    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
496}
497
498/// CPU.2 — a hard-shadow occlusion test for the dynamic-lighting shade.
499/// `occluded(origin, dir, max_t)` returns `true` if a solid voxel blocks
500/// the segment from `origin` (grid-local, already biased off the surface)
501/// in unit direction `dir` within `max_t` voxel units. Terrain hits pass
502/// a [`SamplerShadow`] (marches the current grid only) or a
503/// [`WorldShadow`] (cross-grid + sprites, XS.1/XS.2); sprites that don't
504/// cast/receive shadows pass `None`.
505pub(crate) trait ShadowTester {
506    fn occluded(&mut self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool;
507}
508
509/// XS.1 — a **world-space** occlusion oracle over the whole scene (all grids,
510/// and sprites in XS.2). Implemented in `roxlap-scene` (it needs the grid /
511/// sprite stores); the CPU DDA reaches it through [`DdaEnv::world_shadow`] so
512/// shadow rays cross grid + object boundaries instead of stopping at the
513/// current grid. `occluded_world(origin, dir, max_t)` is in **world** voxel
514/// units: `true` iff any solid voxel anywhere blocks the segment.
515///
516/// SC.2 — `origin` is **f64**: a scaled scene puts a fine grid's voxels at
517/// large world coordinates, and each occluder re-projects the ray into its
518/// own voxel frame (dividing by that grid's `vws`), so the world origin must
519/// keep full precision through the lift. `dir` stays f32 (a unit-ish
520/// direction); implementations narrow to f32 only at their voxel-frame entry.
521///
522/// `Sync` because [`DdaEnv`] (which borrows it) is shared across the
523/// rayon strip workers in [`render_dda_parallel`]; the occluder is a
524/// read-only borrow of the scene, so this holds.
525pub trait WorldOccluder: Sync {
526    /// `true` iff any solid voxel in the scene blocks the segment from
527    /// world-space `origin` (f64) in unit direction `dir` within `max_t`
528    /// world voxel units.
529    fn occluded_world(&self, origin: [f64; 3], dir: [f32; 3], max_t: f32) -> bool;
530}
531
532/// XS.1 — per-grid context for a cross-scene shadow query: the scene-wide
533/// [`WorldOccluder`] plus the **current grid's** local→world transform, so a
534/// grid-local shadow ray (the frame `shade_dynamic` works in) can be lifted
535/// to world space before the scene-wide test. `cols[i]` is the world-space
536/// image of grid-local axis `i` (the grid rotation's columns); `origin` is the
537/// grid's world origin.
538#[derive(Clone, Copy)]
539pub struct WorldShadowCtx<'a> {
540    /// The scene-wide occlusion oracle every lifted shadow ray is
541    /// tested against.
542    pub occluder: &'a dyn WorldOccluder,
543    /// The current grid's world-space origin — the translation part of
544    /// its local→world transform (added to rotated positions;
545    /// directions skip it). SC.2 — **f64**: kept full-precision so a
546    /// scaled grid's large world coordinates survive the lift (see
547    /// [`WorldOccluder`]).
548    pub origin: [f64; 3],
549    /// Columns of the grid's local→world rotation: `cols[i]` is the
550    /// world-space image of grid-local axis `i`. Kept **orthonormal** (pure
551    /// rotation) — the grid's voxel scale rides in `voxel_world_size`, not
552    /// folded into the columns.
553    pub cols: [[f32; 3]; 3],
554    /// SC.2 — the caster grid's `voxel_world_size` (world units per voxel).
555    /// The shade works in the grid's voxel frame, so lifting a shadow ray to
556    /// world scales the local **position** and **direction** by this (a
557    /// voxel offset becomes `vws` world units). `1.0` for an unscaled grid
558    /// (byte-identical to XS.1) and for already-world-space sprites.
559    pub voxel_world_size: f32,
560}
561
562impl<'a> WorldShadowCtx<'a> {
563    /// Identity transform — for shading already in world space (sprites): the
564    /// grid-local ray IS the world ray.
565    #[must_use]
566    pub fn identity(occluder: &'a dyn WorldOccluder) -> Self {
567        Self {
568            occluder,
569            origin: [0.0; 3],
570            cols: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
571            voxel_world_size: 1.0,
572        }
573    }
574}
575
576/// XS.2 — a [`WorldOccluder`] that ORs two others (e.g. the grid occluder +
577/// the sprite occluder), so a single shadow query covers both. `true` if
578/// either blocks the ray.
579pub struct CompositeOccluder<'a> {
580    /// First occluder — queried first (short-circuits `b` on a hit).
581    pub a: &'a dyn WorldOccluder,
582    /// Second occluder — queried only when `a` reports no hit.
583    pub b: &'a dyn WorldOccluder,
584}
585
586impl WorldOccluder for CompositeOccluder<'_> {
587    fn occluded_world(&self, origin: [f64; 3], dir: [f32; 3], max_t: f32) -> bool {
588        self.a.occluded_world(origin, dir, max_t) || self.b.occluded_world(origin, dir, max_t)
589    }
590}
591
592/// XS.1 — [`ShadowTester`] that lifts a grid-local shadow ray to world space
593/// (via [`WorldShadowCtx`]) and queries the scene-wide [`WorldOccluder`], so
594/// occlusion crosses grid + sprite boundaries. Sprites (already world-space)
595/// use an identity [`WorldShadowCtx`] (see [`WorldShadowCtx::identity`]).
596pub(crate) struct WorldShadow<'a> {
597    pub ctx: WorldShadowCtx<'a>,
598}
599
600impl ShadowTester for WorldShadow<'_> {
601    fn occluded(&mut self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool {
602        let c = &self.ctx.cols;
603        // SC.2 — the shade works in the caster grid's VOXEL frame. Lift to
604        // world: a voxel offset becomes `vws` world units, so the local
605        // position scales by vws before rotate+translate, and the ray
606        // direction scales by vws too (the world segment then covers
607        // `t · vws` world units — `max_t` is a caster-voxel distance). The
608        // occluder side divides back out by its own vws, so a grid shadowing
609        // ITSELF is scale-invariant and vws==1.0 is byte-identical to XS.1.
610        let s = self.ctx.voxel_world_size;
611        let l = [origin[0] * s, origin[1] * s, origin[2] * s];
612        // world = grid_origin + R · (local · vws) (R columns = `cols`). The
613        // rotated-local term is small (voxel offset), so it's summed in f32;
614        // the large `origin` stays f64 so `wo` keeps full world precision.
615        let wo = [
616            self.ctx.origin[0] + f64::from(c[0][0] * l[0] + c[1][0] * l[1] + c[2][0] * l[2]),
617            self.ctx.origin[1] + f64::from(c[0][1] * l[0] + c[1][1] * l[1] + c[2][1] * l[2]),
618            self.ctx.origin[2] + f64::from(c[0][2] * l[0] + c[1][2] * l[1] + c[2][2] * l[2]),
619        ];
620        // dir rotates then scales by vws (skips the origin translation).
621        let wd = [
622            (c[0][0] * dir[0] + c[1][0] * dir[1] + c[2][0] * dir[2]) * s,
623            (c[0][1] * dir[0] + c[1][1] * dir[1] + c[2][1] * dir[2]) * s,
624            (c[0][2] * dir[0] + c[1][2] * dir[1] + c[2][2] * dir[2]) * s,
625        ];
626        self.ctx.occluder.occluded_world(wo, wd, max_t)
627    }
628}
629
630/// CPU.1 — dynamic-lighting shade for a terrain voxel (the CPU mirror of the
631/// GPU `shade_lit`): raw albedo × (ambient/AO + sun + point lights), evaluated
632/// **flat per voxel** (at the voxel centre, so a whole face reads one tone —
633/// the retro look). `bands > 0` quantizes (cel) and gradient-maps the sun key
634/// from `shadow_tint` (cool) to the sun colour (warm). **No shadows.** Returns
635/// a packed `0x80RRGGBB` colour (same convention as [`shade`]).
636fn shade_lit_cpu(
637    color: u32,
638    bright_sub: u32,
639    axis: usize,
640    step: [i32; 3],
641    cellc: [i32; 3],
642    cell_size: f32,
643    l: &CpuLights<'_>,
644    shadow: Option<&mut dyn ShadowTester>,
645) -> u32 {
646    let a_b = ((color >> 24) & 0xff).saturating_sub(bright_sub);
647    let ao = a_b as f32 / 128.0;
648    let albedo = [
649        ((color >> 16) & 0xff) as f32 / 255.0,
650        ((color >> 8) & 0xff) as f32 / 255.0,
651        (color & 0xff) as f32 / 255.0,
652    ];
653    let n = face_normal_cpu(axis, step);
654    // Voxel centre (grid-local) — flat per-voxel sample point.
655    let center = [
656        (cellc[0] as f32 + 0.5) * cell_size,
657        (cellc[1] as f32 + 0.5) * cell_size,
658        (cellc[2] as f32 + 0.5) * cell_size,
659    ];
660    shade_dynamic(albedo, ao, n, center, l, shadow)
661}
662
663/// CPU.1/DL.7 — the shared dynamic-lighting core (terrain + sprites): raw
664/// `albedo` × (ambient/AO + sun + point lights), sampled **flat per voxel**
665/// at `sample` with surface normal `n`. `bands > 0` quantizes (cel) and
666/// gradient-maps the sun key from `shadow_tint` (cool) to the sun colour
667/// (warm). **No shadows** (GPU-only). Returns a packed `0x80RRGGBB` colour.
668pub(crate) fn shade_dynamic(
669    albedo: [f32; 3],
670    ao: f32,
671    n: [f32; 3],
672    sample: [f32; 3],
673    l: &CpuLights<'_>,
674    shadow: Option<&mut dyn ShadowTester>,
675) -> u32 {
676    let styled = l.bands > 0;
677    // CPU.2 — shadow ray origin: bias off the surface along the normal to
678    // avoid self-shadow acne (shared by every caster). Light kept in
679    // shadow = `1 - shadow_strength` (1.0 ⇒ shadows effectively off).
680    let mut shadow = shadow;
681    let shadow_origin = [
682        sample[0] + n[0] * l.shadow_bias,
683        sample[1] + n[1] * l.shadow_bias,
684        sample[2] + n[2] * l.shadow_bias,
685    ];
686    let in_shadow = 1.0 - l.shadow_strength;
687
688    // Sun key (0..1): N·L × shadow factor.
689    let sun_key = if l.sun {
690        let ndl = dot3(n, l.sun_dir).max(0.0);
691        if ndl > 0.0 && l.sun_casts_shadow {
692            let occ = shadow
693                .as_deref_mut()
694                .is_some_and(|s| s.occluded(shadow_origin, l.sun_dir, l.shadow_max_dist));
695            if occ {
696                ndl * in_shadow
697            } else {
698                ndl
699            }
700        } else {
701            ndl
702        }
703    } else {
704        0.0
705    };
706
707    // Base term: ambient + sun. Smooth = additive; stylized = gradient map.
708    let mut lit = if styled {
709        let key = cel_band(sun_key, l.bands);
710        let m = |i: usize| {
711            let warm = l.sun_color[i] * l.sun_intensity;
712            (l.shadow_tint[i] + (warm - l.shadow_tint[i]) * key) * ao
713        };
714        [albedo[0] * m(0), albedo[1] * m(1), albedo[2] * m(2)]
715    } else {
716        let base = |i: usize| {
717            albedo[i] * l.ambient[i] * ao + albedo[i] * l.sun_color[i] * l.sun_intensity * sun_key
718        };
719        [base(0), base(1), base(2)]
720    };
721
722    // Point lights (flat per voxel). CPU.2 — a flagged caster's shadow ray
723    // marches to the light; an occluded sample keeps `in_shadow` of it.
724    for p in l.points {
725        let d3 = [
726            p.pos[0] - sample[0],
727            p.pos[1] - sample[1],
728            p.pos[2] - sample[2],
729        ];
730        // PF.7 (C4) — squared-distance reject first: the sqrt only runs
731        // for lights actually within radius (same thresholds squared).
732        let d2 = d3[0] * d3[0] + d3[1] * d3[1] + d3[2] * d3[2];
733        if d2 < p.radius * p.radius && d2 > 1e-8 {
734            let dist = d2.sqrt();
735            let inv = 1.0 / dist;
736            let ldir = [d3[0] * inv, d3[1] * inv, d3[2] * inv];
737            let ndl = dot3(n, ldir).max(0.0);
738            // SL — spot cone mask (1.0 for a pure point light). Computed
739            // before the shadow march so an off-cone spot skips it entirely.
740            let cone = spot_cone(ldir, p.spot_dir, p.cos_inner, p.cos_outer);
741            if ndl > 0.0 && cone > 0.0 {
742                // Shadow ray marches from the surface to the light (`dist`).
743                let sh = if p.casts_shadow
744                    && shadow
745                        .as_deref_mut()
746                        .is_some_and(|s| s.occluded(shadow_origin, ldir, dist))
747                {
748                    in_shadow
749                } else {
750                    1.0
751                };
752                let mut f = ndl * point_falloff(dist, p.radius) * cone * sh;
753                if styled {
754                    f = cel_band(f, l.bands);
755                }
756                for i in 0..3 {
757                    lit[i] += albedo[i] * p.color[i] * p.intensity * f;
758                }
759            }
760        }
761    }
762
763    let pack = |v: f32| -> u32 { (v.clamp(0.0, 1.0) * 255.0) as u32 };
764    0x8000_0000 | (pack(lit[0]) << 16) | (pack(lit[1]) << 8) | pack(lit[2])
765}
766
767/// Blend `color` toward `env.fog_color` by perpendicular `depth`
768/// (linear, fully fogged at `env.fog_max_dist`). No-op when fog is
769/// disabled (`fog_max_dist <= 0`).
770#[inline]
771fn apply_fog(color: u32, depth: f32, env: &DdaEnv<'_>) -> u32 {
772    if env.fog_max_dist <= 0.0 {
773        return color;
774    }
775    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
776    let f = ((depth / env.fog_max_dist).clamp(0.0, 1.0) * 256.0) as u32; // 0..256
777    let g = 256 - f;
778    let fog = env.fog_color;
779    let mix = |shift: u32| -> u32 {
780        let src = (color >> shift) & 0xff;
781        let dst = (fog >> shift) & 0xff;
782        ((src * g + dst * f) >> 8).min(255)
783    };
784    0x8000_0000 | (mix(16) << 16) | (mix(8) << 8) | mix(0)
785}
786
787/// Composite premultiplied `accum` (+ remaining `trans`) over a packed
788/// background colour → packed `0x80RRGGBB`.
789#[inline]
790fn composite_over(accum: [f32; 3], trans: f32, bg: u32) -> u32 {
791    let b = rgb_to_f32(bg);
792    f32_to_rgb([
793        accum[0] + trans * b[0],
794        accum[1] + trans * b[1],
795        accum[2] + trans * b[2],
796    ])
797}
798
799/// Finalize a translucent terrain ray that exited the grid (sky). Returns
800/// `None` when nothing was accumulated (the opaque first-hit path — the
801/// caller's sky handling stands, bit-identical), else the accumulated
802/// layers composited over the sky at `dist`.
803#[inline]
804fn finalize_exit(
805    touched: bool,
806    accum: [f32; 3],
807    trans: f32,
808    env: &DdaEnv<'_>,
809    dir: [f32; 3],
810    dist: f32,
811) -> Option<Hit> {
812    if !touched {
813        return None;
814    }
815    let bg = match env.sky {
816        Some(s) => sample_sky(s, dir),
817        None => 0x8000_0000 | (env.fog_color & 0x00ff_ffff),
818    };
819    Some(Hit {
820        color: composite_over(accum, trans, bg),
821        dist,
822    })
823}
824
825/// Unpack `0x__RRGGBB` to `0..1` float channels (RGB; the high byte is
826/// dropped — it has already been folded into the colour by `shade`/`fog`).
827#[inline]
828#[allow(clippy::cast_precision_loss)]
829fn rgb_to_f32(c: u32) -> [f32; 3] {
830    [
831        ((c >> 16) & 0xff) as f32 / 255.0,
832        ((c >> 8) & 0xff) as f32 / 255.0,
833        (c & 0xff) as f32 / 255.0,
834    ]
835}
836
837/// Repack `0..1` float channels (clamped) into `0x80RRGGBB`.
838#[inline]
839#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
840fn f32_to_rgb(c: [f32; 3]) -> u32 {
841    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
842    0x8000_0000 | (q(c[0]) << 16) | (q(c[1]) << 8) | q(c[2])
843}
844
845/// Sample the sky panorama in ray direction `dir` (need not be
846/// normalised), returning a packed `0x80RRGGBB` colour.
847///
848/// Clean-room equirectangular mapping (not voxlap's `lng`/`lat` asm
849/// search): the texture's x axis is elevation (`asin` of the vertical
850/// component), the y axis is azimuth (`atan2` around the vertical). A
851/// `ysiz == 1` panorama (e.g. [`Sky::blue_gradient`]) is a pure
852/// horizon→zenith gradient.
853#[allow(
854    clippy::cast_possible_truncation,
855    clippy::cast_sign_loss,
856    clippy::cast_precision_loss
857)]
858fn sample_sky(sky: &Sky, dir: [f32; 3]) -> u32 {
859    let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
860    if len < 1e-9 {
861        return 0x8000_0000;
862    }
863    let d = [dir[0] / len, dir[1] / len, dir[2] / len];
864    let xsiz_full = sky.lat.len().max(1) as i32; // original column count
865    let pi = std::f32::consts::PI;
866    // Elevation → x, matching the GPU `sky_color` (scene_dda.wgsl): z is
867    // down, so `acos(-z)` is 0 at the zenith (looking up) and π at the nadir
868    // (looking down); `/π` puts the zenith at x=0 and the nadir at x=xsiz.
869    let elev01 = (-d[2]).clamp(-1.0, 1.0).acos() / pi; // 0 (up) .. 1 (down)
870    let x = (elev01 * xsiz_full as f32) as i32;
871    let x = x.clamp(0, xsiz_full - 1);
872    // Azimuth → y (wrapped).
873    let y = if sky.ysiz <= 1 {
874        0
875    } else {
876        let az = d[1].atan2(d[0]); // -pi..pi
877        let yf = ((az / (pi * 2.0)) + 0.5) * sky.ysiz as f32;
878        (yf as i32).rem_euclid(sky.ysiz)
879    };
880    let idx = (y * xsiz_full + x) as usize;
881    let px = sky.pixels.get(idx).copied().unwrap_or(0) as u32;
882    0x8000_0000 | (px & 0x00ff_ffff)
883}
884
885/// Fill the panorama [`Sky`] into every **background** pixel — one whose
886/// z-buffer entry is still `+∞` (no grid/terrain hit). The per-grid DDA only
887/// samples the sky inside each grid's screen rect (and only its sky-owning
888/// grid); pixels outside any grid — most of a sprite/effect-only view, or the
889/// margins around a small world grid — would otherwise keep the caller's flat
890/// clear colour. This paints the real panorama there while leaving terrain
891/// (finite z) and composited translucent pixels untouched. The z-buffer is
892/// not modified. `cam`/`settings` are the same per-frame projection the
893/// renderer used.
894#[allow(clippy::cast_possible_truncation)]
895pub fn render_sky_fill(
896    fb: &mut [u32],
897    zb: &[f32],
898    pitch_pixels: usize,
899    width: u32,
900    height: u32,
901    cam: &CameraState,
902    settings: &OpticastSettings,
903    sky: &Sky,
904) {
905    // PF.7 (C6) — rayon rows: this was a full-frame single-threaded pass
906    // with an `acos` + `atan2` per background pixel (an entire serial
907    // frame on sky-dominant views). Rows are disjoint (`by_ref` chunks of
908    // the framebuffer); `zb` is read-only. Bit-identical.
909    fb.par_chunks_mut(pitch_pixels)
910        .take(height as usize)
911        .enumerate()
912        .for_each(|(py, frow)| {
913            let row = py * pitch_pixels;
914            #[allow(clippy::cast_possible_truncation)]
915            let py = py as u32;
916            for px in 0..width {
917                let idx = row + px as usize;
918                if zb[idx].is_finite() {
919                    continue; // a grid/terrain hit owns this pixel
920                }
921                let (_origin, dir) = pixel_ray(cam, settings, px, py);
922                frow[px as usize] = sample_sky(sky, dir);
923            }
924        });
925}
926
927/// World-space ray for screen pixel `(px, py)` under opticast's
928/// pinhole: origin is the camera position, direction is
929/// `(px - hx)·right + (py - hy)·down + hz·forward`.
930///
931/// This is the exact ray `camera_math::derive` bakes into its corner
932/// vectors (`corn[0]` is `pixel (0, 0)`'s direction), so the DDA
933/// renderer samples the same rays the voxlap path's frustum is built
934/// from. The direction is **not** normalised — callers that need a
935/// unit ray (and a true Euclidean distance) normalise themselves;
936/// DDA.1 will track perpendicular distance via the forward-projection
937/// instead, matching the engine's z-buffer convention.
938#[must_use]
939pub fn pixel_ray(
940    cs: &CameraState,
941    settings: &OpticastSettings,
942    px: u32,
943    py: u32,
944) -> ([f32; 3], [f32; 3]) {
945    // u32 → f32 is exact for any realistic screen coordinate.
946    #[allow(clippy::cast_precision_loss)]
947    let sx = px as f32 - settings.hx;
948    #[allow(clippy::cast_precision_loss)]
949    let sy = py as f32 - settings.hy;
950    let dir = [
951        sx * cs.right[0] + sy * cs.down[0] + settings.hz * cs.forward[0],
952        sx * cs.right[1] + sy * cs.down[1] + settings.hz * cs.forward[1],
953        sx * cs.right[2] + sy * cs.down[2] + settings.hz * cs.forward[2],
954    ];
955    (cs.pos, dir)
956}
957
958/// Ray ↔ axis-aligned box `[lo, hi]` slab test. Returns the
959/// `(t_enter, t_exit)` parameter interval along `dir` (already clamped
960/// so `t_enter >= 0`, i.e. a camera inside the box starts at `t = 0`),
961/// or `None` if the ray misses the box. `dir` need not be normalised —
962/// `t` is in units of `|dir|`.
963pub(crate) fn intersect_aabb(
964    o: [f32; 3],
965    dir: [f32; 3],
966    lo: [f32; 3],
967    hi: [f32; 3],
968) -> Option<(f32, f32)> {
969    let mut t0 = 0.0f32;
970    let mut t1 = f32::INFINITY;
971    for a in 0..3 {
972        if dir[a].abs() < 1e-9 {
973            // Ray parallel to this slab — must already be inside it.
974            if o[a] < lo[a] || o[a] > hi[a] {
975                return None;
976            }
977        } else {
978            let inv = 1.0 / dir[a];
979            let mut ta = (lo[a] - o[a]) * inv;
980            let mut tb = (hi[a] - o[a]) * inv;
981            if ta > tb {
982                core::mem::swap(&mut ta, &mut tb);
983            }
984            t0 = t0.max(ta);
985            t1 = t1.min(tb);
986            if t0 > t1 {
987                return None;
988            }
989        }
990    }
991    Some((t0, t1))
992}
993
994/// Brick edge length in voxels — one occupancy bit per `BRICK³` block.
995const BRICK: i32 = 8;
996
997/// Per-chunk brick occupancy map for two-level DDA empty-space skip
998/// (Substage DDA.3).
999///
1000/// One bit per `BRICK³` block of the active chunk, set iff any voxel in
1001/// the block is solid. The ray steps the coarse brick grid (8× longer
1002/// strides) and only descends into a per-voxel walk inside occupied
1003/// bricks, so a ray through open air crosses ~`length / 8` empty bricks
1004/// instead of `length` air voxels — each of which would otherwise walk
1005/// the column slab chain via `surface_color`.
1006///
1007/// Built per frame from a [`GridView`] in [`render_dda`]. A persistent
1008/// per-chunk cache with edit-driven invalidation (locked decision #2 in
1009/// `PORTING-DDA.md`) is a later perf refinement.
1010#[derive(Debug)]
1011pub(crate) struct BrickMap {
1012    /// Brick counts along x / y / z (one entry per `BRICK³` cells).
1013    nb: [i32; 3],
1014    /// Brick occupancy bitset; brick `(bx, by, bz)` is bit
1015    /// `(bz * nb[1] + by) * nb[0] + bx`.
1016    bits: Vec<u64>,
1017    /// Super-brick counts (one entry per `BRICK³` *bricks* = `SUPER³`
1018    /// cells), `ceil(nb / BRICK)`.
1019    ns: [i32; 3],
1020    /// Super-brick occupancy (DDA.7 perf): a coarse level so a ray
1021    /// through open air above the terrain skips `SUPER` cells per outer
1022    /// step instead of `BRICK`. A super-brick is set iff any child brick
1023    /// is set.
1024    super_bits: Vec<u64>,
1025}
1026
1027/// Super-brick edge in cells (`BRICK` bricks per axis).
1028const SUPER: i32 = BRICK * BRICK;
1029
1030impl BrickMap {
1031    /// Scan every mip-`mip` column of `grid`, building brick + super-
1032    /// brick occupancy. `mip` must be `< grid.mip_count()`.
1033    #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
1034    fn build(grid: &GridView<'_>, mip: u32) -> Self {
1035        let vsid_m = (grid.vsid >> mip).max(1) as i32;
1036        let z_m = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1) as i32;
1037        let nb = [
1038            (vsid_m + BRICK - 1) / BRICK,
1039            (vsid_m + BRICK - 1) / BRICK,
1040            (z_m + BRICK - 1) / BRICK,
1041        ];
1042        let ns = [
1043            (nb[0] + BRICK - 1) / BRICK,
1044            (nb[1] + BRICK - 1) / BRICK,
1045            (nb[2] + BRICK - 1) / BRICK,
1046        ];
1047        let count = (nb[0] * nb[1] * nb[2]) as usize;
1048        let scount = (ns[0] * ns[1] * ns[2]) as usize;
1049        let mut bits = vec![0u64; count.div_ceil(64)];
1050        let mut super_bits = vec![0u64; scount.div_ceil(64)];
1051        for y in 0..vsid_m {
1052            for x in 0..vsid_m {
1053                let (bx, by) = (x / BRICK, y / BRICK);
1054                grid.for_each_run_mip(x as u32, y as u32, mip, |top, bot| {
1055                    for bz in (top / BRICK)..=((bot - 1) / BRICK) {
1056                        let idx = ((bz * nb[1] + by) * nb[0] + bx) as usize;
1057                        bits[idx / 64] |= 1u64 << (idx % 64);
1058                        let sidx =
1059                            (((bz / BRICK) * ns[1] + by / BRICK) * ns[0] + bx / BRICK) as usize;
1060                        super_bits[sidx / 64] |= 1u64 << (sidx % 64);
1061                    }
1062                });
1063            }
1064        }
1065        Self {
1066            nb,
1067            bits,
1068            ns,
1069            super_bits,
1070        }
1071    }
1072
1073    /// Whether brick `b` is in range and holds any solid voxel.
1074    #[inline]
1075    #[allow(clippy::cast_sign_loss)]
1076    fn occupied(&self, b: [i32; 3]) -> bool {
1077        if b[0] < 0
1078            || b[0] >= self.nb[0]
1079            || b[1] < 0
1080            || b[1] >= self.nb[1]
1081            || b[2] < 0
1082            || b[2] >= self.nb[2]
1083        {
1084            return false;
1085        }
1086        let idx = ((b[2] * self.nb[1] + b[1]) * self.nb[0] + b[0]) as usize;
1087        (self.bits[idx / 64] >> (idx % 64)) & 1 != 0
1088    }
1089
1090    /// Whether super-brick `s` is in range and holds any solid voxel.
1091    #[inline]
1092    #[allow(clippy::cast_sign_loss)]
1093    fn occupied_super(&self, s: [i32; 3]) -> bool {
1094        if s[0] < 0
1095            || s[0] >= self.ns[0]
1096            || s[1] < 0
1097            || s[1] >= self.ns[1]
1098            || s[2] < 0
1099            || s[2] >= self.ns[2]
1100        {
1101            return false;
1102        }
1103        let idx = ((s[2] * self.ns[1] + s[1]) * self.ns[0] + s[0]) as usize;
1104        (self.super_bits[idx / 64] >> (idx % 64)) & 1 != 0
1105    }
1106}
1107
1108/// Per-axis 3D-DDA stepping state for a cell size of `cell` voxels.
1109/// `t_max[a]` is the ray parameter at which the next `a`-boundary is
1110/// crossed; `t_delta[a]` is the parameter increment per cell. An
1111/// axis-parallel component gets `t_max = t_delta = +inf` so it's never
1112/// chosen as the stepping axis.
1113pub(crate) fn dda_setup(
1114    origin: [f32; 3],
1115    dir: [f32; 3],
1116    cell: [i32; 3],
1117    cell_size: f32,
1118) -> ([i32; 3], [f32; 3], [f32; 3]) {
1119    let mut step = [0i32; 3];
1120    let mut t_max = [f32::INFINITY; 3];
1121    let mut t_delta = [f32::INFINITY; 3];
1122    for a in 0..3 {
1123        if dir[a] > 1e-9 {
1124            step[a] = 1;
1125            #[allow(clippy::cast_precision_loss)]
1126            let boundary = (cell[a] + 1) as f32 * cell_size;
1127            t_max[a] = (boundary - origin[a]) / dir[a];
1128            t_delta[a] = cell_size / dir[a];
1129        } else if dir[a] < -1e-9 {
1130            step[a] = -1;
1131            #[allow(clippy::cast_precision_loss)]
1132            let boundary = cell[a] as f32 * cell_size;
1133            t_max[a] = (boundary - origin[a]) / dir[a];
1134            t_delta[a] = -cell_size / dir[a];
1135        }
1136    }
1137    (step, t_max, t_delta)
1138}
1139
1140/// Index of the axis with the smallest `t_max` (the next boundary the
1141/// ray crosses).
1142#[inline]
1143pub(crate) fn min_axis(t_max: [f32; 3]) -> usize {
1144    if t_max[0] <= t_max[1] && t_max[0] <= t_max[2] {
1145        0
1146    } else if t_max[1] <= t_max[2] {
1147        1
1148    } else {
1149        2
1150    }
1151}
1152
1153/// Persistent, cross-frame brick occupancy cache (Substage DDA.7
1154/// perf). Keyed by `(chunk x, y, z, mip)` with the chunk's edit
1155/// `version`; an entry is reused until its chunk's version changes, so a
1156/// static / streamed-once world pays **zero** brick-build cost after the
1157/// first frame (the per-frame rebuild was the dominant DDA cost).
1158///
1159/// Owned by the caller across frames (the scene's `Grid`), populated
1160/// single-threaded via [`Self::ensure`], then borrowed immutably by the
1161/// parallel render bands.
1162#[derive(Debug, Default)]
1163pub struct BrickCache {
1164    maps: HashMap<(i32, i32, i32, u32), (u64, BrickMap)>,
1165}
1166
1167impl BrickCache {
1168    /// An empty cache (same as [`Default`]). Entries appear via
1169    /// [`ensure`](Self::ensure) as chunks are first rendered.
1170    #[must_use]
1171    pub fn new() -> Self {
1172        Self::default()
1173    }
1174
1175    /// Ensure a current mip-`mip` brick map exists for `chunk` (built
1176    /// from `view`); rebuilds only when the cached `version` differs.
1177    pub fn ensure(&mut self, chunk: [i32; 3], mip: u32, version: u64, view: &GridView<'_>) {
1178        let key = (chunk[0], chunk[1], chunk[2], mip);
1179        let stale = self.maps.get(&key).is_none_or(|(v, _)| *v != version);
1180        if stale {
1181            self.maps.insert(key, (version, BrickMap::build(view, mip)));
1182        }
1183    }
1184
1185    #[inline]
1186    fn get(&self, chunk: [i32; 3], mip: u32) -> Option<&BrickMap> {
1187        self.maps
1188            .get(&(chunk[0], chunk[1], chunk[2], mip))
1189            .map(|(_, m)| m)
1190    }
1191
1192    /// Drop cached entries whose chunk fails `keep` — bounds memory as
1193    /// streaming evicts chunks. Called once per frame by the scene.
1194    pub fn retain_chunks(&mut self, keep: impl Fn([i32; 3]) -> bool) {
1195        self.maps.retain(|k, _| keep([k.0, k.1, k.2]));
1196    }
1197
1198    /// PF.9 — occupancy of the `BRICK`³ block containing chunk-local
1199    /// mip-`mip` cell `cell` of `chunk`. `None` when no map is cached for
1200    /// that (chunk, mip) — the caller must fall back to dense stepping.
1201    /// `Some(false)` guarantees the whole 8³ block holds no solid voxel,
1202    /// so an external shadow march may skip it wholesale.
1203    #[must_use]
1204    pub fn brick_occupied_at(&self, chunk: [i32; 3], mip: u32, cell: [i32; 3]) -> Option<bool> {
1205        self.get(chunk, mip)
1206            .map(|m| m.occupied([cell[0] >> 3, cell[1] >> 3, cell[2] >> 3]))
1207    }
1208
1209    /// PF.9 — like [`Self::brick_occupied_at`] for the `SUPER`³ (64³ at
1210    /// mip 0) super-brick level.
1211    #[must_use]
1212    pub fn super_occupied_at(&self, chunk: [i32; 3], mip: u32, cell: [i32; 3]) -> Option<bool> {
1213        self.get(chunk, mip)
1214            .map(|m| m.occupied_super([cell[0] >> 6, cell[1] >> 6, cell[2] >> 6]))
1215    }
1216}
1217
1218/// Build a throwaway [`BrickCache`] covering every populated chunk of
1219/// `grid` at the effective mip — for the sequential [`render_dda`] /
1220/// tests, where no persistent cache is threaded in. Returns
1221/// `(cache, effective_mip)`.
1222#[allow(clippy::cast_possible_wrap)]
1223fn local_cache(grid: &GridView<'_>, requested_mip: u32) -> (BrickCache, u32) {
1224    let mip = effective_mip(grid, requested_mip);
1225    let mut cache = BrickCache::new();
1226    if let Some(cg) = grid.chunk_grid {
1227        for dz in 0..cg.chunks_z as i32 {
1228            for dy in 0..cg.chunks_y as i32 {
1229                for dx in 0..cg.chunks_x as i32 {
1230                    let slot = ((dz * cg.chunks_y as i32 + dy) * cg.chunks_x as i32 + dx) as usize;
1231                    if let Some(Some(view)) = cg.chunks.get(slot) {
1232                        let ch = [
1233                            cg.origin_chunk_xy[0] + dx,
1234                            cg.origin_chunk_xy[1] + dy,
1235                            cg.origin_chunk_z + dz,
1236                        ];
1237                        cache.ensure(ch, mip, 0, view);
1238                    }
1239                }
1240            }
1241        }
1242    } else {
1243        cache.ensure([0, 0, 0], mip, 0, grid);
1244    }
1245    (cache, mip)
1246}
1247
1248/// Clamp a requested render mip to one every populated chunk actually
1249/// has built — so the uniform-mip traversal never under-samples a chunk
1250/// that lacks the requested level (which would punch holes). `0` short-
1251/// circuits (always available).
1252#[must_use]
1253pub fn effective_mip(grid: &GridView<'_>, requested: u32) -> u32 {
1254    if requested == 0 {
1255        return 0;
1256    }
1257    let mut m = requested;
1258    if let Some(cg) = grid.chunk_grid {
1259        for c in cg.chunks.iter().flatten() {
1260            m = m.min(c.mip_count().saturating_sub(1));
1261        }
1262    } else {
1263        m = m.min(grid.mip_count().saturating_sub(1));
1264    }
1265    m
1266}
1267
1268/// Cross-chunk voxel sampler (Substage DDA.4 / DDA.7).
1269///
1270/// Resolves a grid-local voxel coordinate to the chunk that owns it
1271/// (via [`GridView::chunk_at_xyz`]) and answers the DDA's per-voxel hit
1272/// query — brick-gated [`GridView::surface_color`]. It borrows the
1273/// shared immutable [`BrickMaps`] and caches the **current chunk**
1274/// (`cur_*`: view + brick-map reference): a ray usually stays in one
1275/// chunk for many voxels, so the per-voxel cost is a single index
1276/// compare + an O(1) brick bit test — no hashing, no mutation. Holding
1277/// only shared borrows, a `Sampler` is cheap to spin up per render band.
1278///
1279/// Single-chunk grids are the degenerate case: every voxel maps to
1280/// chunk `[0, 0, 0]` (= the view itself).
1281struct Sampler<'a> {
1282    grid: GridView<'a>,
1283    bricks: &'a BrickCache,
1284    /// Effective render mip (DDA.6). Traversal cells are mip-`mip`
1285    /// cells; sampling reads mip-`mip` data.
1286    mip: u32,
1287    /// Chunk size in mip-`mip` cells is a power of two; store it as
1288    /// `log2` (shift) + `size - 1` (mask) so [`Self::locate`] splits a
1289    /// cell into `(chunk, in-chunk)` with a shift + an `&` per axis
1290    /// instead of a signed `div_euclid` — the dominant per-cell cost.
1291    /// Arithmetic `>>` floors toward -∞ (= `div_euclid` for a positive
1292    /// power-of-two divisor) and `& mask` gives the non-negative
1293    /// remainder (= `rem_euclid`) even for negative cells (two's
1294    /// complement), so results are identical to the division form.
1295    xy_shift: u32,
1296    xy_mask: i32,
1297    z_shift: u32,
1298    z_mask: i32,
1299    /// CA.1 — cutaway clip plane in **mip-cells** (`z_clip >> mip`;
1300    /// arithmetic shift = floor, matching the GPU formula). Cells with
1301    /// `c[2] < z_clip_mip` read as air. `i32::MIN` = disabled: no real
1302    /// cell index is ever below it, so the gate is branch-cheap with
1303    /// no `Option` unwrap per cell.
1304    z_clip_mip: i32,
1305    cur_ch: [i32; 3],
1306    cur_view: Option<GridView<'a>>,
1307    cur_brick: Option<&'a BrickMap>,
1308    has_cur: bool,
1309}
1310
1311impl<'a> Sampler<'a> {
1312    fn new(grid: GridView<'a>, bricks: &'a BrickCache, mip: u32, z_clip: Option<i32>) -> Self {
1313        let cs_xy = (grid.chunk_size_xy >> mip).max(1);
1314        let cs_z = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1);
1315        debug_assert!(
1316            cs_xy.is_power_of_two() && cs_z.is_power_of_two(),
1317            "chunk dims must be powers of two for the shift/mask split"
1318        );
1319        #[allow(clippy::cast_possible_wrap)]
1320        Self {
1321            grid,
1322            bricks,
1323            mip,
1324            xy_shift: cs_xy.trailing_zeros(),
1325            xy_mask: cs_xy as i32 - 1,
1326            z_shift: cs_z.trailing_zeros(),
1327            z_mask: cs_z as i32 - 1,
1328            // CA.1 — mip-0 voxel z → mip-cells. Arithmetic `>>` floors
1329            // toward -∞ (clip planes can be negative on stacked-chz
1330            // grids), the same formula the GPU marcher uses — CA.3's
1331            // parity gate depends on the two staying identical.
1332            z_clip_mip: z_clip.map_or(i32::MIN, |z| z >> mip),
1333            cur_ch: [0; 3],
1334            cur_view: None,
1335            cur_brick: None,
1336            has_cur: false,
1337        }
1338    }
1339
1340    /// Refresh the current-chunk cache (view + brick map) for `ch`.
1341    fn select_chunk(&mut self, ch: [i32; 3]) {
1342        if self.has_cur && self.cur_ch == ch {
1343            return;
1344        }
1345        self.cur_view = self.grid.chunk_at_xyz(ch);
1346        self.cur_brick = self.bricks.get(ch, self.mip);
1347        self.cur_ch = ch;
1348        self.has_cur = true;
1349    }
1350
1351    /// Split a grid-local **mip-`mip` cell** index into `(chunk index,
1352    /// in-chunk mip-cell)` via shift + mask (see field docs). Chunk
1353    /// indices are mip-independent; only the per-chunk resolution
1354    /// shrinks with mip.
1355    #[allow(clippy::cast_sign_loss)]
1356    fn locate(&self, c: [i32; 3]) -> ([i32; 3], [u32; 3]) {
1357        let ch = [
1358            c[0] >> self.xy_shift,
1359            c[1] >> self.xy_shift,
1360            c[2] >> self.z_shift,
1361        ];
1362        let loc = [
1363            (c[0] & self.xy_mask) as u32,
1364            (c[1] & self.xy_mask) as u32,
1365            (c[2] & self.z_mask) as u32,
1366        ];
1367        (ch, loc)
1368    }
1369
1370    /// Hit colour for grid-local mip-cell `c`, or `None` for air / empty
1371    /// chunk / uncoloured bedrock. Brick-gated so air inside a populated
1372    /// chunk costs only a bit test, not a slab walk.
1373    #[allow(clippy::cast_possible_wrap)]
1374    fn hit(&mut self, c: [i32; 3]) -> Option<u32> {
1375        #[cfg(test)]
1376        prof::SURF.with(|x| x.set(x.get() + 1));
1377        // CA.1 — cutaway: cells above the clip plane (z-down, so
1378        // `z < z_clip`) read as air. `c[2]` is the grid-local ABSOLUTE
1379        // mip-cell z (spanning stacked chz chunks) — testing the
1380        // in-chunk `loc[2]` alone would pass on 1-chunk fixtures and
1381        // break on multi-deck ships. Shadow marches share this gate
1382        // via [`SamplerShadow`], so hidden decks stop casting too.
1383        if c[2] < self.z_clip_mip {
1384            return None;
1385        }
1386        let (ch, loc) = self.locate(c);
1387        self.select_chunk(ch);
1388        let occupied = self.cur_brick.is_some_and(|bm| {
1389            bm.occupied([
1390                loc[0] as i32 / BRICK,
1391                loc[1] as i32 / BRICK,
1392                loc[2] as i32 / BRICK,
1393            ])
1394        });
1395        if !occupied {
1396            return None;
1397        }
1398        self.cur_view?
1399            .surface_color_mip(loc[0], loc[1], loc[2], self.mip)
1400            .map(|c| c.0)
1401    }
1402
1403    /// Chunk size in mip-cells along XY / Z (always a power of two).
1404    #[inline]
1405    fn cells_per_chunk_xy(&self) -> i32 {
1406        1 << self.xy_shift
1407    }
1408    #[inline]
1409    fn cells_per_chunk_z(&self) -> i32 {
1410        1 << self.z_shift
1411    }
1412
1413    /// Whether the brick at brick-index `brick` (in `BRICK`-mip-cell
1414    /// units) holds any solid voxel. Used by the outer brick-DDA to skip
1415    /// empty space `BRICK` cells at a time. Assumes bricks nest within
1416    /// chunks (caller gates on [`Self::cells_per_chunk_xy`]`>= BRICK`).
1417    #[allow(clippy::cast_sign_loss)]
1418    fn brick_occupied(&mut self, brick: [i32; 3]) -> bool {
1419        // First mip-cell of the brick (BRICK = 8 → `<< 3`).
1420        let c0 = [brick[0] << 3, brick[1] << 3, brick[2] << 3];
1421        let ch = [
1422            c0[0] >> self.xy_shift,
1423            c0[1] >> self.xy_shift,
1424            c0[2] >> self.z_shift,
1425        ];
1426        self.select_chunk(ch);
1427        self.cur_brick.is_some_and(|bm| {
1428            bm.occupied([
1429                (c0[0] & self.xy_mask) >> 3,
1430                (c0[1] & self.xy_mask) >> 3,
1431                (c0[2] & self.z_mask) >> 3,
1432            ])
1433        })
1434    }
1435
1436    /// Whether the super-brick at super-index `s` (in `SUPER`-mip-cell
1437    /// units) holds any solid voxel. Outer-most empty-space skip (steps
1438    /// `SUPER` cells). Assumes super-bricks nest in chunks (caller gates
1439    /// on `cells_per_chunk >= SUPER`).
1440    #[allow(clippy::cast_sign_loss)]
1441    fn super_occupied(&mut self, s: [i32; 3]) -> bool {
1442        // First mip-cell of the super-brick (SUPER = 64 → `<< 6`).
1443        let c0 = [s[0] << 6, s[1] << 6, s[2] << 6];
1444        let ch = [
1445            c0[0] >> self.xy_shift,
1446            c0[1] >> self.xy_shift,
1447            c0[2] >> self.z_shift,
1448        ];
1449        self.select_chunk(ch);
1450        self.cur_brick.is_some_and(|bm| {
1451            bm.occupied_super([
1452                (c0[0] & self.xy_mask) >> 6,
1453                (c0[1] & self.xy_mask) >> 6,
1454                (c0[2] & self.z_mask) >> 6,
1455            ])
1456        })
1457    }
1458}
1459
1460/// CPU.2 — safety cap on a shadow ray's voxel steps (the `shadow_max_dist`
1461/// / light-distance bound is the real limit; this only backstops a
1462/// degenerate ray). Mirrors the GPU `shadow_max_steps`.
1463const SHADOW_MAX_STEPS: u32 = 1024;
1464
1465/// CPU.2 — [`ShadowTester`] backed by the render [`Sampler`]: a hard-shadow
1466/// occlusion march over the grid's mip-`mip` voxels. The march reuses the
1467/// same `sampler.hit()` occupancy the primary ray uses (so a shadow ray is
1468/// blocked by the same surfaces the camera sees) and the same `[lo_c, hi_c)`
1469/// voxel-box bounds, stepping a standard 3D-DDA until it hits a solid cell
1470/// (occluded), leaves the box / exceeds `max_t` (lit), or hits the step cap.
1471struct SamplerShadow<'s, 'a, 'f> {
1472    sampler: &'s mut Sampler<'a>,
1473    cell_size: f32,
1474    lo_c: [i32; 3],
1475    hi_c: [i32; 3],
1476    /// FW.2 review #1 — the fog-of-war styler (if any): a solid cell the
1477    /// observer never saw (`Hide`) must NOT block a shadow ray, or its
1478    /// silhouette leaks as a shadow shape on visible geometry. Same
1479    /// classifier the primary ray uses, so shadows and vision agree.
1480    fow: Option<&'f dyn FowStyler>,
1481}
1482
1483impl ShadowTester for SamplerShadow<'_, '_, '_> {
1484    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
1485    fn occluded(&mut self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool {
1486        let cs = self.cell_size;
1487        // PF.9 (C3) — the shadow march gets the primary ray's empty-space
1488        // skip: fast-forward across empty super-bricks / bricks (skipping
1489        // an EMPTY box can never hide an occluder). The step budget is
1490        // consumed in Manhattan cell distance — exactly what the dense
1491        // walk would have spent crossing the same span — so the
1492        // `SHADOW_MAX_STEPS` truncation fires at the identical point and
1493        // hit/no-hit stays bit-compatible with the pre-skip march.
1494        let has_super =
1495            self.sampler.cells_per_chunk_xy() >= SUPER && self.sampler.cells_per_chunk_z() >= SUPER;
1496        let has_brick =
1497            self.sampler.cells_per_chunk_xy() >= BRICK && self.sampler.cells_per_chunk_z() >= BRICK;
1498        let mut cellc = [
1499            (origin[0] / cs).floor() as i32,
1500            (origin[1] / cs).floor() as i32,
1501            (origin[2] / cs).floor() as i32,
1502        ];
1503        let (step, mut t_max, t_delta) = dda_setup(origin, dir, cellc, cs);
1504        let inv = [
1505            if step[0] != 0 { 1.0 / dir[0] } else { 0.0 },
1506            if step[1] != 0 { 1.0 / dir[1] } else { 0.0 },
1507            if step[2] != 0 { 1.0 / dir[2] } else { 0.0 },
1508        ];
1509        let mut t_curr = 0.0f32;
1510        let mut used = 0u32;
1511        while used < SHADOW_MAX_STEPS {
1512            if cellc[0] < self.lo_c[0]
1513                || cellc[0] >= self.hi_c[0]
1514                || cellc[1] < self.lo_c[1]
1515                || cellc[1] >= self.hi_c[1]
1516                || cellc[2] < self.lo_c[2]
1517                || cellc[2] >= self.hi_c[2]
1518            {
1519                return false; // left the voxel box → no occluder ahead
1520            }
1521            if t_curr > max_t {
1522                return false; // past the cap / the light → unshadowed
1523            }
1524            // Empty-space skip (mirrors `cell_walk_skip`'s landing logic:
1525            // the exit axis is pinned to the integer boundary cell so the
1526            // next box's entry cell is always visited densely).
1527            let skip_shift = if has_super
1528                && !self
1529                    .sampler
1530                    .super_occupied([cellc[0] >> 6, cellc[1] >> 6, cellc[2] >> 6])
1531            {
1532                Some(6u32)
1533            } else if has_brick
1534                && !self
1535                    .sampler
1536                    .brick_occupied([cellc[0] >> 3, cellc[1] >> 3, cellc[2] >> 3])
1537            {
1538                Some(3u32)
1539            } else {
1540                None
1541            };
1542            if let Some(sh) = skip_shift {
1543                let mut best_t = f32::INFINITY;
1544                let mut best_axis = 3usize;
1545                let mut plane = [0i32; 3];
1546                for a in 0..3 {
1547                    if step[a] == 0 {
1548                        continue;
1549                    }
1550                    let idx = cellc[a] >> sh;
1551                    plane[a] = if step[a] > 0 {
1552                        (idx + 1) << sh
1553                    } else {
1554                        idx << sh
1555                    };
1556                    let tb = (plane[a] as f32 * cs - origin[a]) * inv[a];
1557                    if tb < best_t {
1558                        best_t = tb;
1559                        best_axis = a;
1560                    }
1561                }
1562                if best_axis == 3 {
1563                    return false;
1564                }
1565                // Landing mirrors `cell_walk_skip`: exit axis pinned to
1566                // the boundary cell, other axes advanced by exact
1567                // crossing counts from t-differences (position
1568                // reconstruction is ill-conditioned at large t).
1569                let mut nc = cellc;
1570                for a in 0..3 {
1571                    if a == best_axis || step[a] == 0 || t_max[a] > best_t {
1572                        continue;
1573                    }
1574                    #[allow(clippy::cast_possible_truncation)]
1575                    let k = ((best_t - t_max[a]) / t_delta[a]) as i32 + 1;
1576                    nc[a] += k * step[a];
1577                    t_max[a] += k as f32 * t_delta[a];
1578                }
1579                nc[best_axis] = if step[best_axis] > 0 {
1580                    plane[best_axis]
1581                } else {
1582                    plane[best_axis] - 1
1583                };
1584                t_max[best_axis] = best_t + t_delta[best_axis];
1585                // Budget: the dense walk would have spent one step per
1586                // cell (Manhattan distance). If it runs out inside the
1587                // empty box the dense walk would have returned `false`
1588                // there — nothing solid to find inside it.
1589                let crossed =
1590                    cellc[0].abs_diff(nc[0]) + cellc[1].abs_diff(nc[1]) + cellc[2].abs_diff(nc[2]);
1591                if used.saturating_add(crossed) >= SHADOW_MAX_STEPS {
1592                    return false;
1593                }
1594                used += crossed;
1595                cellc = nc;
1596                t_curr = best_t.max(t_curr);
1597                continue;
1598            }
1599            if self.sampler.hit(cellc).is_some() && !self.fow_hidden(cellc) {
1600                return true; // a surface (that the observer knows) blocks the ray
1601            }
1602            let axis = min_axis(t_max);
1603            t_curr = t_max[axis];
1604            cellc[axis] += step[axis];
1605            t_max[axis] += t_delta[axis];
1606            used += 1;
1607        }
1608        false
1609    }
1610}
1611
1612impl SamplerShadow<'_, '_, '_> {
1613    /// FW.2 review #1 — is `cellc` (mip-cell index) a fog-Hidden cell?
1614    /// A Hidden solid casts no shadow (unseen geometry stays invisible,
1615    /// including its shadow silhouette). Sampled at the coarse cell's
1616    /// centre, matching the primary ray's lookup.
1617    #[inline]
1618    fn fow_hidden(&self, cellc: [i32; 3]) -> bool {
1619        let Some(styler) = self.fow else {
1620            return false;
1621        };
1622        let mip = self.sampler.mip;
1623        let half = (1i32 << mip) >> 1;
1624        matches!(
1625            styler.verdict(
1626                (cellc[0] << mip) + half,
1627                (cellc[1] << mip) + half,
1628                (cellc[2] << mip) + half,
1629            ),
1630            FowVerdict::Hide
1631        )
1632    }
1633}
1634
1635/// Walk mip-cells along the ray within `[lo_c, hi_c)` and return the
1636/// first solid hit, with leak-free empty-space skipping (DDA.7 redux).
1637///
1638/// **Why one continuous DDA, not nested level-walks.** The previous
1639/// design ran an outer brick/super DDA that *jumped* whole bricks and
1640/// only descended into occupied ones. Stepping a coarse cell at a time
1641/// lets the ray slip diagonally **past an occupied coarse cell it only
1642/// touches at a shared edge/corner** — a leak that showed as bright
1643/// sky seams across thin diagonal walls (the cave-demo report). Here a
1644/// *single* cell-granularity DDA carries the exact `(cellc, t_max)`
1645/// state for the whole ray; it only ever **fast-forwards across an
1646/// empty super-brick / brick**, where skipping cannot miss anything.
1647/// The exit axis lands on the integer box-boundary cell (no float
1648/// re-floor on the critical axis), so the entry cell of the next —
1649/// possibly occupied — box is always visited densely. Result: hits are
1650/// bit-identical to the dense per-cell reference, with the empty-space
1651/// speed-up retained.
1652///
1653/// `cell_size` is the mip-cell edge in mip-0 voxels (`1 << mip`);
1654/// `fwd_dot = dir·forward` → perpendicular depth.
1655#[allow(
1656    clippy::too_many_arguments,
1657    clippy::cast_possible_truncation,
1658    clippy::cast_sign_loss,
1659    clippy::cast_precision_loss
1660)]
1661fn cell_walk_skip(
1662    origin: [f32; 3],
1663    dir: [f32; 3],
1664    fwd_dot: f32,
1665    sampler: &mut Sampler<'_>,
1666    lo_c: [i32; 3],
1667    hi_c: [i32; 3],
1668    cell_size: f32,
1669    t_enter: f32,
1670    t_exit: f32,
1671    max_dist: f32,
1672    env: &DdaEnv<'_>,
1673) -> Option<Hit> {
1674    let has_super = sampler.cells_per_chunk_xy() >= SUPER && sampler.cells_per_chunk_z() >= SUPER;
1675    let has_brick = sampler.cells_per_chunk_xy() >= BRICK && sampler.cells_per_chunk_z() >= BRICK;
1676    // OC.1 — the cutout's focus plane in mip-cells (arithmetic `>> mip`
1677    // floors — CA.1's exact formula; the OC.2 parity gate pins this
1678    // against the GPU kernel's pair). `i32::MIN` when no cutout: no
1679    // real cell index is ever below it, mirroring `z_clip_mip` — the
1680    // whole per-cell cone test below stays behind this one compare.
1681    let cut_z_mip = env.cutout.map_or(i32::MIN, |c| c.focus_z >> sampler.mip);
1682    // FW.2 — mip shift + half-cell offset to turn a hit's mip-cell index
1683    // into the mip-0 grid-local voxel at the coarse cell's CENTRE for the
1684    // fog-of-war lookup (review #2: sampling the low corner let a coarse
1685    // cell straddling a deck ceiling classify as out-of-band → leak, and
1686    // a Visible cell with an Unseen corner pop). Copied out so the
1687    // per-hit `.filter` closure captures the values, not `sampler`.
1688    let fow_mip_shift = sampler.mip;
1689    let fow_mip_half = (1i32 << fow_mip_shift) >> 1;
1690    // OC.1 — the keyhole cone axis: eye → focus, unit, grid-local.
1691    // Ray-invariant (`origin` is the camera for every primary ray),
1692    // computed once per ray for free. A focus at the eye degenerates
1693    // to "cut nothing" (the facade's margin fold prevents it anyway).
1694    let cut_axis = env.cutout.map_or([0.0; 3], |c| {
1695        let d = [
1696            c.focus_local[0] - origin[0],
1697            c.focus_local[1] - origin[1],
1698            c.focus_local[2] - origin[2],
1699        ];
1700        let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
1701        if len > 1e-6 {
1702            [d[0] / len, d[1] / len, d[2] / len]
1703        } else {
1704            [0.0; 3]
1705        }
1706    });
1707
1708    let start = t_enter + 1e-4;
1709    let p = [
1710        origin[0] + dir[0] * start,
1711        origin[1] + dir[1] * start,
1712        origin[2] + dir[2] * start,
1713    ];
1714    let mut cellc = [
1715        ((p[0] / cell_size).floor() as i32).clamp(lo_c[0], hi_c[0] - 1),
1716        ((p[1] / cell_size).floor() as i32).clamp(lo_c[1], hi_c[1] - 1),
1717        ((p[2] / cell_size).floor() as i32).clamp(lo_c[2], hi_c[2] - 1),
1718    ];
1719    let (step, mut t_max, t_delta) = dda_setup(origin, dir, cellc, cell_size);
1720    // Reciprocal direction → the per-skip box-boundary t and the t_max
1721    // refresh use multiplies instead of divisions (the dominant skip
1722    // cost). `0.0` where `step == 0` (that axis' t_max stays +∞).
1723    let inv = [
1724        if step[0] != 0 { 1.0 / dir[0] } else { 0.0 },
1725        if step[1] != 0 { 1.0 / dir[1] } else { 0.0 },
1726        if step[2] != 0 { 1.0 / dir[2] } else { 0.0 },
1727    ];
1728    let mut t_curr = t_enter;
1729    let mut last_axis = 3usize;
1730    // World ray length per ray-parameter unit; divided by `cell_size` it turns
1731    // a cell's `t` span into its path length in voxel units (Volumetric weight).
1732    let dir_len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
1733    // PF.7 (C4) — hoisted out of the hit block: "does anything cast a
1734    // shadow?" is ray-invariant, and the O(lights) `any()` scan ran per
1735    // hit (per translucent layer on glass/water rays).
1736    let shadow_casts = env.lights.enabled
1737        && env.lights.shadow_strength > 0.0
1738        && (env.lights.sun_casts_shadow || env.lights.points.iter().any(|p| p.casts_shadow));
1739
1740    // TV: front-to-back translucent accumulation. While no translucent voxel
1741    // is hit (`touched` stays false) every return is unchanged — the opaque
1742    // world renders bit-identically. `prev_*` drive per-span compositing (one
1743    // alpha layer per contiguous solid run or material change).
1744    let mut accum = [0.0f32; 3];
1745    let mut trans = 1.0f32;
1746    let mut touched = false;
1747    let mut prev_solid = false;
1748    let mut prev_mat = 0u8;
1749
1750    // Each iteration either advances ≥1 cell (dense) or ≥1 box (skip),
1751    // so the total cell span bounds the loop.
1752    let span = (hi_c[0] - lo_c[0]) + (hi_c[1] - lo_c[1]) + (hi_c[2] - lo_c[2]);
1753    let max_steps = span.max(0) as usize + 16;
1754    for _ in 0..max_steps {
1755        if cellc[0] < lo_c[0]
1756            || cellc[0] >= hi_c[0]
1757            || cellc[1] < lo_c[1]
1758            || cellc[1] >= hi_c[1]
1759            || cellc[2] < lo_c[2]
1760            || cellc[2] >= hi_c[2]
1761        {
1762            return finalize_exit(touched, accum, trans, env, dir, max_dist);
1763        }
1764        let depth = t_curr * fwd_dot;
1765        if depth > max_dist || t_curr > t_exit {
1766            return finalize_exit(touched, accum, trans, env, dir, max_dist);
1767        }
1768        // Fog is fully opaque at `fog_max_dist`: nothing beyond is
1769        // visible, so stop the ray there and return the fog colour
1770        // rather than traversing (and skip/step-counting) to the far box
1771        // wall. Both correct and the dominant perf win for foggy worlds —
1772        // it caps every ray's length at the fog distance.
1773        if env.fog_max_dist > 0.0 && depth >= env.fog_max_dist {
1774            let fog = 0x8000_0000 | (env.fog_color & 0x00ff_ffff);
1775            let color = if touched {
1776                composite_over(accum, trans, fog)
1777            } else {
1778                fog
1779            };
1780            return Some(Hit {
1781                color,
1782                dist: env.fog_max_dist,
1783            });
1784        }
1785
1786        // Empty-space skip: a whole empty super-brick, else an empty
1787        // brick. Skipping only empty boxes can never miss a surface.
1788        let skip_shift = if has_super
1789            && !sampler.super_occupied([cellc[0] >> 6, cellc[1] >> 6, cellc[2] >> 6])
1790        {
1791            Some(6u32)
1792        } else if has_brick
1793            && !sampler.brick_occupied([cellc[0] >> 3, cellc[1] >> 3, cellc[2] >> 3])
1794        {
1795            Some(3u32)
1796        } else {
1797            None
1798        };
1799        if let Some(sh) = skip_shift {
1800            #[cfg(test)]
1801            prof::BRICKS.with(|x| x.set(x.get() + 1));
1802            // Nearest box boundary along the ray (in cell units).
1803            let mut best_t = f32::INFINITY;
1804            let mut best_axis = 3usize;
1805            let mut plane = [0i32; 3];
1806            for a in 0..3 {
1807                if step[a] == 0 {
1808                    continue;
1809                }
1810                let idx = cellc[a] >> sh;
1811                plane[a] = if step[a] > 0 {
1812                    (idx + 1) << sh
1813                } else {
1814                    idx << sh
1815                };
1816                let tb = (plane[a] as f32 * cell_size - origin[a]) * inv[a];
1817                if tb < best_t {
1818                    best_t = tb;
1819                    best_axis = a;
1820                }
1821            }
1822            if best_axis == 3 {
1823                return finalize_exit(touched, accum, trans, env, dir, max_dist);
1824            }
1825            // Land just across the boundary. The exit axis is pinned to
1826            // the integer boundary cell; the OTHER axes advance by their
1827            // exact crossing COUNT, derived from t-DIFFERENCES (t_max vs
1828            // best_t) — well-conditioned at any camera distance. (The
1829            // old landing re-floored an absolute position `origin +
1830            // dir·(best_t + 1e-4)`: at tele-camera distances (t ≈
1831            // 1000+) the reconstruction's cancellation noise ≈ 2·ulp(t)
1832            // exceeds the fixed nudge, landing rays one cell off
1833            // sideways — dark seam lines / sky pinholes along brick
1834            // boundaries on distant mip-0 geometry, the CA.5 report.)
1835            let mut nc = cellc;
1836            for a in 0..3 {
1837                if a == best_axis || step[a] == 0 || t_max[a] > best_t {
1838                    continue;
1839                }
1840                // Crossings of axis `a` at t ≤ best_t: t_max[a] is the
1841                // next one, spaced t_delta[a] apart.
1842                #[allow(clippy::cast_possible_truncation)]
1843                let k = ((best_t - t_max[a]) / t_delta[a]) as i32 + 1;
1844                nc[a] += k * step[a];
1845                t_max[a] += k as f32 * t_delta[a];
1846            }
1847            nc[best_axis] = if step[best_axis] > 0 {
1848                plane[best_axis]
1849            } else {
1850                plane[best_axis] - 1
1851            };
1852            t_max[best_axis] = best_t + t_delta[best_axis];
1853            // The skip crossed a box boundary; if that takes the ray out
1854            // of the grid box it has exited (sky) — return rather than
1855            // clamping back in-bounds, which would spin at the edge.
1856            if nc[0] < lo_c[0]
1857                || nc[0] >= hi_c[0]
1858                || nc[1] < lo_c[1]
1859                || nc[1] >= hi_c[1]
1860                || nc[2] < lo_c[2]
1861                || nc[2] >= hi_c[2]
1862            {
1863                return finalize_exit(touched, accum, trans, env, dir, max_dist);
1864            }
1865            cellc = nc;
1866            // t_max was advanced per axis above (crossing counts), so
1867            // it is already consistent with the landing cell — no
1868            // position-based refresh (that reconstruction is exactly
1869            // the noise source this landing avoids).
1870            t_curr = best_t.max(t_curr);
1871            last_axis = best_axis;
1872            prev_solid = false; // skipped empty space → next hit starts a run
1873            continue;
1874        }
1875
1876        // Occupied brick: dense per-cell surface test.
1877        #[cfg(test)]
1878        prof::CELLS.with(|x| x.set(x.get() + 1));
1879        // OC.1 — keyhole hide rule (view cutout), decided per CELL by
1880        // its centre (whole cubes in or out — no sub-cell fragments;
1881        // see [`CpuCutout`]): a HIT cell above the focus plane whose
1882        // centre lies inside the tapered eye→focus cone closer than
1883        // the character column reads as air for this PRIMARY ray only
1884        // (`SamplerShadow` shares `sampler.hit`, so shadows still see
1885        // the wall — a view aid, not CA's "world as if removed").
1886        // Order matters twice: the occupancy/surface test runs FIRST,
1887        // so the cone math (two sqrt) prices only actual solid hits —
1888        // never the far more numerous marched air cells above the
1889        // plane; and inside the filter the z compare leads, never-true
1890        // when the cutout is off (`cut_z_mip == i32::MIN`).
1891        // OC keyhole hide + FW.2 fog verdict, both evaluated only on a
1892        // solid hit (inside the `.filter`, after the occupancy test — the
1893        // "price the rule on real hits only" pattern). Review perf #2 —
1894        // the KEYHOLE is checked first: its cheap `cut_z_mip` gate
1895        // short-circuits (never-true when the cutout is off), so a hit
1896        // the keyhole discards never pays the fog verdict's dyn-call +
1897        // band scan + tile lookup. The fog verdict runs only for cells
1898        // the keyhole keeps, and carries into the shade branch via
1899        // `fow_verdict`.
1900        let mut fow_verdict = FowVerdict::LIVE;
1901        let cell_hit = sampler.hit(cellc).filter(|_| {
1902            let keyhole_hides = cellc[2] < cut_z_mip
1903                && env.cutout.is_some_and(|c| {
1904                    #[allow(clippy::cast_precision_loss)]
1905                    let pc = [
1906                        (cellc[0] as f32 + 0.5) * cell_size,
1907                        (cellc[1] as f32 + 0.5) * cell_size,
1908                        (cellc[2] as f32 + 0.5) * cell_size,
1909                    ];
1910                    let d = [pc[0] - origin[0], pc[1] - origin[1], pc[2] - origin[2]];
1911                    let along = d[0] * cut_axis[0] + d[1] * cut_axis[1] + d[2] * cut_axis[2];
1912                    if along <= 0.0 {
1913                        return false; // behind the eye
1914                    }
1915                    let d2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
1916                    let tan_c = ((d2 - along * along).max(0.0)).sqrt() / along;
1917                    // Radial taper: full reveal inside the inner cone,
1918                    // linear to zero at the outer cone (a hard edge when
1919                    // the two coincide — the subnormal-free max keeps the
1920                    // degenerate division finite).
1921                    let s = ((c.tan_outer - tan_c)
1922                        / (c.tan_outer - c.tan_inner).max(f32::MIN_POSITIVE))
1923                    .clamp(0.0, 1.0);
1924                    // Column-hugging reveal (see [`CpuCutout::margin`]):
1925                    // reference = the nearest character-column point at
1926                    // the cell's own height — the plane z (feet) mirrored
1927                    // around the focus gives the column top (head).
1928                    #[allow(clippy::cast_precision_loss)]
1929                    let plane = c.focus_z as f32;
1930                    let top = 2.0 * c.focus_local[2] - plane;
1931                    let rz = pc[2].clamp(top.min(plane), top.max(plane));
1932                    let dr = [
1933                        c.focus_local[0] - origin[0],
1934                        c.focus_local[1] - origin[1],
1935                        rz - origin[2],
1936                    ];
1937                    let ref_dist = (dr[0] * dr[0] + dr[1] * dr[1] + dr[2] * dr[2]).sqrt();
1938                    d2.sqrt() < (ref_dist - c.margin).max(0.0) * s
1939                });
1940            if keyhole_hides {
1941                return false;
1942            }
1943            if let Some(styler) = env.fow {
1944                let v = styler.verdict(
1945                    (cellc[0] << fow_mip_shift) + fow_mip_half,
1946                    (cellc[1] << fow_mip_shift) + fow_mip_half,
1947                    (cellc[2] << fow_mip_shift) + fow_mip_half,
1948                );
1949                fow_verdict = v;
1950                if matches!(v, FowVerdict::Hide) {
1951                    return false;
1952                }
1953            }
1954            true
1955        });
1956        if let Some(color) = cell_hit {
1957            let bright_sub = side_shade_sub(env, last_axis, step);
1958            // PF.7 (C4) — one colour→material scan per hit: resolve the id
1959            // and the material together. EV.1 hoisted this above the shade
1960            // so the emissive branch can bypass lighting entirely; with no
1961            // terrain material map the result is `OPAQUE` (emissive 0) and
1962            // every path below is bit-identical to the pre-material code.
1963            let (m, mat_id) = match env.materials {
1964                Some(table) if !env.terrain_materials.is_empty() => {
1965                    let id = material_for_color(env.terrain_materials, color);
1966                    (table.get(id), id)
1967                }
1968                _ => (Material::OPAQUE, 0),
1969            };
1970            // CPU.1 — dynamic lighting (flat per voxel) when a rig is active;
1971            // else the baked-byte `shade` path (byte-identical). CPU.2 — a
1972            // sun/point shadow march reuses this same `sampler` (occupancy +
1973            // box bounds); only built when a caster is actually flagged so
1974            // the no-shadow rig stays march-free. EV.1 — an emissive
1975            // material outranks both: full-bright albedo, no face shade,
1976            // no rig, no shadow march.
1977            // FW.2 — memory / heard cells suppress only the DYNAMIC rig
1978            // (a live torch never relights a remembered room), then dim +
1979            // desaturate below. Emissive is INTRINSIC to the material and
1980            // survives (a crystal the player saw glowing is remembered
1981            // glowing — dimmed — not as dark rock, and its baked halo on
1982            // the surrounding walls stays consistent: FW.2 review #6).
1983            // `Show { dynamic: true }` (visible, and the no-fow default
1984            // `LIVE`) keeps the full path bit-identical.
1985            let fow_dynamic = match fow_verdict {
1986                FowVerdict::Show { dynamic, .. } => dynamic,
1987                FowVerdict::Hide => true, // unreachable: Hide was filtered out
1988            };
1989            let shaded = if m.emissive > 0 {
1990                emissive_shade(color, m.emissive)
1991            } else if fow_dynamic && env.lights.enabled {
1992                let casts = shadow_casts;
1993                // Pick the shadow oracle: the scene-wide one (cross-grid +
1994                // sprites, XS.1) when present, else the single-grid Sampler;
1995                // `None` when no caster is flagged, so the rig stays
1996                // march-free. The two testers live in branch-local slots so
1997                // exactly one is borrowed for the `shade_lit_cpu` call.
1998                let mut world_sh;
1999                let mut sampler_sh;
2000                let tester: Option<&mut dyn ShadowTester> = if !casts {
2001                    None
2002                } else if let Some(ctx) = env.world_shadow {
2003                    world_sh = WorldShadow { ctx };
2004                    Some(&mut world_sh)
2005                } else {
2006                    sampler_sh = SamplerShadow {
2007                        sampler: &mut *sampler,
2008                        cell_size,
2009                        lo_c,
2010                        hi_c,
2011                        fow: env.fow,
2012                    };
2013                    Some(&mut sampler_sh)
2014                };
2015                shade_lit_cpu(
2016                    color,
2017                    bright_sub,
2018                    last_axis,
2019                    step,
2020                    cellc,
2021                    cell_size,
2022                    &env.lights,
2023                    tester,
2024                )
2025            } else {
2026                shade(color, bright_sub)
2027            };
2028            // FW.2 — the memory / FOV-edge taper on the surface colour,
2029            // BEFORE distance fog (fog is atmospheric, applied to the
2030            // styled surface). Identity for a full-visible cell.
2031            let styled = match fow_verdict {
2032                FowVerdict::Show {
2033                    dim, desaturate, ..
2034                } => fow_style(shaded, dim, desaturate),
2035                FowVerdict::Hide => shaded,
2036            };
2037            let lit = apply_fog(styled, depth.max(0.0), env);
2038            if m.is_opaque() {
2039                // Opaque surface: the background. Return the first hit verbatim
2040                // when nothing translucent preceded it (bit-identical), else
2041                // composite the accumulated layers over it.
2042                let color = if touched {
2043                    composite_over(accum, trans, lit)
2044                } else {
2045                    lit
2046                };
2047                return Some(Hit {
2048                    color,
2049                    dist: depth.max(0.0),
2050                });
2051            }
2052            let a = f32::from(m.alpha) / 255.0;
2053            if matches!(m.mode, roxlap_formats::material::BlendMode::Volumetric) {
2054                // Per-cell Beer–Lambert: opacity weighted by the ray's path
2055                // length through this voxel (so a filled volume thickens
2056                // smoothly with depth, a sliver contributes ≈0). Occludes.
2057                let t_exit = t_max[min_axis(t_max)];
2058                let seg_len = (t_exit - t_curr).max(0.0) * dir_len / cell_size;
2059                let eff_a = 1.0 - (1.0 - a).powf(seg_len);
2060                let c = rgb_to_f32(lit);
2061                accum[0] += trans * eff_a * c[0];
2062                accum[1] += trans * eff_a * c[1];
2063                accum[2] += trans * eff_a * c[2];
2064                trans *= 1.0 - eff_a;
2065                touched = true;
2066                prev_mat = mat_id;
2067                if trans < 1.0 / 256.0 {
2068                    return Some(Hit {
2069                        color: f32_to_rgb(accum),
2070                        dist: depth.max(0.0),
2071                    });
2072                }
2073            } else if !prev_solid || mat_id != prev_mat {
2074                // AlphaBlend / Additive: one alpha layer per solid-run entry or
2075                // material change (per-span — avoids the voxel-grid striping
2076                // through a thick glass/water slab; thickness-independent).
2077                let c = rgb_to_f32(lit);
2078                accum[0] += trans * a * c[0];
2079                accum[1] += trans * a * c[1];
2080                accum[2] += trans * a * c[2];
2081                if !matches!(m.mode, roxlap_formats::material::BlendMode::Additive) {
2082                    trans *= 1.0 - a; // AlphaBlend occludes; Additive does not.
2083                }
2084                touched = true;
2085                prev_mat = mat_id;
2086                if trans < 1.0 / 256.0 {
2087                    return Some(Hit {
2088                        color: f32_to_rgb(accum),
2089                        dist: depth.max(0.0),
2090                    });
2091                }
2092            }
2093            prev_solid = true;
2094        } else {
2095            prev_solid = false;
2096        }
2097        let axis = min_axis(t_max);
2098        last_axis = axis;
2099        t_curr = t_max[axis];
2100        cellc[axis] += step[axis];
2101        t_max[axis] += t_delta[axis];
2102    }
2103    None
2104}
2105
2106/// Per-face brightness reduction for the hit face. `axis` is the axis
2107/// the ray crossed to enter the hit voxel (`3` = entry voxel, no face);
2108/// `step[axis]` gives the crossing direction. Maps to the
2109/// `[x-, x+, y-, y+, z-, z+]` `side_shades` entry of the face the ray
2110/// looks at (a `+step` crossing enters through the low / `-` face).
2111#[inline]
2112fn side_shade_sub(env: &DdaEnv<'_>, axis: usize, step: [i32; 3]) -> u32 {
2113    if axis >= 3 {
2114        return 0;
2115    }
2116    let face = axis * 2 + usize::from(step[axis] < 0);
2117    env.side_shades[face].max(0) as u32
2118}
2119
2120/// Cast one ray into the grid and return the first solid hit.
2121///
2122/// **DDA.4:** cross-chunk per-pixel 3D-DDA over the grid's full voxel
2123/// box ([`GridView::voxel_bounds`], spanning every chunk in XY **and**
2124/// Z). The [`Sampler`] resolves each stepped voxel to its chunk and
2125/// brick-gates the slab walk. Cross-chunk look-down (the case the
2126/// voxlap renderer needed the whole virtual-column stack for) falls out
2127/// of the box simply spanning `chunks_z` along Z.
2128fn cast_ray(
2129    origin: [f32; 3],
2130    dir: [f32; 3],
2131    forward: [f32; 3],
2132    sampler: &mut Sampler<'_>,
2133    settings: &OpticastSettings,
2134    env: &DdaEnv<'_>,
2135) -> Option<Hit> {
2136    let (lo_i, hi_i) = sampler.grid.voxel_bounds();
2137    #[allow(clippy::cast_precision_loss)]
2138    let lo_f = [lo_i[0] as f32, lo_i[1] as f32, lo_i[2] as f32];
2139    #[allow(clippy::cast_precision_loss)]
2140    let hi_f = [hi_i[0] as f32, hi_i[1] as f32, hi_i[2] as f32];
2141    let (t_enter, t_exit) = intersect_aabb(origin, dir, lo_f, hi_f)?;
2142    let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
2143    #[allow(clippy::cast_precision_loss)]
2144    let max_dist = settings.max_scan_dist.max(1) as f32;
2145    let cell = 1i32 << sampler.mip;
2146    let cell_size = cell as f32;
2147    let lo_c = [
2148        lo_i[0].div_euclid(cell),
2149        lo_i[1].div_euclid(cell),
2150        lo_i[2].div_euclid(cell),
2151    ];
2152    let hi_c = [
2153        hi_i[0].div_euclid(cell),
2154        hi_i[1].div_euclid(cell),
2155        hi_i[2].div_euclid(cell),
2156    ];
2157    cell_walk_skip(
2158        origin, dir, fwd_dot, sampler, lo_c, hi_c, cell_size, t_enter, t_exit, max_dist, env,
2159    )
2160}
2161
2162/// Render one grid into `sink` with per-pixel 3D-DDA.
2163///
2164/// `camera` is the grid-local pose, `settings`
2165/// ([`OpticastSettings`]) carries the projection + viewport (including
2166/// the `y_start..y_end` strip bound), and `grid` is the per-frame
2167/// [`GridView`] borrow. `pitch_pixels` is the framebuffer
2168/// row stride in pixels (matches `ScalarRasterizer::new`'s argument).
2169///
2170/// On a miss, a textured sky ([`DdaEnv::sky`]) is sampled per ray
2171/// direction and written at `+inf` depth; with no textured sky the miss
2172/// writes nothing, so the caller's solid sky pre-fill shows (the
2173/// `render_scene_composed` path pre-fills it).
2174pub fn render_dda(
2175    camera: &Camera,
2176    settings: &OpticastSettings,
2177    grid: GridView<'_>,
2178    pitch_pixels: usize,
2179    env: &DdaEnv<'_>,
2180    mip: u32,
2181    sink: &mut impl PixelSink,
2182) {
2183    let cs = camera_math::derive(
2184        camera,
2185        settings.xres,
2186        settings.yres,
2187        settings.hx,
2188        settings.hy,
2189        settings.hz,
2190    );
2191
2192    // Sequential path builds a throwaway per-call cache (tests / single
2193    // grid). The parallel path takes a persistent cross-frame cache.
2194    let (cache, mip) = local_cache(&grid, mip);
2195    let mut sampler = Sampler::new(grid, &cache, mip, env.z_clip);
2196
2197    for py in settings.y_start..settings.y_end {
2198        let row = py as usize * pitch_pixels;
2199        for px in settings.x_start..settings.x_end {
2200            if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py) {
2201                sink.put(row + px as usize, color, dist);
2202            }
2203        }
2204    }
2205}
2206
2207/// Resolve one pixel: a shaded + fogged hit colour, a sampled textured
2208/// sky on a miss, or `None` (miss with no textured sky → caller's
2209/// pre-fill stands). Shared by the sequential ([`render_dda`]) and
2210/// parallel ([`render_dda_parallel`]) drivers. The OC keyhole needs no
2211/// per-pixel work here: the hide rule classifies whole CELLS in the
2212/// hit branch (see [`CpuCutout`]).
2213#[inline]
2214fn pixel_result(
2215    cs: &CameraState,
2216    settings: &OpticastSettings,
2217    sampler: &mut Sampler<'_>,
2218    env: &DdaEnv<'_>,
2219    px: u32,
2220    py: u32,
2221) -> Option<(u32, f32)> {
2222    let (origin, dir) = pixel_ray(cs, settings, px, py);
2223    if let Some(hit) = cast_ray(origin, dir, cs.forward, sampler, settings, env) {
2224        Some((hit.color, hit.dist))
2225    } else {
2226        env.sky.map(|sky| (sample_sky(sky, dir), f32::INFINITY))
2227    }
2228}
2229
2230/// Tile-parallel [`render_dda`] writing straight into `(fb, zb)`.
2231///
2232/// DDA pixels are independent, so the framebuffer splits into disjoint
2233/// horizontal bands rendered concurrently (rayon) — **bit-identical**
2234/// to the sequential render regardless of thread count, unlike voxlap's
2235/// per-strip discretisation. Each band spins up its own lightweight
2236/// `Sampler` over the shared, immutable `cache`.
2237///
2238/// `cache` must already hold current brick maps for every chunk at
2239/// `mip` (populate via [`BrickCache::ensure`]); `mip` is the effective
2240/// render mip ([`effective_mip`]). `(fb, zb)` use the standard
2241/// conventions (`0x80RRGGBB`; z = perp distance, smaller = closer); a
2242/// miss writes nothing unless [`DdaEnv::sky`] is set. `pitch_pixels` is
2243/// the row stride.
2244#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
2245pub fn render_dda_parallel(
2246    camera: &Camera,
2247    settings: &OpticastSettings,
2248    grid: GridView<'_>,
2249    fb: &mut [u32],
2250    zb: &mut [f32],
2251    pitch_pixels: usize,
2252    env: &DdaEnv<'_>,
2253    cache: &BrickCache,
2254    mip: u32,
2255) {
2256    debug_assert_eq!(fb.len(), zb.len());
2257    let (y0, y1) = (settings.y_start, settings.y_end);
2258    if y1 <= y0 {
2259        return;
2260    }
2261    let cs = camera_math::derive(
2262        camera,
2263        settings.xres,
2264        settings.yres,
2265        settings.hx,
2266        settings.hy,
2267        settings.hz,
2268    );
2269    let target = RasterTarget::new(fb, zb);
2270
2271    // PF.7 (C5) — small fixed bands + rayon work-stealing instead of one
2272    // equal band per thread: sky-heavy rows finish instantly while
2273    // horizon/terrain rows dominate, so an equal split left threads idle
2274    // for the tail of every frame. 8 rows amortises the per-band
2275    // `Sampler` construction while staying fine-grained enough to
2276    // balance. Bit-identical (pixels are independent; rows disjoint).
2277    let band = 8u32;
2278    let bands: Vec<(u32, u32)> = (y0..y1)
2279        .step_by(band as usize)
2280        .map(|s| (s, (s + band).min(y1)))
2281        .collect();
2282
2283    bands.par_iter().for_each(|&(by0, by1)| {
2284        let mut sampler = Sampler::new(grid, cache, mip, env.z_clip);
2285        for py in by0..by1 {
2286            let row = py as usize * pitch_pixels;
2287            for px in settings.x_start..settings.x_end {
2288                if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py)
2289                {
2290                    let idx = row + px as usize;
2291                    // SAFETY: bands cover disjoint row ranges, so writes
2292                    // never alias across threads; `idx` is in-bounds for
2293                    // a `pitch * height`-sized buffer.
2294                    unsafe {
2295                        target.write_color(idx, color);
2296                        target.write_depth(idx, dist);
2297                    }
2298                }
2299            }
2300        }
2301    });
2302}
2303
2304/// Dense per-voxel reference cast for a **single-chunk** grid: walks
2305/// every voxel of `[0, vsid)² × [0, CHUNK_SIZE_Z)` calling
2306/// [`GridView::surface_color`] directly — no brick gate, no chunk
2307/// resolution. The equivalence oracle the brickmap + sampler
2308/// [`cast_ray`] is checked against in tests.
2309#[cfg(test)]
2310#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
2311fn cast_ray_reference(
2312    origin: [f32; 3],
2313    dir: [f32; 3],
2314    forward: [f32; 3],
2315    grid: &GridView<'_>,
2316    settings: &OpticastSettings,
2317) -> Option<Hit> {
2318    let nx = grid.vsid as f32;
2319    let nz = f32::from(u16::try_from(crate::grid_view::CHUNK_SIZE_Z).unwrap_or(256));
2320    #[allow(clippy::cast_possible_wrap)]
2321    let n_i = [
2322        grid.vsid as i32,
2323        grid.vsid as i32,
2324        crate::grid_view::CHUNK_SIZE_Z as i32,
2325    ];
2326    let (t_enter, t_exit) = intersect_aabb(origin, dir, [0.0; 3], [nx, nx, nz])?;
2327    let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
2328    let max_dist = settings.max_scan_dist.max(1) as f32;
2329
2330    let start = t_enter + 1e-4;
2331    let p = [
2332        origin[0] + dir[0] * start,
2333        origin[1] + dir[1] * start,
2334        origin[2] + dir[2] * start,
2335    ];
2336    let mut voxel = [
2337        (p[0].floor() as i32).clamp(0, n_i[0] - 1),
2338        (p[1].floor() as i32).clamp(0, n_i[1] - 1),
2339        (p[2].floor() as i32).clamp(0, n_i[2] - 1),
2340    ];
2341    let (step, mut t_max, t_delta) = dda_setup(origin, dir, voxel, 1.0);
2342    let mut t_curr = t_enter;
2343    let max_steps = (n_i[0] + n_i[1] + n_i[2]) as usize + 8;
2344    for _ in 0..max_steps {
2345        if voxel[0] < 0
2346            || voxel[0] >= n_i[0]
2347            || voxel[1] < 0
2348            || voxel[1] >= n_i[1]
2349            || voxel[2] < 0
2350            || voxel[2] >= n_i[2]
2351        {
2352            return None;
2353        }
2354        let depth = t_curr * fwd_dot;
2355        if depth > max_dist || t_curr > t_exit {
2356            return None;
2357        }
2358        #[allow(clippy::cast_sign_loss)]
2359        if let Some(color) = grid.surface_color(voxel[0] as u32, voxel[1] as u32, voxel[2] as u32) {
2360            return Some(Hit {
2361                color: shade(color.0, 0),
2362                dist: depth.max(0.0),
2363            });
2364        }
2365        let axis = min_axis(t_max);
2366        t_curr = t_max[axis];
2367        voxel[axis] += step[axis];
2368        t_max[axis] += t_delta[axis];
2369    }
2370    None
2371}
2372
2373#[cfg(test)]
2374mod tests {
2375    use super::*;
2376    use roxlap_formats::VoxColor;
2377
2378    // CPU.1 — luminance of a packed colour's low-24-bit RGB.
2379    fn lum(p: u32) -> u32 {
2380        (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff)
2381    }
2382
2383    #[test]
2384    fn cel_band_quantizes_and_collapses() {
2385        // Two distinct factors round to the same band at bands=2.
2386        assert_eq!(cel_band(0.8, 2), cel_band(0.9, 2));
2387        assert!((cel_band(0.8, 2) - 1.0).abs() < 1e-6);
2388        // ...but a low factor lands on a different band.
2389        assert_ne!(cel_band(0.3, 2), cel_band(0.8, 2));
2390    }
2391
2392    #[test]
2393    fn shade_lit_cpu_sun_lights_by_facing() {
2394        // Grey voxel (brightness 0x80 = full ambient). Floor top face: hit via
2395        // a +z step (axis 2) ⇒ normal points up (-z).
2396        let color = 0x80_80_80_80;
2397        let step = [0, 0, 1];
2398        let base = CpuLights {
2399            enabled: true,
2400            sun: true,
2401            sun_color: [1.0; 3],
2402            sun_intensity: 1.0,
2403            ambient: [0.2; 3],
2404            ..CpuLights::default()
2405        };
2406        let facing = CpuLights {
2407            sun_dir: [0.0, 0.0, -1.0],
2408            ..base
2409        }; // toward sun = up
2410        let back = CpuLights {
2411            sun_dir: [0.0, 0.0, 1.0],
2412            ..base
2413        }; // sun below the face
2414        let lit = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &facing, None);
2415        let dark = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &back, None);
2416        assert!(
2417            lum(lit) > lum(dark),
2418            "sun facing the surface must brighten it: {lit:#08x} vs {dark:#08x}",
2419        );
2420    }
2421
2422    #[test]
2423    fn shade_dynamic_spot_cone_masks_off_axis() {
2424        // Surface at the origin, up-facing normal (-z, voxlap z-down); a light
2425        // 10 units "above" it (at -z). No ambient/AO ⇒ only the light shows.
2426        let albedo = [0.5, 0.5, 0.5];
2427        let n = [0.0, 0.0, -1.0];
2428        let sample = [0.0, 0.0, 0.0];
2429        let inner = 10.0f32.to_radians().cos();
2430        let outer = 15.0f32.to_radians().cos();
2431        let shade = |spot_dir: [f32; 3], cos_inner: f32, cos_outer: f32| {
2432            let pts = [CpuPointLight {
2433                pos: [0.0, 0.0, -10.0],
2434                color: [1.0; 3],
2435                intensity: 1.0,
2436                radius: 64.0,
2437                casts_shadow: false,
2438                spot_dir,
2439                cos_inner,
2440                cos_outer,
2441            }];
2442            let l = CpuLights {
2443                enabled: true,
2444                ambient: [0.0; 3],
2445                points: &pts,
2446                ..CpuLights::default()
2447            };
2448            shade_dynamic(albedo, 0.0, n, sample, &l, None)
2449        };
2450        // A pure point light (cos_outer = -1) ignores the axis entirely.
2451        let point = shade([0.0, 0.0, 1.0], -1.0, -1.0);
2452        // A spot whose axis shines straight down onto the surface (on-axis).
2453        let on_axis = shade([0.0, 0.0, 1.0], inner, outer);
2454        // Same spot aimed sideways ⇒ the surface is outside the cone.
2455        let off_axis = shade([1.0, 0.0, 0.0], inner, outer);
2456
2457        // On-axis (cd == 1) is fully inside the cone ⇒ identical to a point.
2458        assert_eq!(
2459            on_axis, point,
2460            "on-axis spot must equal the point light: {on_axis:#08x} vs {point:#08x}",
2461        );
2462        // Off-axis is masked to zero ⇒ only the (zero) ambient remains.
2463        assert!(
2464            lum(on_axis) > lum(off_axis),
2465            "off-axis spot must be darker: {on_axis:#08x} vs {off_axis:#08x}",
2466        );
2467        assert_eq!(lum(off_axis), 0, "off-cone spot contributes nothing");
2468    }
2469
2470    #[test]
2471    fn shade_lit_cpu_cel_terraces_sun() {
2472        // Two sun elevations with distinct N·L (0.8 / 0.9) collapse to one
2473        // band at bands=2 ⇒ identical stylized colour; smooth (bands=0) differs.
2474        let color = 0x80_80_80_80;
2475        let step = [0, 0, 1];
2476        let mk = |zc: f32, bands: u32| {
2477            let n = (1.0f32 - zc * zc).sqrt();
2478            CpuLights {
2479                enabled: true,
2480                sun: true,
2481                sun_dir: [n, 0.0, -zc], // ndl on the up face = zc
2482                sun_color: [1.0; 3],
2483                sun_intensity: 1.0,
2484                ambient: [0.1; 3],
2485                bands,
2486                ..CpuLights::default()
2487            }
2488        };
2489        let smooth_a = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.8, 0), None);
2490        let smooth_b = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.9, 0), None);
2491        assert_ne!(smooth_a, smooth_b, "smooth diffuse must vary with N·L");
2492        let cel_a = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.8, 2), None);
2493        let cel_b = shade_lit_cpu(color, 0, 2, step, [0, 0, 0], 1.0, &mk(0.9, 2), None);
2494        assert_eq!(
2495            cel_a, cel_b,
2496            "cel banding must terrace both N·L to one level"
2497        );
2498    }
2499
2500    /// CPU.2 — the shadow application math (independent of the march): an
2501    /// occluded sun-lit sample keeps only `1 - shadow_strength` of the sun
2502    /// key, and `shadow_strength == 0` makes shadows invisible.
2503    #[test]
2504    fn shade_dynamic_sun_shadow_darkens() {
2505        struct Mock(bool);
2506        impl ShadowTester for Mock {
2507            fn occluded(&mut self, _: [f32; 3], _: [f32; 3], _: f32) -> bool {
2508                self.0
2509            }
2510        }
2511        let l = CpuLights {
2512            enabled: true,
2513            sun: true,
2514            sun_dir: [0.0, 0.0, -1.0], // up = toward the sun
2515            sun_color: [1.0; 3],
2516            sun_intensity: 1.0,
2517            sun_casts_shadow: true,
2518            ambient: [0.2; 3],
2519            shadow_strength: 0.7,
2520            shadow_bias: 1.5,
2521            shadow_max_dist: 64.0,
2522            ..CpuLights::default()
2523        };
2524        let albedo = [0.8; 3];
2525        let n = [0.0, 0.0, -1.0]; // up face, faces the sun
2526        let s = [0.5, 0.5, 0.5];
2527        let lit = shade_dynamic(albedo, 1.0, n, s, &l, Some(&mut Mock(false)));
2528        let shadowed = shade_dynamic(albedo, 1.0, n, s, &l, Some(&mut Mock(true)));
2529        assert!(
2530            lum(shadowed) < lum(lit),
2531            "an occluded sun face must darken: shadowed={shadowed:#08x} lit={lit:#08x}",
2532        );
2533        // strength 0 ⇒ no visible shadow even when occluded.
2534        let l0 = CpuLights {
2535            shadow_strength: 0.0,
2536            ..l
2537        };
2538        assert_eq!(
2539            shade_dynamic(albedo, 1.0, n, s, &l0, Some(&mut Mock(true))),
2540            shade_dynamic(albedo, 1.0, n, s, &l0, Some(&mut Mock(false))),
2541            "shadow_strength 0 ⇒ shadows invisible",
2542        );
2543    }
2544
2545    /// CPU.2 — the actual [`SamplerShadow`] march casts a sun shadow through
2546    /// the grid: a wall on a floor, lit by a grazing sun, darkens the floor
2547    /// in the wall's shadow. Total scene luminance with shadows enabled is
2548    /// strictly less than with them off (shadows only ever subtract), and
2549    /// the gap is non-trivial (a real shadow, not FP noise).
2550    #[test]
2551    fn sampler_shadow_march_casts_sun_shadow() {
2552        // Floor at z>=60; a thin wall at x==32 rising from the floor (z 30..60).
2553        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, _y, z| {
2554            if z >= 60 {
2555                Some(VoxColor(0x80_80_80_80)) // floor
2556            } else if x == 32 && (30..60).contains(&z) {
2557                Some(VoxColor(0x80_70_70_70)) // wall (distinct so it's not a dead branch)
2558            } else {
2559                None
2560            }
2561        });
2562        let grid = GridView::from_single_vxl(&vxl);
2563        // Straight-down camera over the floor (voxlap z-down: forward = +z).
2564        let cam = Camera {
2565            pos: [32.0, 32.0, 6.0],
2566            right: [1.0, 0.0, 0.0],
2567            down: [0.0, 1.0, 0.0],
2568            forward: [0.0, 0.0, 1.0],
2569        };
2570        // Sun grazing from +x and above ⇒ the wall shadows the floor at x<32.
2571        let inv = 1.0f32 / 2.0f32.sqrt();
2572        let base = CpuLights {
2573            enabled: true,
2574            sun: true,
2575            sun_dir: [inv, 0.0, -inv],
2576            sun_color: [1.0; 3],
2577            sun_intensity: 1.0,
2578            ambient: [0.25; 3],
2579            shadow_strength: 0.8,
2580            shadow_bias: 1.5,
2581            shadow_max_dist: 128.0,
2582            ..CpuLights::default()
2583        };
2584        let (w, h) = (96u32, 96u32);
2585        let lit_env = DdaEnv {
2586            lights: CpuLights {
2587                sun_casts_shadow: false,
2588                ..base
2589            },
2590            ..DdaEnv::default()
2591        };
2592        let shadow_env = DdaEnv {
2593            lights: CpuLights {
2594                sun_casts_shadow: true,
2595                ..base
2596            },
2597            ..DdaEnv::default()
2598        };
2599        let (fb_lit, _) = render_brickmap_env(grid, &cam, w, h, &lit_env);
2600        let (fb_sh, _) = render_brickmap_env(grid, &cam, w, h, &shadow_env);
2601        let sum: fn(&[u32]) -> u64 = |fb| fb.iter().map(|&p| u64::from(lum(p))).sum();
2602        let lit_sum = sum(&fb_lit);
2603        let sh_sum = sum(&fb_sh);
2604        assert!(
2605            sh_sum < lit_sum,
2606            "the wall's shadow must darken the floor: shadow_sum={sh_sum} lit_sum={lit_sum}",
2607        );
2608        // Non-trivial: at least a few % of the lit total was removed.
2609        assert!(
2610            (lit_sum - sh_sum) * 50 > lit_sum,
2611            "shadow should remove >2% of total luminance: lit={lit_sum} shadow={sh_sum}",
2612        );
2613    }
2614
2615    /// CA.2 — "world as if removed": a clipped-away deck neither
2616    /// renders NOR casts sun shadows. The render of a roofed fixture
2617    /// with the roof clipped must be byte-identical to a fixture with
2618    /// no roof at all (same camera, same shadowing sun) — primary rays
2619    /// (CA.1) and the [`SamplerShadow`] march (this substage) both see
2620    /// air above the plane, even though the roof's bricks are still
2621    /// occupancy-occupied.
2622    #[test]
2623    fn cutaway_hidden_deck_neither_renders_nor_shadows() {
2624        const ROOF: VoxColor = VoxColor(0x80_A0_50_20);
2625        const FLOOR: VoxColor = VoxColor(0x80_80_80_80);
2626        let roofed = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
2627            if z == 20 {
2628                Some(ROOF)
2629            } else if z >= 60 {
2630                Some(FLOOR)
2631            } else {
2632                None
2633            }
2634        });
2635        let roofless =
2636            roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| (z >= 60).then_some(FLOOR));
2637        let g_roofed = GridView::from_single_vxl(&roofed);
2638        let g_roofless = GridView::from_single_vxl(&roofless);
2639        let cam = Camera {
2640            pos: [32.0, 32.0, 6.0],
2641            right: [1.0, 0.0, 0.0],
2642            down: [0.0, 1.0, 0.0],
2643            forward: [0.0, 0.0, 1.0],
2644        };
2645        let inv = 1.0f32 / 2.0f32.sqrt();
2646        let lights = CpuLights {
2647            enabled: true,
2648            sun: true,
2649            sun_dir: [inv, 0.0, -inv],
2650            sun_color: [1.0; 3],
2651            sun_intensity: 1.0,
2652            sun_casts_shadow: true,
2653            ambient: [0.25; 3],
2654            shadow_strength: 0.8,
2655            shadow_bias: 1.5,
2656            shadow_max_dist: 128.0,
2657            ..CpuLights::default()
2658        };
2659        let (w, h) = (64u32, 64u32);
2660        let env_clipped = DdaEnv {
2661            lights,
2662            z_clip: Some(40),
2663            ..DdaEnv::default()
2664        };
2665        let env_plain = DdaEnv {
2666            lights,
2667            ..DdaEnv::default()
2668        };
2669        let (fb_clip, zb_clip) = render_brickmap_env(g_roofed, &cam, w, h, &env_clipped);
2670        let (fb_ref, zb_ref) = render_brickmap_env(g_roofless, &cam, w, h, &env_plain);
2671        assert_eq!(
2672            fb_clip, fb_ref,
2673            "clipped roof must render AND shadow exactly like no roof"
2674        );
2675        assert_eq!(zb_clip, zb_ref);
2676        // Negative control: unclipped, the camera sees the roof instead.
2677        let (fb_roof, _) = render_brickmap_env(g_roofed, &cam, w, h, &env_plain);
2678        assert_ne!(fb_roof, fb_ref, "unclipped roof must be visible");
2679    }
2680
2681    /// Recording sink: collects `(idx, color, dist)` puts for tests.
2682    #[derive(Default)]
2683    struct Recorder {
2684        puts: Vec<(usize, u32, f32)>,
2685    }
2686    impl PixelSink for Recorder {
2687        fn put(&mut self, idx: usize, color: u32, dist: f32) {
2688            self.puts.push((idx, color, dist));
2689        }
2690    }
2691
2692    fn oracle_camera() -> Camera {
2693        // Identity-basis camera at origin: ray math is integer-exact.
2694        Camera {
2695            pos: [0.0, 0.0, 0.0],
2696            right: [1.0, 0.0, 0.0],
2697            down: [0.0, 0.0, 1.0],
2698            forward: [0.0, 1.0, 0.0],
2699        }
2700    }
2701
2702    /// Render `grid` from `camera` into a `w × h` framebuffer and
2703    /// return the per-pixel hit mask (`true` where a ray hit a voxel).
2704    fn render_mask(grid: GridView<'_>, camera: &Camera, w: u32, h: u32) -> Vec<bool> {
2705        let n = (w as usize) * (h as usize);
2706        let mut fb = vec![0u32; n]; // sky sentinel = 0
2707        let mut zb = vec![f32::INFINITY; n];
2708        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2709        {
2710            let mut sink = RasterSink::new(&mut fb, &mut zb);
2711            render_dda(
2712                camera,
2713                &settings,
2714                grid,
2715                w as usize,
2716                &DdaEnv::default(),
2717                0,
2718                &mut sink,
2719            );
2720        }
2721        fb.iter().map(|&c| c != 0).collect()
2722    }
2723
2724    /// A silhouette is "row-convex" if every framebuffer row's hit
2725    /// pixels form a single contiguous run (no interior gap). The
2726    /// voxlap silhouette notch is exactly such an interior gap, so this
2727    /// is the headline DDA.1 acceptance check.
2728    fn rows_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
2729        let w = w as usize;
2730        for y in 0..h as usize {
2731            let row = &mask[y * w..(y + 1) * w];
2732            let first = row.iter().position(|&b| b);
2733            let last = row.iter().rposition(|&b| b);
2734            if let (Some(f), Some(l)) = (first, last) {
2735                if row[f..=l].iter().any(|&b| !b) {
2736                    return false;
2737                }
2738            }
2739        }
2740        true
2741    }
2742
2743    /// Same contiguity check down each column.
2744    fn cols_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
2745        let w = w as usize;
2746        let h = h as usize;
2747        for x in 0..w {
2748            let col: Vec<bool> = (0..h).map(|y| mask[y * w + x]).collect();
2749            let first = col.iter().position(|&b| b);
2750            let last = col.iter().rposition(|&b| b);
2751            if let (Some(f), Some(l)) = (first, last) {
2752                if col[f..=l].iter().any(|&b| !b) {
2753                    return false;
2754                }
2755            }
2756        }
2757        true
2758    }
2759
2760    /// The principal-point pixel `(hx, hy)` looks straight down the
2761    /// forward axis, scaled by `hz`.
2762    #[test]
2763    fn center_pixel_ray_is_forward() {
2764        let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
2765        let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
2766        // hx = hy = 320 / 240 → use the exact principal point.
2767        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2768        let (origin, dir) = pixel_ray(&cs, &settings, settings.hx as u32, settings.hy as u32);
2769        assert_eq!(origin, [0.0, 0.0, 0.0]);
2770        // hz·forward = 320·[0,1,0].
2771        assert_eq!(
2772            dir.map(f32::to_bits),
2773            [0.0f32, 320.0, 0.0].map(f32::to_bits)
2774        );
2775    }
2776
2777    /// Pixel `(0, 0)`'s ray equals `camera_math`'s `corn[0]` — proving
2778    /// the DDA renderer samples the same rays the voxlap frustum is
2779    /// built from.
2780    #[test]
2781    fn corner_pixel_ray_matches_camera_corn0() {
2782        let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
2783        let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
2784        let (_origin, dir) = pixel_ray(&cs, &settings, 0, 0);
2785        assert_eq!(dir.map(f32::to_bits), cs.corn[0].map(f32::to_bits));
2786    }
2787
2788    /// The renderer's independent slab decoder
2789    /// ([`GridView::voxel_color`]) must agree with the reference
2790    /// [`roxlap_formats::vxl::Vxl::voxel_color`] for every cell —
2791    /// including a column with an air gap, which exercises the
2792    /// ceiling-colour-list branch.
2793    #[test]
2794    fn gridview_voxel_color_matches_reference() {
2795        // Two solid runs per column separated by air → ceiling list.
2796        let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |x, _, z| {
2797            let lo = (10..=12).contains(&z);
2798            let hi = (40..=42).contains(&z);
2799            (lo || hi).then_some(VoxColor(0x80_10_20_30 + x))
2800        });
2801        let grid = GridView::from_single_vxl(&vxl);
2802        for x in 0..8 {
2803            for y in 0..8 {
2804                for z in 0..64 {
2805                    assert_eq!(
2806                        grid.voxel_color(x, y, z),
2807                        vxl.voxel_color(x, y, z),
2808                        "mismatch at ({x},{y},{z})"
2809                    );
2810                }
2811            }
2812        }
2813    }
2814
2815    /// An all-air grid produces no hits (every ray misses).
2816    #[test]
2817    fn empty_grid_no_hits() {
2818        let vxl = roxlap_formats::vxl::Vxl::empty(64);
2819        let grid = GridView::from_single_vxl(&vxl);
2820        let settings = OpticastSettings::for_oracle_framebuffer(64, 48);
2821        let mut rec = Recorder::default();
2822        render_dda(
2823            &oracle_camera(),
2824            &settings,
2825            grid,
2826            64,
2827            &DdaEnv::default(),
2828            0,
2829            &mut rec,
2830        );
2831        assert!(rec.puts.is_empty(), "all-air grid must produce no hits");
2832    }
2833
2834    /// Camera above a solid floor, looking straight down: every ray
2835    /// hits, the recovered colour is the floor colour, and the centre
2836    /// pixel's depth ≈ the camera's height above the floor.
2837    #[test]
2838    fn floor_seen_from_above() {
2839        const FLOOR_Z: u32 = 40;
2840        const FLOOR_COL: VoxColor = VoxColor(0x80_30_60_90);
2841        let vxl =
2842            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= FLOOR_Z).then_some(FLOOR_COL));
2843        let grid = GridView::from_single_vxl(&vxl);
2844
2845        // Eye above the floor (z is down), looking down (+z).
2846        let cam = Camera {
2847            pos: [16.0, 16.0, 10.0],
2848            right: [1.0, 0.0, 0.0],
2849            down: [0.0, 1.0, 0.0],
2850            forward: [0.0, 0.0, 1.0],
2851        };
2852        let settings = OpticastSettings::for_oracle_framebuffer(48, 48);
2853        let mut rec = Recorder::default();
2854        render_dda(&cam, &settings, grid, 48, &DdaEnv::default(), 0, &mut rec);
2855
2856        assert!(!rec.puts.is_empty(), "floor must be visible");
2857        // Centre pixel looks straight down → depth ≈ FLOOR_Z - eye_z.
2858        let centre = 24usize * 48 + 24;
2859        let hit = rec
2860            .puts
2861            .iter()
2862            .find(|(idx, _, _)| *idx == centre)
2863            .expect("centre ray must hit the floor");
2864        assert_eq!(hit.1 & 0x00ff_ffff, FLOOR_COL.0 & 0x00ff_ffff);
2865        let expected = (FLOOR_Z as f32) - 10.0;
2866        assert!(
2867            (hit.2 - expected).abs() < 1.5,
2868            "centre depth {} not ≈ {}",
2869            hit.2,
2870            expected
2871        );
2872    }
2873
2874    /// DDA.2: a camera looking at the horizon splits the frame into
2875    /// sky (upward rays miss → no write) and floor (downward rays hit).
2876    /// The top of the frame must be mostly sky, the bottom mostly
2877    /// floor.
2878    #[test]
2879    fn horizon_splits_sky_and_floor() {
2880        const FLOOR_Z: u32 = 40;
2881        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
2882            (z >= FLOOR_Z).then_some(VoxColor(0x80_44_66_88))
2883        });
2884        let grid = GridView::from_single_vxl(&vxl);
2885
2886        // At z=30 (above the z=40 floor), looking +y horizontally,
2887        // down = +z. Upward rays (low py) escape through the box top
2888        // (z=0) → sky; downward rays (high py) strike the floor.
2889        let cam = Camera {
2890            pos: [32.0, 4.0, 30.0],
2891            right: [-1.0, 0.0, 0.0],
2892            down: [0.0, 0.0, 1.0],
2893            forward: [0.0, 1.0, 0.0],
2894        };
2895        let (w, h) = (64u32, 64u32);
2896        let mask = render_mask(grid, &cam, w, h);
2897
2898        let count_band = |y0: usize, y1: usize| -> usize {
2899            (y0 * w as usize..y1 * w as usize)
2900                .filter(|&i| mask[i])
2901                .count()
2902        };
2903        let top = count_band(0, h as usize / 4);
2904        let bottom = count_band(3 * h as usize / 4, h as usize);
2905        assert!(mask.iter().any(|&b| b), "floor must be visible");
2906        assert!(mask.iter().any(|&b| !b), "sky must be visible");
2907        assert!(
2908            bottom > top,
2909            "bottom band ({bottom}) should hit more floor than top band ({top})"
2910        );
2911    }
2912
2913    /// Render `grid` from `camera` with the dense reference cast (no
2914    /// brickmap), returning `(colour, depth)` buffers.
2915    fn render_reference(
2916        grid: GridView<'_>,
2917        camera: &Camera,
2918        w: u32,
2919        h: u32,
2920    ) -> (Vec<u32>, Vec<f32>) {
2921        let n = (w as usize) * (h as usize);
2922        let mut fb = vec![0u32; n];
2923        let mut zb = vec![f32::INFINITY; n];
2924        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2925        let cs = camera_math::derive(camera, w, h, settings.hx, settings.hy, settings.hz);
2926        for py in 0..h {
2927            for px in 0..w {
2928                let (o, d) = pixel_ray(&cs, &settings, px, py);
2929                if let Some(hit) = cast_ray_reference(o, d, cs.forward, &grid, &settings) {
2930                    let i = (py * w + px) as usize;
2931                    fb[i] = hit.color;
2932                    zb[i] = hit.dist;
2933                }
2934            }
2935        }
2936        (fb, zb)
2937    }
2938
2939    /// Render `grid` from `camera` via the production brickmap path.
2940    fn render_brickmap(
2941        grid: GridView<'_>,
2942        camera: &Camera,
2943        w: u32,
2944        h: u32,
2945    ) -> (Vec<u32>, Vec<f32>) {
2946        render_brickmap_env(grid, camera, w, h, &DdaEnv::default())
2947    }
2948
2949    /// As [`render_brickmap`] but with an explicit [`DdaEnv`] (fog /
2950    /// textured sky / side shades).
2951    fn render_brickmap_env(
2952        grid: GridView<'_>,
2953        camera: &Camera,
2954        w: u32,
2955        h: u32,
2956        env: &DdaEnv<'_>,
2957    ) -> (Vec<u32>, Vec<f32>) {
2958        let n = (w as usize) * (h as usize);
2959        let mut fb = vec![0u32; n];
2960        let mut zb = vec![f32::INFINITY; n];
2961        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
2962        {
2963            let mut sink = RasterSink::new(&mut fb, &mut zb);
2964            render_dda(camera, &settings, grid, w as usize, env, 0, &mut sink);
2965        }
2966        (fb, zb)
2967    }
2968
2969    /// Regression for the cave-demo "bright sky seams" report: the
2970    /// empty-space-skip walk must not leak past an occupied box the ray
2971    /// only grazes at a shared edge/corner. A 1-voxel-thick diagonal
2972    /// wall (`x+y==64`, voxels edge-connected) with air on both sides is
2973    /// the canonical case. The production skip walk must hit exactly the
2974    /// same pixels as the dense per-cell reference — zero divergence.
2975    #[test]
2976    fn no_sky_leak_through_diagonal_wall() {
2977        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
2978            ((x + y == 64) && (2..62).contains(&z)).then_some(VoxColor(0x80_40_80_60))
2979        });
2980        let grid = GridView::from_single_vxl(&vxl);
2981        let (w, h) = (160u32, 160u32);
2982        let c = [10.0, 10.0, 32.0];
2983        let poses = [
2984            Camera::from_yaw_pitch(c, 0.785, 0.0),
2985            Camera::from_yaw_pitch(c, 0.6, 0.1),
2986            Camera::from_yaw_pitch(c, 0.95, -0.1),
2987            Camera::from_yaw_pitch(c, 0.785, 0.3),
2988            Camera::from_yaw_pitch(c, 0.5, 0.0),
2989        ];
2990        for (i, cam) in poses.iter().enumerate() {
2991            let (fb_b, _) = render_brickmap(grid, cam, w, h);
2992            let (fb_r, _) = render_reference(grid, cam, w, h);
2993            let leak = (0..(w * h) as usize)
2994                .filter(|&k| (fb_b[k] != 0) != (fb_r[k] != 0))
2995                .count();
2996            assert_eq!(leak, 0, "pose {i}: {leak} px diverge from dense reference");
2997        }
2998    }
2999
3000    /// TV terrain transparency: a glass-coloured voxel slab in front of an
3001    /// opaque floor. With no terrain material map the glass is an opaque first
3002    /// hit; with the map it becomes translucent and the floor tints through.
3003    #[test]
3004    fn terrain_glass_tints_floor_behind() {
3005        let glass = VoxColor(0x80_40_C0_E0); // cyan
3006        let floor = VoxColor(0x80_C0_40_40); // red
3007        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3008            if z == 4 {
3009                Some(glass)
3010            } else if z >= 10 {
3011                Some(floor)
3012            } else {
3013                None
3014            }
3015        });
3016        let grid = GridView::from_single_vxl(&vxl);
3017        // Camera above the grid looking straight down (+z), centred.
3018        let cam = Camera {
3019            pos: [8.0, 8.0, 0.0],
3020            right: [1.0, 0.0, 0.0],
3021            down: [0.0, 1.0, 0.0],
3022            forward: [0.0, 0.0, 1.0],
3023        };
3024        let (w, h) = (32u32, 32u32);
3025        let centre = (h / 2 * w + w / 2) as usize;
3026
3027        // Opaque: the glass voxel stops the ray (no terrain materials).
3028        let (fb_op, _) = render_brickmap(grid, &cam, w, h);
3029        assert_eq!(
3030            fb_op[centre] & 0x00ff_ffff,
3031            0x0040_C0E0,
3032            "opaque glass first-hit"
3033        );
3034
3035        // Translucent: glass colour → material 1 (alpha-blend).
3036        let mut table = MaterialTable::new();
3037        table.set(1, Material::alpha_blend(128));
3038        let env = DdaEnv {
3039            materials: Some(&table),
3040            terrain_materials: &[(glass.rgb_part(), 1)],
3041            lights: CpuLights::default(),
3042            ..DdaEnv::default()
3043        };
3044        let (fb_tr, _) = render_brickmap_env(grid, &cam, w, h, &env);
3045        assert_ne!(
3046            fb_tr[centre], fb_op[centre],
3047            "glass should composite over the floor, not stay opaque"
3048        );
3049        let r_op = (fb_op[centre] >> 16) & 0xff; // glass red ≈ 0x40
3050        let r_tr = (fb_tr[centre] >> 16) & 0xff; // + floor red bleeds in
3051        assert!(
3052            r_tr > r_op,
3053            "floor red tints through the glass (op={r_op:02x} tr={r_tr:02x})"
3054        );
3055    }
3056
3057    /// EV.1 — an emissive terrain voxel renders at over-bright albedo,
3058    /// ignoring the baked brightness byte, per-face side shades and the
3059    /// dynamic light rig.
3060    #[test]
3061    fn terrain_emissive_ignores_lighting() {
3062        let crystal = VoxColor(0x40_20_60_80); // deliberately DIM baked byte 0x40
3063        let vxl =
3064            roxlap_formats::vxl::Vxl::from_dense(
3065                16,
3066                |_, _, z| if z >= 4 { Some(crystal) } else { None },
3067            );
3068        let grid = GridView::from_single_vxl(&vxl);
3069        let cam = Camera {
3070            pos: [8.0, 8.0, 0.0],
3071            right: [1.0, 0.0, 0.0],
3072            down: [0.0, 1.0, 0.0],
3073            forward: [0.0, 0.0, 1.0],
3074        };
3075        let (w, h) = (32u32, 32u32);
3076        let centre = (h / 2 * w + w / 2) as usize;
3077
3078        // Control: the baked 0x40 byte halves the albedo.
3079        let (fb_dim, _) = render_brickmap(grid, &cam, w, h);
3080        assert_eq!(
3081            fb_dim[centre] & 0x00ff_ffff,
3082            0x0010_3040,
3083            "baked byte 0x40 = albedo/2"
3084        );
3085
3086        // Emissive: glow(255) ⇒ ×255/128 over-bright, per-channel clamp.
3087        let mut table = MaterialTable::new();
3088        table.set(1, Material::glow(255));
3089        let base = DdaEnv {
3090            materials: Some(&table),
3091            terrain_materials: &[(crystal.rgb_part(), 1)],
3092            ..DdaEnv::default()
3093        };
3094        let (fb_em, _) = render_brickmap_env(grid, &cam, w, h, &base);
3095        assert_eq!(
3096            fb_em[centre] & 0x00ff_ffff,
3097            0x003f_bfff,
3098            "glow(255) ≈ 2× albedo (0x20,0x60,0x80 → 0x3f,0xbf,0xff)"
3099        );
3100
3101        // Max side shades: a normal voxel darkens, an emissive one must not.
3102        let shaded_env = DdaEnv {
3103            side_shades: [64; 6],
3104            ..DdaEnv::default()
3105        };
3106        let (fb_ss_plain, _) = render_brickmap_env(grid, &cam, w, h, &shaded_env);
3107        assert_ne!(
3108            fb_ss_plain[centre], fb_dim[centre],
3109            "control: side shades darken a non-emissive voxel"
3110        );
3111        let em_ss = DdaEnv {
3112            side_shades: [64; 6],
3113            ..base
3114        };
3115        let (fb_em_ss, _) = render_brickmap_env(grid, &cam, w, h, &em_ss);
3116        assert_eq!(
3117            fb_em_ss[centre], fb_em[centre],
3118            "side shades must not touch an emissive voxel"
3119        );
3120
3121        // Dynamic rig active (no sun, zero ambient ⇒ a normal voxel goes
3122        // black): the emissive voxel is rig-independent.
3123        let em_rig = DdaEnv {
3124            lights: CpuLights {
3125                enabled: true,
3126                ..CpuLights::default()
3127            },
3128            ..base
3129        };
3130        let (fb_em_rig, _) = render_brickmap_env(grid, &cam, w, h, &em_rig);
3131        assert_eq!(
3132            fb_em_rig[centre], fb_em[centre],
3133            "the dynamic rig must not touch an emissive voxel"
3134        );
3135    }
3136
3137    /// TV terrain Volumetric: a **filled** grey smoke volume over a red floor.
3138    /// Beer–Lambert opacity grows with the ray's path length, so a deeper smoke
3139    /// column shows more of its own colour (green channel rises toward the
3140    /// smoke grey) — thickness-dependent, unlike per-span AlphaBlend.
3141    #[test]
3142    fn terrain_volumetric_thickness_deepens_opacity() {
3143        let smoke = VoxColor(0x80_90_90_90); // grey
3144        let floor = VoxColor(0x80_C0_20_20); // red (low green)
3145                                             // Centre green channel for a smoke column `depth` voxels deep (filled),
3146                                             // floor at z>=12, camera looking straight down.
3147        let green_at = |depth: u32| -> u32 {
3148            let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3149                if (4..4 + depth).contains(&z) {
3150                    Some(smoke)
3151                } else if z >= 12 {
3152                    Some(floor)
3153                } else {
3154                    None
3155                }
3156            });
3157            let grid = GridView::from_single_vxl(&vxl);
3158            let cam = Camera {
3159                pos: [8.0, 8.0, 0.0],
3160                right: [1.0, 0.0, 0.0],
3161                down: [0.0, 1.0, 0.0],
3162                forward: [0.0, 0.0, 1.0],
3163            };
3164            let (w, h) = (32u32, 32u32);
3165            let mut table = MaterialTable::new();
3166            table.set(1, Material::volumetric(80));
3167            let env = DdaEnv {
3168                materials: Some(&table),
3169                terrain_materials: &[(smoke.rgb_part(), 1)],
3170                lights: CpuLights::default(),
3171                ..DdaEnv::default()
3172            };
3173            let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
3174            (fb[(h / 2 * w + w / 2) as usize] >> 8) & 0xff
3175        };
3176        let shallow = green_at(1);
3177        let deep = green_at(7);
3178        assert!(
3179            deep > shallow,
3180            "deeper Volumetric smoke shows more of its grey (deep g={deep:02x} > shallow g={shallow:02x})"
3181        );
3182    }
3183
3184    /// DDA.5: distance fog blends a hit toward the fog colour. A far
3185    /// floor pixel is closer to the fog colour than a near one.
3186    #[test]
3187    fn distance_fog_blends_toward_fog_color() {
3188        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
3189            (z >= 40).then_some(VoxColor(0x80_FF_FF_FF))
3190        });
3191        let grid = GridView::from_single_vxl(&vxl);
3192        let cam = Camera {
3193            pos: [32.0, 2.0, 38.0],
3194            right: [1.0, 0.0, 0.0],
3195            down: [0.0, 0.0, 1.0],
3196            forward: [0.0, 1.0, 0.0],
3197        };
3198        let env = DdaEnv {
3199            sky: None,
3200            fog_color: 0x00_00_00_00, // black fog → distance darkens
3201            fog_max_dist: 64.0,
3202            side_shades: [0; 6],
3203            materials: None,
3204            terrain_materials: &[],
3205            lights: CpuLights::default(),
3206            world_shadow: None,
3207            z_clip: None,
3208            cutout: None,
3209            fow: None,
3210        };
3211        let (w, h) = (64u32, 64u32);
3212        let (fog, _) = render_brickmap_env(grid, &cam, w, h, &env);
3213        let (nofog, zb) = render_brickmap(grid, &cam, w, h);
3214        let (idx, depth) = zb.iter().enumerate().filter(|(_, z)| z.is_finite()).fold(
3215            (0usize, 0.0f32),
3216            |acc, (i, &z)| {
3217                if z > acc.1 {
3218                    (i, z)
3219                } else {
3220                    acc
3221                }
3222            },
3223        );
3224        assert!(depth > 20.0, "need a deep pixel to test fog (got {depth})");
3225        let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
3226        assert!(
3227            lum(fog[idx]) < lum(nofog[idx]),
3228            "fogged pixel {:08x} not darker than {:08x}",
3229            fog[idx],
3230            nofog[idx]
3231        );
3232    }
3233
3234    /// DDA.5: with a textured sky, miss pixels are filled from the sky
3235    /// panorama (direction-dependent) instead of left at the pre-fill.
3236    #[test]
3237    fn textured_sky_fills_misses() {
3238        let sky = crate::sky::Sky::blue_gradient();
3239        let vxl = roxlap_formats::vxl::Vxl::empty(32); // all air → all miss
3240        let grid = GridView::from_single_vxl(&vxl);
3241        let env = DdaEnv {
3242            sky: Some(&sky),
3243            fog_color: 0,
3244            fog_max_dist: 0.0,
3245            side_shades: [0; 6],
3246            materials: None,
3247            terrain_materials: &[],
3248            lights: CpuLights::default(),
3249            world_shadow: None,
3250            z_clip: None,
3251            cutout: None,
3252            fow: None,
3253        };
3254        let cam = Camera::from_yaw_pitch([16.0, 16.0, 128.0], 0.3, -0.4);
3255        let (w, h) = (48u32, 48u32);
3256        let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
3257        assert!(fb.iter().all(|&c| c >> 24 == 0x80), "all misses sky-filled");
3258        let top = fb[0];
3259        let bottom = fb[(h - 1) as usize * w as usize];
3260        assert_ne!(top, bottom, "sky gradient should vary with elevation");
3261    }
3262
3263    /// Sky elevation orientation matches the GPU `sky_color` (acos(-z)/π):
3264    /// looking **up** (−z) samples panorama column 0 (zenith), looking
3265    /// **down** (+z) samples the last column (nadir). Regression for the
3266    /// CPU up/down inversion.
3267    #[test]
3268    fn sky_elevation_zenith_at_column_zero() {
3269        let mut pixels = vec![0i32; 8];
3270        pixels[0] = 0x0011_1111; // zenith marker
3271        pixels[7] = 0x0099_9999; // nadir marker
3272        let sky = crate::sky::Sky::from_pixels(pixels, 8, 1);
3273        let up = sample_sky(&sky, [0.0, 0.0, -1.0]); // −z is up
3274        let down = sample_sky(&sky, [0.0, 0.0, 1.0]); // +z is down
3275        assert_eq!(
3276            up & 0x00ff_ffff,
3277            0x0011_1111,
3278            "looking up → column 0 (zenith)"
3279        );
3280        assert_eq!(
3281            down & 0x00ff_ffff,
3282            0x0099_9999,
3283            "looking down → last column (nadir)"
3284        );
3285    }
3286
3287    /// `render_sky_fill` paints the panorama for a **gridless** view — the
3288    /// same per-pixel sky sample the miss-ray path uses, with no grid present
3289    /// (the CPU empty-scene background, matching the GPU).
3290    #[test]
3291    fn sky_fill_paints_panorama_gridless() {
3292        let sky = crate::sky::Sky::blue_gradient();
3293        let cam = Camera::from_yaw_pitch([0.0, 0.0, 0.0], 0.3, -0.4);
3294        let (w, h) = (48u32, 48u32);
3295        let cs = crate::camera_math::derive(&cam, w, h, 24.0, 24.0, 24.0);
3296        let settings = crate::opticast::OpticastSettings::for_oracle_framebuffer(w, h);
3297        let mut fb = vec![0u32; (w * h) as usize];
3298        // All-background z-buffer (+∞) → every pixel gets the sky.
3299        let zb = vec![f32::INFINITY; (w * h) as usize];
3300        render_sky_fill(&mut fb, &zb, w as usize, w, h, &cs, &settings, &sky);
3301        assert!(
3302            fb.iter().all(|&c| c >> 24 == 0x80),
3303            "every pixel sky-filled with the brightness byte set"
3304        );
3305        let top = fb[0];
3306        let bottom = fb[(h - 1) as usize * w as usize];
3307        assert_ne!(top, bottom, "sky gradient should vary with elevation");
3308        // A finite-z (terrain) pixel is left untouched.
3309        let mut fb2 = vec![0x1234_5678u32; (w * h) as usize];
3310        let mut zb2 = vec![f32::INFINITY; (w * h) as usize];
3311        zb2[0] = 10.0; // pretend a terrain hit at pixel 0
3312        render_sky_fill(&mut fb2, &zb2, w as usize, w, h, &cs, &settings, &sky);
3313        assert_eq!(fb2[0], 0x1234_5678, "finite-z pixel is not overwritten");
3314    }
3315
3316    /// DDA.5: side shading darkens the hit face by its `side_shades`
3317    /// entry. A top-facing floor (ray crosses +z to enter) gets the
3318    /// `z-` face reduction (index 4).
3319    #[test]
3320    fn side_shades_darken_hit_face() {
3321        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3322            (z >= 8).then_some(VoxColor(0x80_FF_FF_FF))
3323        });
3324        let grid = GridView::from_single_vxl(&vxl);
3325        let cam = Camera {
3326            pos: [8.0, 8.0, 2.0],
3327            right: [1.0, 0.0, 0.0],
3328            down: [0.0, 1.0, 0.0],
3329            forward: [0.0, 0.0, 1.0],
3330        };
3331        let centre = 16 * 32 + 16;
3332        let (plain, _) = render_brickmap(grid, &cam, 32, 32);
3333        let env = DdaEnv {
3334            sky: None,
3335            fog_color: 0,
3336            fog_max_dist: 0.0,
3337            side_shades: [0, 0, 0, 0, 0x40, 0],
3338            materials: None,
3339            terrain_materials: &[],
3340            lights: CpuLights::default(),
3341            world_shadow: None,
3342            z_clip: None,
3343            cutout: None,
3344            fow: None,
3345        };
3346        let (shaded, _) = render_brickmap_env(grid, &cam, 32, 32, &env);
3347        let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
3348        assert!(
3349            lum(shaded[centre]) < lum(plain[centre]),
3350            "side-shaded face {:08x} not darker than {:08x}",
3351            shaded[centre],
3352            plain[centre]
3353        );
3354    }
3355
3356    /// The two-level brick-skip cast closely approximates the dense
3357    /// per-voxel reference. The outer brick DDA re-seeds the inner cell
3358    /// walk at each occupied brick, so a few silhouette-boundary pixels
3359    /// jitter by one voxel (different hit cell → different colour/depth)
3360    /// — visually invisible, and the gain is ~`BRICK`× fewer air steps.
3361    /// Assert the divergence is tiny: coverage (hit/sky mask) is nearly
3362    /// identical and only a small fraction of pixels differ. (The
3363    /// thread-invariance guarantee is the separate, exact
3364    /// `parallel_matches_sequential`.)
3365    #[test]
3366    fn brickmap_approximates_dense_reference() {
3367        // Rolling heightmap + a floating block (air above and below).
3368        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
3369            let surf = 30 + ((x / 5 + y / 7) % 11);
3370            let ground = z >= surf;
3371            let block = (20..=24).contains(&z) && (10..20).contains(&x) && (40..50).contains(&y);
3372            (ground || block).then_some(VoxColor(0x80_30_50_70 + (x ^ y) % 0x40))
3373        });
3374        let grid = GridView::from_single_vxl(&vxl);
3375
3376        let (w, h) = (80u32, 80u32);
3377        let poses = [
3378            Camera::orbit(0.6, 0.5, 90.0, [32.0, 32.0, 40.0]),
3379            Camera::orbit(2.1, 0.2, 70.0, [32.0, 32.0, 35.0]),
3380            Camera::orbit(-1.0, 0.9, 120.0, [32.0, 32.0, 45.0]),
3381        ];
3382        let n = (w * h) as usize;
3383        for (i, cam) in poses.iter().enumerate() {
3384            let (fb_b, zb_b) = render_brickmap(grid, cam, w, h);
3385            let (fb_r, _zb_r) = render_reference(grid, cam, w, h);
3386            // Coverage (hit vs sky) must match almost exactly.
3387            let cov_b = fb_b.iter().filter(|&&c| c != 0).count();
3388            let cov_r = fb_r.iter().filter(|&&c| c != 0).count();
3389            assert!(cov_b > 200, "pose {i} rendered ~empty (cov {cov_b})");
3390            let cov_diff = cov_b.abs_diff(cov_r);
3391            assert!(
3392                cov_diff * 100 <= n, // < 1 % of pixels flip hit↔sky
3393                "pose {i} coverage diverged: brick {cov_b} vs dense {cov_r}"
3394            );
3395            // Colour diffs (boundary-voxel jitter) must be a small slice.
3396            let diffs = fb_b.iter().zip(&fb_r).filter(|(a, b)| a != b).count();
3397            assert!(
3398                diffs * 100 <= n * 3, // < 3 % of pixels differ
3399                "pose {i} too many pixel diffs vs dense: {diffs}/{n}"
3400            );
3401            // Depth must be sane (finite where hit), not wildly off.
3402            for k in 0..n {
3403                if fb_b[k] != 0 {
3404                    assert!(zb_b[k].is_finite(), "pose {i} px {k} non-finite depth");
3405                }
3406            }
3407        }
3408    }
3409
3410    /// DDA.5: a voxel's baked brightness byte darkens its colour. A
3411    /// half-bright voxel (`a = 0x40`) renders at roughly half RGB; a
3412    /// full-bright one (`a = 0x80`) is unchanged.
3413    #[test]
3414    fn baked_brightness_darkens_color() {
3415        // Half brightness: alpha 0x40 (64/128). White RGB → ~mid grey.
3416        let dim = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3417            (z >= 8).then_some(VoxColor(0x40_FF_FF_FF))
3418        });
3419        let grid = GridView::from_single_vxl(&dim);
3420        let cam = Camera {
3421            pos: [8.0, 8.0, 2.0],
3422            right: [1.0, 0.0, 0.0],
3423            down: [0.0, 1.0, 0.0],
3424            forward: [0.0, 0.0, 1.0],
3425        };
3426        let (fb, _) = render_brickmap(grid, &cam, 32, 32);
3427        let centre = 16 * 32 + 16;
3428        // 0xFF * 64 >> 7 = 127 per channel; alpha normalised to 0x80.
3429        assert_eq!(fb[centre], 0x80_7F_7F_7F, "got {:08x}", fb[centre]);
3430
3431        // Full brightness passes RGB through unchanged.
3432        let full = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
3433            (z >= 8).then_some(VoxColor(0x80_FF_FF_FF))
3434        });
3435        let gridf = GridView::from_single_vxl(&full);
3436        let (fbf, _) = render_brickmap(gridf, &cam, 32, 32);
3437        assert_eq!(fbf[centre], 0x80_FF_FF_FF, "got {:08x}", fbf[centre]);
3438    }
3439
3440    /// DDA.4 headline gate: cross-chunk look-down. A camera in an
3441    /// all-air upper chunk (chz=0) looking straight down must see the
3442    /// floor in the *lower* stacked chunk (chz=1), through the chunk-Z
3443    /// boundary. This is exactly the case the voxlap renderer needed the
3444    /// whole virtual-column stack (S4B.6.j / VC) for; the DDA gets it
3445    /// for free from the outer box spanning `chunks_z`.
3446    #[test]
3447    fn cross_chunk_lookdown_sees_lower_stacked_floor() {
3448        const FLOOR_LOCAL_Z: u32 = 40;
3449        const FLOOR_COL: VoxColor = VoxColor(0x80_22_88_44);
3450        let upper = roxlap_formats::vxl::Vxl::empty(32); // all air + bedrock
3451        let lower = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
3452            (z >= FLOOR_LOCAL_Z).then_some(FLOOR_COL)
3453        });
3454        let v_up = GridView::from_single_vxl(&upper);
3455        let v_lo = GridView::from_single_vxl(&lower);
3456        // Z-stack: index (dz*chunks_y+dy)*chunks_x+dx → [upper, lower].
3457        let chunks = [Some(v_up), Some(v_lo)];
3458        let cg = crate::ChunkGrid {
3459            chunks: &chunks,
3460            origin_chunk_xy: [0, 0],
3461            origin_chunk_z: 0,
3462            chunks_x: 1,
3463            chunks_y: 1,
3464            chunks_z: 2,
3465        };
3466        let grid = GridView::from_chunk_grid(&cg, 32);
3467
3468        // Camera in the upper chunk (world z=100), looking straight down.
3469        let cam = Camera {
3470            pos: [16.0, 16.0, 100.0],
3471            right: [1.0, 0.0, 0.0],
3472            down: [0.0, 1.0, 0.0],
3473            forward: [0.0, 0.0, 1.0],
3474        };
3475        let (w, h) = (48u32, 48u32);
3476        let (fb, zb) = render_brickmap(grid, &cam, w, h);
3477        let centre = 24 * 48 + 24;
3478        assert!(
3479            fb[centre] & 0x00ff_ffff == FLOOR_COL.0 & 0x00ff_ffff,
3480            "centre ray must reach the lower-chunk floor (got {:08x})",
3481            fb[centre]
3482        );
3483        // Floor world-z = 256 + 40 = 296; camera z = 100 → depth ≈ 196.
3484        let expected = 296.0 - 100.0;
3485        assert!(
3486            (zb[centre] - expected).abs() < 2.0,
3487            "look-down depth {} not ≈ {expected}",
3488            zb[centre]
3489        );
3490    }
3491
3492    /// CA.1 — render with only a cutaway clip set (rest of the env
3493    /// default).
3494    fn render_clip(
3495        grid: GridView<'_>,
3496        camera: &Camera,
3497        w: u32,
3498        h: u32,
3499        z_clip: Option<i32>,
3500    ) -> (Vec<u32>, Vec<f32>) {
3501        let env = DdaEnv {
3502            z_clip,
3503            ..DdaEnv::default()
3504        };
3505        render_brickmap_env(grid, camera, w, h, &env)
3506    }
3507
3508    /// CA.1 golden — a 3-deck fixture: the clip hides whole decks and
3509    /// the look-down ray lands on the first floor at-or-below the
3510    /// plane, with exact colour + depth.
3511    #[test]
3512    fn cutaway_clip_reveals_lower_decks() {
3513        const DECK_A: VoxColor = VoxColor(0x80_C0_30_30); // z = 60
3514        const DECK_B: VoxColor = VoxColor(0x80_30_C0_30); // z = 120
3515        const DECK_C: VoxColor = VoxColor(0x80_30_30_C0); // z = 180
3516        let vxl = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| match z {
3517            60 => Some(DECK_A),
3518            120 => Some(DECK_B),
3519            180 => Some(DECK_C),
3520            _ => None,
3521        });
3522        let grid = GridView::from_single_vxl(&vxl);
3523        let cam = Camera {
3524            pos: [16.0, 16.0, 10.0],
3525            right: [1.0, 0.0, 0.0],
3526            down: [0.0, 1.0, 0.0],
3527            forward: [0.0, 0.0, 1.0],
3528        };
3529        let (w, h) = (32u32, 32u32);
3530        let centre = (h / 2 * w + w / 2) as usize;
3531        // (clip, expected floor colour, expected floor z)
3532        let cases = [
3533            (None, DECK_A, 60.0),
3534            (Some(100), DECK_B, 120.0), // deck boundary: A hidden
3535            (Some(120), DECK_B, 120.0), // z_clip itself stays visible
3536            (Some(121), DECK_C, 180.0), // one past → B hidden too
3537        ];
3538        for (clip, col, z) in cases {
3539            let (fb, zb) = render_clip(grid, &cam, w, h, clip);
3540            assert_eq!(
3541                fb[centre], col.0,
3542                "clip {clip:?}: expected {:08x}, got {:08x}",
3543                col.0, fb[centre]
3544            );
3545            let expected = z - 10.0;
3546            assert!(
3547                (zb[centre] - expected).abs() < 1.0,
3548                "clip {clip:?}: depth {} not ≈ {expected}",
3549                zb[centre]
3550            );
3551        }
3552    }
3553
3554    /// CA.1 — cutting through a SOLID run: the cut face renders with
3555    /// the run's top-surface colour (the [`GridView::surface_color_mip`]
3556    /// interior fallback — decision 3 of the CA entry doc), not black
3557    /// and not the colour of some other voxel.
3558    #[test]
3559    fn cutaway_cut_face_uses_run_top_colour() {
3560        // Solid from z=100 down; every voxel gets a z-derived colour,
3561        // but RLE only STORES surface colours — the run top is z=100.
3562        let col_at = |z: u32| VoxColor(0x8000_0000 | (0x40 + z));
3563        let vxl =
3564            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= 100).then_some(col_at(z)));
3565        let grid = GridView::from_single_vxl(&vxl);
3566        let cam = Camera {
3567            pos: [16.0, 16.0, 10.0],
3568            right: [1.0, 0.0, 0.0],
3569            down: [0.0, 1.0, 0.0],
3570            forward: [0.0, 0.0, 1.0],
3571        };
3572        let (w, h) = (32u32, 32u32);
3573        let centre = (h / 2 * w + w / 2) as usize;
3574        // Unclipped: the real top face at z=100, its own colour.
3575        let (fb, zb) = render_clip(grid, &cam, w, h, None);
3576        assert_eq!(fb[centre], col_at(100).0);
3577        assert!((zb[centre] - 90.0).abs() < 1.0);
3578        // Clipped mid-run: the cut face at z=150 falls back to the run
3579        // top's colour (interior voxels are colourless in voxlap RLE).
3580        let (fb, zb) = render_clip(grid, &cam, w, h, Some(150));
3581        assert_eq!(
3582            fb[centre],
3583            col_at(100).0,
3584            "cut face must reuse the run-top colour, got {:08x}",
3585            fb[centre]
3586        );
3587        assert!(
3588            (zb[centre] - 140.0).abs() < 1.0,
3589            "cut face depth {} not ≈ 140",
3590            zb[centre]
3591        );
3592    }
3593
3594    /// CA.1 — the clip compares the grid-local ABSOLUTE voxel z, not
3595    /// the in-chunk local z: on a stacked-chz grid a plane inside chz=1
3596    /// (`z_clip > 255`) must hide the chz=0 deck yet keep the deeper
3597    /// chz=1 floor. An in-chunk-z bug (`loc[2] < clip`, always true for
3598    /// clip > 255) would render all-sky here.
3599    #[test]
3600    fn cutaway_clip_is_absolute_z_on_stacked_chunks() {
3601        const UPPER_COL: VoxColor = VoxColor(0x80_C0_C0_30); // abs z = 40
3602        const LOWER_COL: VoxColor = VoxColor(0x80_30_C0_C0); // abs z = 296
3603        let upper =
3604            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z == 40).then_some(UPPER_COL));
3605        let lower =
3606            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= 40).then_some(LOWER_COL));
3607        let v_up = GridView::from_single_vxl(&upper);
3608        let v_lo = GridView::from_single_vxl(&lower);
3609        let chunks = [Some(v_up), Some(v_lo)];
3610        let cg = crate::ChunkGrid {
3611            chunks: &chunks,
3612            origin_chunk_xy: [0, 0],
3613            origin_chunk_z: 0,
3614            chunks_x: 1,
3615            chunks_y: 1,
3616            chunks_z: 2,
3617        };
3618        let grid = GridView::from_chunk_grid(&cg, 32);
3619        let cam = Camera {
3620            pos: [16.0, 16.0, 10.0],
3621            right: [1.0, 0.0, 0.0],
3622            down: [0.0, 1.0, 0.0],
3623            forward: [0.0, 0.0, 1.0],
3624        };
3625        let (w, h) = (32u32, 32u32);
3626        let centre = (h / 2 * w + w / 2) as usize;
3627        // Unclipped: the chz=0 deck at abs z=40.
3628        let (fb, zb) = render_clip(grid, &cam, w, h, None);
3629        assert_eq!(fb[centre], UPPER_COL.0);
3630        assert!((zb[centre] - 30.0).abs() < 1.0);
3631        // Clip inside chz=1: the chz=0 deck vanishes, the chz=1 floor
3632        // (abs z = 256 + 40 = 296 ≥ 290) stays.
3633        let (fb, zb) = render_clip(grid, &cam, w, h, Some(290));
3634        assert_eq!(
3635            fb[centre], LOWER_COL.0,
3636            "expected the chz=1 floor, got {:08x}",
3637            fb[centre]
3638        );
3639        assert!(
3640            (zb[centre] - 286.0).abs() < 1.0,
3641            "depth {} not ≈ 286",
3642            zb[centre]
3643        );
3644    }
3645
3646    /// CA.1 — pins the mip formula `z_clip >> mip` (floor). At mip 1
3647    /// with `z_clip = 101` the plane rounds DOWN to mip-cell 50 (voxels
3648    /// 100..102), so the surface at z=100 stays visible — the accepted
3649    /// coarse-mip bleed. A round-up formula would hide that cell and
3650    /// the depth would jump to 102. CPU and GPU must share this exact
3651    /// formula (CA.3 parity gate).
3652    #[test]
3653    fn cutaway_clip_mip_formula_floors() {
3654        let mut vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
3655            (z >= 100).then_some(VoxColor(0x80_80_80_80))
3656        });
3657        vxl.generate_mips(2);
3658        assert!(vxl.mip_count() >= 2, "need mip 1 built");
3659        let grid = GridView::from_single_vxl(&vxl);
3660        let cam = Camera {
3661            pos: [32.0, 32.0, 10.0],
3662            right: [1.0, 0.0, 0.0],
3663            down: [0.0, 1.0, 0.0],
3664            forward: [0.0, 0.0, 1.0],
3665        };
3666        let (w, h) = (32u32, 32u32);
3667        let n = (w as usize) * (h as usize);
3668        let centre = (h / 2 * w + w / 2) as usize;
3669        let mut fb = vec![0u32; n];
3670        let mut zb = vec![f32::INFINITY; n];
3671        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
3672        let env = DdaEnv {
3673            z_clip: Some(101),
3674            ..DdaEnv::default()
3675        };
3676        {
3677            let mut sink = RasterSink::new(&mut fb, &mut zb);
3678            render_dda(&cam, &settings, grid, w as usize, &env, 1, &mut sink);
3679        }
3680        assert_ne!(fb[centre], 0, "mip-1 clipped surface must still hit");
3681        assert!(
3682            (zb[centre] - 90.0).abs() < 1.5,
3683            "mip-cell 50 (voxel z=100) must be the first visible layer \
3684             (floor formula); depth {} not ≈ 90",
3685            zb[centre]
3686        );
3687    }
3688
3689    /// CA.1 standing gate — `z_clip = None` and a no-op clip (nothing
3690    /// above the plane) are byte-identical to the unclipped render.
3691    #[test]
3692    fn cutaway_clip_disabled_is_byte_identical() {
3693        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
3694            let surf = 28 + ((x / 4 + y / 6) % 13);
3695            (z >= surf).then_some(VoxColor(0x80_40_60_80 + (x ^ y) % 0x30))
3696        });
3697        let grid = GridView::from_single_vxl(&vxl);
3698        let cam = Camera::orbit(0.8, 0.55, 100.0, [32.0, 32.0, 40.0]);
3699        let (w, h) = (96u32, 96u32);
3700        let (fb_base, zb_base) = render_brickmap(grid, &cam, w, h);
3701        let (fb_none, zb_none) = render_clip(grid, &cam, w, h, None);
3702        assert_eq!(fb_base, fb_none, "z_clip=None must not change a pixel");
3703        assert_eq!(zb_base, zb_none);
3704        // `z < 0` hides nothing on a chz≥0 grid — still byte-identical
3705        // (pins the strict `<` comparison).
3706        let (fb_zero, zb_zero) = render_clip(grid, &cam, w, h, Some(0));
3707        assert_eq!(fb_base, fb_zero, "no-op clip must not change a pixel");
3708        assert_eq!(zb_base, zb_zero);
3709    }
3710
3711    /// OC.1 — render with only a view cutout set (rest of the env
3712    /// default).
3713    fn render_cutout(
3714        grid: GridView<'_>,
3715        camera: &Camera,
3716        w: u32,
3717        h: u32,
3718        cutout: Option<CpuCutout>,
3719    ) -> (Vec<u32>, Vec<f32>) {
3720        let env = DdaEnv {
3721            cutout,
3722            ..DdaEnv::default()
3723        };
3724        render_brickmap_env(grid, camera, w, h, &env)
3725    }
3726
3727    /// OC.1 — the wall-between-camera-and-focus fixture: a front wall
3728    /// at y=20 and a room back wall at y=48, camera at y=4 looking +y.
3729    const CUT_WALL: VoxColor = VoxColor(0x80_C0_40_40);
3730    const CUT_BACK: VoxColor = VoxColor(0x80_40_C0_40);
3731    fn keyhole_fixture() -> roxlap_formats::vxl::Vxl {
3732        roxlap_formats::vxl::Vxl::from_dense(64, |_, y, _| match y {
3733            20 => Some(CUT_WALL),
3734            48 => Some(CUT_BACK),
3735            _ => None,
3736        })
3737    }
3738    fn keyhole_camera() -> Camera {
3739        Camera {
3740            pos: [32.0, 4.0, 128.0],
3741            right: [1.0, 0.0, 0.0],
3742            down: [0.0, 0.0, 1.0],
3743            forward: [0.0, 1.0, 0.0],
3744        }
3745    }
3746
3747    /// OC.1 golden — the keyhole reveals the room behind the front
3748    /// wall for cells inside the view cone; outside the cone the wall
3749    /// column is intact; cells at/below the focus plane stay even
3750    /// inside the cone (the floor rule).
3751    #[test]
3752    fn cutout_reveals_wall_inside_window_only() {
3753        let vxl = keyhole_fixture();
3754        let grid = GridView::from_single_vxl(&vxl);
3755        let cam = keyhole_camera();
3756        let (w, h) = (32u32, 32u32);
3757        let centre = (h / 2 * w + w / 2) as usize;
3758        // Cone straight down the +y view axis (focus ahead of the
3759        // wall), hard edge, reveal past the wall (dist ≈ 16.5) but
3760        // short of the back wall (≈ 44.5); focus plane below the
3761        // whole wall span the centre ray crosses (z ≈ 128).
3762        let cut = CpuCutout {
3763            focus_local: [32.0, 30.0, 128.0],
3764            tan_outer: 0.25,
3765            tan_inner: 0.25,
3766            margin: 2.0,
3767            focus_z: 200,
3768        };
3769        let (fb, zb) = render_cutout(grid, &cam, w, h, Some(cut));
3770        assert_eq!(
3771            fb[centre], CUT_BACK.0,
3772            "cone centre must see through the wall, got {:08x}",
3773            fb[centre]
3774        );
3775        assert!(
3776            (zb[centre] - 44.0).abs() < 1.0,
3777            "revealed depth {} not ≈ 44 (the back wall)",
3778            zb[centre]
3779        );
3780        // Outside the cone (pixel (2,16) hits wall cells ~14 voxels
3781        // off-axis: tan ≈ 0.85 > 0.25): the wall is untouched.
3782        let outside = (h / 2 * w + 2) as usize;
3783        assert_eq!(
3784            fb[outside], CUT_WALL.0,
3785            "outside the cone the wall must stay, got {:08x}",
3786            fb[outside]
3787        );
3788        assert!((zb[outside] - 16.0).abs() < 1.0);
3789        // Focus plane above the centre ray's z (128 ≮ 100): the wall
3790        // stays even inside the cone — the floor-in-front rule.
3791        let below = CpuCutout {
3792            focus_z: 100,
3793            ..cut
3794        };
3795        let (fb, zb) = render_cutout(grid, &cam, w, h, Some(below));
3796        assert_eq!(
3797            fb[centre], CUT_WALL.0,
3798            "cells at/below the focus plane must stay, got {:08x}",
3799            fb[centre]
3800        );
3801        assert!((zb[centre] - 16.0).abs() < 1.0);
3802    }
3803
3804    /// OC.1 — the feather tapers the REVEAL DISTANCE across the cone
3805    /// band (visual-pass redesign — no dither): the cut edge is
3806    /// spatially COHERENT (per screen row, the revealed pixels form
3807    /// one contiguous run — no teeth), deterministic frame-to-frame,
3808    /// and strictly narrower than the same cone with a hard edge
3809    /// (pinning that the taper really scales the reveal down).
3810    #[test]
3811    fn cutout_feather_tapers_reveal_radially() {
3812        let vxl = keyhole_fixture();
3813        let grid = GridView::from_single_vxl(&vxl);
3814        let cam = keyhole_camera();
3815        let (w, h) = (64u32, 64u32);
3816        let tapered = CpuCutout {
3817            focus_local: [32.0, 30.0, 128.0],
3818            tan_outer: 0.9,
3819            tan_inner: 0.3,
3820            margin: 2.0,
3821            focus_z: 200,
3822        };
3823        let (fb, _) = render_cutout(grid, &cam, w, h, Some(tapered));
3824        let (fb2, _) = render_cutout(grid, &cam, w, h, Some(tapered));
3825        assert_eq!(fb, fb2, "the feather taper must be deterministic");
3826        // Per-row coherence on the axis row: BACK pixels form ONE
3827        // contiguous run (whole-cell classification — the ragged
3828        // per-pixel teeth the redesign removed would break this).
3829        let row = (h / 2) as usize * w as usize;
3830        let flags: Vec<bool> = (0..w as usize)
3831            .map(|px| fb[row + px] == CUT_BACK.0)
3832            .collect();
3833        let first = flags.iter().position(|&b| b);
3834        let last = flags.iter().rposition(|&b| b);
3835        let (Some(first), Some(last)) = (first, last) else {
3836            panic!("axis row must contain revealed pixels");
3837        };
3838        assert!(
3839            flags[first..=last].iter().all(|&b| b),
3840            "revealed run must be contiguous (no teeth): {flags:?}"
3841        );
3842        assert!(
3843            flags.iter().any(|&b| !b),
3844            "the taper must keep wall pixels on the axis row"
3845        );
3846        // The taper cuts a strictly SMALLER hole than a hard edge at
3847        // the same outer cone.
3848        let hard = CpuCutout {
3849            tan_inner: 0.9,
3850            ..tapered
3851        };
3852        let (fb_hard, _) = render_cutout(grid, &cam, w, h, Some(hard));
3853        let count = |fb: &[u32]| fb.iter().filter(|&&p| p == CUT_BACK.0).count();
3854        let (n_taper, n_hard) = (count(&fb), count(&fb_hard));
3855        assert!(
3856            0 < n_taper && n_taper < n_hard,
3857            "taper must shrink the hole: tapered {n_taper} vs hard {n_hard}"
3858        );
3859    }
3860
3861    /// OC.1 — the reveal surface hugs the character COLUMN (the
3862    /// visual-pass round-4 rule): a full-height pillar the character
3863    /// stands right behind is cut in its ENTIRETY, boots to head — a
3864    /// fixed eye-distance sphere through the chest (the previous rule)
3865    /// kept the pillar's below-chest cells, which sit FARTHER from the
3866    /// elevated eye than the chest itself, leaving a waist-high stump
3867    /// hiding the character.
3868    #[test]
3869    fn cutout_pillar_in_front_cuts_down_to_the_feet() {
3870        const PILLAR: VoxColor = VoxColor(0x80_C0_C0_20);
3871        const FLOOR: VoxColor = VoxColor(0x80_60_60_60);
3872        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
3873            if z >= 132 {
3874                Some(FLOOR)
3875            } else if (31..=33).contains(&x) && (24..=25).contains(&y) && (120..130).contains(&z) {
3876                // A body-height pillar right in front of the focus (one
3877                // voxel OFF the floor: resting on it would merge the
3878                // runs, and the cut face under the footprint would
3879                // legitimately show the run-top = pillar colour).
3880                Some(PILLAR)
3881            } else {
3882                None
3883            }
3884        });
3885        let grid = GridView::from_single_vxl(&vxl);
3886        // Elevated shoulder eye behind; the character column ahead at
3887        // (32, 30), feet on the floor (plane 132), head at 120.
3888        let cam = Camera {
3889            pos: [32.0, 4.0, 112.0],
3890            right: [1.0, 0.0, 0.0],
3891            down: [0.0, 0.0, 1.0],
3892            forward: [0.0, 1.0, 0.0],
3893        };
3894        let cut = CpuCutout {
3895            focus_local: [32.0, 30.0, 126.0],
3896            tan_outer: 1.0,
3897            tan_inner: 1.0,
3898            margin: 2.0,
3899            focus_z: 132,
3900        };
3901        let (w, h) = (64u32, 64u32);
3902        let count = |fb: &[u32], c: VoxColor| fb.iter().filter(|&&p| p == c.0).count();
3903        // Uncut: the pillar is visible.
3904        let (fb, _) = render_cutout(grid, &cam, w, h, None);
3905        assert!(count(&fb, PILLAR) > 0, "uncut pillar must be visible");
3906        // Cut: the WHOLE pillar melts (no waist-high stump), while the
3907        // floor at/below the plane stays.
3908        let (fb, _) = render_cutout(grid, &cam, w, h, Some(cut));
3909        assert_eq!(
3910            count(&fb, PILLAR),
3911            0,
3912            "the pillar must cut down to the feet, not stop at the chest"
3913        );
3914        assert!(count(&fb, FLOOR) > 0, "the floor must survive the cut");
3915    }
3916
3917    /// OC.1 — the focus plane compares the grid-local ABSOLUTE voxel z
3918    /// on stacked-chz grids (the CA.1 absolute-z pin, cutout edition):
3919    /// a focus plane inside chz=1 hides the wall span above it there.
3920    #[test]
3921    fn cutout_focus_plane_is_absolute_z_on_stacked_chunks() {
3922        // chz=0 empty; chz=1 carries the wall pair (abs z 256..512).
3923        let upper = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, _| None);
3924        let lower = roxlap_formats::vxl::Vxl::from_dense(32, |_, y, _| match y {
3925            20 => Some(CUT_WALL),
3926            28 => Some(CUT_BACK),
3927            _ => None,
3928        });
3929        let v_up = GridView::from_single_vxl(&upper);
3930        let v_lo = GridView::from_single_vxl(&lower);
3931        let chunks = [Some(v_up), Some(v_lo)];
3932        let cg = crate::ChunkGrid {
3933            chunks: &chunks,
3934            origin_chunk_xy: [0, 0],
3935            origin_chunk_z: 0,
3936            chunks_x: 1,
3937            chunks_y: 1,
3938            chunks_z: 2,
3939        };
3940        let grid = GridView::from_chunk_grid(&cg, 32);
3941        let cam = Camera {
3942            pos: [16.0, 4.0, 300.0],
3943            right: [1.0, 0.0, 0.0],
3944            down: [0.0, 0.0, 1.0],
3945            forward: [0.0, 1.0, 0.0],
3946        };
3947        let (w, h) = (32u32, 32u32);
3948        let centre = (h / 2 * w + w / 2) as usize;
3949        let cut = CpuCutout {
3950            focus_local: [16.0, 25.0, 300.0],
3951            tan_outer: 1.0,
3952            tan_inner: 1.0,
3953            margin: 2.0,
3954            focus_z: 320, // abs voxel z inside chz=1
3955        };
3956        let (fb, zb) = render_cutout(grid, &cam, w, h, Some(cut));
3957        assert_eq!(
3958            fb[centre], CUT_BACK.0,
3959            "abs-z focus plane must hide the chz=1 wall, got {:08x}",
3960            fb[centre]
3961        );
3962        assert!((zb[centre] - 24.0).abs() < 1.0);
3963        // Plane above the ray's abs z (300 ≮ 280): nothing hides. An
3964        // in-chunk-z bug (300 & 255 = 44 < 280) would still cut here.
3965        let above = CpuCutout {
3966            focus_z: 280,
3967            ..cut
3968        };
3969        let (fb, _) = render_cutout(grid, &cam, w, h, Some(above));
3970        assert_eq!(
3971            fb[centre], CUT_WALL.0,
3972            "focus plane above the ray must keep the wall, got {:08x}",
3973            fb[centre]
3974        );
3975    }
3976
3977    /// OC.1 — pins the mip formula `focus_z >> mip` (floor), the CA.1
3978    /// formula verbatim: at mip 1 with `focus_z = 101` the plane
3979    /// rounds DOWN to mip-cell 50 (voxels 100..102), so the surface at
3980    /// z=100 stays visible — the accepted coarse-mip bleed. CPU and
3981    /// GPU must share this exact formula (the OC.2 parity gate).
3982    #[test]
3983    fn cutout_focus_plane_mip_formula_floors() {
3984        let mut vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
3985            (z >= 100).then_some(VoxColor(0x80_80_80_80))
3986        });
3987        vxl.generate_mips(2);
3988        assert!(vxl.mip_count() >= 2, "need mip 1 built");
3989        let grid = GridView::from_single_vxl(&vxl);
3990        let cam = Camera {
3991            pos: [32.0, 32.0, 10.0],
3992            right: [1.0, 0.0, 0.0],
3993            down: [0.0, 1.0, 0.0],
3994            forward: [0.0, 0.0, 1.0],
3995        };
3996        let (w, h) = (32u32, 32u32);
3997        let n = (w as usize) * (h as usize);
3998        let centre = (h / 2 * w + w / 2) as usize;
3999        let mut fb = vec![0u32; n];
4000        let mut zb = vec![f32::INFINITY; n];
4001        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
4002        let env = DdaEnv {
4003            cutout: Some(CpuCutout {
4004                focus_local: [32.0, 32.0, 120.0],
4005                tan_outer: 4.0, // whole frustum inside the cone
4006                tan_inner: 4.0,
4007                margin: 0.0,
4008                focus_z: 101,
4009            }),
4010            ..DdaEnv::default()
4011        };
4012        {
4013            let mut sink = RasterSink::new(&mut fb, &mut zb);
4014            render_dda(&cam, &settings, grid, w as usize, &env, 1, &mut sink);
4015        }
4016        assert_ne!(fb[centre], 0, "mip-1 cut surface must still hit");
4017        assert!(
4018            (zb[centre] - 90.0).abs() < 1.5,
4019            "mip-cell 50 (voxel z=100) must stay visible (floor \
4020             formula); depth {} not ≈ 90",
4021            zb[centre]
4022        );
4023    }
4024
4025    /// OC.1 standing gate — `cutout = None`, a zero-radius window and
4026    /// a zero-reveal cutout are all byte-identical to the plain render
4027    /// (decision 8: disabled ⇒ byte-identical; pins both "off" folds).
4028    #[test]
4029    fn cutout_disabled_is_byte_identical() {
4030        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
4031            let surf = 28 + ((x / 4 + y / 6) % 13);
4032            (z >= surf).then_some(VoxColor(0x80_40_60_80 + (x ^ y) % 0x30))
4033        });
4034        let grid = GridView::from_single_vxl(&vxl);
4035        let cam = Camera::orbit(0.8, 0.55, 100.0, [32.0, 32.0, 40.0]);
4036        let (w, h) = (96u32, 96u32);
4037        let (fb_base, zb_base) = render_brickmap(grid, &cam, w, h);
4038        let (fb_none, zb_none) = render_cutout(grid, &cam, w, h, None);
4039        assert_eq!(fb_base, fb_none, "cutout=None must not change a pixel");
4040        assert_eq!(zb_base, zb_none);
4041        // Zero-width cone: no cell centre is ever inside it.
4042        let no_cone = CpuCutout {
4043            focus_local: [32.0, 32.0, 40.0],
4044            tan_outer: 0.0,
4045            tan_inner: 0.0,
4046            margin: 0.0,
4047            focus_z: i32::MAX,
4048        };
4049        let (fb, zb) = render_cutout(grid, &cam, w, h, Some(no_cone));
4050        assert_eq!(fb_base, fb, "a zero cone must not change a pixel");
4051        assert_eq!(zb_base, zb);
4052        // Zero reveal (the facade's behind-camera fold): cell
4053        // distances are non-negative, so nothing hides.
4054        let no_reveal = CpuCutout {
4055            focus_local: [32.0, 32.0, 40.0],
4056            tan_outer: 10.0,
4057            tan_inner: 10.0,
4058            margin: 1.0e6,
4059            focus_z: i32::MAX,
4060        };
4061        let (fb, zb) = render_cutout(grid, &cam, w, h, Some(no_reveal));
4062        assert_eq!(fb_base, fb, "zero reveal must not change a pixel");
4063        assert_eq!(zb_base, zb);
4064    }
4065
4066    /// OC.1 — the cutout is a VIEW aid, not world removal (non-goal in
4067    /// the entry doc): a roof hidden by the keyhole still casts its
4068    /// sun shadow onto the floor below — the exact opposite of the CA
4069    /// clip's "world as if removed" gate above.
4070    #[test]
4071    fn cutout_hidden_roof_still_casts_shadows() {
4072        const ROOF: VoxColor = VoxColor(0x80_A0_50_20);
4073        const FLOOR: VoxColor = VoxColor(0x80_80_80_80);
4074        let roofed = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
4075            if z == 20 {
4076                Some(ROOF)
4077            } else if z >= 60 {
4078                Some(FLOOR)
4079            } else {
4080                None
4081            }
4082        });
4083        let roofless =
4084            roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| (z >= 60).then_some(FLOOR));
4085        let g_roofed = GridView::from_single_vxl(&roofed);
4086        let g_roofless = GridView::from_single_vxl(&roofless);
4087        let cam = Camera {
4088            pos: [32.0, 32.0, 6.0],
4089            right: [1.0, 0.0, 0.0],
4090            down: [0.0, 1.0, 0.0],
4091            forward: [0.0, 0.0, 1.0],
4092        };
4093        let inv = 1.0f32 / 2.0f32.sqrt();
4094        let lights = CpuLights {
4095            enabled: true,
4096            sun: true,
4097            sun_dir: [inv, 0.0, -inv],
4098            sun_color: [1.0; 3],
4099            sun_intensity: 1.0,
4100            sun_casts_shadow: true,
4101            ambient: [0.25; 3],
4102            shadow_strength: 0.8,
4103            shadow_bias: 1.5,
4104            shadow_max_dist: 128.0,
4105            ..CpuLights::default()
4106        };
4107        let (w, h) = (64u32, 64u32);
4108        // Cone over the whole frustum; reveal past the farthest roof
4109        // cell (corner distance ≈ 47) but short of the floor's z-gate
4110        // anyway (floor z=60 ≥ plane 50 keeps it); plane between them.
4111        let cut = CpuCutout {
4112            focus_local: [32.0, 32.0, 46.0],
4113            tan_outer: 10.0,
4114            tan_inner: 10.0,
4115            margin: 1.0,
4116            focus_z: 50,
4117        };
4118        let env_cut = DdaEnv {
4119            lights,
4120            cutout: Some(cut),
4121            ..DdaEnv::default()
4122        };
4123        let env_plain = DdaEnv {
4124            lights,
4125            ..DdaEnv::default()
4126        };
4127        let (fb_cut, zb_cut) = render_brickmap_env(g_roofed, &cam, w, h, &env_cut);
4128        let (fb_ref, zb_ref) = render_brickmap_env(g_roofless, &cam, w, h, &env_plain);
4129        // Same geometry visible (the floor)…
4130        assert_eq!(zb_cut, zb_ref, "the cutout must reveal the same floor");
4131        // …but the hidden roof still shadows it: the cut render is
4132        // strictly darker than the roofless reference.
4133        let sum: fn(&[u32]) -> u64 = |fb| {
4134            fb.iter()
4135                .map(|&p| u64::from((p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff)))
4136                .sum()
4137        };
4138        let (dark, light) = (sum(&fb_cut), sum(&fb_ref));
4139        assert!(
4140            dark < light,
4141            "hidden roof must keep shadowing the floor: cut={dark} roofless={light}"
4142        );
4143    }
4144
4145    /// DDA.4: a floor spanning two side-by-side chunks (chunks_x=2)
4146    /// renders continuously across the chunk-XY seam — hits on both
4147    /// sides, no gap column.
4148    #[test]
4149    fn cross_chunk_xy_floor_is_seamless() {
4150        let mk = || {
4151            roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
4152                (z >= 20).then_some(VoxColor(0x80_50_50_50))
4153            })
4154        };
4155        let (c0, c1) = (mk(), mk());
4156        let v0 = GridView::from_single_vxl(&c0);
4157        let v1 = GridView::from_single_vxl(&c1);
4158        let chunks = [Some(v0), Some(v1)];
4159        let cg = crate::ChunkGrid {
4160            chunks: &chunks,
4161            origin_chunk_xy: [0, 0],
4162            origin_chunk_z: 0,
4163            chunks_x: 2,
4164            chunks_y: 1,
4165            chunks_z: 1,
4166        };
4167        let grid = GridView::from_chunk_grid(&cg, 32);
4168
4169        // High above the seam (x=32), looking straight down.
4170        let cam = Camera {
4171            pos: [32.0, 16.0, 4.0],
4172            right: [1.0, 0.0, 0.0],
4173            down: [0.0, 1.0, 0.0],
4174            forward: [0.0, 0.0, 1.0],
4175        };
4176        let (w, h) = (64u32, 64u32);
4177        let mask = render_mask(grid, &cam, w, h);
4178        // Both the left chunk (screen left) and right chunk (screen
4179        // right) must show floor on the centre row.
4180        let row = (h / 2) as usize * w as usize;
4181        let left = (0..w as usize / 2).filter(|&x| mask[row + x]).count();
4182        let right = (w as usize / 2..w as usize)
4183            .filter(|&x| mask[row + x])
4184            .count();
4185        assert!(
4186            left > 5 && right > 5,
4187            "seam not continuous: left={left} right={right}"
4188        );
4189    }
4190
4191    /// Render `grid` from `camera` at render `mip` and return the hit
4192    /// mask.
4193    fn render_mask_mip(grid: GridView<'_>, camera: &Camera, w: u32, h: u32, mip: u32) -> Vec<bool> {
4194        let n = (w as usize) * (h as usize);
4195        let mut fb = vec![0u32; n];
4196        let mut zb = vec![f32::INFINITY; n];
4197        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
4198        {
4199            let mut sink = RasterSink::new(&mut fb, &mut zb);
4200            render_dda(
4201                camera,
4202                &settings,
4203                grid,
4204                w as usize,
4205                &DdaEnv::default(),
4206                mip,
4207                &mut sink,
4208            );
4209        }
4210        fb.iter().map(|&c| c != 0).collect()
4211    }
4212
4213    /// DDA.6: rendering a mip-built grid at a coarse mip stays complete
4214    /// (hole-free silhouette) with roughly the same screen coverage as
4215    /// mip 0 — LOD coarsens detail, it doesn't punch holes or shrink the
4216    /// shape. (DDA has no axis-aligned mip beam — the artifact is
4217    /// structurally impossible with honest per-cell traversal.)
4218    #[test]
4219    fn mip_render_is_coarse_but_complete() {
4220        let mut vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
4221            let surf = 24 + ((x / 3 + y / 5) % 17);
4222            (z >= surf).then_some(VoxColor(0x80_50_70_90))
4223        });
4224        vxl.generate_mips(4);
4225        assert!(vxl.mip_count() >= 3, "need mips built for this test");
4226        let grid = GridView::from_single_vxl(&vxl);
4227        let (w, h) = (96u32, 96u32);
4228        let cam = Camera::orbit(0.7, 0.6, 110.0, [32.0, 32.0, 36.0]);
4229
4230        let m0 = render_mask_mip(grid, &cam, w, h, 0);
4231        let m2 = render_mask_mip(grid, &cam, w, h, 2);
4232
4233        let c0 = m0.iter().filter(|&&b| b).count();
4234        let c2 = m2.iter().filter(|&&b| b).count();
4235        assert!(c0 > 200 && c2 > 200, "both mips visible (c0={c0} c2={c2})");
4236        // Coverage within ~30 % — a coarse-mip silhouette closely tracks
4237        // the fine one (LOD coarsens detail, it doesn't lose the shape).
4238        // (Terrain silhouettes are non-convex — sky shows through
4239        // valleys — so a hole-free invariant doesn't apply here; that's
4240        // the convex single-voxel test's job.)
4241        let ratio = c2 as f32 / c0 as f32;
4242        assert!(
4243            (0.7..1.4).contains(&ratio),
4244            "mip-2 coverage {c2} vs mip-0 {c0} (ratio {ratio:.2}) diverged"
4245        );
4246    }
4247
4248    /// Headless perf bench (run: `cargo test -p roxlap-core --release
4249    /// dda::tests::bench_terrain -- --ignored --nocapture`). Single-
4250    /// thread `render_dda` over a hilly chunk at a horizon pose; prints
4251    /// ms/frame + per-frame traversal counters (cells / bricks /
4252    /// surface_color calls) to locate the bottleneck.
4253    #[test]
4254    #[ignore = "perf benchmark — run explicitly with --ignored"]
4255    fn bench_terrain() {
4256        use std::time::Instant;
4257        // Multi-chunk grid like the demo: NC×NC chunks of 128, hills.
4258        const NC: i32 = 6;
4259        let cs = crate::grid_view::CHUNK_SIZE_Z; // 256, but vsid is 128
4260        let _ = cs;
4261        let mut vxls: Vec<roxlap_formats::vxl::Vxl> = Vec::new();
4262        for cy in 0..NC {
4263            for cx in 0..NC {
4264                let (ox, oy) = (cx * 128, cy * 128);
4265                let mut v = roxlap_formats::vxl::Vxl::from_dense(128, |x, y, z| {
4266                    let (gx, gy) = (ox + x as i32, oy + y as i32);
4267                    let surf = 90 + ((gx / 7 + gy / 9).rem_euclid(40)) + ((gx / 23).rem_euclid(20));
4268                    (z as i32 >= surf).then_some(VoxColor(0x80_50_70_90 + (x ^ y) % 0x30))
4269                });
4270                v.generate_mips(4);
4271                vxls.push(v);
4272            }
4273        }
4274        let views: Vec<Option<GridView>> = vxls
4275            .iter()
4276            .map(|v| Some(GridView::from_single_vxl(v)))
4277            .collect();
4278        let cg = crate::ChunkGrid {
4279            chunks: &views,
4280            origin_chunk_xy: [0, 0],
4281            origin_chunk_z: 0,
4282            chunks_x: NC as u32,
4283            chunks_y: NC as u32,
4284            chunks_z: 1,
4285        };
4286        let grid = GridView::from_chunk_grid(&cg, 128);
4287
4288        let (w, h) = (960u32, 600u32);
4289        let mut settings = OpticastSettings::for_oracle_framebuffer(w, h);
4290        settings.max_scan_dist = 512;
4291        let n = (w * h) as usize;
4292        let mut fb = vec![0u32; n];
4293        let mut zb = vec![f32::INFINITY; n];
4294        let centre = [f64::from(NC * 128) / 2.0, f64::from(NC * 128) / 2.0, 60.0];
4295
4296        // Two poses: eye-level toward horizon (long rays) + looking down
4297        // at nearby terrain (short rays, demo-typical).
4298        let poses = [
4299            (
4300                "horizon",
4301                Camera::from_yaw_pitch([20.0, 20.0, 40.0], 0.6, 0.15),
4302            ),
4303            ("down", Camera::orbit(0.7, 1.0, 130.0, centre)),
4304        ];
4305        for (name, cam) in poses {
4306            {
4307                let mut sink = RasterSink::new(&mut fb, &mut zb);
4308                prof::reset();
4309                render_dda(
4310                    &cam,
4311                    &settings,
4312                    grid,
4313                    w as usize,
4314                    &DdaEnv::default(),
4315                    0,
4316                    &mut sink,
4317                );
4318            }
4319            let (cells, bricks, surf) = prof::read();
4320            let iters = 6;
4321            let t0 = Instant::now();
4322            for _ in 0..iters {
4323                let mut sink = RasterSink::new(&mut fb, &mut zb);
4324                render_dda(
4325                    &cam,
4326                    &settings,
4327                    grid,
4328                    w as usize,
4329                    &DdaEnv::default(),
4330                    0,
4331                    &mut sink,
4332                );
4333            }
4334            let ms = t0.elapsed().as_secs_f64() * 1000.0 / f64::from(iters);
4335            let hits = fb.iter().filter(|&&c| c != 0).count();
4336            eprintln!(
4337                "[{name}] {w}x{h} 1-thread: {ms:.1} ms | hits={hits}/{n} | per-px: cells={:.1} bricks={:.1} surf={:.1}",
4338                cells as f64 / n as f64,
4339                bricks as f64 / n as f64,
4340                surf as f64 / n as f64,
4341            );
4342        }
4343    }
4344
4345    /// DDA.7: the tile-parallel driver is bit-identical to the
4346    /// sequential one — DDA pixels are independent, so banding can't
4347    /// change a pixel.
4348    #[test]
4349    fn parallel_matches_sequential() {
4350        let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
4351            let surf = 28 + ((x / 4 + y / 6) % 13);
4352            (z >= surf).then_some(VoxColor(0x80_40_60_80 + (x ^ y) % 0x30))
4353        });
4354        let grid = GridView::from_single_vxl(&vxl);
4355        let (w, h) = (96u32, 96u32);
4356        let cam = Camera::orbit(0.8, 0.55, 100.0, [32.0, 32.0, 40.0]);
4357        let env = DdaEnv {
4358            sky: None,
4359            fog_color: 0x00_20_30_40,
4360            fog_max_dist: 120.0,
4361            side_shades: [0, 0, 0, 0, 0x30, 0x10],
4362            materials: None,
4363            terrain_materials: &[],
4364            lights: CpuLights::default(),
4365            world_shadow: None,
4366            z_clip: None,
4367            cutout: None,
4368            fow: None,
4369        };
4370
4371        let (seq_fb, seq_zb) = render_brickmap_env(grid, &cam, w, h, &env);
4372
4373        let n = (w * h) as usize;
4374        let mut par_fb = vec![0u32; n];
4375        let mut par_zb = vec![f32::INFINITY; n];
4376        let settings = OpticastSettings::for_oracle_framebuffer(w, h);
4377        let (cache, mip) = local_cache(&grid, 0);
4378        render_dda_parallel(
4379            &cam,
4380            &settings,
4381            grid,
4382            &mut par_fb,
4383            &mut par_zb,
4384            w as usize,
4385            &env,
4386            &cache,
4387            mip,
4388        );
4389        assert!(par_fb == seq_fb, "parallel colour differs from sequential");
4390        assert!(
4391            par_zb
4392                .iter()
4393                .zip(&seq_zb)
4394                .all(|(a, b)| a.to_bits() == b.to_bits()),
4395            "parallel depth differs from sequential"
4396        );
4397    }
4398
4399    /// DDA.2 correctness: a heightmap column's interior is solid even
4400    /// though voxlap only stores a colour for its surface. `voxel_color`
4401    /// returns `None` for an interior voxel, but `surface_color` must
4402    /// return the run's surface colour — otherwise oblique rays striking
4403    /// a cliff *side* would pass straight through (see-through terrain).
4404    #[test]
4405    fn cliff_side_is_solid_not_see_through() {
4406        const TOP_Z: u32 = 50;
4407        const COL: VoxColor = VoxColor(0x80_77_88_99);
4408        let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |_, _, z| (z >= TOP_Z).then_some(COL));
4409        let grid = GridView::from_single_vxl(&vxl);
4410
4411        // Surface voxel: coloured directly.
4412        assert_eq!(grid.voxel_color(4, 4, TOP_Z), Some(COL));
4413        // Interior voxel: voxlap stores no colour …
4414        assert_eq!(grid.voxel_color(4, 4, 150), None);
4415        // … but it is solid, and surface_color bleeds the run-top colour
4416        // down the cliff face → a real hit, not see-through.
4417        assert_eq!(grid.surface_color(4, 4, 150), Some(COL));
4418        // Bedrock-style air above the surface stays air.
4419        assert_eq!(grid.surface_color(4, 4, 10), None);
4420    }
4421
4422    /// DDA.2: a camera embedded in solid material hits its own voxel
4423    /// immediately — every ray reports a hit (no skip / no garbage).
4424    #[test]
4425    fn camera_inside_solid_hits_everywhere() {
4426        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, _| Some(VoxColor(0x80_55_55_55)));
4427        let grid = GridView::from_single_vxl(&vxl);
4428        let cam = Camera {
4429            pos: [8.0, 8.0, 128.0],
4430            right: [1.0, 0.0, 0.0],
4431            down: [0.0, 1.0, 0.0],
4432            forward: [0.0, 0.0, 1.0],
4433        };
4434        let (w, h) = (32u32, 32u32);
4435        let mask = render_mask(grid, &cam, w, h);
4436        assert!(
4437            mask.iter().all(|&b| b),
4438            "every ray must hit when the camera is inside solid"
4439        );
4440    }
4441
4442    /// Headline DDA.1 gate: a single solid voxel viewed obliquely
4443    /// projects to a convex silhouette with **no interior holes** —
4444    /// the artifact class (`tiny_grid_1x1x1` silhouette notch) the
4445    /// voxlap renderer cannot avoid. DDA casts independent per-pixel
4446    /// rays, so the silhouette is hole-free by construction.
4447    #[test]
4448    fn single_voxel_silhouette_has_no_notch() {
4449        const C: VoxColor = VoxColor(0x80_FF_80_40);
4450        let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |x, y, z| {
4451            (x == 8 && y == 8 && z == 8).then_some(C)
4452        });
4453        let grid = GridView::from_single_vxl(&vxl);
4454
4455        // Orbit the voxel centre obliquely so all three faces show and
4456        // the silhouette is a sizeable hexagon (dist 4 → ~12 px wide).
4457        let cam = Camera::orbit(0.7, 0.6, 4.0, [8.5, 8.5, 8.5]);
4458        let (w, h) = (96u32, 96u32);
4459        let mask = render_mask(grid, &cam, w, h);
4460
4461        let hits = mask.iter().filter(|&&b| b).count();
4462        assert!(
4463            hits > 30,
4464            "silhouette too small to be meaningful: {hits} px"
4465        );
4466        assert!(
4467            rows_have_no_holes(&mask, w, h),
4468            "row-interior gap in single-voxel silhouette (notch)"
4469        );
4470        assert!(
4471            cols_have_no_holes(&mask, w, h),
4472            "column-interior gap in single-voxel silhouette (notch)"
4473        );
4474    }
4475
4476    // ---- FW.2: fog-of-war styling ----
4477
4478    struct FixedStyler(FowVerdict);
4479    impl FowStyler for FixedStyler {
4480        fn verdict(&self, _x: i32, _y: i32, _z: i32) -> FowVerdict {
4481            self.0
4482        }
4483    }
4484
4485    #[test]
4486    fn fow_style_dims_and_desaturates() {
4487        // Identity when dim=1, desat=0.
4488        assert_eq!(fow_style(0x80_40_80_C0, 1.0, 0.0), 0x80_40_80_C0);
4489        // Half dim halves each channel, high byte preserved.
4490        assert_eq!(fow_style(0x80_40_80_C0, 0.5, 0.0), 0x80_20_40_60);
4491        // Full desaturate → all channels equal the luma.
4492        let g = fow_style(0x80_40_80_C0, 1.0, 1.0);
4493        assert_eq!((g >> 16) & 0xff, g & 0xff);
4494        assert_eq!((g >> 8) & 0xff, g & 0xff);
4495        assert_eq!(g & 0xff00_0000, 0x8000_0000);
4496    }
4497
4498    /// A flat floor; camera looking down. The three stylers exercise the
4499    /// hit path.
4500    fn floor_scene() -> (roxlap_formats::vxl::Vxl, Camera, u32, u32) {
4501        let vxl = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
4502            (z >= 20).then_some(VoxColor(0x80_40_80_C0))
4503        });
4504        let cam = Camera {
4505            pos: [16.0, 16.0, 5.0],
4506            right: [1.0, 0.0, 0.0],
4507            down: [0.0, 1.0, 0.0],
4508            forward: [0.0, 0.0, 1.0],
4509        };
4510        (vxl, cam, 48, 48)
4511    }
4512
4513    #[test]
4514    fn fow_hide_makes_geometry_vanish() {
4515        let (vxl, cam, w, h) = floor_scene();
4516        let grid = GridView::from_single_vxl(&vxl);
4517        let (base, _) = render_brickmap(grid, &cam, w, h);
4518        assert!(base.iter().any(|&p| p != 0), "baseline must draw the floor");
4519
4520        let styler = FixedStyler(FowVerdict::Hide);
4521        let env = DdaEnv {
4522            fow: Some(&styler),
4523            ..DdaEnv::default()
4524        };
4525        let (hidden, _) = render_brickmap_env(grid, &cam, w, h, &env);
4526        assert!(
4527            hidden.iter().all(|&p| p == 0),
4528            "every Hide cell must read as air (sky)"
4529        );
4530    }
4531
4532    #[test]
4533    fn fow_dim_desaturate_matches_helper() {
4534        let (vxl, cam, w, h) = floor_scene();
4535        let grid = GridView::from_single_vxl(&vxl);
4536        let (base, _) = render_brickmap(grid, &cam, w, h);
4537
4538        let styler = FixedStyler(FowVerdict::Show {
4539            dynamic: true,
4540            dim: 0.5,
4541            desaturate: 0.4,
4542        });
4543        let env = DdaEnv {
4544            fow: Some(&styler),
4545            ..DdaEnv::default()
4546        };
4547        let (styled, _) = render_brickmap_env(grid, &cam, w, h, &env);
4548        for k in 0..(w * h) as usize {
4549            if base[k] != 0 {
4550                assert_eq!(styled[k], fow_style(base[k], 0.5, 0.4), "pixel {k}");
4551            }
4552        }
4553    }
4554
4555    /// A `dynamic: false` verdict (memory) must render the BAKED look
4556    /// even with a dynamic sun in the rig — a live light never relights
4557    /// a remembered room.
4558    #[test]
4559    fn fow_memory_skips_dynamic_lights() {
4560        let (vxl, cam, w, h) = floor_scene();
4561        let grid = GridView::from_single_vxl(&vxl);
4562        // A red point light just above the floor → an obvious red tint
4563        // the baked look does not have.
4564        let pt = [CpuPointLight {
4565            pos: [16.0, 16.0, 18.0],
4566            color: [1.0, 0.0, 0.0],
4567            intensity: 5.0,
4568            radius: 40.0,
4569            casts_shadow: false,
4570            spot_dir: [0.0, 0.0, 0.0],
4571            cos_inner: -1.0,
4572            cos_outer: -1.0,
4573        }];
4574        let lights = CpuLights {
4575            enabled: true,
4576            points: &pt,
4577            ..CpuLights::default()
4578        };
4579        // Baked (no rig) reference.
4580        let (baked, _) = render_brickmap(grid, &cam, w, h);
4581        // Lit reference — the rig changes the floor colour.
4582        let lit_env = DdaEnv {
4583            lights,
4584            ..DdaEnv::default()
4585        };
4586        let (lit, _) = render_brickmap_env(grid, &cam, w, h, &lit_env);
4587        assert_ne!(lit, baked, "the sun rig must change the floor");
4588
4589        // Memory styler + the same rig → the baked look, not the lit one.
4590        let styler = FixedStyler(FowVerdict::Show {
4591            dynamic: false,
4592            dim: 1.0,
4593            desaturate: 0.0,
4594        });
4595        let mem_env = DdaEnv {
4596            lights,
4597            fow: Some(&styler),
4598            ..DdaEnv::default()
4599        };
4600        let (mem, _) = render_brickmap_env(grid, &cam, w, h, &mem_env);
4601        assert_eq!(mem, baked, "memory must skip the dynamic rig (baked look)");
4602    }
4603
4604    /// Styler that hides one column `(x)` (an "unseen wall") and shows
4605    /// everything else — for the shadow-leak test.
4606    struct HideColumnStyler {
4607        wall_x: i32,
4608    }
4609    impl FowStyler for HideColumnStyler {
4610        fn verdict(&self, x: i32, _y: i32, _z: i32) -> FowVerdict {
4611            if x == self.wall_x {
4612                FowVerdict::Hide
4613            } else {
4614                FowVerdict::LIVE
4615            }
4616        }
4617    }
4618
4619    /// Review #1 — an unseen (Hide) wall must cast NO shadow. With a
4620    /// sun rig the wall shadows a strip of floor; hiding the wall via the
4621    /// fog must both remove the wall AND its shadow (else the silhouette
4622    /// leaks).
4623    #[test]
4624    fn fow_hidden_wall_casts_no_shadow() {
4625        const FLOOR: VoxColor = VoxColor(0x80_80_80_80);
4626        const WALL: VoxColor = VoxColor(0x80_40_40_40);
4627        let wall_x = 16;
4628        // Floor at z=30; a tall wall column standing up from the floor.
4629        let vxl = roxlap_formats::vxl::Vxl::from_dense(32, |x, y, z| {
4630            if z >= 30 {
4631                Some(FLOOR)
4632            } else if x as i32 == wall_x && y == 16 && (18..30).contains(&z) {
4633                Some(WALL)
4634            } else {
4635                None
4636            }
4637        });
4638        let grid = GridView::from_single_vxl(&vxl);
4639        let cam = Camera {
4640            pos: [16.0, 16.0, 4.0],
4641            right: [1.0, 0.0, 0.0],
4642            down: [0.0, 1.0, 0.0],
4643            forward: [0.0, 0.0, 1.0],
4644        };
4645        // Sun slanted along +x so the wall shadows floor at x > wall_x.
4646        let lights = CpuLights {
4647            enabled: true,
4648            sun: true,
4649            sun_dir: [-0.7, 0.0, -0.7],
4650            sun_color: [1.0, 1.0, 1.0],
4651            sun_intensity: 1.0,
4652            sun_casts_shadow: true,
4653            shadow_strength: 1.0,
4654            shadow_bias: 1.5,
4655            shadow_max_dist: 64.0,
4656            ..CpuLights::default()
4657        };
4658        let (w, h) = (48u32, 48u32);
4659        // A: wall present, all-LIVE (its shadow lands on the floor).
4660        let live = HideColumnStyler { wall_x: -999 };
4661        let env_a = DdaEnv {
4662            lights,
4663            fow: Some(&live),
4664            ..DdaEnv::default()
4665        };
4666        let (fb_a, _) = render_brickmap_env(grid, &cam, w, h, &env_a);
4667        // B: the wall is Hidden by fog → gone, and casts no shadow.
4668        let hide = HideColumnStyler { wall_x };
4669        let env_b = DdaEnv {
4670            lights,
4671            fow: Some(&hide),
4672            ..DdaEnv::default()
4673        };
4674        let (fb_b, _) = render_brickmap_env(grid, &cam, w, h, &env_b);
4675
4676        let lum = |c: u32| ((c >> 16) & 0xff) + ((c >> 8) & 0xff) + (c & 0xff);
4677        let total: u64 = (0..(w * h) as usize)
4678            .map(|k| u64::from(lum(fb_b[k])).saturating_sub(u64::from(lum(fb_a[k]))))
4679            .sum();
4680        // Hiding the wall removes its shadow → the floor is strictly
4681        // brighter overall in B than A.
4682        assert!(
4683            total > 0,
4684            "hiding the wall must lift its shadow off the floor (Δlum {total})"
4685        );
4686    }
4687
4688    /// Review #6 — an emissive material is INTRINSIC and survives in
4689    /// memory: a remembered glowing crystal renders glowing (dimmed),
4690    /// not as dark baked rock. `dynamic: false` (memory) only suppresses
4691    /// the dynamic light rig, never the emissive full-bright.
4692    #[test]
4693    fn fow_memory_keeps_emissive() {
4694        const GLOW: VoxColor = VoxColor(0x80_20_FF_80);
4695        let vxl = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= 20).then_some(GLOW));
4696        let grid = GridView::from_single_vxl(&vxl);
4697        let cam = Camera {
4698            pos: [16.0, 16.0, 5.0],
4699            right: [1.0, 0.0, 0.0],
4700            down: [0.0, 1.0, 0.0],
4701            forward: [0.0, 0.0, 1.0],
4702        };
4703        let mut table = MaterialTable::new();
4704        table.set(1, roxlap_formats::material::Material::glow(255));
4705        let terrain = [(GLOW.rgb_part(), 1u8)];
4706        let (w, h) = (48u32, 48u32);
4707
4708        // Live (visible) emissive.
4709        let live = FixedStyler(FowVerdict::LIVE);
4710        let env_live = DdaEnv {
4711            materials: Some(&table),
4712            terrain_materials: &terrain,
4713            fow: Some(&live),
4714            ..DdaEnv::default()
4715        };
4716        let (fb_live, _) = render_brickmap_env(grid, &cam, w, h, &env_live);
4717        // Memory (dynamic off, dim 1, desat 0) — must ALSO glow.
4718        let mem = FixedStyler(FowVerdict::Show {
4719            dynamic: false,
4720            dim: 1.0,
4721            desaturate: 0.0,
4722        });
4723        let env_mem = DdaEnv {
4724            materials: Some(&table),
4725            terrain_materials: &terrain,
4726            fow: Some(&mem),
4727            ..DdaEnv::default()
4728        };
4729        let (fb_mem, _) = render_brickmap_env(grid, &cam, w, h, &env_mem);
4730        assert_eq!(
4731            fb_mem, fb_live,
4732            "memory must keep the emissive glow (dim 1 == the live emissive look)"
4733        );
4734        // And that glow is brighter than the plain baked look (emissive
4735        // actually did something).
4736        let baked = FixedStyler(FowVerdict::Show {
4737            dynamic: false,
4738            dim: 1.0,
4739            desaturate: 0.0,
4740        });
4741        let env_baked = DdaEnv {
4742            fow: Some(&baked),
4743            ..DdaEnv::default()
4744        };
4745        let (fb_baked, _) = render_brickmap_env(grid, &cam, w, h, &env_baked);
4746        let lum = |c: u32| ((c >> 16) & 0xff) + ((c >> 8) & 0xff) + (c & 0xff);
4747        let centre = (24 * 48 + 24) as usize;
4748        assert!(
4749            lum(fb_mem[centre]) > lum(fb_baked[centre]),
4750            "emissive memory ({:08x}) must be brighter than baked ({:08x})",
4751            fb_mem[centre],
4752            fb_baked[centre]
4753        );
4754    }
4755
4756    /// `fow = None` is byte-identical to the pre-FW render.
4757    #[test]
4758    fn fow_disabled_byte_identical() {
4759        let (vxl, cam, w, h) = floor_scene();
4760        let grid = GridView::from_single_vxl(&vxl);
4761        let (base, _) = render_brickmap(grid, &cam, w, h);
4762        // A LIVE styler (dim=1, desat=0, dynamic) also reproduces base.
4763        let styler = FixedStyler(FowVerdict::LIVE);
4764        let env = DdaEnv {
4765            fow: Some(&styler),
4766            ..DdaEnv::default()
4767        };
4768        let (live, _) = render_brickmap_env(grid, &cam, w, h, &env);
4769        assert_eq!(live, base, "a fully-visible cell must be bit-identical");
4770    }
4771}