1use crate::{Duration, FrameRate, Node, SceneContext, Time, 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(Time::ZERO);
75 };
76 let ticks = (numerator + denominator / 2) / denominator;
77 Ok(Time::from_ticks(i64::try_from(ticks).map_err(|_| {
78 invalid("shutter", "Exposure exceeds signed ticks")
79 })?))
80 }
81}
82
83fn invalid(field: &str, message: &str) -> ValidationError {
84 ValidationError::invalid("motion_blur", field, message)
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub enum ShutterCurve {
90 #[default]
92 Box,
93 Triangle,
95 Cosine,
97}
98
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct ShutterSampling {
102 pub jitter_seed: Option<u64>,
104 pub adaptive: bool,
106 pub curve: ShutterCurve,
108}
109
110impl ShutterSampling {
111 pub fn capture(
114 self,
115 blur: MotionBlur,
116 context: &SceneContext<'_>,
117 output: crate::PixelSize,
118 mut build: impl FnMut(&SceneContext<'_>) -> Node,
119 ) -> Result<Node, ValidationError> {
120 let shutter = blur.shutter(context.frame_rate())?;
121 output.mapping(context.size())?;
122 if context.render_quality() == crate::RenderQuality::Draft {
123 return Ok(build(context));
124 }
125 let count = if self.adaptive && shutter > Time::ZERO {
126 let left = context.resampled(Time::from_ticks(-shutter.ticks() / 2))?;
127 let right = context.resampled(Time::from_ticks(shutter.ticks() / 2))?;
128 let drawing = crate::DrawContext::default().with_assets(context.assets());
129 let first = build(&left).compile_with_context(
130 context.size(),
131 output,
132 crate::ValidationLimits::default(),
133 &drawing,
134 )?;
135 let last = build(&right).compile_with_context(
136 context.size(),
137 output,
138 crate::ValidationLimits::default(),
139 &drawing,
140 )?;
141 displacement_samples(first.display_list(), last.display_list())
142 } else {
143 blur.samples
144 };
145 let mut offsets = sample_offsets(shutter, count)?;
146 if let Some(seed) = self.jitter_seed {
147 let frame = context.time().ticks() as u64;
148 for (index, offset) in offsets.iter_mut().enumerate() {
149 let random = shutter_random(seed ^ frame.rotate_left(17) ^ index as u64);
150 let numerator = (index as f64 + random) / f64::from(count) - 0.5;
151 *offset = Time::from_ticks((numerator * shutter.ticks() as f64).round() as i64);
152 }
153 }
154 let weights = (0..count)
155 .map(|index| {
156 let phase = (f64::from(index) + 0.5) / f64::from(count);
157 match self.curve {
158 ShutterCurve::Box => 1.0,
159 ShutterCurve::Triangle => 1.0 - (phase * 2.0 - 1.0).abs(),
160 ShutterCurve::Cosine => 0.5 - 0.5 * libm::cos(std::f64::consts::TAU * phase),
161 }
162 })
163 .collect::<Vec<_>>();
164 let total: f64 = weights.iter().sum();
165 let mut nodes = Vec::with_capacity(count as usize);
166 for (offset, weight) in offsets.into_iter().zip(weights) {
167 let node = build(&context.resampled(offset)?);
168 if node.contains_shutter(0) {
169 return Err(invalid(
170 "scope",
171 "Do not combine full-scene and subtree shutter captures",
172 ));
173 }
174 nodes.push((node, weight / total));
175 }
176 Ok(Node::weighted_sum(nodes)?.shutter_sampled())
177 }
178}
179
180pub(crate) fn shutter_random(mut value: u64) -> f64 {
181 value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
182 value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb);
183 value ^= value >> 31;
184 (value >> 11) as f64 / (1u64 << 53) as f64
185}
186
187fn displacement_samples(first: &crate::DisplayList, last: &crate::DisplayList) -> u32 {
188 use crate::DrawCommand;
189 if first.commands.len() != last.commands.len() {
190 return 64;
191 }
192 let mut maximum = 0.0f64;
193 for (first, last) in first.commands.iter().zip(&last.commands) {
194 let points = |command: &DrawCommand| -> Option<Vec<crate::Point>> {
195 match command {
196 DrawCommand::FillRect {
197 bounds, transform, ..
198 } => Some(
199 [
200 crate::Point::new(bounds.left, bounds.top),
201 crate::Point::new(bounds.right, bounds.top),
202 crate::Point::new(bounds.right, bounds.bottom),
203 crate::Point::new(bounds.left, bounds.bottom),
204 ]
205 .map(|point| transform.apply(point))
206 .to_vec(),
207 ),
208 DrawCommand::Fill {
209 path, transform, ..
210 } => Some(
211 path.verbs()
212 .iter()
213 .flat_map(|verb| verb.points().into_iter().flatten())
214 .map(|point| transform.apply(point))
215 .collect(),
216 ),
217 _ => None,
218 }
219 };
220 match (points(first), points(last)) {
221 (Some(first), Some(last)) if first.len() == last.len() => {
222 for (first, last) in first.into_iter().zip(last) {
223 maximum = maximum.max(libm::hypot(first.x - last.x, first.y - last.y));
224 }
225 }
226 (None, None) if first == last => {}
227 _ => return 64,
228 }
229 }
230 (maximum.ceil() as u32).clamp(8, 64)
231}
232
233pub fn sample_offsets(shutter: Duration, samples: u32) -> Result<Vec<Duration>, ValidationError> {
235 if shutter < Time::ZERO || !(1..=64).contains(&samples) {
236 return Err(invalid(
237 "samples",
238 "Require nonnegative shutter and 1..=64 samples",
239 ));
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 Time::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
275 #[test]
276 fn jittered_capture_replays_and_rejects_nested_scopes() {
277 use crate::NodeModifiers;
278 let cache = crate::SceneCache::default();
279 let context = SceneContext::new(
280 Time::from_frame(5, 24.into()),
281 24.into(),
282 crate::Size::new(8.0, 8.0),
283 &cache,
284 );
285 let policy = ShutterSampling {
286 jitter_seed: Some(17),
287 adaptive: true,
288 curve: ShutterCurve::Triangle,
289 };
290 let build = |sample: &SceneContext<'_>| {
291 Node::from(crate::Rect::new(0.0, 0.0, 2.0, 2.0).fill(crate::Color::WHITE))
292 .translate(sample.seconds(), 0.0)
293 };
294 let first = policy
295 .capture(
296 MotionBlur::default(),
297 &context,
298 crate::PixelSize::new(8, 8),
299 build,
300 )
301 .unwrap();
302 let second = policy
303 .capture(
304 MotionBlur::default(),
305 &context,
306 crate::PixelSize::new(8, 8),
307 build,
308 )
309 .unwrap();
310 assert_eq!(
311 first
312 .compile(
313 context.size(),
314 crate::PixelSize::new(8, 8),
315 crate::ValidationLimits::default()
316 )
317 .unwrap(),
318 second
319 .compile(
320 context.size(),
321 crate::PixelSize::new(8, 8),
322 crate::ValidationLimits::default()
323 )
324 .unwrap()
325 );
326 assert!(
327 policy
328 .capture(
329 MotionBlur::default(),
330 &context,
331 crate::PixelSize::new(8, 8),
332 |_| Node::group([first.clone()])
333 )
334 .is_err()
335 );
336 assert!(
337 ShutterSampling::default()
338 .capture(
339 MotionBlur::default(),
340 &context,
341 crate::PixelSize::new(8, 8),
342 |_| Node::group([first.clone()])
343 )
344 .is_err(),
345 "The default policy rejects nested captures too"
346 );
347 }
348
349 #[test]
350 fn draft_subtree_shutter_samples_once_but_still_validates() {
351 let cache = crate::SceneCache::default();
352 let context = SceneContext::new(
353 Time::from_ticks(0),
354 30.into(),
355 crate::Size::new(8.0, 4.0),
356 &cache,
357 );
358 for (quality, expected) in [
359 (crate::RenderQuality::Draft, 1),
360 (crate::RenderQuality::Final, 4),
361 ] {
362 let mut calls = 0;
363 capture(&context.quality(quality), Time::from_ticks(100), 4, |_| {
364 calls += 1;
365 Node::empty()
366 })
367 .unwrap();
368 assert_eq!(calls, expected);
369 }
370 assert!(
371 capture(
372 &context.quality(crate::RenderQuality::Draft),
373 Time::from_ticks(100),
374 0,
375 |_| panic!("invalid shutter must not evaluate")
376 )
377 .is_err()
378 );
379 }
380}