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}
46
47/// **The** roster of built-in systems: every variant, its canonical name, and
48/// the parameter names its scene consumes, in the order the engine builds their
49/// scenes.
50///
51/// The single place all three lists live. [`SystemKind::ALL`],
52/// [`SystemKind::from_name`], [`SystemKind::as_str`] and
53/// [`SystemKind::param_names`] all read this, so they cannot disagree with each
54/// other; what keeps *this* honest is [`SystemKind::row`], the one exhaustive
55/// match over the enum, which fails the build when a variant has no entry.
56///
57/// The param lists themselves live beside each scene's own `set_param` match
58/// (`declared_params_match_set_param` in `core/tests/preset.rs` guards that
59/// pair); this is where they are gathered for the loader's typo check
60/// (ADR-0020). They do **not** include the global compositing params, which any
61/// preset may bind whatever its system -- [`is_known_param`] unions those in.
62const TABLE: [(SystemKind, &str, &[ParamSpec]); SystemKind::VARIANT_COUNT] = {
63    use crate::render::scenes;
64    [
65        (
66            SystemKind::FragmentField,
67            "fragment_field",
68            scenes::fragment_field::PARAMS,
69        ),
70        (SystemKind::Swarm, "swarm", scenes::swarm::PARAMS),
71        (
72            SystemKind::ParametricCurve,
73            "parametric_curve",
74            scenes::lines::parametric::PARAMS,
75        ),
76        (
77            SystemKind::LSystem,
78            "lsystem",
79            scenes::lines::lsystem::PARAMS,
80        ),
81        (
82            SystemKind::StarPattern,
83            "star_pattern",
84            scenes::lines::star::PARAMS,
85        ),
86        (
87            SystemKind::ReactionDiffusion,
88            "reaction_diffusion",
89            scenes::reaction_diffusion::PARAMS,
90        ),
91        (
92            SystemKind::Attractor,
93            "attractor",
94            scenes::particles::PARAMS,
95        ),
96        (
97            SystemKind::Spectrum,
98            "spectrum",
99            scenes::lines::spectrum::PARAMS,
100        ),
101        (SystemKind::Emitter, "emitter", scenes::emitter::PARAMS),
102        (
103            SystemKind::ShapeField,
104            "shape_field",
105            scenes::shape_field::PARAMS,
106        ),
107        (SystemKind::WarpMesh, "warp_mesh", scenes::warp_mesh::PARAMS),
108        (
109            SystemKind::ShapeCollage,
110            "shape_collage",
111            scenes::shape_collage::PARAMS,
112        ),
113    ]
114};
115
116/// Every [`TABLE`] row sits at the index its own variant's [`SystemKind::row`]
117/// names. Checked at compile time, because the two are written by hand and a
118/// mismatch would silently give one system another's name and params.
119const _: () = {
120    let mut i = 0;
121    while i < SystemKind::VARIANT_COUNT {
122        assert!(
123            TABLE[i].0.row() == i,
124            "TABLE row order must match SystemKind::row"
125        );
126        i += 1;
127    }
128};
129
130impl SystemKind {
131    /// How many variants [`SystemKind`] has. Kept honest by `row`: a new
132    /// variant fails the build there until it is rostered, and `TABLE` is
133    /// typed off this count, so bumping the count without adding a row does not
134    /// compile either. Both are module-private, so this names them rather than
135    /// linking them.
136    pub const VARIANT_COUNT: usize = 12;
137
138    /// This variant's index into [`TABLE`].
139    ///
140    /// **The one exhaustive match over the enum, and the reason the roster
141    /// cannot go stale**: a new variant makes this non-exhaustive and fails the
142    /// build, which in turn forces a [`TABLE`] row, a scene into the exhaustive
143    /// factory in `render::scenes`, and a fixture into the golden drift guard.
144    const fn row(self) -> usize {
145        match self {
146            SystemKind::FragmentField => 0,
147            SystemKind::Swarm => 1,
148            SystemKind::ParametricCurve => 2,
149            SystemKind::LSystem => 3,
150            SystemKind::StarPattern => 4,
151            SystemKind::ReactionDiffusion => 5,
152            SystemKind::Attractor => 6,
153            SystemKind::Spectrum => 7,
154            SystemKind::Emitter => 8,
155            SystemKind::ShapeField => 9,
156            SystemKind::WarpMesh => 10,
157            SystemKind::ShapeCollage => 11,
158        }
159    }
160
161    /// Every [`SystemKind`], in the order the engine builds their scenes. The
162    /// scene factory (`render::scenes::create_all`) and the golden drift guard
163    /// both iterate this rather than keeping lists of their own.
164    ///
165    /// Typed `[SystemKind; VARIANT_COUNT]`, so a roster that has drifted from
166    /// the variant count is a compile error, not a test failure.
167    pub const ALL: [SystemKind; Self::VARIANT_COUNT] = {
168        let mut out = [SystemKind::FragmentField; Self::VARIANT_COUNT];
169        let mut i = 0;
170        while i < Self::VARIANT_COUNT {
171            out[i] = TABLE[i].0;
172            i += 1;
173        }
174        out
175    };
176
177    /// Parse a canonical system name (as written in a preset's `system = "..."`
178    /// field) into its [`SystemKind`], or `None` if unknown. The inverse of
179    /// [`SystemKind::as_str`]; the `shot` CLI reuses the pair so it declares no
180    /// match of its own.
181    pub fn from_name(name: &str) -> Option<Self> {
182        TABLE
183            .iter()
184            .find(|(_, canonical, _)| *canonical == name)
185            .map(|(kind, _, _)| *kind)
186    }
187
188    /// The canonical name of this system -- the exact string
189    /// [`SystemKind::from_name`] accepts and a preset writes in its `system`
190    /// field.
191    pub fn as_str(self) -> &'static str {
192        TABLE[self.row()].1
193    }
194
195    /// The parameters this system's scene consumes, as the scene declares them
196    /// (the module-private `TABLE`).
197    ///
198    /// The specs carry the default and the doc line as well as the name
199    /// (ADR-0170), which is what lets the generated reference in
200    /// `presets/README.md` be derived from the same declaration the loader
201    /// checks a binding against.
202    pub fn param_specs(self) -> &'static [ParamSpec] {
203        TABLE[self.row()].2
204    }
205
206    /// Just the names, for a caller that wants to print or join them.
207    ///
208    /// Allocates. Use [`declares`] for a membership test, which is what almost
209    /// every caller actually wants.
210    pub fn param_names(self) -> Vec<&'static str> {
211        spec_names(TABLE[self.row()].2)
212    }
213}
214
215/// The parameter names any preset may bind regardless of its system: the five
216/// compositing stages that run around the scene (`bg_*`, `trails`, `kaleido_*`,
217/// `exposure`, `ink_*`/`paper_*`). Gathered from each stage's own declared
218/// vocabulary so there is no third copy to drift.
219///
220/// **These do not all route through the renderer**, whatever the name suggests:
221/// `trails` and `kaleido_*` are offered by the `PostChain` (ADR-0031),
222/// `exposure` by the tonemap (ADR-0046) and `ink_*`/`paper_*` by the terminal ink
223/// pass (ADR-0032); only `bg_*` goes to a pass the renderer drives directly. The
224/// *names* are what this const is about — see `render::ParamRoute` for who
225/// actually owns each.
226pub const GLOBAL_PARAMS: [&[ParamSpec]; 7] = [
227    crate::render::background::PARAMS,
228    crate::render::trails::PARAMS,
229    crate::render::kaleidoscope::PARAMS,
230    crate::render::bloom::PARAMS,
231    // The composite seam's own vocabulary (`occlude`, ADR-0085) — owned by the
232    // chain rather than by any stage in it, which is why it is a seventh entry
233    // and not part of one of the three above.
234    crate::render::post::CHAIN_PARAMS,
235    crate::render::tonemap::PARAMS,
236    crate::render::ink::PARAMS,
237];
238
239/// Whether `name` is a parameter `system` (or the global compositing layer)
240/// actually consumes. An unknown name is a load-time **warning**, not an error:
241/// the preset still loads and applies its good bindings (ADR-0020, NFR 10).
242pub fn is_known_param(system: SystemKind, name: &str) -> bool {
243    declares(system.param_specs(), name) || GLOBAL_PARAMS.iter().any(|stage| declares(stage, name))
244}
245
246/// The [`ParamKind`] `name` is declared with, searching `system`'s own roster
247/// first and then the global compositing stages — the same rosters in the same
248/// order [`is_known_param`] tests, so a name that is known there has a kind
249/// here.
250///
251/// [`ParamKind::Modal`] for a name no roster declares, which is the ADR-0020
252/// warning case: the binding is kept, nothing reads it, and quantizing a value
253/// no scene receives would be a decision about nothing.
254pub fn kind_of_param(system: SystemKind, name: &str) -> ParamKind {
255    kind_of(system.param_specs(), name)
256        .or_else(|| GLOBAL_PARAMS.iter().find_map(|stage| kind_of(stage, name)))
257        .unwrap_or_default()
258}