Skip to main content

rlx_core/render/scenes/voxel/
rules.rs

1//! The voxel system's rules: a 3D automaton in the Generations sense, and the
2//! closed roster of named ones a preset lists in `[voxel] rules` (ADR-0268).
3//!
4//! A rule is a pair of masks over the live-neighbour count — bit `k` of `birth`
5//! set means a dead cell with `k` live neighbours is born, bit `k` of `survive`
6//! that a live one with `k` stays live — a count of states, and a
7//! neighbourhood. A cell is dead (0), live (1), or in one of the decay stages
8//! `2..states`; a live cell that does not survive enters the first decay stage,
9//! a decaying cell advances one stage a generation and falls back to dead past
10//! the last, and only a live cell counts as a neighbour. With `states = 2` there
11//! are no decay stages, and the rule is a plain 3D life-like one.
12//!
13//! **The masks are integers end to end.** A Moore count reaches 26, so a mask
14//! is 27 bits, past what an `f32` holds exactly; that is why a rule is chosen
15//! from a list by a held index rather than bound as a mask (ADR-0268).
16
17// Hot-path panic-denial pragma (the hygiene guard scans `render/`). The rule
18// table is read every frame a generation runs.
19#![deny(
20    clippy::unwrap_used,
21    clippy::expect_used,
22    clippy::indexing_slicing,
23    clippy::panic,
24    clippy::unreachable
25)]
26
27/// Which cells count as a cell's neighbours.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum Neighbourhood {
30    /// The 26 cells sharing a face, an edge or a corner.
31    #[default]
32    Moore,
33    /// The 6 cells sharing a face.
34    VonNeumann,
35}
36
37impl Neighbourhood {
38    /// The largest live-neighbour count this neighbourhood reaches.
39    pub fn max_count(self) -> u32 {
40        match self {
41            Neighbourhood::Moore => 26,
42            Neighbourhood::VonNeumann => 6,
43        }
44    }
45
46    /// The integer the step shader's neighbourhood `select` reads.
47    pub(crate) fn index(self) -> u32 {
48        match self {
49            Neighbourhood::Moore => 0,
50            Neighbourhood::VonNeumann => 1,
51        }
52    }
53}
54
55/// The most states a rule may hold: dead, live and 253 decay stages, so a
56/// state fits the 8 bits the state texel gives it.
57pub const MAX_STATES: u32 = 255;
58
59/// One compiled rule, as the step shader reads it.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Rule {
62    /// Bit `k`: a dead cell with `k` live neighbours is born.
63    pub birth: u32,
64    /// Bit `k`: a live cell with `k` live neighbours stays live.
65    pub survive: u32,
66    /// Dead, live and the decay stages: `2..=`[`MAX_STATES`].
67    pub states: u32,
68    /// Which cells are counted.
69    pub neighbourhood: Neighbourhood,
70}
71
72/// A mask with bit `k` set for every `k` in each inclusive `lo..=hi` span.
73#[allow(
74    clippy::indexing_slicing,
75    reason = "a const fn cannot call `slice::get`; the index is bounded by the loop condition"
76)]
77const fn mask(spans: &[(u32, u32)]) -> u32 {
78    let mut out = 0u32;
79    let mut i = 0;
80    while i < spans.len() {
81        let (lo, hi) = spans[i];
82        let mut k = lo;
83        while k <= hi {
84            out |= 1 << k;
85            k += 1;
86        }
87        i += 1;
88    }
89    out
90}
91
92impl Rule {
93    /// An inline rule from its birth and survive counts, or the reason it is not
94    /// one: a count past the neighbourhood's [`max_count`](Neighbourhood::max_count)
95    /// (or below zero), or `states` outside `2..=`[`MAX_STATES`].
96    pub fn inline(
97        birth: &[i64],
98        survive: &[i64],
99        states: i64,
100        neighbourhood: Neighbourhood,
101    ) -> Result<Self, String> {
102        let max = i64::from(neighbourhood.max_count());
103        let masked = |counts: &[i64], what: &str| -> Result<u32, String> {
104            counts.iter().try_fold(0u32, |m, &k| {
105                if (0..=max).contains(&k) {
106                    Ok(m | (1 << k))
107                } else {
108                    Err(format!(
109                        "{what} count {k} is outside 0..={max} for this neighbourhood"
110                    ))
111                }
112            })
113        };
114        if !(2..=i64::from(MAX_STATES)).contains(&states) {
115            return Err(format!("states {states} is outside 2..={MAX_STATES}"));
116        }
117        Ok(Self {
118            birth: masked(birth, "birth")?,
119            survive: masked(survive, "survive")?,
120            states: states as u32,
121            neighbourhood,
122        })
123    }
124}
125
126impl Neighbourhood {
127    /// Every neighbourhood, in the order the reference lists them.
128    pub const ALL: [Neighbourhood; 2] = [Neighbourhood::Moore, Neighbourhood::VonNeumann];
129
130    /// Parse an inline rule's `neighbourhood`, or `None`.
131    pub fn from_name(name: &str) -> Option<Self> {
132        Self::ALL.into_iter().find(|n| n.as_str() == name)
133    }
134
135    /// The name an inline rule writes.
136    pub fn as_str(self) -> &'static str {
137        match self {
138            Neighbourhood::Moore => "moore",
139            Neighbourhood::VonNeumann => "von_neumann",
140        }
141    }
142}
143
144/// The closed roster of named rules. Each entry's notation in the comment is
145/// survive/birth/states/neighbourhood, as the 3D automaton catalogues write it;
146/// every one is held by `every_roster_rule_lives_from_its_own_seed` to a live
147/// fraction strictly inside `(0, 0.9)` at generations 100 and 400.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum RosterRule {
150    /// `4/4/5/M`: a sparse ball that burns out to a few still cells.
151    R445,
152    /// `9-26/5-7,12-13,15/5/M`: a sparse field that swells into a mass filling
153    /// most of the cube.
154    Amoeba,
155    /// `2,6,9/4,6,8-9/10/M`: a burst of long-decaying growth that leaves a
156    /// still scatter of cells behind.
157    Builder,
158    /// `13-26/13-14,17-19/2/M`: a dense field that settles into rounded
159    /// clouds.
160    Clouds,
161    /// `5-8/6-7,9,12/4/M`: a slowly churning reef with a skin of decaying
162    /// cells.
163    Coral,
164    /// `0-6/1,3/2/VN`: a small seed growing into a lattice that fills most of
165    /// the cube.
166    Crystal,
167    /// `4-7/6-8/10/M`: a restless, never-settling foam of cells and long
168    /// decay.
169    Pyroclastic,
170    /// `1,4,8,11,13-26/13-26/5/M`: a dense field that freezes almost at once.
171    SlowDecay,
172}
173
174impl RosterRule {
175    /// Every roster rule, in the order the generated reference lists them.
176    pub const ALL: [RosterRule; 8] = [
177        RosterRule::R445,
178        RosterRule::Amoeba,
179        RosterRule::Builder,
180        RosterRule::Clouds,
181        RosterRule::Coral,
182        RosterRule::Crystal,
183        RosterRule::Pyroclastic,
184        RosterRule::SlowDecay,
185    ];
186
187    /// Parse a roster name, or `None`.
188    pub fn from_name(name: &str) -> Option<Self> {
189        Self::ALL.into_iter().find(|rule| rule.as_str() == name)
190    }
191
192    /// The name a preset writes — [`from_name`](Self::from_name)'s inverse.
193    pub fn as_str(self) -> &'static str {
194        match self {
195            RosterRule::R445 => "445",
196            RosterRule::Amoeba => "amoeba",
197            RosterRule::Builder => "builder",
198            RosterRule::Clouds => "clouds",
199            RosterRule::Coral => "coral",
200            RosterRule::Crystal => "crystal",
201            RosterRule::Pyroclastic => "pyroclastic",
202            RosterRule::SlowDecay => "slow_decay",
203        }
204    }
205
206    /// The seed this rule lives from, as `(seed_radius, seed_fill)`: what an
207    /// absent `[voxel] seed_radius` and `seed_fill` mean when the list opens
208    /// with this rule. A 3D rule is particular about its seed — `clouds`
209    /// survives only a dense field and dies from a sparse one, where `crystal`
210    /// grows from a small sparse ball.
211    pub fn seed(self) -> (f32, f32) {
212        match self {
213            RosterRule::R445 => (0.35, 0.2),
214            RosterRule::Amoeba => (2.0, 0.5),
215            RosterRule::Builder => (0.5, 0.5),
216            RosterRule::Clouds => (1.0, 0.6),
217            RosterRule::Coral => (0.5, 0.5),
218            RosterRule::Crystal => (0.2, 0.3),
219            RosterRule::Pyroclastic => (0.5, 0.3),
220            RosterRule::SlowDecay => (1.0, 0.5),
221        }
222    }
223
224    /// The compiled rule.
225    pub fn rule(self) -> Rule {
226        let moore = |birth: &[(u32, u32)], survive: &[(u32, u32)], states: u32| Rule {
227            birth: mask(birth),
228            survive: mask(survive),
229            states,
230            neighbourhood: Neighbourhood::Moore,
231        };
232        match self {
233            RosterRule::R445 => moore(&[(4, 4)], &[(4, 4)], 5),
234            RosterRule::Amoeba => moore(&[(5, 7), (12, 13), (15, 15)], &[(9, 26)], 5),
235            RosterRule::Builder => moore(&[(4, 4), (6, 6), (8, 9)], &[(2, 2), (6, 6), (9, 9)], 10),
236            RosterRule::Clouds => moore(&[(13, 14), (17, 19)], &[(13, 26)], 2),
237            RosterRule::Coral => moore(&[(6, 7), (9, 9), (12, 12)], &[(5, 8)], 4),
238            RosterRule::Crystal => Rule {
239                birth: mask(&[(1, 1), (3, 3)]),
240                survive: mask(&[(0, 6)]),
241                states: 2,
242                neighbourhood: Neighbourhood::VonNeumann,
243            },
244            RosterRule::Pyroclastic => moore(&[(6, 8)], &[(4, 7)], 10),
245            RosterRule::SlowDecay => moore(
246                &[(13, 26)],
247                &[(1, 1), (4, 4), (8, 8), (11, 11), (13, 26)],
248                5,
249            ),
250        }
251    }
252}
253
254/// The most rules one `[voxel] rules` list may hold.
255pub const MAX_RULES: usize = 8;
256
257/// A preset's rule list, compiled: one to [`MAX_RULES`] rules in a fixed array,
258/// so the config stays `Copy` and nothing per frame allocates.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub struct RuleList {
261    rules: [Rule; MAX_RULES],
262    len: usize,
263}
264
265/// The rule an absent `[voxel] rules` key means.
266pub const DEFAULT_RULE: RosterRule = RosterRule::Clouds;
267
268impl Default for RuleList {
269    /// [`DEFAULT_RULE`] alone — what an absent `rules` key means.
270    fn default() -> Self {
271        Self::one(DEFAULT_RULE.rule())
272    }
273}
274
275impl RuleList {
276    /// A list of `rule` alone.
277    pub fn one(rule: Rule) -> Self {
278        Self {
279            rules: [rule; MAX_RULES],
280            len: 1,
281        }
282    }
283
284    /// `rules` as a list, or `None` when it is empty or longer than
285    /// [`MAX_RULES`].
286    pub fn new(rules: &[Rule]) -> Option<Self> {
287        let first = *rules.first()?;
288        if rules.len() > MAX_RULES {
289            return None;
290        }
291        let mut out = Self::one(first);
292        for (slot, rule) in out.rules.iter_mut().zip(rules) {
293            *slot = *rule;
294        }
295        out.len = rules.len();
296        Some(out)
297    }
298
299    /// How many rules the list holds, at least one.
300    pub fn len(&self) -> usize {
301        self.len
302    }
303
304    /// Never true: a list holds at least one rule. Present for the `len`
305    /// convention.
306    pub fn is_empty(&self) -> bool {
307        self.len == 0
308    }
309
310    /// The list's first rule.
311    pub fn first(&self) -> Rule {
312        let [first, ..] = self.rules;
313        first
314    }
315
316    /// The rule a bound `rule` index of `index` picks: rounded, held inside the
317    /// list, and the first rule where it is not finite. The engine has already
318    /// rounded a structural value, so the round here only matters to a caller
319    /// that has not.
320    pub fn pick(&self, index: f32) -> Rule {
321        let last = self.len.saturating_sub(1) as f32;
322        let i = if index.is_finite() {
323            index.round().clamp(0.0, last) as usize
324        } else {
325            0
326        };
327        self.rules.get(i).copied().unwrap_or_else(|| self.first())
328    }
329}