From 8fb24b6cba2bf968211e6091b4523fe0be80b6da Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 23:57:19 +0000 Subject: [PATCH] address review: in_span uses the MAX_CHAIN-bounded span, forward key range cannot overflow --- crates/track/src/roto.rs | 23 ++++++++++++++++------- crates/track/src/roto/tests.rs | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/track/src/roto.rs b/crates/track/src/roto.rs index 5ea996014..c51a9f784 100644 --- a/crates/track/src/roto.rs +++ b/crates/track/src/roto.rs @@ -155,8 +155,16 @@ impl RotoData { pub fn strokes_of(&self, frame: i64, object: u32) -> Vec<&Stroke> { self.strokes.iter().filter(|s| s.frame == frame && (s.object == object || matches!(s.kind, StrokeKind::Refine | StrokeKind::RefineErase))).collect() } + /// The stored span cut to at most MAX_CHAIN frames either side of the base: a project's + /// numbers must not make one render build billions of keys. Frames past it have no key. + fn bounded_span(&self, base: i64) -> [i64; 2] { + [self.span[0].max(base.saturating_sub(MAX_CHAIN)), self.span[1].min(base.saturating_add(MAX_CHAIN))] + } pub fn in_span(&self, f: i64) -> bool { - self.base.is_some() && f >= self.span[0] && f <= self.span[1] + self.base.is_some_and(|b| { + let span = self.bounded_span(b); + f >= span[0] && f <= span[1] + }) } /// Add a stroke: the first segmentation stroke sets the base frame and a span of /// `default_span` frames each side (clamped to `limits`); strokes outside the span extend it. @@ -201,12 +209,13 @@ impl RotoData { let k0 = key(seed, base); out.insert(base, k0); let mut k = k0; - // A stored span reaches at most MAX_CHAIN frames either side of the base: a project's - // numbers must not make one render build billions of keys. - let span = [self.span[0].max(base.saturating_sub(MAX_CHAIN)), self.span[1].min(base.saturating_add(MAX_CHAIN))]; - for f in base + 1..=span[1] { - k = key(k, f); - out.insert(f, k); + let span = self.bounded_span(base); + // `base + 1` only when there is a frame after the base (it overflows at i64::MAX). + if base < span[1] { + for f in base + 1..=span[1] { + k = key(k, f); + out.insert(f, k); + } } k = k0; for f in (span[0]..base).rev() { diff --git a/crates/track/src/roto/tests.rs b/crates/track/src/roto/tests.rs index dc58d778c..2cab1b361 100644 --- a/crates/track/src/roto/tests.rs +++ b/crates/track/src/roto/tests.rs @@ -731,3 +731,17 @@ fn a_stored_span_reaches_a_bounded_number_of_frames() { assert_eq!(k.len() as i64, 2 * super::MAX_CHAIN + 1); assert_eq!(k.keys().next(), Some(&-super::MAX_CHAIN)); } + +#[test] +fn in_span_matches_the_frames_that_have_keys_and_extreme_bases_do_not_overflow() { + let d = RotoData { base: Some(0), span: [i64::MIN, i64::MAX], strokes: vec![] }; + assert!(d.in_span(super::MAX_CHAIN) && !d.in_span(super::MAX_CHAIN + 1)); + assert!(d.in_span(-super::MAX_CHAIN) && !d.in_span(-super::MAX_CHAIN - 1)); + assert_eq!(d.source_of(super::MAX_CHAIN + 1), None); + assert!(d.chain_keys(1).keys().all(|f| d.in_span(*f))); + // A base at the end of i64: no forward frames, and no `base + 1` overflow. + let d = RotoData { base: Some(i64::MAX), span: [i64::MIN, i64::MAX], strokes: vec![] }; + let k = d.chain_keys(1); + assert_eq!(k.len() as i64, super::MAX_CHAIN + 1); + assert!(k.contains_key(&i64::MAX)); +}