From 66c00c8a1db6c393b97c10cd3bd6b6e46432b61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sun, 20 Sep 2026 13:55:15 +0200 Subject: [PATCH] Optimize rollback hot paths --- src/fighter/baked_animation/mod.rs | 16 ++++- src/fighter/baked_animation/runtime.rs | 36 +++++++--- src/fighter/collision.rs | 50 +++++++------- src/replay.rs | 96 ++++++++++++++++++++------ 4 files changed, 143 insertions(+), 55 deletions(-) diff --git a/src/fighter/baked_animation/mod.rs b/src/fighter/baked_animation/mod.rs index 4159352..a0b4663 100644 --- a/src/fighter/baked_animation/mod.rs +++ b/src/fighter/baked_animation/mod.rs @@ -4,8 +4,8 @@ mod asset_baking; mod runtime; mod visual_binding; -pub use asset_baking::{BakedAnimationAssetProcessingPlugin, BakedAnimationPlugin}; pub use crate::math::fixed_transform::FixedMat4; +pub use asset_baking::{BakedAnimationAssetProcessingPlugin, BakedAnimationPlugin}; pub use runtime::{ BakedAnimationClip, BakedFighterAnimations, FighterBoneIndexMap, FighterBoneMatrices, update_fighter_bone_matrices, @@ -164,6 +164,20 @@ mod tests { ); } + #[test] + fn repeated_pose_updates_reuse_scratch_and_output_buffers() { + let mut matrices = FighterBoneMatrices::default(); + let names = ["root".into(), "hand".into()]; + let parents = [None, Some(0)]; + let pose = [FixedMat4::IDENTITY, FixedMat4::IDENTITY]; + + matrices.set_pose(&names, &parents, &pose, FGi32::ONE); + let capacities = matrices.buffer_capacities(); + matrices.set_pose(&names, &parents, &pose, FGi32::ONE); + + assert_eq!(capacities, matrices.buffer_capacities()); + } + #[test] fn model_scale_affects_gameplay_bones_without_changing_visual_locals() { let mut root = FixedMat4::IDENTITY; diff --git a/src/fighter/baked_animation/runtime.rs b/src/fighter/baked_animation/runtime.rs index 82fa42e..80bc110 100644 --- a/src/fighter/baked_animation/runtime.rs +++ b/src/fighter/baked_animation/runtime.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use bevy::prelude::*; use serde::{Deserialize, Serialize}; -use super::{BAKED_ANIMATION_FPS}; +use super::BAKED_ANIMATION_FPS; use crate::{ fighter::{ @@ -12,8 +12,8 @@ use crate::{ manifest::FighterManifest, visual::FighterAnimations, }, + math::fixed_transform::FixedMat4, math::int::FGi32, - math::fixed_transform::FixedMat4 }; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -70,12 +70,24 @@ pub struct FighterBoneMatrices { previous_frame_matrices: Vec, local_matrices: Vec, previous_local_matrices: Vec, + model_matrices: Vec>, } #[derive(Component, Clone, Debug, Default)] pub struct FighterBoneIndexMap(pub HashMap); impl FighterBoneMatrices { + #[cfg(test)] + pub(super) fn buffer_capacities(&self) -> [usize; 5] { + [ + self.matrices.capacity(), + self.local_matrices.capacity(), + self.previous_frame_matrices.capacity(), + self.previous_local_matrices.capacity(), + self.model_matrices.capacity(), + ] + } + pub fn get(&self, bone: &str) -> Option<(FixedMat4, FixedMat4)> { let index = self.bone_names.iter().position(|name| name == bone)?; Some(( @@ -142,13 +154,19 @@ impl FighterBoneMatrices { assert_eq!(self.bone_names, bone_names, "baked bone names changed"); assert_eq!(self.parents, parents, "baked bone parents changed"); } + self.matrices.reserve(local.len()); + self.previous_frame_matrices.reserve(local.len()); + self.local_matrices.reserve(local.len()); + self.previous_local_matrices.reserve(local.len()); + self.model_matrices.reserve(local.len()); std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices); std::mem::swap(&mut self.local_matrices, &mut self.previous_local_matrices); self.local_matrices.clear(); self.local_matrices.extend_from_slice(local); - let mut model = vec![None; local.len()]; + self.model_matrices.resize(local.len(), None); + self.model_matrices.fill(None); for index in 0..local.len() { - resolve_model_transform(index, parents, local, &mut model) + resolve_model_transform(index, parents, local, &mut self.model_matrices) .expect("baked bone hierarchy should be valid"); } let model_scale = FixedMat4 { @@ -159,10 +177,12 @@ impl FighterBoneMatrices { [FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE], ], }; - self.matrices = model - .into_iter() - .map(|matrix| model_scale.mul(matrix.expect("baked bone hierarchy should be valid"))) - .collect(); + self.matrices.clear(); + self.matrices.extend( + self.model_matrices.iter().map(|matrix| { + model_scale.mul(matrix.expect("baked bone hierarchy should be valid")) + }), + ); } } diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs index f6cc379..b93ed3e 100644 --- a/src/fighter/collision.rs +++ b/src/fighter/collision.rs @@ -50,31 +50,29 @@ pub fn air_collide_with_stage( translation.0 + ecb.get_bottom_point(), ); - let mut intersect_results = vec![]; - - for (poly_idx, poly) in stage_collision.stage_polys.iter().enumerate() { - if let Some(intersect_result) = poly.intersect_ray(ray_segment) { - intersect_results.push(( - AirCollisionWithStageResult { - line_id: { - StageLineID { - polygon: poly_idx, - segment: intersect_result.segment_idx, - } - }, - hit_position: intersect_result.position, - hit_normal: intersect_result.normal, - }, - intersect_result - .position - .distance_squared(ray_segment.point2()), - )); - } - } - intersect_results.sort_by(|(_, a), (_, b)| a.cmp(b)); - - intersect_results + stage_collision + .stage_polys .iter() - .last() - .map(|(val, _)| val.to_owned().clone()) + .enumerate() + .filter_map(|(poly_idx, poly)| { + poly.intersect_ray(ray_segment).map(|intersect_result| { + ( + AirCollisionWithStageResult { + line_id: { + StageLineID { + polygon: poly_idx, + segment: intersect_result.segment_idx, + } + }, + hit_position: intersect_result.position, + hit_normal: intersect_result.normal, + }, + intersect_result + .position + .distance_squared(ray_segment.point2()), + ) + }) + }) + .max_by(|(_, a), (_, b)| a.cmp(b)) + .map(|(result, _)| result) } diff --git a/src/replay.rs b/src/replay.rs index 95055c4..d3f37c8 100644 --- a/src/replay.rs +++ b/src/replay.rs @@ -152,6 +152,20 @@ fn validate(replay: &Replay) -> Result<(), String> { { return Err("replay frames must be strictly ordered".into()); } + if replay + .frames + .windows(2) + .any(|frames| frames[0].frame.checked_add(1) != Some(frames[1].frame)) + { + return Err("replay frames must be contiguous".into()); + } + if replay + .checksums + .windows(2) + .any(|checksums| checksums[0].frame >= checksums[1].frame) + { + return Err("replay checksums must be strictly ordered".into()); + } if replay .frames .iter() @@ -173,6 +187,21 @@ impl Replay { handles.sort_unstable(); handles != (0..handles.len()).collect::>() } + + fn frame(&self, frame: i32) -> Option<&ReplayFrame> { + let first_frame = self.frames.first()?.frame; + let index = frame.checked_sub(first_frame)? as usize; + self.frames + .get(index) + .filter(|recorded| recorded.frame == frame) + } + + fn checksum(&self, frame: i32) -> Option { + self.checksums + .binary_search_by_key(&frame, |checksum| checksum.frame) + .ok() + .map(|index| self.checksums[index].checksum) + } } fn start_replay(mut commands: Commands, args: Res, loaded: Option>) { @@ -210,11 +239,7 @@ fn replay_inputs( .insert(frame.0, inputs.iter().map(|(input, _)| *input).collect()); } ReplayMode::Playback { replay, mismatch } if !*mismatch => { - let Some(recorded) = replay - .frames - .iter() - .find(|recorded| recorded.frame == frame.0) - else { + let Some(recorded) = replay.frame(frame.0) else { error!("Replay has no input for frame {}", frame.0); *mismatch = true; return; @@ -235,21 +260,15 @@ fn record_checksum( let Some(mut runtime) = runtime else { return }; if matches!(&runtime.mode, ReplayMode::Record { .. }) { runtime.checksums.insert(frame.0, checksum.0); - } else if let ReplayMode::Playback { replay, mismatch } = &mut runtime.mode { - if let Some(expected) = replay - .checksums - .iter() - .find(|value| value.frame == frame.0) - .map(|value| value.checksum) - { - if expected != checksum.0 { - error!( - "Replay desync at frame {}: expected {:X}, got {:X}", - frame.0, expected, checksum.0 - ); - *mismatch = true; - } - } + } else if let ReplayMode::Playback { replay, mismatch } = &mut runtime.mode + && let Some(expected) = replay.checksum(frame.0) + && expected != checksum.0 + { + error!( + "Replay desync at frame {}: expected {:X}, got {:X}", + frame.0, expected, checksum.0 + ); + *mismatch = true; } } @@ -407,4 +426,41 @@ mod tests { "unsupported replay version 1" ); } + + #[test] + fn playback_lookups_use_frame_offsets_and_support_sparse_checksums() { + let mut replay = sample(); + replay.frames[0].frame = 10; + replay.frames.push(ReplayFrame { + frame: 11, + inputs: replay.frames[0].inputs.clone(), + }); + replay.checksums.push(ReplayChecksum { + frame: 11, + checksum: 84, + }); + replay.checksums[0].frame = 10; + validate(&replay).unwrap(); + + assert_eq!(replay.frame(10).unwrap().frame, 10); + assert_eq!(replay.frame(11).unwrap().frame, 11); + assert!(replay.frame(9).is_none()); + assert_eq!(replay.checksum(10), Some(42)); + assert_eq!(replay.checksum(11), Some(84)); + assert_eq!(replay.checksum(12), None); + } + + #[test] + fn noncontiguous_input_frames_are_rejected() { + let mut replay = sample(); + replay.frames.push(ReplayFrame { + frame: 3, + inputs: replay.frames[0].inputs.clone(), + }); + + assert_eq!( + validate(&replay).unwrap_err(), + "replay frames must be contiguous" + ); + } }