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