mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-10 08:07:56 -06:00
refactor: use strum::EnumCount for const compile time system mapping
This commit is contained in:
@@ -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>,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)>,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user