1use crate::{Duration, FrameRate, Node, SceneContext, TimeOffset, ValidationError};
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct MotionBlur {
28 pub shutter_angle: f64,
30 pub samples: u32,
32}
33
34impl Default for MotionBlur {
35 fn default() -> Self {
36 Self {
37 shutter_angle: 180.0,
38 samples: 8,
39 }
40 }
41}
42
43impl MotionBlur {
44 pub fn validate(self) -> Result<(), ValidationError> {
46 if !self.shutter_angle.is_finite()
47 || !(0.0..=360.0).contains(&self.shutter_angle)
48 || !(1..=64).contains(&self.samples)
49 {
50 Err(invalid(
51 "configuration",
52 "Require finite shutter angle in 0..=360 and 1..=64 samples",
53 ))
54 } else {
55 Ok(())
56 }
57 }
58
59 pub fn shutter(self, rate: FrameRate) -> Result<Duration, ValidationError> {
61 self.validate()?;
62 let bits = self.shutter_angle.to_bits();
63 let exponent = ((bits >> 52) & 0x7ff) as u32;
64 let mantissa = (bits & ((1u64 << 52) - 1)) | if exponent == 0 { 0 } else { 1u64 << 52 };
65 let numerator = i128::from(crate::TICKS_PER_SECOND)
66 * i128::from(rate.denominator())
67 * i128::from(mantissa);
68 let shift = if exponent == 0 { 1074 } else { 1075 - exponent };
69 let Some(denominator) = 1i128
70 .checked_shl(shift)
71 .and_then(|scale| scale.checked_mul(i128::from(rate.numerator()) * 360))
72 .filter(|value| *value > 0)
73 else {
74 return Ok(Duration::ZERO);
75 };
76 let ticks = (numerator + denominator / 2) / denominator;
77 Ok(Duration::from_ticks(
78 i64::try_from(ticks).map_err(|_| invalid("shutter", "Exposure exceeds signed ticks"))?
79 as u64,
80 ))
81 }
82}
83
84fn invalid(field: &str, message: &str) -> ValidationError {
85 ValidationError::invalid("motion_blur", field, message)
86}
87
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub enum ShutterCurve {
91 #[default]
93 Box,
94 Triangle,
96 Cosine,
98}
99
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
102pub struct ShutterSampling {
103 pub jitter_seed: Option<u64>,
105 pub adaptive: bool,
107 pub curve: ShutterCurve,
109}
110
111impl ShutterSampling {
112 pub fn capture(
115 self,
116 blur: MotionBlur,
117 context: &SceneContext<'_>,
118 output: crate::PixelSize,
119 mut build: impl FnMut(&SceneContext<'_>) -> Node,
120 ) -> Result<Node, ValidationError> {
121 let shutter = blur.shutter(context.frame_rate())?;
122 output.mapping(context.size())?;
123 if context.render_quality() == crate::RenderQuality::Draft {
124 return Ok(build(context));
125 }
126 let count = if self.adaptive && shutter > Duration::ZERO {
127 let half = TimeOffset::from_ticks((shutter.ticks() / 2) as i64);
128 let left = context.resampled(-half)?;
129 let right = context.resampled(half)?;
130 let drawing = crate::DrawContext::default().with_assets(context.assets());
131 let first = build(&left).compile_with_context(
132 context.size(),
133 output,
134 crate::ValidationLimits::default(),
135 &drawing,
136 )?;
137 let last = build(&right).compile_with_context(
138 context.size(),
139 output,
140 crate::ValidationLimits::default(),
141 &drawing,
142 )?;
143 displacement_samples(first.display_list(), last.display_list())
144 } else {
145 blur.samples
146 };
147 let mut offsets = sample_offsets(shutter, count)?;
148 if let Some(seed) = self.jitter_seed {
149 let frame = context.time().ticks() as u64;
150 for (index, offset) in offsets.iter_mut().enumerate() {
151 let random = shutter_random(seed ^ frame.rotate_left(17) ^ index as u64);
152 let numerator = (index as f64 + random) / f64::from(count) - 0.5;
153 *offset =
154 TimeOffset::from_ticks((numerator * shutter.ticks() as f64).round() as i64);
155 }
156 }
157 let weights = (0..count)
158 .map(|index| {
159 let phase = (f64::from(index) + 0.5) / f64::from(count);
160 match self.curve {
161 ShutterCurve::Box => 1.0,
162 ShutterCurve::Triangle => 1.0 - (phase * 2.0 - 1.0).abs(),
163 ShutterCurve::Cosine => 0.5 - 0.5 * libm::cos(std::f64::consts::TAU * phase),
164 }
165 })
166 .collect::<Vec<_>>();
167 let total: f64 = weights.iter().sum();
168 let mut nodes = Vec::with_capacity(count as usize);
169 for (offset, weight) in offsets.into_iter().zip(weights) {
170 let node = build(&context.resampled(offset)?);
171 if node.contains_shutter(0) {
172 return Err(invalid(
173 "scope",
174 "Do not combine full-scene and subtree shutter captures",
175 ));
176 }
177 nodes.push((node, weight / total));
178 }
179 Ok(Node::weighted_sum(nodes)?.shutter_sampled())
180 }
181}
182
183pub(crate) fn shutter_random(mut value: u64) -> f64 {
184 value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
185 value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb);
186 value ^= value >> 31;
187 (value >> 11) as f64 / (1u64 << 53) as f64
188}
189
190fn displacement_samples(first: &crate::DisplayList, last: &crate::DisplayList) -> u32 {
191 use crate::DrawCommand;
192 if first.commands.len() != last.commands.len() {
193 return 64;
194 }
195 let mut maximum = 0.0f64;
196 for (first, last) in first.commands.iter().zip(&last.commands) {
197 let points = |command: &DrawCommand| -> Option<Vec<crate::Point>> {
198 match command {
199 DrawCommand::FillRect {
200 bounds, transform, ..
201 } => Some(
202 [
203 crate::Point::new(bounds.left, bounds.top),
204 crate::Point::new(bounds.right, bounds.top),
205 crate::Point::new(bounds.right, bounds.bottom),
206 crate::Point::new(bounds.left, bounds.bottom),
207 ]
208 .map(|point| transform.apply(point))
209 .to_vec(),
210 ),
211 DrawCommand::Fill {
212 path, transform, ..
213 } => Some(
214 path.verbs()
215 .iter()
216 .flat_map(|verb| verb.points().into_iter().flatten())
217 .map(|point| transform.apply(point))
218 .collect(),
219 ),
220 _ => None,
221 }
222 };
223 match (points(first), points(last)) {
224 (Some(first), Some(last)) if first.len() == last.len() => {
225 for (first, last) in first.into_iter().zip(last) {
226 maximum = maximum.max(libm::hypot(first.x - last.x, first.y - last.y));
227 }
228 }
229 (None, None) if first == last => {}
230 _ => return 64,
231 }
232 }
233 (maximum.ceil() as u32).clamp(8, 64)
234}
235
236pub fn sample_offsets(shutter: Duration, samples: u32) -> Result<Vec<TimeOffset>, ValidationError> {
238 if !(1..=64).contains(&samples) {
239 return Err(invalid("samples", "Require 1..=64 samples"));
240 }
241 let denominator = i128::from(samples) * 2;
242 Ok((0..samples)
243 .map(|index| {
244 let numerator =
245 (i128::from(index) * 2 + 1 - i128::from(samples)) * i128::from(shutter.ticks());
246 TimeOffset::from_ticks(
247 (((numerator.abs() + denominator / 2) / denominator) * numerator.signum()) as i64,
248 )
249 })
250 .collect())
251}
252
253pub(crate) fn capture(
254 context: &SceneContext<'_>,
255 shutter: Duration,
256 samples: u32,
257 mut build: impl FnMut(&SceneContext<'_>) -> Node,
258) -> Result<Node, ValidationError> {
259 let offsets = sample_offsets(shutter, samples)?;
260 if context.render_quality() == crate::RenderQuality::Draft {
261 return Ok(build(context));
262 }
263 let contexts = offsets
264 .into_iter()
265 .map(|offset| context.resampled(offset))
266 .collect::<Result<Vec<_>, _>>()?;
267 let nodes = contexts.iter().map(&mut build).collect::<Vec<_>>();
268 Ok(Node::accumulate(nodes)?.shutter_sampled())
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::{Shape, Time};
275
276 #[test]
277 fn jittered_capture_replays_and_rejects_nested_scopes() {
278 use crate::NodeModifiers;
279 let cache = crate::SceneCache::default();
280 let context = SceneContext::new(
281 Duration::from_frame(5, crate::fps!(24)).after_origin(),
282 crate::fps!(24),
283 crate::Size::new(8.0, 8.0),
284 &cache,
285 );
286 let policy = ShutterSampling {
287 jitter_seed: Some(17),
288 adaptive: true,
289 curve: ShutterCurve::Triangle,
290 };
291 let build = |sample: &SceneContext<'_>| {
292 Node::from(crate::Rect::new(0.0, 0.0, 2.0, 2.0).fill(crate::Color::WHITE))
293 .translate(sample.seconds(), 0.0)
294 };
295 let first = policy
296 .capture(
297 MotionBlur::default(),
298 &context,
299 crate::PixelSize::new(8, 8),
300 build,
301 )
302 .unwrap();
303 let second = policy
304 .capture(
305 MotionBlur::default(),
306 &context,
307 crate::PixelSize::new(8, 8),
308 build,
309 )
310 .unwrap();
311 assert_eq!(
312 first
313 .compile(
314 context.size(),
315 crate::PixelSize::new(8, 8),
316 crate::ValidationLimits::default()
317 )
318 .unwrap(),
319 second
320 .compile(
321 context.size(),
322 crate::PixelSize::new(8, 8),
323 crate::ValidationLimits::default()
324 )
325 .unwrap()
326 );
327 assert!(
328 policy
329 .capture(
330 MotionBlur::default(),
331 &context,
332 crate::PixelSize::new(8, 8),
333 |_| Node::group([first.clone()])
334 )
335 .is_err()
336 );
337 assert!(
338 ShutterSampling::default()
339 .capture(
340 MotionBlur::default(),
341 &context,
342 crate::PixelSize::new(8, 8),
343 |_| Node::group([first.clone()])
344 )
345 .is_err(),
346 "The default policy rejects nested captures too"
347 );
348 }
349
350 #[test]
351 fn draft_subtree_shutter_samples_once_but_still_validates() {
352 let cache = crate::SceneCache::default();
353 let context = SceneContext::new(
354 Time::from_ticks(0),
355 crate::fps!(30),
356 crate::Size::new(8.0, 4.0),
357 &cache,
358 );
359 for (quality, expected) in [
360 (crate::RenderQuality::Draft, 1),
361 (crate::RenderQuality::Final, 4),
362 ] {
363 let mut calls = 0;
364 capture(
365 &context.clone().with_quality(quality),
366 Duration::from_ticks(100),
367 4,
368 |_| {
369 calls += 1;
370 Node::empty()
371 },
372 )
373 .unwrap();
374 assert_eq!(calls, expected);
375 }
376 assert!(
377 capture(
378 &context.clone().with_quality(crate::RenderQuality::Draft),
379 Duration::from_ticks(100),
380 0,
381 |_| panic!("invalid shutter must not evaluate")
382 )
383 .is_err()
384 );
385 }
386}