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