Skip to main content

alpha2_render/
effect.rs

1use alpha2_gpu::CapabilityTier;
2use alpha2_time::MediaTime;
3use std::borrow::{Borrow, Cow};
4use std::collections::BTreeMap;
5use std::hash::{Hash, Hasher};
6use std::sync::Arc;
7
8/// Representation an effect resource binding expects from its registered data.
9#[derive(Copy, Clone, Debug, Eq, PartialEq)]
10pub enum EffectResourceKind {
11    /// A cubic, filterable RGB lookup table, normally authored as a `.cube` file.
12    Lut3d,
13}
14
15/// Stable identity of one registered effect version.
16///
17/// Cloning one never allocates: the engine's own passes name a static
18/// string, and an identity made from a `String` shares it. Two identities are
19/// equal when their text and version are, however each holds its text.
20#[derive(Clone, Debug)]
21pub struct EffectId {
22    id: IdText,
23    version: u32,
24}
25
26#[derive(Clone, Debug)]
27enum IdText {
28    Static(&'static str),
29    Shared(Arc<str>),
30}
31
32impl EffectId {
33    /// Creates an exact runtime-effect identity.
34    #[must_use]
35    pub fn new(id: String, version: u32) -> Self {
36        Self {
37            id: IdText::Shared(id.into()),
38            version,
39        }
40    }
41
42    /// An identity whose text is a constant, as the renderer's and the
43    /// engine's own passes are named; making one allocates nothing.
44    #[must_use]
45    pub const fn from_static(id: &'static str, version: u32) -> Self {
46        Self {
47            id: IdText::Static(id),
48            version,
49        }
50    }
51
52    /// Returns the author-defined identifier.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        match &self.id {
56            IdText::Static(id) => id,
57            IdText::Shared(id) => id,
58        }
59    }
60
61    /// Returns the schema version.
62    #[must_use]
63    pub const fn version(&self) -> u32 {
64        self.version
65    }
66}
67
68impl PartialEq for EffectId {
69    fn eq(&self, other: &Self) -> bool {
70        self.version == other.version && self.as_str() == other.as_str()
71    }
72}
73
74impl Eq for EffectId {}
75
76impl Hash for EffectId {
77    fn hash<H: Hasher>(&self, state: &mut H) {
78        // The same hash as the key it borrows as, so a lookup by key finds it.
79        (self as &dyn EffectKey).hash(state);
80    }
81}
82
83/// An effect identity as text and version, so a registry keyed by
84/// [`EffectId`] can be asked about an `(&str, u32)` without making one.
85pub(crate) trait EffectKey {
86    fn key(&self) -> (&str, u32);
87}
88
89impl EffectKey for EffectId {
90    fn key(&self) -> (&str, u32) {
91        (self.as_str(), self.version)
92    }
93}
94
95impl EffectKey for (&str, u32) {
96    fn key(&self) -> (&str, u32) {
97        *self
98    }
99}
100
101impl<'a> Borrow<dyn EffectKey + 'a> for EffectId {
102    fn borrow(&self) -> &(dyn EffectKey + 'a) {
103        self
104    }
105}
106
107impl Hash for dyn EffectKey + '_ {
108    fn hash<H: Hasher>(&self, state: &mut H) {
109        let (id, version) = self.key();
110        id.hash(state);
111        version.hash(state);
112    }
113}
114
115impl PartialEq for dyn EffectKey + '_ {
116    fn eq(&self, other: &Self) -> bool {
117        self.key() == other.key()
118    }
119}
120
121impl Eq for dyn EffectKey + '_ {}
122
123/// Runtime shader category.
124#[derive(Copy, Clone, Debug, Eq, PartialEq)]
125pub enum EffectKind {
126    /// An image effect: one source image (`src`, `sample_src`), plus the
127    /// named inputs its definition declares.
128    Effect,
129    /// Two-input timeline transition.
130    Transition,
131    /// Zero-input picture source: its body defines `fn generate(uv: vec2f) ->
132    /// vec4f`, returning a premultiplied, sRGB-encoded color.
133    Generator,
134}
135
136/// Host-side parameter representation.
137#[derive(Copy, Clone, Debug, Eq, PartialEq)]
138pub enum ParamType {
139    /// One 32-bit floating-point value.
140    Float,
141    /// Two 32-bit floating-point values.
142    Vec2,
143    /// Four independent 32-bit floating-point values, such as per-channel
144    /// thresholds. They keyframe component by component.
145    Vec4,
146    /// A straight (not premultiplied), sRGB-encoded RGBA color in `0..=1`.
147    /// Its unit is always [`ParamUnit::Color`]; a four-component value that
148    /// is not a color is a [`ParamType::Vec4`].
149    Color,
150    /// Integer index into an author-defined option list.
151    Enum,
152    /// Boolean encoded as a host-shareable `u32` in WGSL.
153    Bool,
154}
155
156/// Semantic unit exposed to authoring hosts.
157#[derive(Copy, Clone, Debug, Eq, PartialEq)]
158pub enum ParamUnit {
159    /// Resolution-independent fraction.
160    Normalized,
161    /// Angle measured in degrees.
162    Degrees,
163    /// Straight, sRGB-encoded RGBA color.
164    Color,
165    /// Index into named choices.
166    Enum,
167    /// Boolean toggle.
168    Bool,
169}
170
171/// Typed default parameter value.
172#[derive(Clone, Debug, PartialEq)]
173pub enum ParamValue {
174    /// Scalar value.
175    Float(f32),
176    /// Two-dimensional value.
177    Vec2([f32; 2]),
178    /// Four-dimensional value.
179    Vec4([f32; 4]),
180    /// RGBA color.
181    Color([f32; 4]),
182    /// Option index.
183    Enum(u32),
184    /// Boolean value.
185    Bool(bool),
186}
187
188/// Named values supplied to one runtime effect invocation.
189///
190/// A name is borrowed for `'a` or owned: the renderer's own passes name
191/// static strings and a project's names live as long as the frame drawn from
192/// it, so building a frame's parameters allocates only the list that holds
193/// them. The list is kept sorted by name, one value per name.
194#[derive(Clone, Debug, Default, PartialEq)]
195pub struct EffectParams<'a> {
196    values: Vec<(Cow<'a, str>, ParamValue)>,
197}
198
199/// Named resource identities supplied to one effect invocation.
200#[derive(Clone, Debug, Default, Eq, PartialEq)]
201pub struct EffectResources {
202    values: BTreeMap<String, String>,
203}
204
205impl EffectResources {
206    /// Creates an empty resource set.
207    #[must_use]
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Associates one definition slot with a registered resource identity.
213    #[must_use]
214    pub fn with(mut self, name: impl Into<String>, id: impl Into<String>) -> Self {
215        self.values.insert(name.into(), id.into());
216        self
217    }
218
219    pub(crate) fn get(&self, name: &str) -> Option<&str> {
220        self.values.get(name).map(String::as_str)
221    }
222
223    pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
224        self.values
225            .iter()
226            .map(|(name, id)| (name.as_str(), id.as_str()))
227    }
228}
229
230impl<'a> EffectParams<'a> {
231    /// Creates an empty set that falls back to every schema default.
232    #[must_use]
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /// Creates an empty set with room for `capacity` named values.
238    #[must_use]
239    pub fn with_capacity(capacity: usize) -> Self {
240        Self {
241            values: Vec::with_capacity(capacity),
242        }
243    }
244
245    /// Adds or replaces one named value.
246    #[must_use]
247    pub fn with(mut self, name: impl Into<Cow<'a, str>>, value: ParamValue) -> Self {
248        self.insert(name, value);
249        self
250    }
251
252    /// Adds or replaces one named value in place.
253    pub fn insert(&mut self, name: impl Into<Cow<'a, str>>, value: ParamValue) {
254        let name = name.into();
255        match self.position(&name) {
256            Ok(index) => self.values[index].1 = value,
257            Err(index) => self.values.insert(index, (name, value)),
258        }
259    }
260
261    fn position(&self, name: &str) -> Result<usize, usize> {
262        self.values
263            .binary_search_by(|(held, _)| held.as_ref().cmp(name))
264    }
265
266    pub(crate) fn get(&self, name: &str) -> Option<&ParamValue> {
267        self.position(name).ok().map(|index| &self.values[index].1)
268    }
269
270    pub(crate) fn names(&self) -> impl Iterator<Item = &str> {
271        self.values.iter().map(|(name, _)| name.as_ref())
272    }
273
274    /// Every value by name, in name order.
275    pub(crate) fn sorted_values(&self) -> impl Iterator<Item = (&str, &ParamValue)> {
276        self.values
277            .iter()
278            .map(|(name, value)| (name.as_ref(), value))
279    }
280}
281
282/// Per-invocation values supplied by the timeline runner.
283#[derive(Copy, Clone, Debug, PartialEq)]
284pub struct EffectRuntime {
285    /// The time the pass's settings are sampled at, exact in ticks: the
286    /// timeline time for a timeline transition, and for a composition step
287    /// the composition time it is sampled at (the composition's time plus
288    /// the step's offset). The engine passes zero to an effect that cannot
289    /// read it ([`crate::Renderer::effect_reads_time`]) and to a pass whose
290    /// picture does not change with time. A shader reads it as seconds with
291    /// `time()`, an `f32` that cannot tell frames apart on a long timeline
292    /// (1/32 s apart from about 73 hours on), and exactly with
293    /// `time_ticks()`; the cache key holds the ticks.
294    pub time: MediaTime,
295    /// Transition progress in the inclusive unit interval.
296    pub progress: f32,
297    /// Output width divided by output height.
298    pub ratio: f32,
299}
300
301impl Default for EffectRuntime {
302    fn default() -> Self {
303        Self {
304            time: MediaTime::ZERO,
305            progress: 0.0,
306            ratio: 1.0,
307        }
308    }
309}
310
311/// One runtime effect parameter.
312#[derive(Clone, Debug, PartialEq)]
313pub struct ParamSpec {
314    /// WGSL-safe field name.
315    pub name: String,
316    /// Storage and authoring type.
317    pub param_type: ParamType,
318    /// Value used when a project omits this parameter.
319    pub default: ParamValue,
320    /// Optional inclusive scalar minimum.
321    pub min: Option<f32>,
322    /// Optional inclusive scalar maximum.
323    pub max: Option<f32>,
324    /// Optional positive authoring increment.
325    pub step: Option<f32>,
326    /// UI and serialization unit.
327    pub unit: ParamUnit,
328    /// Named values for [`ParamType::Enum`].
329    pub enum_values: Vec<String>,
330}
331
332/// One named external resource required by an effect definition.
333#[derive(Clone, Debug, Eq, PartialEq)]
334pub struct ResourceSpec {
335    /// WGSL-safe slot name used to generate a `sample_<name>` helper.
336    pub name: String,
337    /// Required registered resource representation.
338    pub kind: EffectResourceKind,
339}
340
341/// One image an effect samples besides its source, by the name its shader
342/// reads it under: `sample_<name>(uv)` and `size_<name>()`.
343#[derive(Clone, Debug, Eq, PartialEq)]
344pub struct InputSpec {
345    /// WGSL-safe name, unique among the definition's parameters, resources,
346    /// and inputs.
347    pub name: String,
348}
349
350/// Data-only runtime effect, transition, or generator definition.
351///
352/// The renderer's quality controls reach every effect at run time, not
353/// through its parameters: the shader reads its filter-sample budget from
354/// the prelude's `max_filter_samples()` and scales its own cost by it, as
355/// the built-in blur does.
356#[derive(Clone, Debug, PartialEq)]
357pub struct EffectDef {
358    /// Stable author-defined identifier.
359    pub id: String,
360    /// Schema and shader version.
361    pub version: u32,
362    /// Effect or transition contract.
363    pub kind: EffectKind,
364    /// Images the effect samples besides its source, at most
365    /// [`crate::MAX_EFFECT_INPUTS`]; only an [`EffectKind::Effect`] declares
366    /// them.
367    pub inputs: Vec<InputSpec>,
368    /// Ordered parameter schema.
369    pub params: Vec<ParamSpec>,
370    /// Ordered external resource schema.
371    pub resources: Vec<ResourceSpec>,
372    /// Author function implementing the selected contract.
373    pub wgsl_body: String,
374    /// Minimum required GPU capability tier.
375    pub tier_min: CapabilityTier,
376    /// SPDX expression or attribution text.
377    pub license: Option<String>,
378}
379
380/// Automatically computed uniform member placement.
381#[derive(Clone, Debug, Eq, PartialEq)]
382pub struct UniformField {
383    /// Parameter name.
384    pub name: String,
385    /// Byte offset from the uniform buffer start.
386    pub offset: u32,
387    /// Bytes reserved for the member.
388    pub size: u32,
389}
390
391/// Host layout generated from a parameter schema.
392#[derive(Clone, Debug, Eq, PartialEq)]
393pub struct UniformLayout {
394    /// Members in schema order.
395    pub fields: Vec<UniformField>,
396    /// Total buffer size rounded to WGSL uniform alignment.
397    pub size: u32,
398}
399
400/// Shader compiler message severity.
401#[derive(Copy, Clone, Debug, Eq, PartialEq)]
402pub enum DiagnosticSeverity {
403    /// Registration cannot continue.
404    Error,
405    /// Shader is valid but deserves attention.
406    Warning,
407    /// Informational compiler message.
408    Info,
409}
410
411/// One schema or WGSL diagnostic.
412#[derive(Clone, Debug, Eq, PartialEq)]
413pub struct ShaderDiagnostic {
414    /// Severity category.
415    pub severity: DiagnosticSeverity,
416    /// Human-readable description.
417    pub message: String,
418    /// Optional one-based generated WGSL line.
419    pub line: Option<u32>,
420    /// Optional one-based generated WGSL byte column.
421    pub column: Option<u32>,
422}
423
424/// Diagnostics returned when runtime registration fails.
425#[derive(Clone, Debug, Eq, PartialEq)]
426pub struct ShaderDiagnostics {
427    /// All collected messages.
428    pub messages: Vec<ShaderDiagnostic>,
429}
430
431impl ShaderDiagnostics {
432    pub(crate) fn error(message: impl Into<String>) -> Self {
433        Self {
434            messages: vec![ShaderDiagnostic {
435                severity: DiagnosticSeverity::Error,
436                message: message.into(),
437                line: None,
438                column: None,
439            }],
440        }
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use std::collections::{HashMap, HashSet};
447
448    use super::{EffectId, EffectKey, EffectParams, ParamValue};
449
450    #[test]
451    fn an_effect_identity_is_its_text_and_version_however_it_holds_the_text() {
452        let shared = EffectId::new("alpha2.effect.vignette".to_owned(), 1);
453        let constant = EffectId::from_static("alpha2.effect.vignette", 1);
454        assert_eq!(shared, constant);
455        assert_ne!(constant, EffectId::from_static("alpha2.effect.vignette", 2));
456        assert_ne!(constant, EffectId::from_static("alpha2.effect.sharpen", 1));
457        // Equal identities hash alike, so a map keyed by one finds the other or the bare key.
458        let map = HashMap::from([(shared.clone(), "vignette")]);
459        assert_eq!(map.get(&constant), Some(&"vignette"));
460        let key: &dyn EffectKey = &("alpha2.effect.vignette", 1);
461        assert_eq!(map.get_key_value(key).map(|(id, _)| id), Some(&shared));
462        let wrong: &dyn EffectKey = &("alpha2.effect.vignette", 2);
463        assert!(!map.contains_key(wrong));
464        assert_eq!(HashSet::from([shared, constant]).len(), 1);
465    }
466
467    #[test]
468    fn parameters_keep_one_value_per_name_in_name_order() {
469        let project_name = String::from("b_owned");
470        let params = EffectParams::with_capacity(4)
471            .with("c", ParamValue::Float(3.0))
472            .with(project_name.as_str(), ParamValue::Float(2.0))
473            .with(String::from("a"), ParamValue::Float(1.0))
474            .with("c", ParamValue::Float(4.0));
475        assert_eq!(
476            params.sorted_values().collect::<Vec<_>>(),
477            [
478                ("a", &ParamValue::Float(1.0)),
479                ("b_owned", &ParamValue::Float(2.0)),
480                ("c", &ParamValue::Float(4.0)),
481            ]
482        );
483        assert_eq!(params.get("c"), Some(&ParamValue::Float(4.0)));
484        assert_eq!(params.get("d"), None);
485        assert_eq!(params.names().collect::<Vec<_>>(), ["a", "b_owned", "c"]);
486        // However the names were given, equal values in any order are equal parameters.
487        let again = EffectParams::new()
488            .with(String::from("c"), ParamValue::Float(4.0))
489            .with("a", ParamValue::Float(1.0))
490            .with(String::from("b_owned"), ParamValue::Float(2.0));
491        assert_eq!(params, again);
492    }
493}