1use 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(¶meter.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}