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}