Skip to main content

rlx_core/preset/schema/
mod.rs

1//! TOML preset schema: which built-in system a preset drives and the
2//! expression bound to each of its named parameters.
3//!
4//! Parsing happens once at load: the raw TOML is deserialized, each parameter
5//! expression is compiled (a malformed one is rejected with a surfaced error),
6//! and the result is an in-memory [`Preset`] whose bindings are ready to
7//! evaluate. A bad preset returns `Err` — it never panics, so the caller can
8//! degrade to the last good preset (ADR-0002 / NFR 10).
9
10use std::collections::BTreeMap;
11use std::fmt;
12use std::path::PathBuf;
13
14use serde::Deserialize;
15
16use super::expr::{self, Expr, ExprError};
17use crate::render::feedback::{Deposit, FeedbackConfig, Warp};
18use crate::render::palette::{NamedPalette, PaletteConfig};
19use crate::render::scenes::ParamKind;
20use crate::render::scenes::lines::star::{DEFAULT_RING_SCALE, MAX_RING_COUNT, Motif, RingSpec};
21use crate::render::scenes::lines::{
22    CurveFamily, GeneratorConfig, MAX_LSYSTEM_DEPTH, SpectrumLayout, hankin,
23};
24use crate::render::scenes::particles::AttractorFamily;
25use crate::render::scenes::particles::ifs::IfsFigure;
26
27// The six concerns this file holds apart. `system` is the roster of built-in
28// systems, `easing` the attack/release pair, `hold` the musical edge a binding
29// re-samples on, `raw` the on-disk tables, `load` the TOML-to-`Preset` path,
30// `error` the failure enum. What stays here is the compiled shape a preset
31// becomes.
32mod easing;
33mod error;
34pub mod export;
35mod hold;
36mod load;
37mod raw;
38mod system;
39
40pub use easing::Easing;
41pub use error::PresetError;
42pub use export::{KeyDesc, KeyKind, Roster, TableDesc};
43pub use hold::HoldEdge;
44pub use system::{GLOBAL_PARAMS, SystemKind, is_known_param, kind_of_param};
45
46use raw::*;
47
48/// Where a preset's second scene joins the composite (ADR-0090): before the
49/// post chain, sharing every stage with the main scene, or between the
50/// kaleidoscope and bloom in its own offscreen (Phase 3).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum LayerJoin {
53    /// The layer draws into the same scene target as the main scene, before the
54    /// chain — one substance, shared trails/fold/bloom. The default.
55    #[default]
56    Under,
57    /// The layer renders into its own offscreen and blends into the chain
58    /// between the kaleidoscope and bloom — crisp geometry, shared glow.
59    Over,
60}
61
62impl LayerJoin {
63    /// Both join points, for the load error's "expected one of" listing and for
64    /// the schema export, which renders this rather than restating it.
65    pub const ALL: [LayerJoin; 2] = [LayerJoin::Under, LayerJoin::Over];
66
67    /// Parse the canonical `join = "..."` value, or `None` if unknown.
68    pub fn from_name(name: &str) -> Option<Self> {
69        Some(match name {
70            "under" => LayerJoin::Under,
71            "over" => LayerJoin::Over,
72            _ => return None,
73        })
74    }
75
76    /// The canonical name — [`from_name`](Self::from_name)'s inverse.
77    pub fn as_str(self) -> &'static str {
78        match self {
79            LayerJoin::Under => "under",
80            LayerJoin::Over => "over",
81        }
82    }
83}
84
85/// How an `over` layer blends into the chain (ADR-0090): fixed at load, like
86/// every structural key, and applied in linear light within the layer's
87/// premultiplied-alpha footprint. Parsed now; consumed by the blend pass
88/// (Plan 0076 Phase 3).
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90pub enum LayerBlend {
91    /// Linear-light addition — the engine's native compositing idiom.
92    Add,
93    /// `1 - (1-a)(1-b)`: bounded brightening. The default — ADR-0090's
94    /// illustrative mode, and the one that cannot blow out.
95    #[default]
96    Screen,
97    /// Darkens where the layer has coverage.
98    Multiply,
99    /// Multiply below mid-grey, screen above.
100    Overlay,
101}
102
103impl LayerBlend {
104    /// Every mode, for the load error's "expected one of" listing.
105    pub const ALL: [LayerBlend; 4] = [
106        LayerBlend::Add,
107        LayerBlend::Screen,
108        LayerBlend::Multiply,
109        LayerBlend::Overlay,
110    ];
111
112    /// Parse the canonical `blend = "..."` value, or `None` if unknown.
113    pub fn from_name(name: &str) -> Option<Self> {
114        Some(match name {
115            "add" => LayerBlend::Add,
116            "screen" => LayerBlend::Screen,
117            "multiply" => LayerBlend::Multiply,
118            "overlay" => LayerBlend::Overlay,
119            _ => return None,
120        })
121    }
122
123    /// The canonical name — [`from_name`](Self::from_name)'s inverse.
124    pub fn as_str(self) -> &'static str {
125        match self {
126            LayerBlend::Add => "add",
127            LayerBlend::Screen => "screen",
128            LayerBlend::Multiply => "multiply",
129            LayerBlend::Overlay => "overlay",
130        }
131    }
132}
133
134/// The optional second scene layer (ADR-0090 / Plan 0076): a full authoring
135/// surface — its own system, params, bindings, `[layer.smoothing]` and
136/// structural tables — joined to the composite at [`LayerJoin`]. The preset's
137/// single `[palette]` serves both layers (one colour language, one baked LUT),
138/// and layer params are **namespaced to the layer**: they reach the layer's
139/// scene only, never the main scene's first-owner-wins routing and never the
140/// compositing stages, which belong to the preset as a whole.
141#[derive(Debug)]
142pub struct Layer {
143    /// The built-in system this layer drives.
144    pub system: SystemKind,
145    /// Where the layer joins the composite.
146    pub join: LayerJoin,
147    /// How an `over` layer blends in (ignored, with a load warning, on an
148    /// `under` join — there is no junction for it to apply at).
149    pub blend: LayerBlend,
150    /// The bindable mix amount at the `over` join (ADR-0090): how much of the
151    /// layer the blend applies, evaluated per frame like any binding so audio
152    /// can surge the second layer. `None` — the default — is full strength.
153    pub mix: Option<Binding>,
154    /// The layer's parameter bindings, name-sorted like the preset's own.
155    pub params: Vec<Binding>,
156    /// The layer's `[layer.per_vertex]` bindings — see
157    /// [`Preset::per_vertex`](Preset::per_vertex).
158    pub per_vertex: Vec<Binding>,
159    /// The layer's declarative structural config (ADR-0007), from its own
160    /// `[layer.curve]` / `[layer.generator]` / `[layer.particles]` /
161    /// `[layer.spectrum]` tables — validated by the same per-system rules as
162    /// the top level.
163    pub config: Option<GeneratorConfig>,
164}
165
166/// A named parameter bound to a compiled expression.
167#[derive(Debug)]
168pub struct Binding {
169    /// The system parameter this drives (e.g. `warp`, `hue`).
170    pub name: String,
171    /// The compiled expression producing its per-frame value.
172    pub expr: Expr,
173    /// This binding's easing constants (ADR-0019 / ADR-0035), read out of the
174    /// preset's `[smoothing]` table **once, here at load**.
175    /// [`Easing::INSTANT`] — the default for an unlisted param — means no
176    /// smoothing. Resolved at parse time rather than looked up per binding per
177    /// frame (Plan 0031 Phase 3); it is a fact about the preset, and the preset
178    /// does not change while it renders.
179    pub tau: Easing,
180    /// The musical edge this binding re-samples on (ADR-0180 rule 2), read out
181    /// of the preset's `[hold]` table **once, here at load**, for `tau`'s
182    /// reason and at `tau`'s boundary.
183    ///
184    /// `None` -- the default for an unlisted param, and what every binding in
185    /// the shipped set carried before holds existed -- means the scene sees
186    /// every frame's value. `Some` means it sees the value taken at the last
187    /// edge, and the render layer holds that value; nothing here does.
188    pub hold: Option<HoldEdge>,
189    /// What the parameter this binding drives is **for** (ADR-0180 rule 2),
190    /// read off its [`ParamSpec`](crate::render::scenes::ParamSpec) here at
191    /// load — `tau`'s boundary, for `tau`'s reason. A
192    /// [`Structural`](ParamKind::Structural) value is rounded once, after the
193    /// hold and after the smoother, before the scene sees it.
194    ///
195    /// Folded rather than searched per frame: which kind a name carries is a
196    /// fact about the *engine*, and the engine does not change while it runs.
197    pub kind: ParamKind,
198}
199
200/// One `[latch]` entry, compiled (ADR-0137): a gate armed on one condition and
201/// fired by the first rising edge of another inside the arming window.
202///
203/// Its **slot is its position in [`Preset::latches`]**, and that is the only
204/// place the mapping exists: the loader resolved the author's name onto a
205/// reserved variable slot while compiling the bindings, so nothing per-frame
206/// looks a latch up by name. The same reasoning that keeps `[smoothing]` off
207/// [`Preset`] as a table — a fact about the preset, resolved once, and the
208/// preset does not change while it renders.
209///
210/// The two expressions are compiled **without** any latch name in scope, so a
211/// latch cannot read a latch. That is not a restriction waiting to be lifted: it
212/// is what makes "evaluate every latch, then the params that read them" a
213/// complete order rather than one with a dependency graph inside it.
214#[derive(Debug)]
215pub struct Latch {
216    /// The author's name for it, which its bindings reference.
217    pub name: String,
218    /// While this holds (`> 0.5`), the latch is armed. Its fall re-arms.
219    pub arm: Expr,
220    /// The rising edge that fires an armed latch.
221    pub fire: Expr,
222    /// How long the fired latch reads `1.0`, in seconds. `0` is one frame.
223    pub hold: f32,
224}
225
226/// A loaded, ready-to-evaluate preset.
227#[derive(Debug)]
228pub struct Preset {
229    /// Human-readable name (defaults to the system name if omitted).
230    pub name: String,
231    /// Which built-in system this preset drives.
232    pub system: SystemKind,
233    /// The absolute path this preset was read from, when it came from a
234    /// directory. `None` for the embedded set, which has no file on disk, and
235    /// for anything compiled straight from a string.
236    ///
237    /// Set by [`crate::preset::load_dir`] rather than here: `from_toml_str` is
238    /// handed source text and has no way to know where it came from, and a
239    /// caller that does know is the one that can say. A consumer that offers to
240    /// edit a preset needs this to be able to distinguish "not editable" from
241    /// "the write failed" (ADR-0184).
242    pub source: Option<PathBuf>,
243    /// Parameter bindings, sorted by name for deterministic iteration.
244    pub params: Vec<Binding>,
245    /// The `[per_vertex]` table's bindings (Plan 0100 Phase 1): the warp mesh's
246    /// per-vertex program, evaluated once **per mesh vertex** per frame with
247    /// `x`/`y`/`rad`/`ang` bound to that vertex's position.
248    ///
249    /// A separate table rather than a naming convention inside `[params]`,
250    /// because the cost is categorically different: one of these is `N`
251    /// evaluations where an ordinary binding is one, and an author has to be able
252    /// to see which of their bindings they are paying `N` for. Empty for every
253    /// system but the warp mesh, and for a warp-mesh preset that accepts the
254    /// identity transform.
255    ///
256    /// Never eased: like a per-element binding, a per-vertex one has no single
257    /// value for the smoother to hold. A `[smoothing]` entry naming one is a
258    /// load warning.
259    pub per_vertex: Vec<Binding>,
260    /// The `[latch]` table's entries (ADR-0137), in slot order — the one part of
261    /// the preset surface whose value depends on frame history.
262    ///
263    /// Empty for a preset declaring no table, which is the overwhelmingly common
264    /// case and costs exactly what it cost before latches existed: the render
265    /// layer's bank advances nothing and every reserved slot stays at its rest
266    /// value of `0.0`.
267    pub latches: Vec<Latch>,
268    /// Declarative structural config for a line scene (ADR-0007), applied once
269    /// at preset load via `Scene::configure`. `None` for the fragment/swarm
270    /// systems and for curve presets that accept the family default.
271    pub config: Option<GeneratorConfig>,
272    // The `[smoothing]` table itself is deliberately **not** kept: it is validated
273    // at load and folded into each binding's `tau` there (Plan 0031 Phase 3), so
274    // there is nothing left for a frame to look up. An entry naming a param this
275    // preset does not bind was inert before and is inert now.
276    /// Optional color palette selection (ADR-0021 / Plan 0020), from a `[palette]`
277    /// table — a built-in `name` or custom `stops`, validated and baked-ready at
278    /// this boundary. `None` means the default `spectrum` (the exact current
279    /// cosine), so a preset without `[palette]` is visually unchanged. The
280    /// renderer bakes it into a LUT and hands it to the active scene via
281    /// `Scene::set_palette` on each preset switch.
282    pub palette: Option<PaletteConfig>,
283    /// The `[feedback]` structural table (ADR-0048): which curated warp the
284    /// accumulation buffers resample their past through, and how this frame's
285    /// light is deposited onto it.
286    ///
287    /// Not an `Option`: the absent table and the all-defaults table mean the same
288    /// thing, and a plain value is what lets the renderer hand it over on **every**
289    /// preset switch — so the outgoing preset's warp can never survive into the
290    /// incoming one. Load-time by `[curve] family`'s reasoning; the strength that
291    /// rides on it is the bindable `fb_warp`.
292    pub feedback: FeedbackConfig,
293    /// Optional **second** palette (ADR-0021 / Plan 0020 Phase 4), from a
294    /// `[palette_b]` table. When present, the renderer bakes an A/B pair and a
295    /// bindable `palette_mix` param crossfades between them per frame. `None`
296    /// means no crossfade (palette A only).
297    pub palette_b: Option<PaletteConfig>,
298    /// The salt this preset's `hash()`/`noise()` calls mix into their argument
299    /// **in the live app** (ADR-0051): folded at load from the `[generator] seed`
300    /// key (Plan 0010 reserved it, Plan 0047 gave it meaning), or drawn once from
301    /// OS entropy where the preset declares `seed = "random"`. `0` when it
302    /// declares nothing — a perfectly good salt, and the one the whole shipped
303    /// library used before any preset asked for another.
304    ///
305    /// A load-time constant. Nothing per-frame recomputes it, and no expression
306    /// can read it except through the two functions it salts.
307    pub salt: u32,
308    /// The salt every **capture** path uses in place of [`salt`](Self::salt):
309    /// the declared number, or `0` for `seed = "random"`.
310    ///
311    /// Equal to `salt` unless the preset opted into per-run variety — the whole
312    /// point of the pair (ADR-0051, following ADR-0045's tier pinning). The live
313    /// app varies and the harness pins, so `shot`, the goldens, `--report` and
314    /// the behavioral gates stay pure functions of their inputs while a preset
315    /// can still be different every time the user starts the app.
316    ///
317    /// It is the *renderer* that chooses between the two, not the loader, and
318    /// deliberately: `default_presets()` feeds both the live C-ABI path and the
319    /// capture gates, so a decision taken at load would be wrong for one of them.
320    pub pinned_salt: u32,
321    /// Parameters whose `clamp()` bounds are **meant** to pin, from an
322    /// `[occupancy] exempt = [...]` table (ADR-0062). Sorted and deduplicated at
323    /// load.
324    ///
325    /// A safety rail exists to bind at peak, and the saturation gate would
326    /// otherwise convict it of the defect it was written to prevent. The
327    /// exemption silences `core/tests/saturation.rs`, and **only** that: the
328    /// binding still appears in `--report`'s `occ` count and `SAT` lines,
329    /// because an exemption is a place to hide and the one mitigation available
330    /// is that it stays visible.
331    ///
332    /// A preset-level table naming params rather than a per-expression
333    /// annotation, deliberately: the grammar stays a pure expression language
334    /// (ADR-0020), and this is metadata *about* a binding rather than part of
335    /// it. Harness-only — nothing per-frame reads it.
336    pub occupancy_exempt: Vec<String>,
337    /// Whether this preset is one of its family's **representatives** — the
338    /// sample the `dev` lane's per-phase test tier renders (ADR-0157).
339    ///
340    /// Absent means `false`. Harness-only, like `occupancy_exempt`: nothing
341    /// per-frame reads it, and it changes nothing about how the preset looks or
342    /// what the close and CI render, which is the whole library either way. It
343    /// is **declared, not derived** — a first-N or hash-rotation rule would
344    /// either never sample a newly landed preset or make the same tree gate
345    /// differently on different commits.
346    ///
347    /// A floor is enforced in `core/tests/preset.rs`: every family carries at
348    /// least two. That catches a sample decayed to nothing; it cannot catch two
349    /// representatives that have stopped representing a family that grew around
350    /// them, which is a curation duty with no gate behind it.
351    pub representative: bool,
352    /// The optional second scene layer (ADR-0090 / Plan 0076), from a `[layer]`
353    /// table. `None` — the overwhelmingly common case — takes exactly the code
354    /// path a preset took before layers existed: no new pass, no new target.
355    pub layer: Option<Layer>,
356    /// Non-fatal problems found while loading — today, bindings naming a
357    /// parameter this system does not consume (ADR-0020). The preset loaded and
358    /// its good bindings apply; these are surfaced so a typo stops failing
359    /// silently. Empty for a clean preset. Load-time only — never read per
360    /// frame.
361    pub warnings: Vec<String>,
362}
363
364#[cfg(test)]
365mod tests;