Skip to main content

rlx_core/render/scenes/voxel/
mod.rs

1//! The voxel system (ADR-0268): a 3D cellular automaton in a cube of cells,
2//! drawn as a glowing, absorbing volume — or as lit solid cells (ADR-0269) —
3//! seen through the shared orbit camera.
4//!
5//! # The state
6//!
7//! A ping-pong pair of 3D textures of side `grid`, one `u32` a cell packing its
8//! state and its age (`shader::STEP_SHADER`). One generation is one compute
9//! dispatch that counts each cell's live neighbours and applies the rule
10//! ([`rules`]). The pair is built when a preset is configured, never on a frame.
11//!
12//! `[voxel] grid` is content, not a resolution: a structure is a fixed number of
13//! cells, so the grid decides how large every structure looks. A grid past the
14//! scene's cap is clamped and announced from `configure` as a
15//! [`CapOverflow`](super::CapOverflow), never silently reduced.
16//!
17//! # Generations, not frames
18//!
19//! `step_rate` is in generations per second, integrated over the injected `dt`
20//! by the shared `common::GenerationClock`, so the automaton
21//! advances by the same count in the same wall time at any refresh rate.
22//!
23//! # Every cell is drawn from the seed
24//!
25//! The field is seeded from an integer hash of each cell's coordinates and a
26//! seed derived from the preset's pinned salt (ADR-0051) — never from a clock —
27//! and each step is integer counting, so a volume is a pure function of its
28//! config and the sequence of bound values and `dt`s it was driven with. Each
29//! `reseed` rise refills a ball whose centre and cells are hashed from the rise
30//! count and the salt.
31//!
32//! # The rule list and the music
33//!
34//! `[voxel] rules` is a list of one to eight rules, and the structural `rule`
35//! parameter indexes it, so a preset moves the rule on the bar through `[hold]`
36//! without a mask ever passing through an `f32` (ADR-0268). The rule a frame
37//! picks runs every generation that frame encodes. The spectrum lights the
38//! cube's radial shells, bass at the centre, by `1 + shell_gain * level`: an
39//! exact identity at zero gain, and the simulation never reads it.
40//!
41//! # The present
42//!
43//! A fullscreen march: each pixel's ray is walked cell by cell through the cube
44//! `[-1, 1]^3` under emission-absorption (`shader::MARCH_SHADER`), and stops
45//! once its transmittance is under one 8-bit step. The ray is recovered from the
46//! camera's world-to-clip rows, whose aspect is the render target's (ADR-0037).
47//! `focus` and `aperture` are inert here: the circle of confusion is per
48//! primitive (ADR-0257), and a march has no primitives.
49//!
50//! The march draws into a target of its own — the tier's
51//! [`voxel_march_scale`](crate::render::TierConfig::voxel_march_scale) of the
52//! render target, held to its
53//! [`voxel_march_cap`](crate::render::TierConfig::voxel_march_cap) — and a
54//! plain stretch presents it. The target is a resolution, not a shape: the
55//! march's aspect stays the render target's whatever the target's own is.
56//!
57//! # Solid
58//!
59//! Under `[voxel] present = "solid"` (ADR-0269) the same march stops at the
60//! first live cell. The decay stages in front of it glow and absorb as they do
61//! under `glow`; the live cell is shaded as a face — its normal the face the
62//! ray entered through, exact from the traversal's last step — under one
63//! directional key light and an ambient term:
64//!
65//! ```text
66//! shade = palette(age) * brightness * shell * (ambient * occ + key * max(0, n . l))
67//! ```
68//!
69//! composited under the light gathered so far, at full coverage. The light is
70//! given in camera space (`light_dir`), so the lit side turns with an orbit.
71//! `occ` is the face's voxel ambient occlusion: each of its four corners is
72//! closed by the live cells beside it in the layer in front of the face — two
73//! sides close it fully, otherwise each of the two sides and the diagonal a
74//! third — and the four are interpolated across the face at the hit point and
75//! mixed in by `ao`.
76//!
77//! # Bounce
78//!
79//! `bounce` (ADR-0272) lets a solid face take light from the live cells in
80//! front of it. A light field of side `grid / LIGHT_CELL` holds, per texel, the
81//! live fraction of the `LIGHT_CELL`³ block of cells it covers and that block's
82//! summed age weighted the same way; a second texture holds it blurred once over
83//! 3³ texels by a 1-2-1 tent on each axis. The march samples the blurred field
84//! trilinearly half a cell in front of the face it hit, and adds
85//!
86//! ```text
87//! bounce * palette(coord at L.age) * L.intensity * brightness * fog * shell
88//! ```
89//!
90//! where `L.age` is the sampled age over the sampled intensity. The colour is
91//! looked up at shade time, so only the intensity and the age wait for the
92//! next refill. The field has no occlusion: light passes through walls.
93//!
94//! The field is refilled beside the brick pass, after every frame that changed
95//! the state, and on the first frame `bounce` turns on after frames that did
96//! not fill it. It exists only once a frame under `solid` has `bounce > 0`:
97//! before that the march binds a one-texel stand-in and the uniform skips the
98//! fetch, so a preset that never binds `bounce` builds and reads nothing.
99//!
100//! # The section plane
101//!
102//! `cut`, `cut_yaw` and `cut_pitch` (ADR-0271) hide every cell whose centre `c`,
103//! in the cube's `[-1, 1]^3` frame, lies beyond one plane:
104//!
105//! ```text
106//! dot(c, n) > sqrt(3) * (1 - 2 * cut)
107//! n = (cos(cut_pitch) * sin(cut_yaw), sin(cut_pitch), cos(cut_pitch) * cos(cut_yaw))
108//! ```
109//!
110//! so at `cut = 0.5` the plane passes through the centre and at 1 it removes
111//! every cell. The normal is in the cube's frame, not the camera's, so the
112//! section turns with the structure. A cut cell is dead to the march, under
113//! either present, and to the solid present's occlusion, which reads it as
114//! open. The automaton still runs it: only the view is cut. At `cut = 0` the
115//! test is switched off by a flag, so every pixel is as it is without the
116//! params.
117//!
118//! # Bricks
119//!
120//! After every frame that changed the state, a pass marks each 8³ brick of
121//! cells that holds any non-dead cell. The march jumps a whole empty brick at
122//! once, landing on exactly the cell, crossing parameters and position stepping
123//! through it would have (the crossings are computed from the boundary index,
124//! never accumulated), so skipping bricks moves no pixel. A brick is skipped
125//! on its own empty flag alone: one holding cut cells is marched cell by cell,
126//! and the cut is applied there.
127
128// Hot-path panic-denial pragma (Plan 0002 Phase 2, extended to scenes by Plan
129// 0003 Phase 0). Encodes its passes every displayed frame.
130#![deny(
131    clippy::unwrap_used,
132    clippy::expect_used,
133    clippy::indexing_slicing,
134    clippy::panic,
135    clippy::unreachable
136)]
137
138pub mod rules;
139mod shader;
140
141use super::common::{self, PaletteParams, PanParams};
142use super::lines::GeneratorConfig;
143use super::{FamilyParam, FamilyRange, ParamGroup, ParamKind, ParamSpec, Scene, default_of};
144use crate::dsp::AnalysisFrame;
145use crate::render::camera::{self, CameraParams};
146use crate::render::gpu;
147use crate::render::palette::{self, Palette};
148use crate::render::tier::GridScale;
149pub use rules::{DEFAULT_RULE, Neighbourhood, RosterRule, Rule, RuleList};
150use std::f32::consts::{FRAC_PI_2, PI, TAU};
151
152/// The grid an absent `[voxel] grid` means.
153pub const DEFAULT_GRID: u32 = 64;
154/// The smallest grid the loader accepts. Below it a seeded ball is a handful of
155/// cells and every rule's structure is cut by the faces.
156pub const MIN_GRID: u32 = 16;
157/// The largest grid the loader accepts and any tier runs: 2.1 M cells, two
158/// 4-byte texels each across the ping-pong pair, 16.8 MB.
159pub const MAX_GRID: u32 = 128;
160
161/// The fastest `step_rate` the clock integrates, in generations per second.
162/// Well past the declared range's top, so it bounds a runaway binding rather
163/// than an author.
164pub const MAX_STEP_RATE: f32 = 120.0;
165
166/// Where the age field stops counting: generations since a cell last changed
167/// state, saturating here.
168const AGE_CAP: u32 = 1023;
169/// The generations over which a live cell's colour slides the whole of
170/// `age_tint` along the palette.
171const AGE_SPAN: f32 = 48.0;
172
173/// The texel every cell is stored in: the state in the low byte and the age
174/// above it, an integer format a compute pass can write.
175const STATE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
176
177/// Each side of the step pass's workgroup, in cells — `@workgroup_size(4, 4,
178/// 4)` in `shader::STEP_SHADER`.
179const WORKGROUP: u32 = 4;
180
181/// Each side of a brick, in cells — `BRICK` in `shader::MARCH_SHADER` and the
182/// brick pass's workgroup, which scans one brick.
183const BRICK: u32 = 8;
184
185/// Each side of the block of cells one light-field texel covers —
186/// `LIGHT_CELL` in `shader::LIGHT_SPLAT_SHADER` and `shader::MARCH_SHADER`.
187const LIGHT_CELL: u32 = 4;
188
189/// The light field's texel: intensity in `r`, intensity-weighted age in `g`.
190/// Filterable, so the march samples it trilinearly, and a storage format.
191const LIGHT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
192
193/// Each side of the light passes' workgroup, in texels — `@workgroup_size(4,
194/// 4, 4)` in both light shaders.
195const LIGHT_WORKGROUP: u32 = 4;
196
197/// The largest `bounce` the march reads: well past the declared range, so it
198/// bounds a runaway binding rather than an author.
199const MAX_BOUNCE: f32 = 16.0;
200
201/// `shader::MARCH_SHADER` with the constants it is written against prepended;
202/// the vertex prelude goes in front of this.
203fn march_body() -> String {
204    format!(
205        "const MAX_MARCH: i32 = {};\nconst AGE_SPAN: f32 = {:?};\nconst BRICK: i32 = {};\n\
206         const LIGHT_CELL: f32 = {:?};\n{}",
207        3 * MAX_GRID,
208        AGE_SPAN,
209        BRICK,
210        LIGHT_CELL as f32,
211        shader::MARCH_SHADER
212    )
213}
214
215/// The step the march target's axes are rounded to (`grid::grid_size`).
216const MARCH_STEP: u32 = 8;
217
218/// The radius of the sphere bounding the cube `[-1, 1]^3`: the volume the
219/// camera's `fog` is measured across.
220const CUBE_RADIUS: f32 = 1.732_050_8;
221
222/// Mixed into the preset's salt before it seeds the field, so a salt of `0`
223/// still hashes to a field rather than to the hash's fixed point. The ASCII
224/// bytes of "VOXL"; opaque, since changing it moves every seeded field.
225const FIELD_SEED_MIX: u32 = 0x564F_584C;
226/// Mixed into the salt for the stream of reseed balls, so a ball is not drawn
227/// from the field's own sequence. The ASCII bytes of "RSVX".
228const STAMP_SEED_MIX: u32 = 0x5253_5658;
229
230/// `reseed` rises past this to refill one ball — edge-triggered, so a value
231/// held high refills once, not once per frame.
232const RESEED_THRESHOLD: f32 = 0.5;
233
234/// The most radial shells the spectrum can be laid across: four `vec4`s of the
235/// march's uniform.
236pub const MAX_SHELLS: u32 = 16;
237
238/// The largest `shell_gain` the march reads: well past the declared range, so
239/// it bounds a runaway binding rather than an author.
240const MAX_SHELL_GAIN: f32 = 16.0;
241
242/// `[voxel] present` — how the march draws a live cell (ADR-0269).
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
244pub enum Present {
245    /// Every cell emits and absorbs; the ray runs on through the volume.
246    #[default]
247    Glow,
248    /// The ray stops at the first live cell and shades its face under one
249    /// camera-space key light and an ambient term. The decay stages in front
250    /// of it still glow.
251    Solid,
252}
253
254impl Present {
255    /// Every present, in the order the reference lists them.
256    pub const ALL: [Present; 2] = [Present::Glow, Present::Solid];
257
258    /// The name a preset writes.
259    pub fn as_str(self) -> &'static str {
260        match self {
261            Present::Glow => "glow",
262            Present::Solid => "solid",
263        }
264    }
265
266    /// The present `name` spells, or `None` for a name outside the roster.
267    pub fn from_name(name: &str) -> Option<Self> {
268        Self::ALL.into_iter().find(|p| p.as_str() == name)
269    }
270}
271
272/// `[voxel]` — the structural configuration, fixed while the preset is loaded
273/// and delivered through `Scene::configure`.
274#[derive(Debug, Clone, Copy, PartialEq)]
275pub struct VoxelConfig {
276    /// Cells per side of the cube, validated at load into
277    /// [`MIN_GRID`]`..=`[`MAX_GRID`].
278    pub grid: u32,
279    /// The rules a preset lists, compiled.
280    pub rules: RuleList,
281    /// The seeded ball's radius, as a fraction of the cube's half-side.
282    pub seed_radius: f32,
283    /// The probability a cell inside the seeded ball is live.
284    pub seed_fill: f32,
285    /// `true`: the cube is a 3-torus. `false`: every cell past a face is dead.
286    pub wrap: bool,
287    /// How many radial shells the spectrum lights, `0..=`[`MAX_SHELLS`]; `0`
288    /// lights none.
289    pub shells: u32,
290    /// How a live cell is drawn: as glowing light, or as a lit solid face.
291    pub present: Present,
292    /// The preset's pinned salt (ADR-0051), which every seeded cell is drawn
293    /// from.
294    pub salt: u32,
295}
296
297impl Default for VoxelConfig {
298    fn default() -> Self {
299        let (seed_radius, seed_fill) = rules::DEFAULT_RULE.seed();
300        Self {
301            grid: DEFAULT_GRID,
302            rules: RuleList::default(),
303            seed_radius,
304            seed_fill,
305            // Dead outside: a structure crossing a face of a torus reappears on
306            // the opposite one, which an orbiting camera reads as a cut.
307            wrap: false,
308            shells: 0,
309            present: Present::Glow,
310            salt: 0,
311        }
312    }
313}
314
315const DEFAULT_STEP_RATE: f32 = default_of(PARAMS, "step_rate");
316const DEFAULT_DENSITY: f32 = default_of(PARAMS, "density");
317const DEFAULT_TRAIL: f32 = default_of(PARAMS, "trail");
318const DEFAULT_AGE_TINT: f32 = default_of(PARAMS, "age_tint");
319const DEFAULT_HUE: f32 = default_of(PARAMS, "hue");
320const DEFAULT_HUE_SPREAD: f32 = default_of(PARAMS, "hue_spread");
321const DEFAULT_BRIGHTNESS: f32 = default_of(PARAMS, "brightness");
322const DEFAULT_ZOOM: f32 = default_of(PARAMS, "zoom");
323const DEFAULT_RESEED_RADIUS: f32 = default_of(PARAMS, "reseed_radius");
324const DEFAULT_SHELL_GAIN: f32 = default_of(PARAMS, "shell_gain");
325const DEFAULT_KEY: f32 = default_of(PARAMS, "key");
326const DEFAULT_AMBIENT: f32 = default_of(PARAMS, "ambient");
327const DEFAULT_AO: f32 = default_of(PARAMS, "ao");
328const DEFAULT_BOUNCE: f32 = default_of(PARAMS, "bounce");
329const DEFAULT_LIGHT_YAW: f32 = default_of(PARAMS, "light_yaw");
330const DEFAULT_LIGHT_PITCH: f32 = default_of(PARAMS, "light_pitch");
331const DEFAULT_CUT: f32 = default_of(PARAMS, "cut");
332const DEFAULT_CUT_YAW: f32 = default_of(PARAMS, "cut_yaw");
333const DEFAULT_CUT_PITCH: f32 = default_of(PARAMS, "cut_pitch");
334
335// The shared camera block (ADR-0258), re-declared with this system's own doc
336// lines where they differ: the editor schema keys a row by the whole
337// declaration. Default, range and kind stay the shared block's.
338const FOCUS: ParamSpec = ParamSpec {
339    doc: "Inert on voxel: depth of field blurs each drawn primitive, and the march draws none.",
340    ..camera::FOCUS
341};
342const APERTURE: ParamSpec = ParamSpec {
343    doc: "Inert on voxel: depth of field blurs each drawn primitive, and the march draws none.",
344    ..camera::APERTURE
345};
346const FOG: ParamSpec = ParamSpec {
347    doc: "Fades the volume toward black with depth: at 1 the cube's farthest point is black and its nearest keeps its light. 0 is off.",
348    ..camera::FOG
349};
350
351/// The parameter names this scene consumes — the vocabulary a preset binding is
352/// checked against at load (ADR-0020). **Keep in sync with `set_param` below**;
353/// `declared_params_match_set_param` in `core/tests/suite/preset.rs` fails if the
354/// two drift.
355pub const PARAMS: &[ParamSpec] = &[
356    ParamSpec {
357        name: "rule",
358        default: 0.0,
359        range: Some([0.0, (rules::MAX_RULES - 1) as f32]),
360        doc: "Which of the [voxel] rules runs, counting from 0; past the list's end it runs \
361              the last. A change takes effect at the next generation.",
362        kind: ParamKind::Structural,
363        group: ParamGroup::Shape,
364        main: true,
365    },
366    ParamSpec {
367        name: "step_rate",
368        default: 8.0,
369        range: Some([0.0, 30.0]),
370        doc: "How many generations the automaton runs per second, whatever the frame rate; 0 \
371              freezes it.",
372        kind: ParamKind::Modal,
373        group: ParamGroup::Motion,
374        main: true,
375    },
376    ParamSpec {
377        name: "reseed",
378        default: 0.0,
379        range: Some([0.0, 1.0]),
380        doc: "A rise past 0.5 fills one ball of the cube with fresh seeded cells, once per \
381              rise; bind an onset or a beat to it.",
382        kind: ParamKind::Modal,
383        group: ParamGroup::Motion,
384        main: false,
385    },
386    ParamSpec {
387        name: "reseed_radius",
388        default: 0.25,
389        range: Some([0.05, 1.0]),
390        doc: "The radius of the ball a reseed fills, as a fraction of the cube's half-side.",
391        kind: ParamKind::Modal,
392        group: ParamGroup::Shape,
393        main: false,
394    },
395    ParamSpec {
396        name: "cut",
397        default: 0.0,
398        range: Some([0.0, 1.0]),
399        doc: "How much of the cube a section plane hides, so the inside shows: 0.5 cuts it \
400              through the centre, 1 hides it all, 0 is off. Only the view is cut; the \
401              automaton runs on.",
402        kind: ParamKind::Modal,
403        group: ParamGroup::Shape,
404        main: true,
405    },
406    ParamSpec {
407        name: "cut_yaw",
408        default: 0.0,
409        range: Some([0.0, TAU]),
410        doc: "Which way the section plane faces around the cube's vertical axis, in radians: \
411              the side it hides. It turns with the cube, not the camera.",
412        kind: ParamKind::Modal,
413        group: ParamGroup::Shape,
414        main: false,
415    },
416    ParamSpec {
417        name: "cut_pitch",
418        default: 0.0,
419        range: Some([-FRAC_PI_2, FRAC_PI_2]),
420        doc: "How far the section plane's hidden side tilts up toward the cube's top, in \
421              radians; negative tilts it down.",
422        kind: ParamKind::Modal,
423        group: ParamGroup::Shape,
424        main: false,
425    },
426    ParamSpec {
427        name: "shell_gain",
428        default: 0.0,
429        range: Some([0.0, 4.0]),
430        doc: "How strongly the spectrum lights the cube's radial shells, bass at the centre \
431              and treble at the faces; 0 leaves the light as the cells give it.",
432        kind: ParamKind::Modal,
433        group: ParamGroup::Light,
434        main: false,
435    },
436    ParamSpec {
437        name: "density",
438        default: 1.0,
439        range: Some([0.0, 8.0]),
440        doc: "How strongly the volume absorbs the light behind it, per unit of the cube's \
441              width; 0 is pure additive glow with no front or back.",
442        kind: ParamKind::Modal,
443        group: ParamGroup::Light,
444        main: true,
445    },
446    ParamSpec {
447        name: "trail",
448        default: 0.6,
449        range: Some([0.0, 1.0]),
450        doc: "How much of its light a decaying cell keeps at each stage of its decay; 0 draws \
451              only the live cells.",
452        kind: ParamKind::Modal,
453        group: ParamGroup::Light,
454        main: false,
455    },
456    ParamSpec {
457        name: "age_tint",
458        default: 0.3,
459        range: Some([0.0, 1.0]),
460        doc: "How far along the palette a cell's colour travels as it ages; a decaying cell \
461              takes the far end.",
462        kind: ParamKind::Modal,
463        group: ParamGroup::Colour,
464        main: false,
465    },
466    common::hue(0.0),
467    ParamSpec {
468        name: "hue_spread",
469        default: 0.3,
470        range: Some([0.0, 1.0]),
471        doc: "How far along the palette the colour travels from the cube's centre to its \
472              corners.",
473        kind: ParamKind::Modal,
474        group: ParamGroup::Colour,
475        main: false,
476    },
477    common::brightness(1.0),
478    ParamSpec {
479        name: "key",
480        default: 1.0,
481        range: Some([0.0, 4.0]),
482        doc: "How strongly the key light lights a solid face turned toward it; 0 leaves only \
483              the ambient light.",
484        kind: ParamKind::Modal,
485        group: ParamGroup::Light,
486        main: true,
487    },
488    ParamSpec {
489        name: "ambient",
490        default: 0.3,
491        range: Some([0.0, 1.0]),
492        doc: "The light every solid face gets whichever way it turns; 1 with no key light \
493              draws each face in its own colour.",
494        kind: ParamKind::Modal,
495        group: ParamGroup::Light,
496        main: false,
497    },
498    ParamSpec {
499        name: "ao",
500        default: 1.0,
501        range: Some([0.0, 1.0]),
502        doc: "How far the cells around a solid face darken its ambient light toward the \
503              creases and inner corners; 0 is none.",
504        kind: ParamKind::Modal,
505        group: ParamGroup::Light,
506        main: false,
507    },
508    ParamSpec {
509        name: "bounce",
510        default: 0.0,
511        range: Some([0.0, 2.0]),
512        doc: "How much light a solid face takes from the live cells in front of it, so a \
513              structure glows onto itself; soft, reaching a few cells and through thin \
514              walls. 0 is off.",
515        kind: ParamKind::Modal,
516        group: ParamGroup::Light,
517        main: false,
518    },
519    ParamSpec {
520        name: "light_yaw",
521        default: -0.6,
522        range: Some([-PI, PI]),
523        doc: "Where the key light stands around the viewer, in radians: 0 behind the camera, \
524              positive to its right. It turns with the camera.",
525        kind: ParamKind::Modal,
526        group: ParamGroup::Light,
527        main: false,
528    },
529    ParamSpec {
530        name: "light_pitch",
531        default: 0.6,
532        range: Some([-1.5, 1.5]),
533        doc: "How far above the viewer the key light stands, in radians; negative lights from \
534              below. It turns with the camera.",
535        kind: ParamKind::Modal,
536        group: ParamGroup::Light,
537        main: false,
538    },
539    camera::YAW,
540    camera::PITCH,
541    camera::DISTANCE,
542    camera::FOV,
543    FOCUS,
544    APERTURE,
545    FOG,
546    common::SATURATION,
547    common::PALETTE_MIX,
548    common::PALETTE_STEPS,
549    common::zoom(1.0),
550    common::PAN_X,
551    common::PAN_Y,
552];
553
554/// One row of [`FAMILY_PARAMS`]: a parameter only `solid` reads, with its
555/// range there, in [`Present::ALL`]'s order.
556macro_rules! solid_only {
557    ($name:literal: $range:expr) => {
558        FamilyParam {
559            name: $name,
560            ranges: &[
561                FamilyRange {
562                    family: "glow",
563                    range: None,
564                },
565                FamilyRange {
566                    family: "solid",
567                    range: $range,
568                },
569            ],
570        }
571    };
572}
573
574/// Every parameter only one present reads (ADR-0180 rule 4): the light, which
575/// a glowing volume has no surface to take. A parameter missing from here
576/// reads the same on both.
577pub const FAMILY_PARAMS: &[FamilyParam] = &[
578    solid_only!("key": Some([0.0, 4.0])),
579    solid_only!("ambient": Some([0.0, 1.0])),
580    solid_only!("ao": Some([0.0, 1.0])),
581    solid_only!("bounce": Some([0.0, 2.0])),
582    solid_only!("light_yaw": Some([-PI, PI])),
583    solid_only!("light_pitch": Some([-1.5, 1.5])),
584];
585
586/// The seed the whole field is hashed from, for a preset whose salt is `salt`.
587pub(crate) fn field_seed(salt: u32) -> u32 {
588    salt ^ FIELD_SEED_MIX
589}
590
591/// A fill probability as the step shader compares it: the fraction of the
592/// 24-bit hash range under which a cell is seeded live.
593pub(crate) fn live_threshold(fill: f32) -> u32 {
594    (fill.clamp(0.0, 1.0) * 16_777_216.0) as u32
595}
596
597/// The seeded ball's radius squared in half-cells², as the step shader compares
598/// a cell's offset from the centre: `seed_radius` of the half-side is
599/// `seed_radius * grid` half-cells.
600pub(crate) fn seed_radius_sq(seed_radius: f32, grid: u32) -> u32 {
601    let r = seed_radius.clamp(0.0, 2.0) * grid as f32;
602    (r * r) as u32
603}
604
605/// One round of the lowbias32 mixer `gpu::HASH_WGSL` defines, on the CPU, for
606/// the few values a reseed draws per rise.
607fn mix32(v: u32) -> u32 {
608    let mut h = v;
609    h ^= h >> 16;
610    h = h.wrapping_mul(0x7FEB_352D);
611    h ^= h >> 15;
612    h = h.wrapping_mul(0x846C_A68B);
613    h ^= h >> 16;
614    h
615}
616
617/// One scheduled `reseed`: the ball's centre in cells, its radius squared in
618/// cells², and the seed its cells are hashed from.
619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub(crate) struct Stamp {
621    pub(crate) centre: [u32; 3],
622    pub(crate) radius_sq: u32,
623    pub(crate) seed: u32,
624}
625
626/// The ball the `rise`-th reseed of a preset salted `salt` fills on a grid of
627/// `grid`, at `radius` of the half-side. Hashed from the rise count and the
628/// salt, never from a clock, so a preset replays its balls identically.
629pub(crate) fn stamp(salt: u32, rise: u32, grid: u32, radius: f32) -> Stamp {
630    let base = mix32(rise ^ mix32(salt ^ STAMP_SEED_MIX));
631    let side = grid.max(1);
632    let r = radius.clamp(0.0, 2.0) * side as f32 * 0.5;
633    Stamp {
634        centre: [
635            mix32(base ^ 1) % side,
636            mix32(base ^ 2) % side,
637            mix32(base ^ 3) % side,
638        ],
639        radius_sq: (r * r) as u32,
640        seed: mix32(base ^ 4),
641    }
642}
643
644/// `spectrum` laid across `shells` radial bands, bass at the centre: band `i`
645/// is the mean of its share of the bins, held finite and non-negative. Bands
646/// past `shells` stay zero.
647pub(crate) fn shell_levels(spectrum: &[f32], shells: u32) -> [f32; MAX_SHELLS as usize] {
648    let mut out = [0.0; MAX_SHELLS as usize];
649    let s = (shells.min(MAX_SHELLS) as usize).min(spectrum.len());
650    if s == 0 {
651        return out;
652    }
653    for (i, level) in out.iter_mut().take(s).enumerate() {
654        let lo = i * spectrum.len() / s;
655        let hi = ((i + 1) * spectrum.len() / s).max(lo + 1);
656        let band = spectrum.get(lo..hi).unwrap_or(&[]);
657        let mean = band.iter().sum::<f32>() / band.len().max(1) as f32;
658        *level = if mean.is_finite() { mean.max(0.0) } else { 0.0 };
659    }
660    out
661}
662
663/// A bound `step_rate` as the clock integrates it: clamped into
664/// `0..=`[`MAX_STEP_RATE`], the declared default where it is not finite.
665pub(crate) fn applied_step_rate(value: f32) -> f32 {
666    if value.is_finite() {
667        value.clamp(0.0, MAX_STEP_RATE)
668    } else {
669        DEFAULT_STEP_RATE
670    }
671}
672
673/// `value` where it is finite, `fallback` where it is not.
674fn finite_or(value: f32, fallback: f32) -> f32 {
675    if value.is_finite() { value } else { fallback }
676}
677
678/// `config` with its grid held to `cap`, and the clamp to report if that moved
679/// it. A grid is structural, so this runs once, at `configure`.
680pub(crate) fn grid_clamp(
681    config: VoxelConfig,
682    cap: u32,
683) -> (VoxelConfig, Option<super::CapOverflow>) {
684    if config.grid <= cap {
685        return (config, None);
686    }
687    let overflow = super::CapOverflow {
688        dropped: (config.grid - cap) as usize,
689        context: super::OverflowContext::Voxels(config.grid),
690        cap: cap as usize,
691    };
692    (
693        VoxelConfig {
694            grid: cap,
695            ..config
696        },
697        Some(overflow),
698    )
699}
700
701/// The three non-zero rows of `view`'s world-to-clip matrix — screen x, screen
702/// y and view depth — each as `[x, y, z, translation]`. The matrix is stored
703/// column-major, and its third row is zero (no depth attachment, ADR-0257).
704fn rows(view: &camera::CameraView) -> [[f32; 4]; 3] {
705    let m = view.view_proj;
706    let row = |r: usize| {
707        let at = |c: usize| m.get(c).and_then(|col| col.get(r)).copied().unwrap_or(0.0);
708        [at(0), at(1), at(2), at(3)]
709    };
710    [row(0), row(1), row(3)]
711}
712
713/// The eye: the one point every non-zero row of `view` maps to zero, solved by
714/// Cramer's rule. The three rows are the camera's right, up and view axes,
715/// scaled and sheared by the lens and the pan, so they are never coplanar.
716fn eye(view: &camera::CameraView) -> [f32; 3] {
717    let [r0, r1, r3] = rows(view);
718    let (a, b, c) = (xyz(r0), xyz(r1), xyz(r3));
719    let rhs = [-r0[3], -r1[3], -r3[3]];
720    let det = dot(a, cross(b, c));
721    if det.abs() < 1e-12 {
722        return [0.0, 0.0, view.distance];
723    }
724    // Column `i` of the system replaced by the right-hand side.
725    let solve = |i: usize| {
726        let swap = |row: [f32; 3], k: usize| {
727            let mut r = row;
728            if let Some(slot) = r.get_mut(i) {
729                *slot = rhs.get(k).copied().unwrap_or(0.0);
730            }
731            r
732        };
733        dot(swap(a, 0), cross(swap(b, 1), swap(c, 2))) / det
734    };
735    [solve(0), solve(1), solve(2)]
736}
737
738/// The unit world direction toward a key light standing at `light_yaw` around
739/// the viewer and `light_pitch` above them, in `view`'s camera space: at `(0,
740/// 0)` it shines from the eye along the view axis. The camera's basis is taken
741/// from the rows, so the light turns with every orbit, and the lens's scale and
742/// the pan's shear are removed from them first.
743pub(crate) fn light_dir(view: &camera::CameraView, light_yaw: f32, light_pitch: f32) -> [f32; 3] {
744    let [r0, _, r3] = rows(view);
745    let normalize = |v: [f32; 3]| {
746        let len = dot(v, v).sqrt();
747        if len > 1e-12 {
748            [v[0] / len, v[1] / len, v[2] / len]
749        } else {
750            v
751        }
752    };
753    let forward = normalize(xyz(r3));
754    // Screen x is `right * sx + forward * pan`: drop the forward share.
755    let r = xyz(r0);
756    let along = dot(r, forward);
757    let right = normalize([
758        r[0] - along * forward[0],
759        r[1] - along * forward[1],
760        r[2] - along * forward[2],
761    ]);
762    let up = cross(right, forward);
763    let (sy, cy) = finite_or(light_yaw, DEFAULT_LIGHT_YAW).sin_cos();
764    let (sp, cp) = finite_or(light_pitch, DEFAULT_LIGHT_PITCH)
765        .clamp(-FRAC_PI_2, FRAC_PI_2)
766        .sin_cos();
767    let (x, y, z) = (sy * cp, sp, cy * cp);
768    // `z` points back at the eye, against the view axis.
769    normalize([
770        right[0] * x + up[0] * y - forward[0] * z,
771        right[1] * x + up[1] * y - forward[1] * z,
772        right[2] * x + up[2] * y - forward[2] * z,
773    ])
774}
775
776/// The section plane as the march reads it: `n.xyz` the unit normal toward
777/// the hidden side in the cube's frame, and `w` the plane's offset `d`, so a
778/// cell whose centre `c` has `dot(c, n) > d` is cut. `None` at `cut <= 0`,
779/// which the march takes as no plane at all. A `cut` past 1 is held there.
780pub(crate) fn cut_plane(cut: f32, cut_yaw: f32, cut_pitch: f32) -> Option<[f32; 4]> {
781    let cut = finite_or(cut, DEFAULT_CUT).min(1.0);
782    if cut <= 0.0 {
783        return None;
784    }
785    let (sy, cy) = finite_or(cut_yaw, DEFAULT_CUT_YAW).sin_cos();
786    let (sp, cp) = finite_or(cut_pitch, DEFAULT_CUT_PITCH)
787        .clamp(-FRAC_PI_2, FRAC_PI_2)
788        .sin_cos();
789    Some([cp * sy, sp, cp * cy, CUBE_RADIUS * (1.0 - 2.0 * cut)])
790}
791
792fn xyz(v: [f32; 4]) -> [f32; 3] {
793    [v[0], v[1], v[2]]
794}
795
796fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
797    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
798}
799
800fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
801    [
802        a[1] * b[2] - a[2] * b[1],
803        a[2] * b[0] - a[0] * b[2],
804        a[0] * b[1] - a[1] * b[0],
805    ]
806}
807
808/// The one uniform the seed and step passes read, written once a frame.
809///
810/// **One buffer for both, and that is load-bearing**, for `cellular`'s reason:
811/// which pass is running is a constant compiled into its pipeline, never a
812/// field here, so every pass is bound to identical resources and an adapter
813/// cannot hand one pass another's uniform (ADR-0058).
814#[repr(C)]
815#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
816struct StepParams {
817    /// x: grid, y: wrap (1 torus), z: the field's seed, w: live threshold.
818    a: [u32; 4],
819    /// x: birth mask, y: survive mask, z: states, w: neighbourhood.
820    b: [u32; 4],
821    /// x: the seed ball's radius squared in half-cells², y: the reseed ball's
822    /// seed; zw unused.
823    c: [u32; 4],
824    /// xyz: the reseed ball's centre (cells), w: its radius squared (cells²).
825    d: [u32; 4],
826}
827
828/// The march's uniform, written once a frame.
829#[repr(C)]
830#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
831struct MarchParams {
832    r0: [f32; 4],
833    r1: [f32; 4],
834    r3: [f32; 4],
835    /// xyz: the eye; w: grid.
836    eye: [f32; 4],
837    /// x: hue, y: brightness, z: saturation, w: palette_mix.
838    a: [f32; 4],
839    /// x: palette_steps, y: occlude, z: density, w: trail.
840    b: [f32; 4],
841    /// x: age_tint, y: hue_spread, z: fog, w: shell_gain.
842    c: [f32; 4],
843    /// x: the cube's nearest view depth, y: its depth span, z: shells, w: 1 to
844    /// jump empty bricks.
845    d: [f32; 4],
846    /// xyz: the unit world direction toward the key light; w: 1 under
847    /// `solid`.
848    light: [f32; 4],
849    /// x: key, y: ambient, z: ao (0..1); w: 1 when the section plane is on.
850    lit: [f32; 4],
851    /// The section plane ([`cut_plane`]): xyz its unit normal, w its offset.
852    /// Zero, and never read, while `lit.w` is 0.
853    cut: [f32; 4],
854    /// x: the bounce the march adds, 0 to skip the light field's fetch — always
855    /// 0 under `glow`; yzw unused.
856    bounce: [f32; 4],
857    /// The shells' levels, four to a `vec4`, bass first.
858    shells: [[f32; 4]; (MAX_SHELLS / 4) as usize],
859}
860
861/// The march target for a render target of `target` pixels: `scale` of it on
862/// each axis, held to `cap` by one factor and rounded to [`MARCH_STEP`]
863/// (`grid::grid_size`, ADR-0037). `scale` is the tier's march scale times the
864/// renderer's own grid scale, floored at the smallest scale a grid takes.
865pub(crate) fn march_size(target: (u32, u32), scale: f32, cap: (u32, u32)) -> (u32, u32) {
866    let scale =
867        GridScale::new(scale.clamp(GridScale::MIN, GridScale::MAX)).unwrap_or(GridScale::FULL);
868    crate::render::grid::grid_size(target, scale, cap, MARCH_STEP)
869}
870
871/// What one pass of the step shader does: its `MODE` constant, compiled in.
872#[derive(Clone, Copy)]
873enum StepPass {
874    /// Advance every cell one generation by the rule.
875    Step,
876    /// Hash every cell from the field's seed.
877    Seed,
878    /// Hash the cells inside the scheduled reseed ball from its own seed.
879    Stamp,
880}
881
882impl StepPass {
883    fn mode(self) -> u32 {
884        match self {
885            StepPass::Step => 0,
886            StepPass::Seed => 1,
887            StepPass::Stamp => 2,
888        }
889    }
890
891    fn label(self) -> &'static str {
892        match self {
893            StepPass::Step => "voxel-step",
894            StepPass::Seed => "voxel-seed",
895            StepPass::Stamp => "voxel-stamp",
896        }
897    }
898
899    /// This pass's shader: its constants, the hash and the body. Built at
900    /// construction only.
901    fn source(self) -> String {
902        format!(
903            "const MODE: u32 = {}u;\nconst AGE_CAP: u32 = {}u;\n{}{}{}",
904            self.mode(),
905            AGE_CAP,
906            gpu::HASH_WGSL,
907            common::CELL_HASH_WGSL,
908            shader::STEP_SHADER
909        )
910    }
911}
912
913/// One of the pair of state textures, with the two views the passes take of it.
914struct StateTexture {
915    /// Held beside its views; read back by the probes.
916    #[cfg_attr(not(test), allow(dead_code))]
917    texture: wgpu::Texture,
918    /// Sampled by `textureLoad`, as the step's source and the march's field.
919    read: wgpu::TextureView,
920    /// Written by the step pass.
921    write: wgpu::TextureView,
922}
923
924impl StateTexture {
925    fn new(device: &wgpu::Device, grid: u32, label: &str) -> Self {
926        let texture = device.create_texture(&wgpu::TextureDescriptor {
927            label: Some(label),
928            size: wgpu::Extent3d {
929                width: grid,
930                height: grid,
931                depth_or_array_layers: grid,
932            },
933            mip_level_count: 1,
934            sample_count: 1,
935            dimension: wgpu::TextureDimension::D3,
936            format: STATE_FORMAT,
937            // The copies only so a probe can read the cells back and write a
938            // structure in.
939            usage: wgpu::TextureUsages::TEXTURE_BINDING
940                | wgpu::TextureUsages::STORAGE_BINDING
941                | wgpu::TextureUsages::COPY_SRC
942                | wgpu::TextureUsages::COPY_DST,
943            view_formats: &[],
944        });
945        let read = texture.create_view(&wgpu::TextureViewDescriptor::default());
946        let write = texture.create_view(&wgpu::TextureViewDescriptor::default());
947        Self {
948            texture,
949            read,
950            write,
951        }
952    }
953}
954
955/// The light field (ADR-0272): the splat texture each refill reduces the cells
956/// into, the blurred texture the march samples, and the two passes between
957/// them. Built on the first frame that reads it, never at `configure`.
958struct LightField {
959    /// Held beside the views the passes take of them.
960    _splat: wgpu::Texture,
961    _blurred: wgpu::Texture,
962    /// The blurred field, sampled by the march.
963    blurred_view: wgpu::TextureView,
964    splat_pipeline: wgpu::ComputePipeline,
965    blur_pipeline: wgpu::ComputePipeline,
966    /// Splat the cells of A, and of B.
967    splat_from_a: wgpu::BindGroup,
968    splat_from_b: wgpu::BindGroup,
969    blur_bg: wgpu::BindGroup,
970    /// Texels a side.
971    side: u32,
972}
973
974impl LightField {
975    fn build(device: &wgpu::Device, grid: u32, a: &StateTexture, b: &StateTexture) -> Self {
976        let side = grid.div_ceil(LIGHT_CELL);
977        let field = |label: &str| {
978            device.create_texture(&wgpu::TextureDescriptor {
979                label: Some(label),
980                size: wgpu::Extent3d {
981                    width: side,
982                    height: side,
983                    depth_or_array_layers: side,
984                },
985                mip_level_count: 1,
986                sample_count: 1,
987                dimension: wgpu::TextureDimension::D3,
988                format: LIGHT_FORMAT,
989                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING,
990                view_formats: &[],
991            })
992        };
993        let splat = field("voxel-light-splat");
994        let blurred = field("voxel-light-blurred");
995        let splat_read = splat.create_view(&wgpu::TextureViewDescriptor::default());
996        let splat_write = splat.create_view(&wgpu::TextureViewDescriptor::default());
997        let blurred_write = blurred.create_view(&wgpu::TextureViewDescriptor::default());
998        let blurred_view = blurred.create_view(&wgpu::TextureViewDescriptor::default());
999
1000        // These two layouts and `voxel-brick-layout` all bind one 3D texture and
1001        // one 3D storage texture to compute, and must still be three distinct
1002        // shapes (ADR-0058; `tonemap/tests.rs` asserts it). Hence the order and
1003        // the source bound twice: a tidier arrangement re-collides a pair. Every
1004        // entry is a full literal, because that scan reads only the spellings
1005        // its `MARKERS` lists and a local helper is not one of them.
1006        // `[StorageTexture<rgba16float>, Texture<u32>]`: the splat, the cells.
1007        let splat_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1008            label: Some("voxel-light-splat-layout"),
1009            entries: &[
1010                wgpu::BindGroupLayoutEntry {
1011                    binding: 0,
1012                    visibility: wgpu::ShaderStages::COMPUTE,
1013                    ty: wgpu::BindingType::StorageTexture {
1014                        access: wgpu::StorageTextureAccess::WriteOnly,
1015                        format: LIGHT_FORMAT,
1016                        view_dimension: wgpu::TextureViewDimension::D3,
1017                    },
1018                    count: None,
1019                },
1020                wgpu::BindGroupLayoutEntry {
1021                    binding: 1,
1022                    visibility: wgpu::ShaderStages::COMPUTE,
1023                    ty: wgpu::BindingType::Texture {
1024                        sample_type: wgpu::TextureSampleType::Uint,
1025                        view_dimension: wgpu::TextureViewDimension::D3,
1026                        multisampled: false,
1027                    },
1028                    count: None,
1029                },
1030            ],
1031        });
1032        // `[Texture<f32>, StorageTexture<rgba16float>, Texture<f32>]`: the
1033        // splat, the blurred, and the splat again, which the shader never reads.
1034        let blur_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1035            label: Some("voxel-light-blur-layout"),
1036            entries: &[
1037                wgpu::BindGroupLayoutEntry {
1038                    binding: 0,
1039                    visibility: wgpu::ShaderStages::COMPUTE,
1040                    ty: wgpu::BindingType::Texture {
1041                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
1042                        view_dimension: wgpu::TextureViewDimension::D3,
1043                        multisampled: false,
1044                    },
1045                    count: None,
1046                },
1047                wgpu::BindGroupLayoutEntry {
1048                    binding: 1,
1049                    visibility: wgpu::ShaderStages::COMPUTE,
1050                    ty: wgpu::BindingType::StorageTexture {
1051                        access: wgpu::StorageTextureAccess::WriteOnly,
1052                        format: LIGHT_FORMAT,
1053                        view_dimension: wgpu::TextureViewDimension::D3,
1054                    },
1055                    count: None,
1056                },
1057                wgpu::BindGroupLayoutEntry {
1058                    binding: 2,
1059                    visibility: wgpu::ShaderStages::COMPUTE,
1060                    ty: wgpu::BindingType::Texture {
1061                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
1062                        view_dimension: wgpu::TextureViewDimension::D3,
1063                        multisampled: false,
1064                    },
1065                    count: None,
1066                },
1067            ],
1068        });
1069        // `views[i]` is bound at binding `i`.
1070        let bind = |layout: &wgpu::BindGroupLayout, views: &[&wgpu::TextureView]| {
1071            let entries: Vec<wgpu::BindGroupEntry<'_>> = (0u32..)
1072                .zip(views)
1073                .map(|(binding, view)| wgpu::BindGroupEntry {
1074                    binding,
1075                    resource: wgpu::BindingResource::TextureView(view),
1076                })
1077                .collect();
1078            device.create_bind_group(&wgpu::BindGroupDescriptor {
1079                label: Some("voxel-light-bg"),
1080                layout,
1081                entries: &entries,
1082            })
1083        };
1084        let splat_from_a = bind(&splat_layout, &[&splat_write, &a.read]);
1085        let splat_from_b = bind(&splat_layout, &[&splat_write, &b.read]);
1086        let blur_bg = bind(&blur_layout, &[&splat_read, &blurred_write, &splat_read]);
1087        let pipeline = |label: &str, layout: &wgpu::BindGroupLayout, source: String| {
1088            let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1089                label: Some(label),
1090                source: wgpu::ShaderSource::Wgsl(source.into()),
1091            });
1092            let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1093                label: Some(label),
1094                bind_group_layouts: &[Some(layout)],
1095                immediate_size: 0,
1096            });
1097            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1098                label: Some(label),
1099                layout: Some(&pipeline_layout),
1100                module: &module,
1101                entry_point: Some("main"),
1102                compilation_options: Default::default(),
1103                cache: None,
1104            })
1105        };
1106        let splat_pipeline = pipeline(
1107            "voxel-light-splat",
1108            &splat_layout,
1109            format!(
1110                "const LIGHT_CELL: u32 = {LIGHT_CELL}u;\nconst AGE_SPAN: f32 = {AGE_SPAN:?};\n{}",
1111                shader::LIGHT_SPLAT_SHADER
1112            ),
1113        );
1114        let blur_pipeline = pipeline(
1115            "voxel-light-blur",
1116            &blur_layout,
1117            shader::LIGHT_BLUR_SHADER.to_string(),
1118        );
1119        Self {
1120            _splat: splat,
1121            _blurred: blurred,
1122            blurred_view,
1123            splat_pipeline,
1124            blur_pipeline,
1125            splat_from_a,
1126            splat_from_b,
1127            blur_bg,
1128            side,
1129        }
1130    }
1131
1132    /// Refill the field from the generation `reading_a` names: the splat, then
1133    /// the blur, each its own pass so the blur reads a complete splat.
1134    fn encode(&self, encoder: &mut wgpu::CommandEncoder, reading_a: bool) {
1135        let groups = self.side.div_ceil(LIGHT_WORKGROUP);
1136        {
1137            let mut cpass = gpu::compute_pass(encoder, "voxel-light-splat");
1138            cpass.set_pipeline(&self.splat_pipeline);
1139            cpass.set_bind_group(
1140                0,
1141                if reading_a {
1142                    &self.splat_from_a
1143                } else {
1144                    &self.splat_from_b
1145                },
1146                &[],
1147            );
1148            cpass.dispatch_workgroups(groups, groups, groups);
1149        }
1150        let mut cpass = gpu::compute_pass(encoder, "voxel-light-blur");
1151        cpass.set_pipeline(&self.blur_pipeline);
1152        cpass.set_bind_group(0, &self.blur_bg, &[]);
1153        cpass.dispatch_workgroups(groups, groups, groups);
1154    }
1155}
1156
1157/// The march's own target and the bind group that presents it, rebuilt when
1158/// the size the frame asks for changes.
1159struct MarchTarget {
1160    size: (u32, u32),
1161    /// Held beside its view.
1162    _texture: wgpu::Texture,
1163    view: wgpu::TextureView,
1164    present_bg: wgpu::BindGroup,
1165}
1166
1167/// The GPU-side state, built when a preset is configured and rebuilt when one
1168/// asks for a different grid.
1169struct Resources {
1170    /// The grid these textures were built for.
1171    grid: u32,
1172    /// The pair, kept so the bind groups' views stay valid.
1173    #[cfg_attr(not(test), allow(dead_code))]
1174    a: StateTexture,
1175    #[cfg_attr(not(test), allow(dead_code))]
1176    b: StateTexture,
1177    /// Whether `a` holds the current generation.
1178    reading_a: bool,
1179    seed_pipeline: wgpu::ComputePipeline,
1180    stamp_pipeline: wgpu::ComputePipeline,
1181    step_pipeline: wgpu::ComputePipeline,
1182    step_uniform: wgpu::Buffer,
1183    /// Read A write B, and read B write A.
1184    step_from_a: wgpu::BindGroup,
1185    step_from_b: wgpu::BindGroup,
1186    /// One texel a brick: non-zero where the brick holds a non-dead cell.
1187    /// Held beside the views the passes take of it.
1188    _bricks: wgpu::Texture,
1189    /// The march's view of the bricks, kept so its bind groups can be rebuilt
1190    /// when the light field arrives.
1191    bricks_read: wgpu::TextureView,
1192    brick_pipeline: wgpu::ComputePipeline,
1193    /// Mark the bricks of A, and of B.
1194    brick_from_a: wgpu::BindGroup,
1195    brick_from_b: wgpu::BindGroup,
1196    march_layout: wgpu::BindGroupLayout,
1197    march_pipeline: wgpu::RenderPipeline,
1198    march_uniform: wgpu::Buffer,
1199    /// Each bound to the light field once it exists, to `light_stand_in`
1200    /// before.
1201    march_from_a: wgpu::BindGroup,
1202    march_from_b: wgpu::BindGroup,
1203    /// A one-texel field the march binds until the light field is built; the
1204    /// uniform never lets it be read. Held beside the bind groups' view of it.
1205    _light_stand_in: wgpu::Texture,
1206    /// Clamps at the cube's faces and filters trilinearly.
1207    light_sampler: wgpu::Sampler,
1208    /// Built on the first frame under `solid` with `bounce > 0`.
1209    light: Option<LightField>,
1210    /// Whether `light` holds the current generation. Cleared by every frame
1211    /// that changes the state without refilling it.
1212    light_fresh: bool,
1213    present_layout: wgpu::BindGroupLayout,
1214    present_pipeline: wgpu::RenderPipeline,
1215    present_sampler: wgpu::Sampler,
1216    /// Built by the first `render`, at the size that frame asks for.
1217    target: Option<MarchTarget>,
1218    /// The shared gradient LUT pair (ADR-0021). A fresh pair is dirty, so a
1219    /// (re)build uploads on its first frame.
1220    luts: palette::LutPair,
1221}
1222
1223impl Resources {
1224    fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat, grid: u32) -> Self {
1225        let a = StateTexture::new(device, grid, "voxel-state-a");
1226        let b = StateTexture::new(device, grid, "voxel-state-b");
1227        let side = grid.div_ceil(BRICK);
1228        let bricks = device.create_texture(&wgpu::TextureDescriptor {
1229            label: Some("voxel-bricks"),
1230            size: wgpu::Extent3d {
1231                width: side,
1232                height: side,
1233                depth_or_array_layers: side,
1234            },
1235            mip_level_count: 1,
1236            sample_count: 1,
1237            dimension: wgpu::TextureDimension::D3,
1238            format: STATE_FORMAT,
1239            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING,
1240            view_formats: &[],
1241        });
1242        let bricks_read = bricks.create_view(&wgpu::TextureViewDescriptor::default());
1243        let bricks_write = bricks.create_view(&wgpu::TextureViewDescriptor::default());
1244        let step_uniform = gpu::uniform_buffer(
1245            device,
1246            "voxel-step-params",
1247            std::mem::size_of::<StepParams>(),
1248        );
1249        let march_uniform = gpu::uniform_buffer(
1250            device,
1251            "voxel-march-params",
1252            std::mem::size_of::<MarchParams>(),
1253        );
1254
1255        // `[Texture, StorageTexture, Uniform]`, all compute-visible: the source
1256        // generation, the one being written, the uniform.
1257        let step_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1258            label: Some("voxel-step-layout"),
1259            entries: &[
1260                wgpu::BindGroupLayoutEntry {
1261                    binding: 0,
1262                    visibility: wgpu::ShaderStages::COMPUTE,
1263                    ty: wgpu::BindingType::Texture {
1264                        sample_type: wgpu::TextureSampleType::Uint,
1265                        view_dimension: wgpu::TextureViewDimension::D3,
1266                        multisampled: false,
1267                    },
1268                    count: None,
1269                },
1270                wgpu::BindGroupLayoutEntry {
1271                    binding: 1,
1272                    visibility: wgpu::ShaderStages::COMPUTE,
1273                    ty: wgpu::BindingType::StorageTexture {
1274                        access: wgpu::StorageTextureAccess::WriteOnly,
1275                        format: STATE_FORMAT,
1276                        view_dimension: wgpu::TextureViewDimension::D3,
1277                    },
1278                    count: None,
1279                },
1280                gpu::uniform(2, wgpu::ShaderStages::COMPUTE),
1281            ],
1282        });
1283        let step_bind = |src: &StateTexture, dst: &StateTexture| {
1284            device.create_bind_group(&wgpu::BindGroupDescriptor {
1285                label: Some("voxel-step-bg"),
1286                layout: &step_layout,
1287                entries: &[
1288                    wgpu::BindGroupEntry {
1289                        binding: 0,
1290                        resource: wgpu::BindingResource::TextureView(&src.read),
1291                    },
1292                    wgpu::BindGroupEntry {
1293                        binding: 1,
1294                        resource: wgpu::BindingResource::TextureView(&dst.write),
1295                    },
1296                    wgpu::BindGroupEntry {
1297                        binding: 2,
1298                        resource: step_uniform.as_entire_binding(),
1299                    },
1300                ],
1301            })
1302        };
1303        let step_from_a = step_bind(&a, &b);
1304        let step_from_b = step_bind(&b, &a);
1305        let step_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1306            label: Some("voxel-step-pipeline-layout"),
1307            bind_group_layouts: &[Some(&step_layout)],
1308            immediate_size: 0,
1309        });
1310        let compute = |pass: StepPass| {
1311            let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1312                label: Some(pass.label()),
1313                source: wgpu::ShaderSource::Wgsl(pass.source().into()),
1314            });
1315            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1316                label: Some(pass.label()),
1317                layout: Some(&step_pipeline_layout),
1318                module: &module,
1319                entry_point: Some("main"),
1320                compilation_options: Default::default(),
1321                cache: None,
1322            })
1323        };
1324        let seed_pipeline = compute(StepPass::Seed);
1325        let stamp_pipeline = compute(StepPass::Stamp);
1326        let step_pipeline = compute(StepPass::Step);
1327
1328        // `[Texture, StorageTexture]`, compute-visible: the generation to mark,
1329        // and the bricks. Its size comes from the texture, so it takes no
1330        // uniform.
1331        let brick_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1332            label: Some("voxel-brick-layout"),
1333            entries: &[
1334                wgpu::BindGroupLayoutEntry {
1335                    binding: 0,
1336                    visibility: wgpu::ShaderStages::COMPUTE,
1337                    ty: wgpu::BindingType::Texture {
1338                        sample_type: wgpu::TextureSampleType::Uint,
1339                        view_dimension: wgpu::TextureViewDimension::D3,
1340                        multisampled: false,
1341                    },
1342                    count: None,
1343                },
1344                wgpu::BindGroupLayoutEntry {
1345                    binding: 1,
1346                    visibility: wgpu::ShaderStages::COMPUTE,
1347                    ty: wgpu::BindingType::StorageTexture {
1348                        access: wgpu::StorageTextureAccess::WriteOnly,
1349                        format: STATE_FORMAT,
1350                        view_dimension: wgpu::TextureViewDimension::D3,
1351                    },
1352                    count: None,
1353                },
1354            ],
1355        });
1356        let brick_bind = |src: &StateTexture| {
1357            device.create_bind_group(&wgpu::BindGroupDescriptor {
1358                label: Some("voxel-brick-bg"),
1359                layout: &brick_layout,
1360                entries: &[
1361                    wgpu::BindGroupEntry {
1362                        binding: 0,
1363                        resource: wgpu::BindingResource::TextureView(&src.read),
1364                    },
1365                    wgpu::BindGroupEntry {
1366                        binding: 1,
1367                        resource: wgpu::BindingResource::TextureView(&bricks_write),
1368                    },
1369                ],
1370            })
1371        };
1372        let brick_from_a = brick_bind(&a);
1373        let brick_from_b = brick_bind(&b);
1374        let brick_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1375            label: Some("voxel-bricks"),
1376            source: wgpu::ShaderSource::Wgsl(
1377                format!("const BRICK: u32 = {BRICK}u;\n{}", shader::BRICK_SHADER).into(),
1378            ),
1379        });
1380        let brick_pipeline_layout =
1381            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1382                label: Some("voxel-brick-pipeline-layout"),
1383                bind_group_layouts: &[Some(&brick_layout)],
1384                immediate_size: 0,
1385            });
1386        let brick_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1387            label: Some("voxel-bricks"),
1388            layout: Some(&brick_pipeline_layout),
1389            module: &brick_module,
1390            entry_point: Some("main"),
1391            compilation_options: Default::default(),
1392            cache: None,
1393        });
1394
1395        let luts = palette::LutPair::new(device, "voxel");
1396        // `[Uniform+size, Texture, Texture, Texture, Texture, Sampler, Texture3D,
1397        // Sampler]`: the uniform first and sized, so the shape is not
1398        // `cellular-present-layout`'s field-LUTs-sampler-uniform (ADR-0058).
1399        let march_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1400            label: Some("voxel-march-layout"),
1401            entries: &[
1402                wgpu::BindGroupLayoutEntry {
1403                    binding: 0,
1404                    visibility: wgpu::ShaderStages::FRAGMENT,
1405                    ty: wgpu::BindingType::Buffer {
1406                        ty: wgpu::BufferBindingType::Uniform,
1407                        has_dynamic_offset: false,
1408                        min_binding_size: wgpu::BufferSize::new(
1409                            std::mem::size_of::<MarchParams>() as u64
1410                        ),
1411                    },
1412                    count: None,
1413                },
1414                wgpu::BindGroupLayoutEntry {
1415                    binding: 1,
1416                    visibility: wgpu::ShaderStages::FRAGMENT,
1417                    ty: wgpu::BindingType::Texture {
1418                        sample_type: wgpu::TextureSampleType::Uint,
1419                        view_dimension: wgpu::TextureViewDimension::D3,
1420                        multisampled: false,
1421                    },
1422                    count: None,
1423                },
1424                wgpu::BindGroupLayoutEntry {
1425                    binding: 2,
1426                    visibility: wgpu::ShaderStages::FRAGMENT,
1427                    ty: wgpu::BindingType::Texture {
1428                        sample_type: wgpu::TextureSampleType::Uint,
1429                        view_dimension: wgpu::TextureViewDimension::D3,
1430                        multisampled: false,
1431                    },
1432                    count: None,
1433                },
1434                gpu::texture(3, true),
1435                gpu::texture(4, true),
1436                gpu::sampler(5),
1437                wgpu::BindGroupLayoutEntry {
1438                    binding: 6,
1439                    visibility: wgpu::ShaderStages::FRAGMENT,
1440                    ty: wgpu::BindingType::Texture {
1441                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
1442                        view_dimension: wgpu::TextureViewDimension::D3,
1443                        multisampled: false,
1444                    },
1445                    count: None,
1446                },
1447                gpu::sampler(7),
1448            ],
1449        });
1450        let light_stand_in = device.create_texture(&wgpu::TextureDescriptor {
1451            label: Some("voxel-light-stand-in"),
1452            size: wgpu::Extent3d {
1453                width: 1,
1454                height: 1,
1455                depth_or_array_layers: 1,
1456            },
1457            mip_level_count: 1,
1458            sample_count: 1,
1459            dimension: wgpu::TextureDimension::D3,
1460            format: LIGHT_FORMAT,
1461            usage: wgpu::TextureUsages::TEXTURE_BINDING,
1462            view_formats: &[],
1463        });
1464        let light_stand_in_view =
1465            light_stand_in.create_view(&wgpu::TextureViewDescriptor::default());
1466        let light_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1467            label: Some("voxel-light-sampler"),
1468            address_mode_u: wgpu::AddressMode::ClampToEdge,
1469            address_mode_v: wgpu::AddressMode::ClampToEdge,
1470            address_mode_w: wgpu::AddressMode::ClampToEdge,
1471            mag_filter: wgpu::FilterMode::Linear,
1472            min_filter: wgpu::FilterMode::Linear,
1473            ..Default::default()
1474        });
1475        let march_bind = |field: &StateTexture| {
1476            march_bind_group(
1477                device,
1478                &march_layout,
1479                &march_uniform,
1480                &field.read,
1481                &bricks_read,
1482                &luts,
1483                &light_stand_in_view,
1484                &light_sampler,
1485            )
1486        };
1487        let march_from_a = march_bind(&a);
1488        let march_from_b = march_bind(&b);
1489        let march_shader =
1490            gpu::fullscreen_shader(device, "voxel-march", gpu::FULLSCREEN_VS_NDC, &march_body());
1491        // Into the march's own target, which is cleared every frame, so the
1492        // march writes its premultiplied colour and coverage as they are.
1493        let march_pipeline = gpu::fullscreen_pipeline(
1494            device,
1495            &march_shader,
1496            &[&march_layout],
1497            surface_format,
1498            wgpu::BlendState::REPLACE,
1499            "voxel-march",
1500        );
1501
1502        // `[Sampler:VERTEX_FRAGMENT, Texture]`: the sampler is declared to both
1503        // stages only so the shape is not `bloom-blur-layout`'s
1504        // `[Sampler, Texture]` (ADR-0058).
1505        let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1506            label: Some("voxel-present-layout"),
1507            entries: &[
1508                wgpu::BindGroupLayoutEntry {
1509                    binding: 0,
1510                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1511                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1512                    count: None,
1513                },
1514                gpu::texture(1, true),
1515            ],
1516        });
1517        let present_shader = gpu::fullscreen_shader(
1518            device,
1519            "voxel-present",
1520            gpu::FULLSCREEN_VS_UV_FLIPPED,
1521            shader::PRESENT_SHADER,
1522        );
1523        let present_pipeline = gpu::fullscreen_pipeline(
1524            device,
1525            &present_shader,
1526            &[&present_layout],
1527            surface_format,
1528            // Premultiplied OVER the backdrop (ADR-0201): the march's light and
1529            // the coverage its absorption computed.
1530            wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
1531            "voxel-present",
1532        );
1533        let present_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1534            label: Some("voxel-present-sampler"),
1535            mag_filter: wgpu::FilterMode::Linear,
1536            min_filter: wgpu::FilterMode::Linear,
1537            ..Default::default()
1538        });
1539
1540        Self {
1541            grid,
1542            a,
1543            b,
1544            reading_a: true,
1545            seed_pipeline,
1546            stamp_pipeline,
1547            step_pipeline,
1548            step_uniform,
1549            step_from_a,
1550            step_from_b,
1551            _bricks: bricks,
1552            bricks_read,
1553            brick_pipeline,
1554            brick_from_a,
1555            brick_from_b,
1556            march_layout,
1557            march_pipeline,
1558            march_uniform,
1559            march_from_a,
1560            march_from_b,
1561            _light_stand_in: light_stand_in,
1562            light_sampler,
1563            light: None,
1564            light_fresh: false,
1565            present_layout,
1566            present_pipeline,
1567            present_sampler,
1568            target: None,
1569            luts,
1570        }
1571    }
1572
1573    /// Build the light field if it does not exist, and rebind the march to it.
1574    fn ensure_light(&mut self, device: &wgpu::Device) {
1575        if self.light.is_some() {
1576            return;
1577        }
1578        let light = LightField::build(device, self.grid, &self.a, &self.b);
1579        let bind = |field: &StateTexture| {
1580            march_bind_group(
1581                device,
1582                &self.march_layout,
1583                &self.march_uniform,
1584                &field.read,
1585                &self.bricks_read,
1586                &self.luts,
1587                &light.blurred_view,
1588                &self.light_sampler,
1589            )
1590        };
1591        self.march_from_a = bind(&self.a);
1592        self.march_from_b = bind(&self.b);
1593        self.light = Some(light);
1594        self.light_fresh = false;
1595    }
1596
1597    /// Refill the light field from the current generation, if it exists.
1598    fn encode_light(&mut self, encoder: &mut wgpu::CommandEncoder) {
1599        if let Some(light) = self.light.as_ref() {
1600            light.encode(encoder, self.reading_a);
1601            self.light_fresh = true;
1602        }
1603    }
1604
1605    /// Mark every brick of the current generation that holds a non-dead cell.
1606    fn encode_bricks(&self, encoder: &mut wgpu::CommandEncoder) {
1607        let side = self.grid.div_ceil(BRICK);
1608        let mut cpass = gpu::compute_pass(encoder, "voxel-bricks");
1609        cpass.set_pipeline(&self.brick_pipeline);
1610        cpass.set_bind_group(
1611            0,
1612            if self.reading_a {
1613                &self.brick_from_a
1614            } else {
1615                &self.brick_from_b
1616            },
1617            &[],
1618        );
1619        cpass.dispatch_workgroups(side, side, side);
1620    }
1621
1622    /// Hold the march's target at `size`, rebuilding it only when the size
1623    /// moved.
1624    fn ensure_target(
1625        &mut self,
1626        device: &wgpu::Device,
1627        format: wgpu::TextureFormat,
1628        size: (u32, u32),
1629    ) {
1630        if self.target.as_ref().is_none_or(|t| t.size != size) {
1631            let texture = device.create_texture(&wgpu::TextureDescriptor {
1632                label: Some("voxel-march-target"),
1633                size: wgpu::Extent3d {
1634                    width: size.0,
1635                    height: size.1,
1636                    depth_or_array_layers: 1,
1637                },
1638                mip_level_count: 1,
1639                sample_count: 1,
1640                dimension: wgpu::TextureDimension::D2,
1641                format,
1642                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1643                    | wgpu::TextureUsages::TEXTURE_BINDING,
1644                view_formats: &[],
1645            });
1646            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1647            let present_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1648                label: Some("voxel-present-bg"),
1649                layout: &self.present_layout,
1650                entries: &[
1651                    wgpu::BindGroupEntry {
1652                        binding: 0,
1653                        resource: wgpu::BindingResource::Sampler(&self.present_sampler),
1654                    },
1655                    wgpu::BindGroupEntry {
1656                        binding: 1,
1657                        resource: wgpu::BindingResource::TextureView(&view),
1658                    },
1659                ],
1660            });
1661            self.target = Some(MarchTarget {
1662                size,
1663                _texture: texture,
1664                view,
1665                present_bg,
1666            });
1667        }
1668    }
1669
1670    /// The texture holding the current generation.
1671    #[cfg(test)]
1672    fn current(&self) -> &wgpu::Texture {
1673        if self.reading_a {
1674            &self.a.texture
1675        } else {
1676            &self.b.texture
1677        }
1678    }
1679
1680    /// Encode one `pass` of the step shader: read the current generation,
1681    /// write the other texture, swap. Its own compute pass, so the write is
1682    /// complete before the next pass samples it.
1683    fn encode_step(&mut self, encoder: &mut wgpu::CommandEncoder, pass: StepPass) {
1684        let pipeline = match pass {
1685            StepPass::Step => &self.step_pipeline,
1686            StepPass::Seed => &self.seed_pipeline,
1687            StepPass::Stamp => &self.stamp_pipeline,
1688        };
1689        let groups = self.grid.div_ceil(WORKGROUP);
1690        {
1691            let mut cpass = gpu::compute_pass(encoder, pass.label());
1692            cpass.set_pipeline(pipeline);
1693            cpass.set_bind_group(
1694                0,
1695                if self.reading_a {
1696                    &self.step_from_a
1697                } else {
1698                    &self.step_from_b
1699                },
1700                &[],
1701            );
1702            cpass.dispatch_workgroups(groups, groups, groups);
1703        }
1704        self.reading_a = !self.reading_a;
1705    }
1706}
1707
1708/// The march's bind group over the generation `field`, its bricks, the palette
1709/// LUTs, and `light` — the light field, or the stand-in before it exists.
1710#[allow(clippy::too_many_arguments)]
1711fn march_bind_group(
1712    device: &wgpu::Device,
1713    layout: &wgpu::BindGroupLayout,
1714    uniform: &wgpu::Buffer,
1715    field: &wgpu::TextureView,
1716    bricks: &wgpu::TextureView,
1717    luts: &palette::LutPair,
1718    light: &wgpu::TextureView,
1719    light_sampler: &wgpu::Sampler,
1720) -> wgpu::BindGroup {
1721    let [lut_a, lut_b, lut_sampler] = luts.bind_entries(3, 4, 5);
1722    device.create_bind_group(&wgpu::BindGroupDescriptor {
1723        label: Some("voxel-march-bg"),
1724        layout,
1725        entries: &[
1726            wgpu::BindGroupEntry {
1727                binding: 0,
1728                resource: uniform.as_entire_binding(),
1729            },
1730            wgpu::BindGroupEntry {
1731                binding: 1,
1732                resource: wgpu::BindingResource::TextureView(field),
1733            },
1734            wgpu::BindGroupEntry {
1735                binding: 2,
1736                resource: wgpu::BindingResource::TextureView(bricks),
1737            },
1738            lut_a,
1739            lut_b,
1740            lut_sampler,
1741            wgpu::BindGroupEntry {
1742                binding: 6,
1743                resource: wgpu::BindingResource::TextureView(light),
1744            },
1745            wgpu::BindGroupEntry {
1746                binding: 7,
1747                resource: wgpu::BindingResource::Sampler(light_sampler),
1748            },
1749        ],
1750    })
1751}
1752
1753/// A 3D automaton in a cube, marched as an emitting, absorbing volume, or as
1754/// lit solid cells inside the glow of their own decay.
1755pub struct VoxelScene {
1756    /// Cloned device handle that builds [`Resources`] at `configure`.
1757    device: wgpu::Device,
1758    surface_format: wgpu::TextureFormat,
1759    res: Option<Resources>,
1760    /// The `[voxel]` table of the preset last configured.
1761    config: VoxelConfig,
1762    /// The tier's [`voxel_grid`](crate::render::TierConfig::voxel_grid): the
1763    /// largest grid a preset runs on.
1764    grid_cap: u32,
1765    /// The tier's [`voxel_march_scale`](crate::render::TierConfig::voxel_march_scale).
1766    march_scale: GridScale,
1767    /// The tier's [`voxel_march_cap`](crate::render::TierConfig::voxel_march_cap).
1768    march_cap: (u32, u32),
1769    /// The render target's size, handed in every frame.
1770    target: (u32, u32),
1771    /// The share of the renderer's grid scale the target does not already
1772    /// carry, handed in every frame (ADR-0245).
1773    field_scale: GridScale,
1774    /// Whether the march jumps empty bricks. Always on; a probe turns it off to
1775    /// hold the jump to the step it replaces.
1776    skip_bricks: bool,
1777    /// Whether the next `render` seeds the field before stepping it — set by
1778    /// every `configure`.
1779    needs_seed: bool,
1780    clock: common::GenerationClock,
1781    /// This frame's `dt`, stored by `advance` and integrated in `update`, where
1782    /// this frame's `step_rate` has landed.
1783    dt: f32,
1784    /// Generations `update` scheduled for the next `render` to encode.
1785    pending_generations: u32,
1786    /// How many reseeds have risen since `configure`: the count the next ball
1787    /// is hashed from.
1788    rises: u32,
1789    /// A ball scheduled by a `reseed` rising edge for the next `render`.
1790    pending_stamp: Option<Stamp>,
1791    /// Last frame's `reseed`, for the rising edge.
1792    prev_reseed: f32,
1793    /// This frame's spectrum laid across the shells, written by `update`.
1794    levels: [f32; MAX_SHELLS as usize],
1795    rule: f32,
1796    step_rate: f32,
1797    reseed: f32,
1798    reseed_radius: f32,
1799    shell_gain: f32,
1800    density: f32,
1801    trail: f32,
1802    age_tint: f32,
1803    hue_spread: f32,
1804    key: f32,
1805    ambient: f32,
1806    ao: f32,
1807    bounce: f32,
1808    /// How many times the light field has been refilled since this scene was
1809    /// built. Diagnostic: a preset that never binds `bounce` keeps it at 0.
1810    light_builds: u32,
1811    light_yaw: f32,
1812    light_pitch: f32,
1813    cut: f32,
1814    cut_yaw: f32,
1815    cut_pitch: f32,
1816    zoom: f32,
1817    colour: PaletteParams,
1818    pan: PanParams,
1819    camera: CameraParams,
1820    /// How much of this scene's coverage the backdrop resolves against
1821    /// (ADR-0085), set by the renderer every frame.
1822    occlude: f32,
1823    /// The active baked palette, held because `set_palette` can arrive before
1824    /// the resources exist.
1825    palette: Palette,
1826}
1827
1828impl VoxelScene {
1829    /// The CPU-side state, holding a preset's grid to `grid_cap` and marching at
1830    /// `march_scale` of the render target held to `march_cap` — the tier's
1831    /// three voxel values. GPU resources are built at the first `configure`.
1832    pub fn new(
1833        device: &wgpu::Device,
1834        surface_format: wgpu::TextureFormat,
1835        grid_cap: u32,
1836        march_scale: GridScale,
1837        march_cap: (u32, u32),
1838    ) -> Self {
1839        Self {
1840            device: device.clone(),
1841            surface_format,
1842            res: None,
1843            config: VoxelConfig::default(),
1844            grid_cap: grid_cap.clamp(MIN_GRID, MAX_GRID),
1845            march_scale,
1846            march_cap,
1847            target: (1, 1),
1848            field_scale: GridScale::FULL,
1849            skip_bricks: true,
1850            needs_seed: true,
1851            clock: common::GenerationClock::default(),
1852            dt: 0.0,
1853            pending_generations: 0,
1854            rises: 0,
1855            pending_stamp: None,
1856            prev_reseed: 0.0,
1857            levels: [0.0; MAX_SHELLS as usize],
1858            rule: 0.0,
1859            step_rate: DEFAULT_STEP_RATE,
1860            reseed: 0.0,
1861            reseed_radius: DEFAULT_RESEED_RADIUS,
1862            shell_gain: DEFAULT_SHELL_GAIN,
1863            density: DEFAULT_DENSITY,
1864            trail: DEFAULT_TRAIL,
1865            age_tint: DEFAULT_AGE_TINT,
1866            hue_spread: DEFAULT_HUE_SPREAD,
1867            key: DEFAULT_KEY,
1868            ambient: DEFAULT_AMBIENT,
1869            ao: DEFAULT_AO,
1870            bounce: DEFAULT_BOUNCE,
1871            light_builds: 0,
1872            light_yaw: DEFAULT_LIGHT_YAW,
1873            light_pitch: DEFAULT_LIGHT_PITCH,
1874            cut: DEFAULT_CUT,
1875            cut_yaw: DEFAULT_CUT_YAW,
1876            cut_pitch: DEFAULT_CUT_PITCH,
1877            zoom: DEFAULT_ZOOM,
1878            colour: PaletteParams::new(DEFAULT_HUE, DEFAULT_BRIGHTNESS),
1879            pan: PanParams::default(),
1880            camera: CameraParams::default(),
1881            occlude: crate::render::post::DEFAULT_OCCLUDE,
1882            palette: Palette::default_spectrum(),
1883        }
1884    }
1885
1886    /// This frame's step uniform: the rule `rule` picks, the field's seed, and
1887    /// the ball `stamp` refills if one is scheduled.
1888    fn step_params(&self, stamp: Option<Stamp>) -> StepParams {
1889        let rule = self.config.rules.pick(self.rule);
1890        let stamp = stamp.unwrap_or(Stamp {
1891            centre: [0, 0, 0],
1892            radius_sq: 0,
1893            seed: 0,
1894        });
1895        StepParams {
1896            a: [
1897                self.config.grid,
1898                u32::from(self.config.wrap),
1899                field_seed(self.config.salt),
1900                live_threshold(self.config.seed_fill),
1901            ],
1902            b: [
1903                rule.birth,
1904                rule.survive,
1905                rule.states,
1906                rule.neighbourhood.index(),
1907            ],
1908            c: [
1909                seed_radius_sq(self.config.seed_radius, self.config.grid),
1910                stamp.seed,
1911                0,
1912                0,
1913            ],
1914            d: [
1915                stamp.centre[0],
1916                stamp.centre[1],
1917                stamp.centre[2],
1918                stamp.radius_sq,
1919            ],
1920        }
1921    }
1922
1923    /// How many times the light field has been refilled. See
1924    /// [`Self::light_builds`].
1925    #[cfg(test)]
1926    pub(crate) fn light_build_count(&self) -> u32 {
1927        self.light_builds
1928    }
1929
1930    /// The bounce the march adds this frame: the bound value, held finite and
1931    /// inside `0..=`[`MAX_BOUNCE`], and 0 under `glow`, which reads no light.
1932    fn applied_bounce(&self) -> f32 {
1933        match self.config.present {
1934            Present::Glow => 0.0,
1935            Present::Solid => finite_or(self.bounce, DEFAULT_BOUNCE).clamp(0.0, MAX_BOUNCE),
1936        }
1937    }
1938
1939    /// This frame's march uniform, for a render target of `aspect`.
1940    fn march_params(&self, aspect: f32) -> MarchParams {
1941        let zoom = if self.zoom.is_finite() && self.zoom > 1e-3 {
1942            self.zoom
1943        } else {
1944            DEFAULT_ZOOM
1945        };
1946        let view = self.camera.camera().view(
1947            aspect,
1948            zoom,
1949            [finite_or(self.pan.x, 0.0), finite_or(self.pan.y, 0.0)],
1950        );
1951        let [r0, r1, r3] = rows(&view);
1952        let [ex, ey, ez] = eye(&view);
1953        let cut = cut_plane(self.cut, self.cut_yaw, self.cut_pitch);
1954        MarchParams {
1955            r0,
1956            r1,
1957            r3,
1958            eye: [ex, ey, ez, self.config.grid as f32],
1959            a: [
1960                self.colour.hue,
1961                self.colour.brightness,
1962                self.colour.saturation,
1963                self.colour.mix,
1964            ],
1965            b: [
1966                palette::band_steps(self.colour.steps),
1967                self.occlude,
1968                finite_or(self.density, DEFAULT_DENSITY).max(0.0),
1969                finite_or(self.trail, DEFAULT_TRAIL).clamp(0.0, 1.0),
1970            ],
1971            c: [
1972                finite_or(self.age_tint, DEFAULT_AGE_TINT),
1973                finite_or(self.hue_spread, DEFAULT_HUE_SPREAD),
1974                finite_or(self.camera.fog, 0.0).clamp(0.0, 1.0),
1975                finite_or(self.shell_gain, DEFAULT_SHELL_GAIN).clamp(0.0, MAX_SHELL_GAIN),
1976            ],
1977            d: [
1978                view.distance - CUBE_RADIUS,
1979                2.0 * CUBE_RADIUS,
1980                self.config.shells.min(MAX_SHELLS) as f32,
1981                if self.skip_bricks { 1.0 } else { 0.0 },
1982            ],
1983            light: {
1984                let [lx, ly, lz] = light_dir(&view, self.light_yaw, self.light_pitch);
1985                let solid = match self.config.present {
1986                    Present::Glow => 0.0,
1987                    Present::Solid => 1.0,
1988                };
1989                [lx, ly, lz, solid]
1990            },
1991            lit: [
1992                finite_or(self.key, DEFAULT_KEY).max(0.0),
1993                finite_or(self.ambient, DEFAULT_AMBIENT).max(0.0),
1994                finite_or(self.ao, DEFAULT_AO).clamp(0.0, 1.0),
1995                if cut.is_some() { 1.0 } else { 0.0 },
1996            ],
1997            cut: cut.unwrap_or([0.0; 4]),
1998            bounce: [self.applied_bounce(), 0.0, 0.0, 0.0],
1999            shells: {
2000                let mut out = [[0.0; 4]; (MAX_SHELLS / 4) as usize];
2001                for (slot, level) in out.iter_mut().flatten().zip(self.levels) {
2002                    *slot = level;
2003                }
2004                out
2005            },
2006        }
2007    }
2008}
2009
2010impl Scene for VoxelScene {
2011    fn name(&self) -> &'static str {
2012        "voxel"
2013    }
2014
2015    fn advance(&mut self, dt: f32) {
2016        self.dt = dt;
2017    }
2018
2019    fn set_target_size(&mut self, width: u32, height: u32) {
2020        // Recorded, never built here (ADR-0030): `render` compares the size it
2021        // asks for with the target it holds.
2022        self.target = (width.max(1), height.max(1));
2023    }
2024
2025    fn set_grid_scale(&mut self, scale: GridScale) {
2026        self.field_scale = scale;
2027    }
2028
2029    fn set_occlude(&mut self, occlude: f32) {
2030        self.occlude = occlude;
2031    }
2032
2033    fn set_palette(&mut self, palette: &Palette) {
2034        self.palette = palette.clone();
2035        if let Some(res) = self.res.as_mut() {
2036            res.luts.set(palette);
2037        }
2038    }
2039
2040    fn configure(&mut self, cfg: &GeneratorConfig) -> Option<super::CapOverflow> {
2041        let GeneratorConfig::Voxel(config) = cfg else {
2042            return None;
2043        };
2044        // A switch starts the incoming preset from its own seed. The grid is
2045        // held to the cap here, once, and the clamp is returned for the
2046        // renderer to announce with the preset. The state pair is built here,
2047        // off the frame, and only when the grid changes.
2048        let (config, overflow) = grid_clamp(*config, self.grid_cap);
2049        self.config = config;
2050        if self.res.as_ref().is_none_or(|res| res.grid != config.grid) {
2051            let mut built = Resources::build(&self.device, self.surface_format, config.grid);
2052            built.luts.set(&self.palette);
2053            self.res = Some(built);
2054        }
2055        self.needs_seed = true;
2056        self.clock = common::GenerationClock::default();
2057        self.pending_generations = 0;
2058        // The incoming preset's balls are its own stream, from its first.
2059        self.rises = 0;
2060        self.pending_stamp = None;
2061        self.prev_reseed = 0.0;
2062        overflow
2063    }
2064
2065    fn reset_params(&mut self) {
2066        self.rule = 0.0;
2067        self.step_rate = DEFAULT_STEP_RATE;
2068        self.reseed = 0.0;
2069        self.reseed_radius = DEFAULT_RESEED_RADIUS;
2070        self.shell_gain = DEFAULT_SHELL_GAIN;
2071        self.density = DEFAULT_DENSITY;
2072        self.trail = DEFAULT_TRAIL;
2073        self.age_tint = DEFAULT_AGE_TINT;
2074        self.hue_spread = DEFAULT_HUE_SPREAD;
2075        self.key = DEFAULT_KEY;
2076        self.ambient = DEFAULT_AMBIENT;
2077        self.ao = DEFAULT_AO;
2078        self.bounce = DEFAULT_BOUNCE;
2079        self.light_yaw = DEFAULT_LIGHT_YAW;
2080        self.light_pitch = DEFAULT_LIGHT_PITCH;
2081        self.cut = DEFAULT_CUT;
2082        self.cut_yaw = DEFAULT_CUT_YAW;
2083        self.cut_pitch = DEFAULT_CUT_PITCH;
2084        self.zoom = DEFAULT_ZOOM;
2085        self.colour.reset();
2086        self.pan.reset();
2087        self.camera.reset();
2088    }
2089
2090    fn set_param(&mut self, name: &str, value: f32) {
2091        // The shared param blocks first, this scene's own names after
2092        // (`scenes::common`).
2093        if self.colour.set(name, value) || self.pan.set(name, value) {
2094            return;
2095        }
2096        match name {
2097            // The camera block's names, matched here rather than delegated to
2098            // `CameraParams::set`: the march declares all of it but `solid`,
2099            // which sorts strokes and a march has none, and a delegation would
2100            // answer that undeclared name too. `focus` and `aperture` are
2101            // stored and never read.
2102            "yaw" => self.camera.yaw = value,
2103            "pitch" => self.camera.pitch = value,
2104            "distance" => self.camera.distance = value,
2105            "fov" => self.camera.fov = value,
2106            "focus" => self.camera.focus = value,
2107            "aperture" => self.camera.aperture = value,
2108            "fog" => self.camera.fog = value,
2109            "rule" => self.rule = value,
2110            "step_rate" => self.step_rate = value,
2111            "reseed" => self.reseed = value,
2112            "reseed_radius" => self.reseed_radius = value,
2113            "shell_gain" => self.shell_gain = value,
2114            "density" => self.density = value,
2115            "trail" => self.trail = value,
2116            "age_tint" => self.age_tint = value,
2117            "hue_spread" => self.hue_spread = value,
2118            "key" => self.key = value,
2119            "ambient" => self.ambient = value,
2120            "ao" => self.ao = value,
2121            "bounce" => self.bounce = value,
2122            "light_yaw" => self.light_yaw = value,
2123            "light_pitch" => self.light_pitch = value,
2124            "cut" => self.cut = value,
2125            "cut_yaw" => self.cut_yaw = value,
2126            "cut_pitch" => self.cut_pitch = value,
2127            "zoom" => self.zoom = value,
2128            _ => {}
2129        }
2130    }
2131
2132    fn update(&mut self, frame: &AnalysisFrame) {
2133        self.pending_generations = self
2134            .clock
2135            .advance(applied_step_rate(self.step_rate), self.dt);
2136        self.levels = shell_levels(&frame.spectrum, self.config.shells);
2137        // Rising edge only, so a beat flag held for several frames — or a
2138        // latch's hold — refills one ball rather than one per frame. A NaN
2139        // compares false both ways and is stored as zero, so it neither fires
2140        // nor arms a spurious edge on the next finite value.
2141        let reseed = finite_or(self.reseed, 0.0);
2142        if reseed >= RESEED_THRESHOLD && self.prev_reseed < RESEED_THRESHOLD {
2143            self.pending_stamp = Some(stamp(
2144                self.config.salt,
2145                self.rises,
2146                self.config.grid,
2147                finite_or(self.reseed_radius, DEFAULT_RESEED_RADIUS),
2148            ));
2149            self.rises = self.rises.wrapping_add(1);
2150        }
2151        self.prev_reseed = reseed;
2152    }
2153
2154    fn render(
2155        &mut self,
2156        queue: &wgpu::Queue,
2157        encoder: &mut wgpu::CommandEncoder,
2158        view: &wgpu::TextureView,
2159        aspect: f32,
2160    ) {
2161        // The aspect is the render target's, handed in here (ADR-0037).
2162        let march = self.march_params(aspect);
2163        let seed = std::mem::replace(&mut self.needs_seed, false);
2164        let stamp = self.pending_stamp.take();
2165        let generations = std::mem::take(&mut self.pending_generations);
2166        // The rule `rule` picks this frame runs every generation below, so a
2167        // change lands at the next generation boundary.
2168        let step = self.step_params(stamp);
2169        let size = march_size(
2170            self.target,
2171            self.march_scale.get() * self.field_scale.get(),
2172            self.march_cap,
2173        );
2174        let Some(res) = self.res.as_mut() else {
2175            return;
2176        };
2177        res.luts.flush(queue);
2178        queue.write_buffer(&res.march_uniform, 0, bytemuck::bytes_of(&march));
2179        // One write serves every pass below: the seed, then the ball, then the
2180        // generations, in that order, so a ball scheduled on a preset's first
2181        // frame lands on its seeded field rather than under it.
2182        let changed = seed || stamp.is_some() || generations > 0;
2183        if changed {
2184            queue.write_buffer(&res.step_uniform, 0, bytemuck::bytes_of(&step));
2185        }
2186        if seed {
2187            res.encode_step(encoder, StepPass::Seed);
2188        }
2189        if stamp.is_some() {
2190            res.encode_step(encoder, StepPass::Stamp);
2191        }
2192        for _ in 0..generations {
2193            res.encode_step(encoder, StepPass::Step);
2194        }
2195        // The march reads only the frame's last generation, so the bricks are
2196        // marked once, after it.
2197        if changed {
2198            res.encode_bricks(encoder);
2199            res.light_fresh = false;
2200        }
2201        // The light field follows the bricks, and a field built or switched
2202        // back on this frame is filled this frame, state change or not.
2203        if march.bounce[0] > 0.0 {
2204            res.ensure_light(&self.device);
2205            if !res.light_fresh {
2206                res.encode_light(encoder);
2207                self.light_builds = self.light_builds.wrapping_add(1);
2208            }
2209        }
2210
2211        res.ensure_target(&self.device, self.surface_format, size);
2212        let Some(target) = res.target.as_ref() else {
2213            return;
2214        };
2215        {
2216            // The march writes every pixel of its own target, which starts each
2217            // frame empty.
2218            let mut pass = gpu::color_pass(
2219                encoder,
2220                "voxel-march-pass",
2221                &target.view,
2222                wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2223            );
2224            pass.set_pipeline(&res.march_pipeline);
2225            pass.set_bind_group(
2226                0,
2227                if res.reading_a {
2228                    &res.march_from_a
2229                } else {
2230                    &res.march_from_b
2231                },
2232                &[],
2233            );
2234            pass.draw(0..3, 0..1);
2235        }
2236        // A plain stretch over the engine backdrop (ADR-0018): a ray through
2237        // empty cells wrote no light and no coverage.
2238        let mut pass = gpu::color_pass(encoder, "voxel-present-pass", view, wgpu::LoadOp::Load);
2239        pass.set_pipeline(&res.present_pipeline);
2240        pass.set_bind_group(0, &target.present_bg, &[]);
2241        pass.draw(0..3, 0..1);
2242    }
2243}
2244
2245#[cfg(test)]
2246mod tests;