refactor: use strum::EnumCount for const compile time system mapping

This commit is contained in:
2025-08-15 20:52:48 -05:00
parent 3d0bc66e40
commit 514a447162
6 changed files with 72 additions and 43 deletions

View File

@@ -9,6 +9,7 @@ use crate::events::GameEvent;
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::systems::blinking::Blinking; use crate::systems::blinking::Blinking;
use crate::systems::movement::{Movable, MovementState, Position}; use crate::systems::movement::{Movable, MovementState, Position};
use crate::systems::profiling::SystemId;
use crate::systems::{ use crate::systems::{
audio::{audio_system, AudioEvent, AudioResource}, audio::{audio_system, AudioEvent, AudioResource},
blinking::blinking_system, blinking::blinking_system,
@@ -20,7 +21,7 @@ use crate::systems::{
}, },
control::player_system, control::player_system,
debug::{debug_render_system, DebugState, DebugTextureResource}, debug::{debug_render_system, DebugState, DebugTextureResource},
ghost::ghost_ai_system, ghost::ghost_system,
input::input_system, input::input_system,
item::item_system, item::item_system,
movement::movement_system, movement::movement_system,
@@ -212,20 +213,20 @@ impl Game {
); );
schedule.add_systems( schedule.add_systems(
( (
profile("input", input_system), profile(SystemId::Input, input_system),
profile("player", player_system), profile(SystemId::Player, player_system),
profile("ghost_ai", ghost_ai_system), profile(SystemId::Ghost, ghost_system),
profile("movement", movement_system), profile(SystemId::Movement, movement_system),
profile("collision", collision_system), profile(SystemId::Collision, collision_system),
profile("item", item_system), profile(SystemId::Item, item_system),
profile("audio", audio_system), profile(SystemId::Audio, audio_system),
profile("blinking", blinking_system), profile(SystemId::Blinking, blinking_system),
profile("directional_render", directional_render_system), profile(SystemId::DirectionalRender, directional_render_system),
profile("dirty_render", dirty_render_system), profile(SystemId::DirtyRender, dirty_render_system),
profile("render", render_system), profile(SystemId::Render, render_system),
profile("debug_render", debug_render_system), profile(SystemId::DebugRender, debug_render_system),
profile( profile(
"present", SystemId::Present,
|mut canvas: NonSendMut<&mut Canvas<Window>>, |mut canvas: NonSendMut<&mut Canvas<Window>>,
backbuffer: NonSendMut<BackbufferResource>, backbuffer: NonSendMut<BackbufferResource>,
debug_state: Res<DebugState>, debug_state: Res<DebugState>,

View File

@@ -76,20 +76,14 @@ fn render_timing_display(
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap(); let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
// Format timing information using the formatting module // Format timing information using the formatting module
let timing_text = timings.format_timing_display(); let lines = timings.format_timing_display();
// Split text by newlines and render each line separately
let lines: Vec<&str> = timing_text.lines().collect();
if lines.is_empty() {
return;
}
let line_height = 14; // Approximate line height for 12pt font let line_height = 14; // Approximate line height for 12pt font
let padding = 10; let padding = 10;
// Calculate background dimensions // Calculate background dimensions
let max_width = lines let max_width = lines
.iter() .iter()
.filter(|&&l| !l.is_empty()) // Don't consider empty lines for width .filter(|l| !l.is_empty()) // Don't consider empty lines for width
.map(|line| font.size_of(line).unwrap().0) .map(|line| font.size_of(line).unwrap().0)
.max() .max()
.unwrap_or(0); .unwrap_or(0);

View File

@@ -1,6 +1,9 @@
use num_width::NumberWidth; use num_width::NumberWidth;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::time::Duration; use std::time::Duration;
use strum::EnumCount;
use crate::systems::profiling::SystemId;
// Helper to split a duration into a integer, decimal, and unit // Helper to split a duration into a integer, decimal, and unit
fn get_value(duration: &Duration) -> (u64, u32, &'static str) { fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
@@ -34,7 +37,9 @@ fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
} }
/// Formats timing data into a vector of strings with proper alignment /// Formats timing data into a vector of strings with proper alignment
pub fn format_timing_display(timing_data: impl IntoIterator<Item = (String, Duration, Duration)>) -> SmallVec<[String; 12]> { pub fn format_timing_display(
timing_data: impl IntoIterator<Item = (String, Duration, Duration)>,
) -> SmallVec<[String; SystemId::COUNT]> {
let mut iter = timing_data.into_iter().peekable(); let mut iter = timing_data.into_iter().peekable();
if iter.peek().is_none() { if iter.peek().is_none() {
return SmallVec::new(); return SmallVec::new();
@@ -98,5 +103,5 @@ pub fn format_timing_display(timing_data: impl IntoIterator<Item = (String, Dura
max_std_int_width = max_std_int_width, max_std_int_width = max_std_int_width,
max_std_decimal_width = max_std_decimal_width max_std_decimal_width = max_std_decimal_width
) )
}).collect::<SmallVec<[String; 12]>>() }).collect::<SmallVec<[String; SystemId::COUNT]>>()
} }

View File

@@ -15,7 +15,7 @@ use crate::{
/// ///
/// This system runs on all ghosts and makes periodic decisions about /// This system runs on all ghosts and makes periodic decisions about
/// which direction to move in when they reach intersections. /// which direction to move in when they reach intersections.
pub fn ghost_ai_system( pub fn ghost_system(
map: Res<Map>, map: Res<Map>,
delta_time: Res<DeltaTime>, delta_time: Res<DeltaTime>,
mut ghosts: Query<(&mut GhostBehavior, &mut Movable, &Position, &EntityType, &GhostType)>, mut ghosts: Query<(&mut GhostBehavior, &mut Movable, &Position, &EntityType, &GhostType)>,

View File

@@ -3,14 +3,43 @@ use bevy_ecs::system::{IntoSystem, System};
use circular_buffer::CircularBuffer; use circular_buffer::CircularBuffer;
use micromap::Map; use micromap::Map;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use smallvec::SmallVec;
use std::fmt::Display;
use std::time::Duration; use std::time::Duration;
use strum::EnumCount;
use strum_macros::{EnumCount, IntoStaticStr};
use thousands::Separable; use thousands::Separable;
use crate::systems::formatting;
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic. /// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
const MAX_SYSTEMS: usize = 13; const MAX_SYSTEMS: usize = SystemId::COUNT;
/// The number of durations to keep in the circular buffer. /// The number of durations to keep in the circular buffer.
const TIMING_WINDOW_SIZE: usize = 30; const TIMING_WINDOW_SIZE: usize = 30;
#[derive(EnumCount, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum SystemId {
Input,
Player,
Ghost,
Movement,
Audio,
Blinking,
DirectionalRender,
DirtyRender,
Render,
DebugRender,
Present,
Collision,
Item,
}
impl Display for SystemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", Into::<&'static str>::into(self).to_ascii_lowercase())
}
}
#[derive(Resource, Default, Debug)] #[derive(Resource, Default, Debug)]
pub struct SystemTimings { pub struct SystemTimings {
/// Map of system names to a queue of durations, using a circular buffer. /// Map of system names to a queue of durations, using a circular buffer.
@@ -20,18 +49,18 @@ pub struct SystemTimings {
/// ///
/// Also, we use a micromap::Map as the number of systems is generally quite small. /// Also, we use a micromap::Map as the number of systems is generally quite small.
/// Just make sure to set the capacity appropriately, or it will panic. /// Just make sure to set the capacity appropriately, or it will panic.
pub timings: RwLock<Map<&'static str, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>, pub timings: RwLock<Map<SystemId, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>,
} }
impl SystemTimings { impl SystemTimings {
pub fn add_timing(&self, name: &'static str, duration: Duration) { pub fn add_timing(&self, id: SystemId, duration: Duration) {
// acquire a upgradable read lock // acquire a upgradable read lock
let mut timings = self.timings.upgradable_read(); let mut timings = self.timings.upgradable_read();
// happy path, the name is already in the map (no need to mutate the hashmap) // happy path, the name is already in the map (no need to mutate the hashmap)
if timings.contains_key(name) { if timings.contains_key(&id) {
let queue = timings let queue = timings
.get(name) .get(&id)
.expect("System name not found in map after contains_key check"); .expect("System name not found in map after contains_key check");
let mut queue = queue.lock(); let mut queue = queue.lock();
@@ -41,16 +70,16 @@ impl SystemTimings {
// otherwise, acquire a write lock and insert a new queue // otherwise, acquire a write lock and insert a new queue
timings.with_upgraded(|timings| { timings.with_upgraded(|timings| {
let queue = timings.entry(name).or_insert_with(|| Mutex::new(CircularBuffer::new())); let queue = timings.entry(id).or_insert_with(|| Mutex::new(CircularBuffer::new()));
queue.lock().push_back(duration); queue.lock().push_back(duration);
}); });
} }
pub fn get_stats(&self) -> Map<&'static str, (Duration, Duration), MAX_SYSTEMS> { pub fn get_stats(&self) -> Map<SystemId, (Duration, Duration), MAX_SYSTEMS> {
let timings = self.timings.read(); let timings = self.timings.read();
let mut stats = Map::new(); let mut stats = Map::new();
for (name, queue) in timings.iter() { for (id, queue) in timings.iter() {
if queue.lock().is_empty() { if queue.lock().is_empty() {
continue; continue;
} }
@@ -65,7 +94,7 @@ impl SystemTimings {
let std_dev = variance.sqrt(); let std_dev = variance.sqrt();
stats.insert( stats.insert(
*name, *id,
( (
Duration::from_secs_f64(mean / 1000.0), Duration::from_secs_f64(mean / 1000.0),
Duration::from_secs_f64(std_dev / 1000.0), Duration::from_secs_f64(std_dev / 1000.0),
@@ -101,7 +130,7 @@ impl SystemTimings {
) )
} }
pub fn format_timing_display(&self) -> String { pub fn format_timing_display(&self) -> SmallVec<[String; SystemId::COUNT]> {
let stats = self.get_stats(); let stats = self.get_stats();
let (total_avg, total_std) = self.get_total_stats(); let (total_avg, total_std) = self.get_total_stats();
@@ -126,11 +155,11 @@ impl SystemTimings {
} }
// Use the formatting module to format the data // Use the formatting module to format the data
crate::systems::formatting::format_timing_display(timing_data).join("\n") formatting::format_timing_display(timing_data)
} }
} }
pub fn profile<S, M>(name: &'static str, system: S) -> impl FnMut(&mut bevy_ecs::world::World) pub fn profile<S, M>(id: SystemId, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
where where
S: IntoSystem<(), (), M> + 'static, S: IntoSystem<(), (), M> + 'static,
{ {
@@ -147,7 +176,7 @@ where
let duration = start.elapsed(); let duration = start.elapsed();
if let Some(timings) = world.get_resource::<SystemTimings>() { if let Some(timings) = world.get_resource::<SystemTimings>() {
timings.add_timing(name, duration); timings.add_timing(id, duration);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
use pacman::systems::profiling::SystemTimings; use pacman::systems::profiling::{SystemId, SystemTimings};
use std::time::Duration; use std::time::Duration;
#[test] #[test]
@@ -6,12 +6,12 @@ fn test_timing_statistics() {
let timings = SystemTimings::default(); let timings = SystemTimings::default();
// Add some test data // Add some test data
timings.add_timing("test_system", Duration::from_millis(10)); timings.add_timing(SystemId::PlayerControls, Duration::from_millis(10));
timings.add_timing("test_system", Duration::from_millis(12)); timings.add_timing(SystemId::PlayerControls, Duration::from_millis(12));
timings.add_timing("test_system", Duration::from_millis(8)); timings.add_timing(SystemId::PlayerControls, Duration::from_millis(8));
let stats = timings.get_stats(); let stats = timings.get_stats();
let (avg, std_dev) = stats.get("test_system").unwrap(); let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap();
// Average should be 10ms, standard deviation should be small // Average should be 10ms, standard deviation should be small
assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0); assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0);