From b81f3a846995b6a2706b97c26760f90422553c7f Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Mon, 13 Jul 2026 21:46:52 +0300 Subject: [PATCH] curve fetching optimization for animation system - gives 2x-3x performance boost on average - essentially turns binary search of a span into simple indexing using stored index value from the previous frame (so called "fetch hint") --- fyrox-animation/src/container.rs | 81 +++++++++++++++++++++----------- fyrox-animation/src/lib.rs | 11 +++-- fyrox-animation/src/track.rs | 14 +++++- fyrox-math/src/curve.rs | 79 ++++++++++++++++++++++--------- fyrox-scripts/src/camera.rs | 8 +++- 5 files changed, 134 insertions(+), 59 deletions(-) diff --git a/fyrox-animation/src/container.rs b/fyrox-animation/src/container.rs index fe268fd94..ea8ab47b4 100644 --- a/fyrox-animation/src/container.rs +++ b/fyrox-animation/src/container.rs @@ -21,6 +21,7 @@ //! Track data container is a flexible source of data for numeric parameters, it is built using a set //! of parametric curves. See [`TrackDataContainer`] docs for more info. +use crate::track::HintContainer; use crate::{ core::{ algebra::{Vector2, Vector3, Vector4}, @@ -159,7 +160,7 @@ impl TrackDataContainer { } #[inline(always)] - fn fetch_vector2(&self, time: f32) -> Option { + fn fetch_vector2(&self, time: f32, fetch_hints: &mut HintContainer) -> Option { if self.curves.len() < 2 { return None; } @@ -167,29 +168,39 @@ impl TrackDataContainer { // SAFETY: The indices are guaranteed to be correct by the above check. unsafe { Some(TrackValue::Vector2(Vector2::new( - self.curves.get_unchecked(0).value_at(time), - self.curves.get_unchecked(1).value_at(time), + self.curves + .get_unchecked(0) + .value_at(time, &mut fetch_hints[0]), + self.curves + .get_unchecked(1) + .value_at(time, &mut fetch_hints[1]), ))) } } #[inline(always)] - fn fetch_vector3(&self, time: f32) -> Option { + fn fetch_vector3(&self, time: f32, fetch_hints: &mut HintContainer) -> Option { if self.curves.len() < 3 { return None; } unsafe { Some(TrackValue::Vector3(Vector3::new( - self.curves.get_unchecked(0).value_at(time), - self.curves.get_unchecked(1).value_at(time), - self.curves.get_unchecked(2).value_at(time), + self.curves + .get_unchecked(0) + .value_at(time, &mut fetch_hints[0]), + self.curves + .get_unchecked(1) + .value_at(time, &mut fetch_hints[1]), + self.curves + .get_unchecked(2) + .value_at(time, &mut fetch_hints[2]), ))) } } #[inline(always)] - fn fetch_vector4(&self, time: f32) -> Option { + fn fetch_vector4(&self, time: f32, fetch_hints: &mut HintContainer) -> Option { if self.curves.len() < 4 { return None; } @@ -197,16 +208,28 @@ impl TrackDataContainer { // SAFETY: The indices are guaranteed to be correct by the above check. unsafe { Some(TrackValue::Vector4(Vector4::new( - self.curves.get_unchecked(0).value_at(time), - self.curves.get_unchecked(1).value_at(time), - self.curves.get_unchecked(2).value_at(time), - self.curves.get_unchecked(3).value_at(time), + self.curves + .get_unchecked(0) + .value_at(time, &mut fetch_hints[0]), + self.curves + .get_unchecked(1) + .value_at(time, &mut fetch_hints[1]), + self.curves + .get_unchecked(2) + .value_at(time, &mut fetch_hints[2]), + self.curves + .get_unchecked(3) + .value_at(time, &mut fetch_hints[3]), ))) } } #[inline(always)] - fn fetch_quaternion_euler(&self, time: f32) -> Option { + fn fetch_quaternion_euler( + &self, + time: f32, + fetch_hints: &mut HintContainer, + ) -> Option { if self.curves.len() < 3 { return None; } @@ -219,9 +242,9 @@ impl TrackDataContainer { // Convert Euler angles to quaternion let (x, y, z) = ( - x_curve.value_at(time), - y_curve.value_at(time), - z_curve.value_at(time), + x_curve.value_at(time, &mut fetch_hints[0]), + y_curve.value_at(time, &mut fetch_hints[1]), + z_curve.value_at(time, &mut fetch_hints[1]), ); Some(TrackValue::UnitQuaternion(quat_from_euler( @@ -232,7 +255,7 @@ impl TrackDataContainer { } #[inline(always)] - fn fetch_quaternion(&self, time: f32) -> Option { + fn fetch_quaternion(&self, time: f32, fetch_hints: &mut HintContainer) -> Option { if self.curves.len() < 4 { return None; } @@ -246,10 +269,10 @@ impl TrackDataContainer { // Convert Euler angles to quaternion let (x, y, z, w) = ( - x_curve.value_at(time), - y_curve.value_at(time), - z_curve.value_at(time), - w_curve.value_at(time), + x_curve.value_at(time, &mut fetch_hints[0]), + y_curve.value_at(time, &mut fetch_hints[1]), + z_curve.value_at(time, &mut fetch_hints[2]), + w_curve.value_at(time, &mut fetch_hints[3]), ); Some(TrackValue::UnitQuaternion(UnitQuaternion::from_quaternion( @@ -261,14 +284,16 @@ impl TrackDataContainer { /// Tries to get a value at a given time. The method could fail if the internal set of curves is malformed /// and cannot produce a desired value (for example, [`Vector3`] can be fetched only if the amount of curves /// is 3). - pub fn fetch(&self, time: f32) -> Option { + pub fn fetch(&self, time: f32, fetch_hints: &mut HintContainer) -> Option { match self.kind { - TrackValueKind::Real => Some(TrackValue::Real(self.curves.first()?.value_at(time))), - TrackValueKind::Vector2 => self.fetch_vector2(time), - TrackValueKind::Vector3 => self.fetch_vector3(time), - TrackValueKind::Vector4 => self.fetch_vector4(time), - TrackValueKind::UnitQuaternionEuler => self.fetch_quaternion_euler(time), - TrackValueKind::UnitQuaternion => self.fetch_quaternion(time), + TrackValueKind::Real => Some(TrackValue::Real( + self.curves.first()?.value_at(time, &mut fetch_hints[0]), + )), + TrackValueKind::Vector2 => self.fetch_vector2(time, fetch_hints), + TrackValueKind::Vector3 => self.fetch_vector3(time, fetch_hints), + TrackValueKind::Vector4 => self.fetch_vector4(time, fetch_hints), + TrackValueKind::UnitQuaternionEuler => self.fetch_quaternion_euler(time, fetch_hints), + TrackValueKind::UnitQuaternion => self.fetch_quaternion(time, fetch_hints), } } diff --git a/fyrox-animation/src/lib.rs b/fyrox-animation/src/lib.rs index 387d32982..3cb0ee78a 100644 --- a/fyrox-animation/src/lib.rs +++ b/fyrox-animation/src/lib.rs @@ -25,7 +25,7 @@ #![allow(clippy::doc_lazy_continuation)] #![allow(mismatched_lifetime_syntaxes)] -use crate::track::TrackBinding; +use crate::track::{HintContainer, TrackBinding}; use crate::{ core::{ algebra::{UnitQuaternion, Vector3}, @@ -504,7 +504,7 @@ impl Animation { tracks .iter() .find(|track| track.value_binding() == &ValueBinding::Position) - .and_then(|track| track.fetch(time)) + .and_then(|track| track.fetch(time, &mut HintContainer::default())) .and_then(|value| { if let TrackValue::Vector3(position) = value.value { Some(position) @@ -519,7 +519,7 @@ impl Animation { tracks .iter() .find(|track| track.value_binding() == &ValueBinding::Rotation) - .and_then(|track| track.fetch(time)) + .and_then(|track| track.fetch(time, &mut HintContainer::default())) .and_then(|value| { if let TrackValue::UnitQuaternion(rotation) = value.value { Some(rotation) @@ -898,12 +898,13 @@ impl Animation { self.pose.reset(); for track in tracks_data.tracks.iter() { - let Some(binding) = self.track_bindings.get(&track.id()) else { + let Some(binding) = self.track_bindings.get_mut(&track.id()) else { continue; }; if binding.is_enabled() { - if let Some(bound_value) = track.fetch(self.time_position) { + if let Some(bound_value) = track.fetch(self.time_position, &mut binding.fetch_hints) + { self.pose.add_to_node_pose(binding.target(), bound_value); } } diff --git a/fyrox-animation/src/track.rs b/fyrox-animation/src/track.rs index 28bc591d2..08f6ae69a 100644 --- a/fyrox-animation/src/track.rs +++ b/fyrox-animation/src/track.rs @@ -28,6 +28,11 @@ use crate::{ }; use std::fmt::Debug; +/// A set of indices of keys, where each index points on the right side of a curve span. Each index +/// corresponds in the array to a curve in the curve set. It is used to remember last curve fetching +/// position to eliminate searching of a span for most cases. +pub type HintContainer = [usize; 4]; + /// Track binding contains a handle to a target object that will be animated by an animation track. /// Additionally, the binding could be disabled to temporarily prevent animation from affecting the /// target. @@ -39,6 +44,9 @@ pub struct TrackBinding { /// A target bound to a track. The actual track id is stored as a key in hash map of bindings in /// the animation. pub target: T, + #[visit(skip)] + #[reflect(hidden)] + pub(super) fetch_hints: HintContainer, } impl Default for TrackBinding { @@ -46,6 +54,7 @@ impl Default for TrackBinding { Self { enabled: true, target: Default::default(), + fetch_hints: Default::default(), } } } @@ -56,6 +65,7 @@ impl TrackBinding { Self { enabled: true, target, + fetch_hints: Default::default(), } } @@ -171,8 +181,8 @@ impl Track { } /// Tries to get a new property value at a given time position. - pub fn fetch(&self, time: f32) -> Option { - self.frames.fetch(time).map(|v| BoundValue { + pub fn fetch(&self, time: f32, fetch_hints: &mut HintContainer) -> Option { + self.frames.fetch(time, fetch_hints).map(|v| BoundValue { binding: self.binding.clone(), value: v, }) diff --git a/fyrox-math/src/curve.rs b/fyrox-math/src/curve.rs index 31c047105..2e2513f8c 100644 --- a/fyrox-math/src/curve.rs +++ b/fyrox-math/src/curve.rs @@ -251,22 +251,57 @@ impl Curve { } #[inline] - fn fetch_at(&self, location: f32, interpolator: I) -> f32 + fn fetch_at(&self, location: f32, hint: &mut usize, interpolator: I) -> f32 where I: FnOnce(&CurveKey, &CurveKey, f32) -> f32, { + #[inline(always)] + fn interpolate_keys( + left: &CurveKey, + right: &CurveKey, + location: f32, + interpolator: I, + ) -> f32 + where + I: FnOnce(&CurveKey, &CurveKey, f32) -> f32, + { + let t = (location - left.location) / (right.location - left.location); + interpolator(left, right, t) + } + if let (Some(first), Some(last)) = (self.keys.first(), self.keys.last()) { if location <= first.location { + *hint = 0; first.value } else if location >= last.location { + *hint = self.keys().len().saturating_sub(1); last.value } else { + // Try to predict the span using the given hint. + if let (Some(predicted_left), Some(predicted_right)) = ( + self.keys.get((*hint).saturating_sub(1)), + self.keys.get(*hint), + ) { + if location >= predicted_left.location && location < predicted_right.location { + return interpolate_keys( + predicted_left, + predicted_right, + location, + interpolator, + ); + } else { + // TODO: Usually curve fetching is continuous, so we should also check the + // next or previous span here, this will completely eliminate binary search + // below on case when playback time step is lower than mid-key time distance. + } + } + // Use binary search for multiple spans. - let pos = self.keys.partition_point(|k| k.location < location); - let left = self.keys.get(pos.saturating_sub(1)).unwrap(); - let right = self.keys.get(pos).unwrap(); - let t = (location - left.location) / (right.location - left.location); - interpolator(left, right, t) + *hint = self.keys.partition_point(|k| k.location < location); + let span_end = *hint; + let left = self.keys.get(span_end.saturating_sub(1)).unwrap(); + let right = self.keys.get(span_end).unwrap(); + interpolate_keys(left, right, location, interpolator) } } else { 0.0 @@ -274,13 +309,13 @@ impl Curve { } #[inline] - pub fn value_at(&self, location: f32) -> f32 { - self.fetch_at(location, |a, b, t| a.interpolate(b, t)) + pub fn value_at(&self, location: f32, hint: &mut usize) -> f32 { + self.fetch_at(location, hint, |a, b, t| a.interpolate(b, t)) } #[inline] - pub fn angle_at(&self, location: f32) -> f32 { - self.fetch_at(location, |a, b, t| a.interpolate_angles(b, t)) + pub fn angle_at(&self, location: f32, hint: &mut usize) -> f32 { + self.fetch_at(location, hint, |a, b, t| a.interpolate_angles(b, t)) } pub fn bounds(&self) -> Rect { @@ -396,21 +431,21 @@ mod test { let mut curve = Curve::default(); // Test fetching from empty curve. - assert_eq!(curve.value_at(0.0), 0.0); + assert_eq!(curve.value_at(0.0, &mut 0), 0.0); curve.add_key(CurveKey::new(0.0, 1.0, CurveKeyKind::Linear)); // One-key curves must always return its single key value. - assert_eq!(curve.value_at(-1.0), 1.0); - assert_eq!(curve.value_at(1.0), 1.0); - assert_eq!(curve.value_at(0.0), 1.0); + assert_eq!(curve.value_at(-1.0, &mut 0), 1.0); + assert_eq!(curve.value_at(1.0, &mut 0), 1.0); + assert_eq!(curve.value_at(0.0, &mut 0), 1.0); curve.add_key(CurveKey::new(1.0, 0.0, CurveKeyKind::Linear)); // Two-key curves must always use interpolation. - assert_eq!(curve.value_at(-1.0), 1.0); - assert_eq!(curve.value_at(2.0), 0.0); - assert_eq!(curve.value_at(0.5), 0.5); + assert_eq!(curve.value_at(-1.0, &mut 0), 1.0); + assert_eq!(curve.value_at(2.0, &mut 0), 0.0); + assert_eq!(curve.value_at(0.5, &mut 0), 0.5); // Add one more key and do more checks. curve.add_key(CurveKey::new(2.0, 1.0, CurveKeyKind::Linear)); @@ -420,15 +455,15 @@ mod test { assert!(curve.keys[1].location <= curve.keys[2].location); // Check generic out-of-bounds fetching. - assert_eq!(curve.value_at(-1.0), 1.0); // Left side oob - assert_eq!(curve.value_at(3.0), 1.0); // Right side oob. + assert_eq!(curve.value_at(-1.0, &mut 0), 1.0); // Left side oob + assert_eq!(curve.value_at(3.0, &mut 0), 1.0); // Right side oob. // Check edge cases. - assert_eq!(curve.value_at(0.0), 1.0); // Left edge. - assert_eq!(curve.value_at(2.0), 1.0); // Right edge. + assert_eq!(curve.value_at(0.0, &mut 0), 1.0); // Left edge. + assert_eq!(curve.value_at(2.0, &mut 0), 1.0); // Right edge. // Check interpolation. - assert_eq!(curve.value_at(0.5), 0.5); + assert_eq!(curve.value_at(0.5, &mut 0), 0.5); // Check id. let id = Uuid::new_v4(); diff --git a/fyrox-scripts/src/camera.rs b/fyrox-scripts/src/camera.rs index 445518a0f..ead2103fc 100644 --- a/fyrox-scripts/src/camera.rs +++ b/fyrox-scripts/src/camera.rs @@ -260,7 +260,9 @@ impl ScriptTrait for FlyingCameraController { .min(1.0); *self.target_velocity = new_normalized_velocity.scale( *self.speed - * self.acceleration_curve.value_at(self.acceleration_coeff) + * self + .acceleration_curve + .value_at(self.acceleration_coeff, &mut 0) * context.dt, ); } else { @@ -270,7 +272,9 @@ impl ScriptTrait for FlyingCameraController { if let Some(normalized_velocity) = self.target_velocity.try_normalize(f32::EPSILON) { *self.target_velocity = normalized_velocity.scale( *self.speed - * self.deceleration_curve.value_at(self.acceleration_coeff) + * self + .deceleration_curve + .value_at(self.acceleration_coeff, &mut 0) * context.dt, ); } else {