Skip to main content

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    /// The recent spectrum as a landscape: one row per moment, receding from
59    /// the camera as it ages, seen through the shared perspective camera
60    /// (ADR-0258). Its own system rather than a `spectrum` layout, because it
61    /// keeps a history of rows and draws through a renderer of its own
62    /// (ADR-0180 rule 1).
63    Waterfall,
64    /// A 3D cellular automaton in a cube of cells, marched as an emitting,
65    /// absorbing volume through the shared perspective camera (ADR-0268). Its
66    /// own system rather than a `cellular` family, because its state, its pass
67    /// and half its parameters are its own (ADR-0180 rule 1).
68    Voxel,
69}
70
71/// **The** roster of built-in systems: every variant, its canonical name, its
72/// family, and the parameter names its scene consumes, in the order the engine
73/// builds their scenes.
74///
75/// The single place all four lists live. [`SystemKind::ALL`],
76/// [`SystemKind::from_name`], [`SystemKind::as_str`], [`SystemKind::family`] and
77/// [`SystemKind::param_names`] all read this, so they cannot disagree with each
78/// other; what keeps *this* honest is [`SystemKind::row`], the one exhaustive
79/// match over the enum, which fails the build when a variant has no entry.
80///
81/// The param lists themselves live beside each scene's own `set_param` match
82/// (`declared_params_match_set_param` in `core/tests/suite/preset.rs` guards that
83/// pair); this is where they are gathered for the loader's typo check
84/// (ADR-0020). They do **not** include the global compositing params, which any
85/// preset may bind whatever its system -- [`is_known_param`] unions those in.
86///
87/// The family column is written down rather than derived, because no rule
88/// derives it: `shape_field` and `shape_collage` share the segment `shape`, and
89/// the collage's family is its *second* segment.
90const TABLE: [(SystemKind, &str, &str, &[ParamSpec]); SystemKind::VARIANT_COUNT] = {
91    use crate::render::scenes;
92    [
93        (
94            SystemKind::FragmentField,
95            "fragment_field",
96            "fragment",
97            scenes::fragment_field::PARAMS,
98        ),
99        (SystemKind::Swarm, "swarm", "swarm", scenes::swarm::PARAMS),
100        (
101            SystemKind::ParametricCurve,
102            "parametric_curve",
103            "curve",
104            scenes::lines::parametric::PARAMS,
105        ),
106        (
107            SystemKind::LSystem,
108            "lsystem",
109            "lsystem",
110            scenes::lines::lsystem::PARAMS,
111        ),
112        (
113            SystemKind::StarPattern,
114            "star_pattern",
115            "star",
116            scenes::lines::star::PARAMS,
117        ),
118        (
119            SystemKind::ReactionDiffusion,
120            "reaction_diffusion",
121            "reaction",
122            scenes::reaction_diffusion::PARAMS,
123        ),
124        (
125            SystemKind::Attractor,
126            "attractor",
127            "attractor",
128            scenes::particles::PARAMS,
129        ),
130        (
131            SystemKind::Spectrum,
132            "spectrum",
133            "spectrum",
134            scenes::lines::spectrum::PARAMS,
135        ),
136        (
137            SystemKind::Emitter,
138            "emitter",
139            "emitter",
140            scenes::emitter::PARAMS,
141        ),
142        (
143            SystemKind::ShapeField,
144            "shape_field",
145            "shape",
146            scenes::shape_field::PARAMS,
147        ),
148        (
149            SystemKind::WarpMesh,
150            "warp_mesh",
151            "warp",
152            scenes::warp_mesh::PARAMS,
153        ),
154        (
155            SystemKind::ShapeCollage,
156            "shape_collage",
157            "collage",
158            scenes::shape_collage::PARAMS,
159        ),
160        (
161            SystemKind::AnalyticField,
162            "analytic_field",
163            "analytic",
164            scenes::analytic_field::PARAMS,
165        ),
166        (
167            SystemKind::Cellular,
168            "cellular",
169            "cellular",
170            scenes::cellular::PARAMS,
171        ),
172        (
173            SystemKind::Plexus,
174            "plexus",
175            "plexus",
176            scenes::plexus::PARAMS,
177        ),
178        (
179            SystemKind::Waterfall,
180            "waterfall",
181            "waterfall",
182            scenes::lines::waterfall::PARAMS,
183        ),
184        (SystemKind::Voxel, "voxel", "voxel", scenes::voxel::PARAMS),
185    ]
186};
187
188/// Every [`TABLE`] row sits at the index its own variant's [`SystemKind::row`]
189/// names. Checked at compile time, because the two are written by hand and a
190/// mismatch would silently give one system another's name and params.
191const _: () = {
192    let mut i = 0;
193    while i < SystemKind::VARIANT_COUNT {
194        assert!(
195            TABLE[i].0.row() == i,
196            "TABLE row order must match SystemKind::row"
197        );
198        i += 1;
199    }
200};
201
202/// Every family is one `_`-separated segment of its own system's name, and no
203/// two systems share a family. Checked at compile time for the same reason as
204/// the row order: a library filename selects its editor schema by family, so two
205/// systems on one family would hand one of them the other's parameter set.
206const _: () = {
207    let mut i = 0;
208    while i < SystemKind::VARIANT_COUNT {
209        assert!(
210            is_segment(TABLE[i].1.as_bytes(), TABLE[i].2.as_bytes()),
211            "a system's family must be one `_`-separated segment of its name"
212        );
213        let mut j = i + 1;
214        while j < SystemKind::VARIANT_COUNT {
215            assert!(
216                !bytes_eq(TABLE[i].2.as_bytes(), TABLE[j].2.as_bytes()),
217                "no two systems may share a family"
218            );
219            j += 1;
220        }
221        i += 1;
222    }
223};
224
225/// Byte equality, usable in a `const` context where `==` on slices is not.
226const fn bytes_eq(a: &[u8], b: &[u8]) -> bool {
227    if a.len() != b.len() {
228        return false;
229    }
230    let mut i = 0;
231    while i < a.len() {
232        if a[i] != b[i] {
233            return false;
234        }
235        i += 1;
236    }
237    true
238}
239
240/// Whether `segment` is non-empty and equals one of `name`'s `_`-separated
241/// segments.
242const fn is_segment(name: &[u8], segment: &[u8]) -> bool {
243    if segment.is_empty() {
244        return false;
245    }
246    let mut start = 0;
247    let mut i = 0;
248    while i <= name.len() {
249        if i == name.len() || name[i] == b'_' {
250            if i - start == segment.len() {
251                let mut k = 0;
252                while k < segment.len() && name[start + k] == segment[k] {
253                    k += 1;
254                }
255                if k == segment.len() {
256                    return true;
257                }
258            }
259            start = i + 1;
260        }
261        i += 1;
262    }
263    false
264}
265
266impl SystemKind {
267    /// How many variants [`SystemKind`] has. Kept honest by `row`: a new
268    /// variant fails the build there until it is rostered, and `TABLE` is
269    /// typed off this count, so bumping the count without adding a row does not
270    /// compile either. Both are module-private, so this names them rather than
271    /// linking them.
272    pub const VARIANT_COUNT: usize = 17;
273
274    /// This variant's index into [`TABLE`].
275    ///
276    /// **The one exhaustive match over the enum, and the reason the roster
277    /// cannot go stale**: a new variant makes this non-exhaustive and fails the
278    /// build, which in turn forces a [`TABLE`] row, a scene into the exhaustive
279    /// factory in `render::scenes`, and a fixture into the golden drift guard.
280    const fn row(self) -> usize {
281        match self {
282            SystemKind::FragmentField => 0,
283            SystemKind::Swarm => 1,
284            SystemKind::ParametricCurve => 2,
285            SystemKind::LSystem => 3,
286            SystemKind::StarPattern => 4,
287            SystemKind::ReactionDiffusion => 5,
288            SystemKind::Attractor => 6,
289            SystemKind::Spectrum => 7,
290            SystemKind::Emitter => 8,
291            SystemKind::ShapeField => 9,
292            SystemKind::WarpMesh => 10,
293            SystemKind::ShapeCollage => 11,
294            SystemKind::AnalyticField => 12,
295            SystemKind::Cellular => 13,
296            SystemKind::Plexus => 14,
297            SystemKind::Waterfall => 15,
298            SystemKind::Voxel => 16,
299        }
300    }
301
302    /// Every [`SystemKind`], in the order the engine builds their scenes. The
303    /// scene factory (`render::scenes::create_all`) and the golden drift guard
304    /// both iterate this rather than keeping lists of their own.
305    ///
306    /// Typed `[SystemKind; VARIANT_COUNT]`, so a roster that has drifted from
307    /// the variant count is a compile error, not a test failure.
308    pub const ALL: [SystemKind; Self::VARIANT_COUNT] = {
309        let mut out = [SystemKind::FragmentField; Self::VARIANT_COUNT];
310        let mut i = 0;
311        while i < Self::VARIANT_COUNT {
312            out[i] = TABLE[i].0;
313            i += 1;
314        }
315        out
316    };
317
318    /// Parse a canonical system name (as written in a preset's `system = "..."`
319    /// field) into its [`SystemKind`], or `None` if unknown. The inverse of
320    /// [`SystemKind::as_str`]; the `shot` CLI reuses the pair so it declares no
321    /// match of its own.
322    pub fn from_name(name: &str) -> Option<Self> {
323        TABLE
324            .iter()
325            .find(|(_, canonical, _, _)| *canonical == name)
326            .map(|(kind, _, _, _)| *kind)
327    }
328
329    /// The canonical name of this system -- the exact string
330    /// [`SystemKind::from_name`] accepts and a preset writes in its `system`
331    /// field.
332    pub fn as_str(self) -> &'static str {
333        TABLE[self.row()].1
334    }
335
336    /// The family this system's library presets are named for: the filename
337    /// prefix, up to the first `_`, of every `presets/*.toml` that drives it
338    /// (`collage` for `shape_collage`).
339    ///
340    /// One `_`-separated segment of [`SystemKind::as_str`], and unique across
341    /// systems — both checked at compile time. The editor's schema association
342    /// and `ritmolux --check`'s `file-name` rule both read it, so a filename the
343    /// checker accepts is one the editor gives that system's schema.
344    pub fn family(self) -> &'static str {
345        TABLE[self.row()].2
346    }
347
348    /// The parameters this system's scene consumes, as the scene declares them
349    /// (the module-private `TABLE`).
350    ///
351    /// The specs carry the default and the doc line as well as the name
352    /// (ADR-0170), which is what lets the generated reference in
353    /// `presets/README.md` be derived from the same declaration the loader
354    /// checks a binding against.
355    pub fn param_specs(self) -> &'static [ParamSpec] {
356        TABLE[self.row()].3
357    }
358
359    /// Just the names, for a caller that wants to print or join them.
360    ///
361    /// Allocates. Use [`declares`] for a membership test, which is what almost
362    /// every caller actually wants.
363    pub fn param_names(self) -> Vec<&'static str> {
364        spec_names(TABLE[self.row()].3)
365    }
366}
367
368/// The parameter names any preset may bind regardless of its system: the five
369/// compositing stages that run around the scene (`bg_*`, `trails`, `kaleido_*`,
370/// `exposure`, `ink_*`/`paper_*`). Gathered from each stage's own declared
371/// vocabulary so there is no third copy to drift.
372///
373/// **These do not all route through the renderer**, whatever the name suggests:
374/// `trails` and `kaleido_*` are offered by the `PostChain` (ADR-0031),
375/// `exposure` by the tonemap (ADR-0046) and `ink_*`/`paper_*` by the terminal ink
376/// pass (ADR-0032); only `bg_*` goes to a pass the renderer drives directly. The
377/// *names* are what this const is about — see `render::ParamRoute` for who
378/// actually owns each.
379pub const GLOBAL_PARAMS: [&[ParamSpec]; 7] = [
380    crate::render::background::PARAMS,
381    crate::render::trails::PARAMS,
382    crate::render::kaleidoscope::PARAMS,
383    crate::render::bloom::PARAMS,
384    // The composite seam's own vocabulary (`occlude`, ADR-0085) — owned by the
385    // chain rather than by any stage in it, which is why it is a seventh entry
386    // and not part of one of the three above.
387    crate::render::post::CHAIN_PARAMS,
388    crate::render::tonemap::PARAMS,
389    crate::render::ink::PARAMS,
390];
391
392/// Whether `name` is a parameter `system` (or the global compositing layer)
393/// actually consumes. An unknown name is a load-time **warning**, not an error:
394/// the preset still loads and applies its good bindings (ADR-0020, NFR 10).
395pub fn is_known_param(system: SystemKind, name: &str) -> bool {
396    declares(system.param_specs(), name) || GLOBAL_PARAMS.iter().any(|stage| declares(stage, name))
397}
398
399/// The [`ParamKind`] `name` is declared with, searching `system`'s own roster
400/// first and then the global compositing stages — the same rosters in the same
401/// order [`is_known_param`] tests, so a name that is known there has a kind
402/// here.
403///
404/// [`ParamKind::Modal`] for a name no roster declares, which is the ADR-0020
405/// warning case: the binding is kept, nothing reads it, and quantizing a value
406/// no scene receives would be a decision about nothing.
407pub fn kind_of_param(system: SystemKind, name: &str) -> ParamKind {
408    kind_of(system.param_specs(), name)
409        .or_else(|| GLOBAL_PARAMS.iter().find_map(|stage| kind_of(stage, name)))
410        .unwrap_or_default()
411}