Skip to main content

rdi_core/
effect_graph.rs

1//! Validated, stateless procedural render recipes.
2
3use crate::DesktopError;
4use std::collections::HashSet;
5use std::sync::Arc;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum ParameterKind {
9    Float,
10    Integer,
11    Boolean,
12}
13
14#[derive(Clone, Debug, PartialEq)]
15pub struct EffectParameter {
16    pub name: String,
17    pub kind: ParameterKind,
18    pub default: f32,
19    pub min: f32,
20    pub max: f32,
21}
22
23impl EffectParameter {
24    pub fn accepts(&self, value: f32) -> bool {
25        value.is_finite()
26            && value >= self.min
27            && value <= self.max
28            && match self.kind {
29                ParameterKind::Float => true,
30                ParameterKind::Integer => value.fract() == 0.0,
31                ParameterKind::Boolean => value == 0.0 || value == 1.0,
32            }
33    }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum DrawTopology {
38    Triangles,
39    TriangleStrip,
40    Lines,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum PassBlend {
45    Over,
46    Add,
47    Replace,
48}
49
50impl Default for PassBlend {
51    fn default() -> Self {
52        Self::Over
53    }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct DrawSpec {
58    pub vertices: u32,
59    pub parameter: Option<usize>,
60    pub multiplier: u32,
61    pub topology: DrawTopology,
62}
63
64impl Default for DrawSpec {
65    fn default() -> Self {
66        Self {
67            vertices: 6,
68            parameter: None,
69            multiplier: 0,
70            topology: DrawTopology::Triangles,
71        }
72    }
73}
74
75impl DrawSpec {
76    pub fn count(&self, values: &[f32]) -> Result<u32, DesktopError> {
77        let additional = match self.parameter {
78            Some(index) => {
79                let value = *values
80                    .get(index)
81                    .ok_or_else(|| invalid("draw parameter is missing"))?;
82                if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value > 65536.0 {
83                    return Err(invalid(
84                        "draw parameter must be a bounded nonnegative integer",
85                    ));
86                }
87                (value as u32)
88                    .checked_mul(self.multiplier)
89                    .ok_or_else(|| invalid("draw overflow"))?
90            }
91            None => 0,
92        };
93        let count = self
94            .vertices
95            .checked_add(additional)
96            .ok_or_else(|| invalid("draw overflow"))?;
97        if count == 0
98            || count > 65536
99            || match self.topology {
100                DrawTopology::Triangles => count % 3 != 0,
101                DrawTopology::TriangleStrip => count < 3,
102                DrawTopology::Lines => count % 2 != 0,
103            }
104        {
105            return Err(invalid("invalid topology or vertex count (limit 65536)"));
106        }
107        Ok(count)
108    }
109}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct EffectTarget {
113    pub width: u32,
114    pub height: u32,
115}
116
117#[derive(Clone, Debug, PartialEq)]
118pub struct EffectPass {
119    pub vertex_bytecode: Arc<[u8]>,
120    pub pixel_bytecode: Arc<[u8]>,
121    pub draw: DrawSpec,
122    pub inputs: Vec<usize>,
123    pub output: Option<usize>,
124    pub blend: PassBlend,
125}
126
127#[derive(Clone, Debug, Default, PartialEq)]
128pub struct EffectExecution {
129    pub parameters: Vec<EffectParameter>,
130    pub targets: Vec<EffectTarget>,
131    pub passes: Vec<EffectPass>,
132    pub body_artwork: bool,
133    pub label_artwork: bool,
134}
135
136fn invalid(message: &str) -> DesktopError {
137    DesktopError::InvalidEffect(message.into())
138}
139
140impl EffectExecution {
141    pub fn defaults(&self) -> [f32; 16] {
142        let mut values = [0.0; 16];
143        for (value, parameter) in values.iter_mut().zip(&self.parameters) {
144            *value = parameter.default;
145        }
146        values
147    }
148
149    pub fn target_bytes(&self) -> u64 {
150        self.targets
151            .iter()
152            .map(|target| target.width as u64 * target.height as u64 * 4)
153            .sum()
154    }
155
156    pub fn validate(&self, values: &[f32]) -> Result<(), DesktopError> {
157        if self.parameters.len() > 16
158            || self.targets.len() > 4
159            || self.passes.is_empty()
160            || self.passes.len() > 8
161        {
162            return Err(invalid(
163                "procedural limits: 16 parameters, 4 targets, 1..8 passes",
164            ));
165        }
166        let mut names = HashSet::new();
167        for (index, parameter) in self.parameters.iter().enumerate() {
168            if parameter.name.is_empty()
169                || !parameter
170                    .name
171                    .bytes()
172                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
173                || !names.insert(&parameter.name)
174                || !parameter.min.is_finite()
175                || !parameter.max.is_finite()
176                || parameter.min > parameter.max
177                || !parameter.accepts(parameter.min)
178                || !parameter.accepts(parameter.max)
179                || !parameter.accepts(parameter.default)
180                || !values
181                    .get(index)
182                    .is_some_and(|value| parameter.accepts(*value))
183            {
184                return Err(invalid("invalid parameter schema or value"));
185            }
186        }
187        if self.targets.iter().any(|target| {
188            target.width == 0 || target.height == 0 || target.width > 8192 || target.height > 8192
189        }) || self.target_bytes() > 64 * 1024 * 1024
190        {
191            return Err(invalid(
192                "intermediate targets exceed dimensions or 64 MiB budget",
193            ));
194        }
195        let mut written = HashSet::new();
196        for (index, pass) in self.passes.iter().enumerate() {
197            if pass.vertex_bytecode.is_empty()
198                || pass.pixel_bytecode.is_empty()
199                || pass.inputs.len() > 4
200                || pass.inputs.iter().any(|input| !written.contains(input))
201            {
202                return Err(invalid(
203                    "empty stages or input target has no preceding producer",
204                ));
205            }
206            if let Some(parameter) = pass.draw.parameter {
207                let schema = self
208                    .parameters
209                    .get(parameter)
210                    .ok_or_else(|| invalid("unknown draw parameter"))?;
211                if schema.kind != ParameterKind::Integer || schema.min < 0.0 {
212                    return Err(invalid(
213                        "draw counts require a nonnegative integer parameter",
214                    ));
215                }
216            }
217            pass.draw.count(values)?;
218            let mut maximum = self.defaults();
219            if let Some(parameter) = pass.draw.parameter {
220                maximum[parameter] = self.parameters[parameter].max;
221            }
222            pass.draw.count(&maximum)?;
223            if let Some(parameter) = pass.draw.parameter {
224                maximum[parameter] = self.parameters[parameter].min;
225                pass.draw.count(&maximum)?;
226                if self.parameters[parameter].min < self.parameters[parameter].max {
227                    maximum[parameter] += 1.0;
228                    pass.draw.count(&maximum)?;
229                }
230            }
231            if let Some(target) = pass.output {
232                if target >= self.targets.len()
233                    || pass.inputs.contains(&target)
234                    || !written.insert(target)
235                {
236                    return Err(invalid(
237                        "target must have one producer and cannot be read while written",
238                    ));
239                }
240            } else if index + 1 != self.passes.len() {
241                return Err(invalid("only the last pass may composite to the scene"));
242            }
243        }
244        if self.passes.last().unwrap().output.is_some() || written.len() != self.targets.len() {
245            return Err(invalid(
246                "graph must end at the scene and produce every declared target",
247            ));
248        }
249        Ok(())
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    fn graph() -> EffectExecution {
257        let pass = EffectPass {
258            vertex_bytecode: Arc::from([1]),
259            pixel_bytecode: Arc::from([1]),
260            draw: DrawSpec::default(),
261            inputs: vec![],
262            output: Some(0),
263            blend: PassBlend::Over,
264        };
265        EffectExecution {
266            targets: vec![EffectTarget {
267                width: 32,
268                height: 32,
269            }],
270            passes: vec![
271                pass.clone(),
272                EffectPass {
273                    inputs: vec![0],
274                    output: None,
275                    ..pass
276                },
277            ],
278            ..Default::default()
279        }
280    }
281    #[test]
282    fn validates_graph_order_and_budgets() {
283        let valid = graph();
284        valid.validate(&[]).unwrap();
285        let mut invalid = valid.clone();
286        invalid.passes[0].inputs = vec![0];
287        assert!(invalid.validate(&[]).is_err());
288        let mut invalid = valid.clone();
289        invalid.passes[1].output = Some(0);
290        assert!(invalid.validate(&[]).is_err());
291        let mut invalid = valid.clone();
292        invalid.targets[0].width = 8193;
293        assert!(invalid.validate(&[]).is_err());
294        let mut invalid = valid;
295        invalid.passes[0].draw.vertices = 65538;
296        assert!(invalid.validate(&[]).is_err());
297    }
298
299    #[test]
300    fn validates_parameters_and_draw_ranges() {
301        let mut recipe = graph();
302        recipe.parameters = vec![EffectParameter {
303            name: "count".into(),
304            kind: ParameterKind::Integer,
305            default: 2.0,
306            min: 1.0,
307            max: 8.0,
308        }];
309        recipe.passes[0].draw = DrawSpec {
310            vertices: 0,
311            parameter: Some(0),
312            multiplier: 6,
313            ..Default::default()
314        };
315        recipe.validate(&[2.0]).unwrap();
316        for value in [0.0, 9.0, 2.5, f32::NAN, f32::INFINITY] {
317            assert!(recipe.validate(&[value]).is_err());
318        }
319        let mut invalid = recipe.clone();
320        invalid.parameters.push(invalid.parameters[0].clone());
321        assert!(invalid.validate(&[2.0, 2.0]).is_err());
322        let mut invalid = recipe.clone();
323        invalid.parameters[0].max = 8.5;
324        assert!(invalid.validate(&[2.0]).is_err());
325        let mut invalid = recipe.clone();
326        invalid.passes[0].draw.multiplier = 65536;
327        assert!(invalid.validate(&[2.0]).is_err());
328        let mut invalid = recipe.clone();
329        invalid.parameters[0].kind = ParameterKind::Float;
330        assert!(invalid.validate(&[2.0]).is_err());
331        let mut invalid = recipe;
332        invalid.passes[0].draw.multiplier = 1;
333        assert!(invalid.validate(&[6.0]).is_err());
334        let mut invalid = graph();
335        invalid.targets[0] = EffectTarget {
336            width: 8192,
337            height: 8192,
338        };
339        assert!(invalid.validate(&[]).is_err());
340        let mut invalid = graph();
341        invalid.passes[1].inputs = vec![1];
342        assert!(invalid.validate(&[]).is_err());
343    }
344}