rlx_core/preset/schema/system.rs
1//! [`SystemKind`]: which built-in system a preset drives, and the one roster
2//! every other list of systems derives from.
3
4use crate::render::scenes::{ParamKind, ParamSpec, declares, kind_of, spec_names};
5
6/// The built-in system a preset drives. Extend as Plan 0003 (and later plans)
7/// add systems; unknown names are rejected at load.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SystemKind {
10 /// The fullscreen fragment-field scene.
11 FragmentField,
12 /// The CPU particle-swarm scene.
13 Swarm,
14 /// The parametric line-curve scene (Maurer rose, ...) — ADR-0007.
15 ParametricCurve,
16 /// The L-system generator scene — ADR-0007.
17 LSystem,
18 /// The Hankin star-pattern generator scene — ADR-0007.
19 StarPattern,
20 /// The Gray-Scott reaction-diffusion feedback scene — ADR-0012.
21 ReactionDiffusion,
22 /// The GPU compute-particle strange-attractor scene — ADR-0015.
23 Attractor,
24 /// The N-element spectrum readout — ADR-0036. A line scene like the three
25 /// above (it draws through the same shared renderer), driven by the analysis
26 /// frame's log-spaced band array rather than by a generator.
27 Spectrum,
28 /// The mark roster drawn at frame scale as a signed-distance field —
29 /// ADR-0105. The one scene whose palette coordinate is a *distance*,
30 /// which is what makes `palette_steps` draw concentric offset contours
31 /// of a shape.
32 ShapeField,
33 /// The ballistic emitter — objects that spawn, fall on a parabola and die
34 /// (ADR-0057). The first scene whose population is not fixed.
35 Emitter,
36 /// The warp mesh — a per-vertex UV grid that resamples the previous frame
37 /// (ADR-0113). Generalizes ADR-0048's single shared feedback transform to
38 /// one transform *per vertex*, driven by a `[per_vertex]` table.
39 WarpMesh,
40 /// Flat opaque elements painted on their own paper, composited in painter
41 /// order in one fullscreen distance-field pass (ADR-0123). The engine's
42 /// first **graphic** world rather than a luminous one: the only system in
43 /// which one object is genuinely in front of another.
44 ShapeCollage,
45 /// One fullscreen pass whose output is a closed-form function of position
46 /// (ADR-0180 rule 1): the Chladni plate, and every later stateless
47 /// per-pixel world, as a `[field] family` rather than a system each.
48 AnalyticField,
49 /// A discrete cellular automaton on a ping-pong grid (ADR-0180 rule 1,
50 /// ADR-0012): every grid rule — birth/survival, larger neighbourhoods, the
51 /// cyclic automaton — as a `[cellular] family` rather than a system each.
52 Cellular,
53 /// Points in 3D joined by a line wherever two lie within a link distance,
54 /// seen through the shared perspective camera (ADR-0257): every
55 /// arrangement of the points as a `[plexus] layout` rather than a system
56 /// each.
57 Plexus,
58}
59
60/// **The** roster of built-in systems: every variant, its canonical name, its
61/// family, and the parameter names its scene consumes, in the order the engine
62/// builds their scenes.
63///
64/// The single place all four lists live. [`SystemKind::ALL`],
65/// [`SystemKind::from_name`], [`SystemKind::as_str`], [`SystemKind::family`] and
66/// [`SystemKind::param_names`] all read this, so they cannot disagree with each
67/// other; what keeps *this* honest is [`SystemKind::row`], the one exhaustive
68/// match over the enum, which fails the build when a variant has no entry.
69///
70/// The param lists themselves live beside each scene's own `set_param` match
71/// (`declared_params_match_set_param` in `core/tests/suite/preset.rs` guards that
72/// pair); this is where they are gathered for the loader's typo check
73/// (ADR-0020). They do **not** include the global compositing params, which any
74/// preset may bind whatever its system -- [`is_known_param`] unions those in.
75///
76/// The family column is written down rather than derived, because no rule
77/// derives it: `shape_field` and `shape_collage` share the segment `shape`, and
78/// the collage's family is its *second* segment.
79const TABLE: [(SystemKind, &str, &str, &[ParamSpec]); SystemKind::VARIANT_COUNT] = {
80 use crate::render::scenes;
81 [
82 (
83 SystemKind::FragmentField,
84 "fragment_field",
85 "fragment",
86 scenes::fragment_field::PARAMS,
87 ),
88 (SystemKind::Swarm, "swarm", "swarm", scenes::swarm::PARAMS),
89 (
90 SystemKind::ParametricCurve,
91 "parametric_curve",
92 "curve",
93 scenes::lines::parametric::PARAMS,
94 ),
95 (
96 SystemKind::LSystem,
97 "lsystem",
98 "lsystem",
99 scenes::lines::lsystem::PARAMS,
100 ),
101 (
102 SystemKind::StarPattern,
103 "star_pattern",
104 "star",
105 scenes::lines::star::PARAMS,
106 ),
107 (
108 SystemKind::ReactionDiffusion,
109 "reaction_diffusion",
110 "reaction",
111 scenes::reaction_diffusion::PARAMS,
112 ),
113 (
114 SystemKind::Attractor,
115 "attractor",
116 "attractor",
117 scenes::particles::PARAMS,
118 ),
119 (
120 SystemKind::Spectrum,
121 "spectrum",
122 "spectrum",
123 scenes::lines::spectrum::PARAMS,
124 ),
125 (
126 SystemKind::Emitter,
127 "emitter",
128 "emitter",
129 scenes::emitter::PARAMS,
130 ),
131 (
132 SystemKind::ShapeField,
133 "shape_field",
134 "shape",
135 scenes::shape_field::PARAMS,
136 ),
137 (
138 SystemKind::WarpMesh,
139 "warp_mesh",
140 "warp",
141 scenes::warp_mesh::PARAMS,
142 ),
143 (
144 SystemKind::ShapeCollage,
145 "shape_collage",
146 "collage",
147 scenes::shape_collage::PARAMS,
148 ),
149 (
150 SystemKind::AnalyticField,
151 "analytic_field",
152 "analytic",
153 scenes::analytic_field::PARAMS,
154 ),
155 (
156 SystemKind::Cellular,
157 "cellular",
158 "cellular",
159 scenes::cellular::PARAMS,
160 ),
161 (
162 SystemKind::Plexus,
163 "plexus",
164 "plexus",
165 scenes::plexus::PARAMS,
166 ),
167 ]
168};
169
170/// Every [`TABLE`] row sits at the index its own variant's [`SystemKind::row`]
171/// names. Checked at compile time, because the two are written by hand and a
172/// mismatch would silently give one system another's name and params.
173const _: () = {
174 let mut i = 0;
175 while i < SystemKind::VARIANT_COUNT {
176 assert!(
177 TABLE[i].0.row() == i,
178 "TABLE row order must match SystemKind::row"
179 );
180 i += 1;
181 }
182};
183
184/// Every family is one `_`-separated segment of its own system's name, and no
185/// two systems share a family. Checked at compile time for the same reason as
186/// the row order: a library filename selects its editor schema by family, so two
187/// systems on one family would hand one of them the other's parameter set.
188const _: () = {
189 let mut i = 0;
190 while i < SystemKind::VARIANT_COUNT {
191 assert!(
192 is_segment(TABLE[i].1.as_bytes(), TABLE[i].2.as_bytes()),
193 "a system's family must be one `_`-separated segment of its name"
194 );
195 let mut j = i + 1;
196 while j < SystemKind::VARIANT_COUNT {
197 assert!(
198 !bytes_eq(TABLE[i].2.as_bytes(), TABLE[j].2.as_bytes()),
199 "no two systems may share a family"
200 );
201 j += 1;
202 }
203 i += 1;
204 }
205};
206
207/// Byte equality, usable in a `const` context where `==` on slices is not.
208const fn bytes_eq(a: &[u8], b: &[u8]) -> bool {
209 if a.len() != b.len() {
210 return false;
211 }
212 let mut i = 0;
213 while i < a.len() {
214 if a[i] != b[i] {
215 return false;
216 }
217 i += 1;
218 }
219 true
220}
221
222/// Whether `segment` is non-empty and equals one of `name`'s `_`-separated
223/// segments.
224const fn is_segment(name: &[u8], segment: &[u8]) -> bool {
225 if segment.is_empty() {
226 return false;
227 }
228 let mut start = 0;
229 let mut i = 0;
230 while i <= name.len() {
231 if i == name.len() || name[i] == b'_' {
232 if i - start == segment.len() {
233 let mut k = 0;
234 while k < segment.len() && name[start + k] == segment[k] {
235 k += 1;
236 }
237 if k == segment.len() {
238 return true;
239 }
240 }
241 start = i + 1;
242 }
243 i += 1;
244 }
245 false
246}
247
248impl SystemKind {
249 /// How many variants [`SystemKind`] has. Kept honest by `row`: a new
250 /// variant fails the build there until it is rostered, and `TABLE` is
251 /// typed off this count, so bumping the count without adding a row does not
252 /// compile either. Both are module-private, so this names them rather than
253 /// linking them.
254 pub const VARIANT_COUNT: usize = 15;
255
256 /// This variant's index into [`TABLE`].
257 ///
258 /// **The one exhaustive match over the enum, and the reason the roster
259 /// cannot go stale**: a new variant makes this non-exhaustive and fails the
260 /// build, which in turn forces a [`TABLE`] row, a scene into the exhaustive
261 /// factory in `render::scenes`, and a fixture into the golden drift guard.
262 const fn row(self) -> usize {
263 match self {
264 SystemKind::FragmentField => 0,
265 SystemKind::Swarm => 1,
266 SystemKind::ParametricCurve => 2,
267 SystemKind::LSystem => 3,
268 SystemKind::StarPattern => 4,
269 SystemKind::ReactionDiffusion => 5,
270 SystemKind::Attractor => 6,
271 SystemKind::Spectrum => 7,
272 SystemKind::Emitter => 8,
273 SystemKind::ShapeField => 9,
274 SystemKind::WarpMesh => 10,
275 SystemKind::ShapeCollage => 11,
276 SystemKind::AnalyticField => 12,
277 SystemKind::Cellular => 13,
278 SystemKind::Plexus => 14,
279 }
280 }
281
282 /// Every [`SystemKind`], in the order the engine builds their scenes. The
283 /// scene factory (`render::scenes::create_all`) and the golden drift guard
284 /// both iterate this rather than keeping lists of their own.
285 ///
286 /// Typed `[SystemKind; VARIANT_COUNT]`, so a roster that has drifted from
287 /// the variant count is a compile error, not a test failure.
288 pub const ALL: [SystemKind; Self::VARIANT_COUNT] = {
289 let mut out = [SystemKind::FragmentField; Self::VARIANT_COUNT];
290 let mut i = 0;
291 while i < Self::VARIANT_COUNT {
292 out[i] = TABLE[i].0;
293 i += 1;
294 }
295 out
296 };
297
298 /// Parse a canonical system name (as written in a preset's `system = "..."`
299 /// field) into its [`SystemKind`], or `None` if unknown. The inverse of
300 /// [`SystemKind::as_str`]; the `shot` CLI reuses the pair so it declares no
301 /// match of its own.
302 pub fn from_name(name: &str) -> Option<Self> {
303 TABLE
304 .iter()
305 .find(|(_, canonical, _, _)| *canonical == name)
306 .map(|(kind, _, _, _)| *kind)
307 }
308
309 /// The canonical name of this system -- the exact string
310 /// [`SystemKind::from_name`] accepts and a preset writes in its `system`
311 /// field.
312 pub fn as_str(self) -> &'static str {
313 TABLE[self.row()].1
314 }
315
316 /// The family this system's library presets are named for: the filename
317 /// prefix, up to the first `_`, of every `presets/*.toml` that drives it
318 /// (`collage` for `shape_collage`).
319 ///
320 /// One `_`-separated segment of [`SystemKind::as_str`], and unique across
321 /// systems — both checked at compile time. The editor's schema association
322 /// and `ritmolux --check`'s `file-name` rule both read it, so a filename the
323 /// checker accepts is one the editor gives that system's schema.
324 pub fn family(self) -> &'static str {
325 TABLE[self.row()].2
326 }
327
328 /// The parameters this system's scene consumes, as the scene declares them
329 /// (the module-private `TABLE`).
330 ///
331 /// The specs carry the default and the doc line as well as the name
332 /// (ADR-0170), which is what lets the generated reference in
333 /// `presets/README.md` be derived from the same declaration the loader
334 /// checks a binding against.
335 pub fn param_specs(self) -> &'static [ParamSpec] {
336 TABLE[self.row()].3
337 }
338
339 /// Just the names, for a caller that wants to print or join them.
340 ///
341 /// Allocates. Use [`declares`] for a membership test, which is what almost
342 /// every caller actually wants.
343 pub fn param_names(self) -> Vec<&'static str> {
344 spec_names(TABLE[self.row()].3)
345 }
346}
347
348/// The parameter names any preset may bind regardless of its system: the five
349/// compositing stages that run around the scene (`bg_*`, `trails`, `kaleido_*`,
350/// `exposure`, `ink_*`/`paper_*`). Gathered from each stage's own declared
351/// vocabulary so there is no third copy to drift.
352///
353/// **These do not all route through the renderer**, whatever the name suggests:
354/// `trails` and `kaleido_*` are offered by the `PostChain` (ADR-0031),
355/// `exposure` by the tonemap (ADR-0046) and `ink_*`/`paper_*` by the terminal ink
356/// pass (ADR-0032); only `bg_*` goes to a pass the renderer drives directly. The
357/// *names* are what this const is about — see `render::ParamRoute` for who
358/// actually owns each.
359pub const GLOBAL_PARAMS: [&[ParamSpec]; 7] = [
360 crate::render::background::PARAMS,
361 crate::render::trails::PARAMS,
362 crate::render::kaleidoscope::PARAMS,
363 crate::render::bloom::PARAMS,
364 // The composite seam's own vocabulary (`occlude`, ADR-0085) — owned by the
365 // chain rather than by any stage in it, which is why it is a seventh entry
366 // and not part of one of the three above.
367 crate::render::post::CHAIN_PARAMS,
368 crate::render::tonemap::PARAMS,
369 crate::render::ink::PARAMS,
370];
371
372/// Whether `name` is a parameter `system` (or the global compositing layer)
373/// actually consumes. An unknown name is a load-time **warning**, not an error:
374/// the preset still loads and applies its good bindings (ADR-0020, NFR 10).
375pub fn is_known_param(system: SystemKind, name: &str) -> bool {
376 declares(system.param_specs(), name) || GLOBAL_PARAMS.iter().any(|stage| declares(stage, name))
377}
378
379/// The [`ParamKind`] `name` is declared with, searching `system`'s own roster
380/// first and then the global compositing stages — the same rosters in the same
381/// order [`is_known_param`] tests, so a name that is known there has a kind
382/// here.
383///
384/// [`ParamKind::Modal`] for a name no roster declares, which is the ADR-0020
385/// warning case: the binding is kept, nothing reads it, and quantizing a value
386/// no scene receives would be a decision about nothing.
387pub fn kind_of_param(system: SystemKind, name: &str) -> ParamKind {
388 kind_of(system.param_specs(), name)
389 .or_else(|| GLOBAL_PARAMS.iter().find_map(|stage| kind_of(stage, name)))
390 .unwrap_or_default()
391}