mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-10 10:08:02 -06:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e630bcbef | ||
|
|
33775166a7 | ||
|
|
f2732a7ff7 | ||
|
|
6771dea02b |
@@ -89,7 +89,7 @@ impl App {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
let dt = self.last_tick.elapsed().as_secs_f32();
|
let dt = self.last_tick.elapsed().as_secs_f32();
|
||||||
self.last_tick = Instant::now();
|
self.last_tick = start;
|
||||||
|
|
||||||
let exit = self.game.tick(dt);
|
let exit = self.game.tick(dt);
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use glam::UVec2;
|
|||||||
///
|
///
|
||||||
/// Calculated as 1/60th of a second (≈16.67ms).
|
/// Calculated as 1/60th of a second (≈16.67ms).
|
||||||
///
|
///
|
||||||
/// Written out explicitly to satisfy const-eval constraints.
|
/// Uses integer arithmetic to avoid floating-point precision loss.
|
||||||
pub const LOOP_TIME: Duration = Duration::from_nanos((1_000_000_000.0 / 60.0) as u64);
|
pub const LOOP_TIME: Duration = Duration::from_nanos(1_000_000_000 / 60);
|
||||||
|
|
||||||
/// The size of each cell, in pixels.
|
/// The size of each cell, in pixels.
|
||||||
pub const CELL_SIZE: u32 = 8;
|
pub const CELL_SIZE: u32 = 8;
|
||||||
|
|||||||
17
src/game.rs
17
src/game.rs
@@ -29,8 +29,9 @@ use bevy_ecs::event::EventRegistry;
|
|||||||
use bevy_ecs::observer::Trigger;
|
use bevy_ecs::observer::Trigger;
|
||||||
use bevy_ecs::schedule::common_conditions::resource_changed;
|
use bevy_ecs::schedule::common_conditions::resource_changed;
|
||||||
use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule, SystemSet};
|
use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule, SystemSet};
|
||||||
use bevy_ecs::system::ResMut;
|
use bevy_ecs::system::{Local, ResMut};
|
||||||
use bevy_ecs::world::World;
|
use bevy_ecs::world::World;
|
||||||
|
use glam::UVec2;
|
||||||
use sdl2::event::EventType;
|
use sdl2::event::EventType;
|
||||||
use sdl2::image::LoadTexture;
|
use sdl2::image::LoadTexture;
|
||||||
use sdl2::render::{BlendMode, Canvas, ScaleMode, TextureCreator};
|
use sdl2::render::{BlendMode, Canvas, ScaleMode, TextureCreator};
|
||||||
@@ -42,7 +43,7 @@ use crate::{
|
|||||||
asset::{get_asset_bytes, Asset},
|
asset::{get_asset_bytes, Asset},
|
||||||
events::GameCommand,
|
events::GameCommand,
|
||||||
map::render::MapRenderer,
|
map::render::MapRenderer,
|
||||||
systems::debug::TtfAtlasResource,
|
systems::debug::{BatchedLinesResource, TtfAtlasResource},
|
||||||
systems::input::{Bindings, CursorPosition},
|
systems::input::{Bindings, CursorPosition},
|
||||||
texture::sprite::{AtlasMapper, SpriteAtlas},
|
texture::sprite::{AtlasMapper, SpriteAtlas},
|
||||||
};
|
};
|
||||||
@@ -126,7 +127,7 @@ impl Game {
|
|||||||
EventType::Display,
|
EventType::Display,
|
||||||
EventType::Window,
|
EventType::Window,
|
||||||
EventType::MouseWheel,
|
EventType::MouseWheel,
|
||||||
EventType::MouseMotion,
|
// EventType::MouseMotion,
|
||||||
EventType::MouseButtonDown,
|
EventType::MouseButtonDown,
|
||||||
EventType::MouseButtonUp,
|
EventType::MouseButtonUp,
|
||||||
EventType::MouseButtonDown,
|
EventType::MouseButtonDown,
|
||||||
@@ -299,6 +300,10 @@ impl Game {
|
|||||||
EventRegistry::register_event::<GameEvent>(&mut world);
|
EventRegistry::register_event::<GameEvent>(&mut world);
|
||||||
EventRegistry::register_event::<AudioEvent>(&mut world);
|
EventRegistry::register_event::<AudioEvent>(&mut world);
|
||||||
|
|
||||||
|
let scale =
|
||||||
|
(UVec2::from(canvas.output_size().unwrap()).as_vec2() / UVec2::from(canvas.logical_size()).as_vec2()).min_element();
|
||||||
|
|
||||||
|
world.insert_resource(BatchedLinesResource::new(&map, scale));
|
||||||
world.insert_resource(Self::create_ghost_animations(&atlas)?);
|
world.insert_resource(Self::create_ghost_animations(&atlas)?);
|
||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
@@ -360,7 +365,11 @@ impl Game {
|
|||||||
schedule.add_systems((
|
schedule.add_systems((
|
||||||
forced_dirty_system.run_if(resource_changed::<ScoreResource>.or(resource_changed::<StartupSequence>)),
|
forced_dirty_system.run_if(resource_changed::<ScoreResource>.or(resource_changed::<StartupSequence>)),
|
||||||
(
|
(
|
||||||
input_system,
|
input_system.run_if(|mut local: Local<u8>| {
|
||||||
|
*local = local.wrapping_add(1u8);
|
||||||
|
// run every nth frame
|
||||||
|
*local % 2 == 0
|
||||||
|
}),
|
||||||
player_control_system,
|
player_control_system,
|
||||||
player_movement_system,
|
player_movement_system,
|
||||||
startup_stage_system,
|
startup_stage_system,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ use sdl2::pixels::Color;
|
|||||||
use sdl2::rect::{Point, Rect};
|
use sdl2::rect::{Point, Rect};
|
||||||
use sdl2::render::{Canvas, Texture};
|
use sdl2::render::{Canvas, Texture};
|
||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug, Copy, Clone)]
|
#[derive(Resource, Default, Debug, Copy, Clone)]
|
||||||
pub struct DebugState {
|
pub struct DebugState {
|
||||||
@@ -28,6 +31,118 @@ pub struct DebugTextureResource(pub Texture);
|
|||||||
/// Resource to hold the TTF text atlas
|
/// Resource to hold the TTF text atlas
|
||||||
pub struct TtfAtlasResource(pub TtfAtlas);
|
pub struct TtfAtlasResource(pub TtfAtlas);
|
||||||
|
|
||||||
|
/// Resource to hold pre-computed batched line segments
|
||||||
|
#[derive(Resource, Default, Debug, Clone)]
|
||||||
|
pub struct BatchedLinesResource {
|
||||||
|
horizontal_lines: Vec<(i32, i32, i32)>, // (y, x_start, x_end)
|
||||||
|
vertical_lines: Vec<(i32, i32, i32)>, // (x, y_start, y_end)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BatchedLinesResource {
|
||||||
|
/// Computes and caches batched line segments for the map graph
|
||||||
|
pub fn new(map: &Map, scale: f32) -> Self {
|
||||||
|
let mut horizontal_segments: HashMap<i32, Vec<(i32, i32)>> = HashMap::new();
|
||||||
|
let mut vertical_segments: HashMap<i32, Vec<(i32, i32)>> = HashMap::new();
|
||||||
|
let mut processed_edges: HashSet<(u16, u16)> = HashSet::new();
|
||||||
|
|
||||||
|
// Process all edges and group them by axis
|
||||||
|
for (start_node_id, edge) in map.graph.edges() {
|
||||||
|
// Acquire a stable key for the edge (from < to)
|
||||||
|
let edge_key = (start_node_id.min(edge.target), start_node_id.max(edge.target));
|
||||||
|
|
||||||
|
// Skip if we've already processed this edge in the reverse direction
|
||||||
|
if processed_edges.contains(&edge_key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processed_edges.insert(edge_key);
|
||||||
|
|
||||||
|
let start_pos = map.graph.get_node(start_node_id).unwrap().position;
|
||||||
|
let end_pos = map.graph.get_node(edge.target).unwrap().position;
|
||||||
|
|
||||||
|
let start = transform_position_with_offset(start_pos, scale);
|
||||||
|
let end = transform_position_with_offset(end_pos, scale);
|
||||||
|
|
||||||
|
// Determine if this is a horizontal or vertical line
|
||||||
|
if (start.y - end.y).abs() < 2 {
|
||||||
|
// Horizontal line (allowing for slight vertical variance)
|
||||||
|
let y = start.y;
|
||||||
|
let x_min = start.x.min(end.x);
|
||||||
|
let x_max = start.x.max(end.x);
|
||||||
|
horizontal_segments.entry(y).or_default().push((x_min, x_max));
|
||||||
|
} else if (start.x - end.x).abs() < 2 {
|
||||||
|
// Vertical line (allowing for slight horizontal variance)
|
||||||
|
let x = start.x;
|
||||||
|
let y_min = start.y.min(end.y);
|
||||||
|
let y_max = start.y.max(end.y);
|
||||||
|
vertical_segments.entry(x).or_default().push((y_min, y_max));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges overlapping or adjacent segments into continuous lines
|
||||||
|
fn merge_segments(segments: Vec<(i32, i32)>) -> Vec<(i32, i32)> {
|
||||||
|
if segments.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut merged = Vec::new();
|
||||||
|
let mut current_start = segments[0].0;
|
||||||
|
let mut current_end = segments[0].1;
|
||||||
|
|
||||||
|
for &(start, end) in segments.iter().skip(1) {
|
||||||
|
if start <= current_end + 1 {
|
||||||
|
// Adjacent or overlapping
|
||||||
|
current_end = current_end.max(end);
|
||||||
|
} else {
|
||||||
|
merged.push((current_start, current_end));
|
||||||
|
current_start = start;
|
||||||
|
current_end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.push((current_start, current_end));
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to flat vectors for fast iteration during rendering
|
||||||
|
let horizontal_lines = horizontal_segments
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(y, mut segments)| {
|
||||||
|
segments.sort_unstable_by_key(|(start, _)| *start);
|
||||||
|
let merged = merge_segments(segments);
|
||||||
|
merged.into_iter().map(move |(x_start, x_end)| (y, x_start, x_end))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let vertical_lines = vertical_segments
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(x, mut segments)| {
|
||||||
|
segments.sort_unstable_by_key(|(start, _)| *start);
|
||||||
|
let merged = merge_segments(segments);
|
||||||
|
merged.into_iter().map(move |(y_start, y_end)| (x, y_start, y_end))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
horizontal_lines,
|
||||||
|
vertical_lines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, canvas: &mut Canvas<Window>) {
|
||||||
|
// Render horizontal lines
|
||||||
|
for &(y, x_start, x_end) in &self.horizontal_lines {
|
||||||
|
let points = [Point::new(x_start, y), Point::new(x_end, y)];
|
||||||
|
let _ = canvas.draw_lines(&points[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render vertical lines
|
||||||
|
for &(x, y_start, y_end) in &self.vertical_lines {
|
||||||
|
let points = [Point::new(x, y_start), Point::new(x, y_end)];
|
||||||
|
let _ = canvas.draw_lines(&points[..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset)
|
/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset)
|
||||||
fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 {
|
fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 {
|
||||||
((pos + BOARD_PIXEL_OFFSET.as_vec2()) * scale).as_ivec2()
|
((pos + BOARD_PIXEL_OFFSET.as_vec2()) * scale).as_ivec2()
|
||||||
@@ -91,6 +206,7 @@ pub fn debug_render_system(
|
|||||||
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
mut debug_texture: NonSendMut<DebugTextureResource>,
|
mut debug_texture: NonSendMut<DebugTextureResource>,
|
||||||
mut ttf_atlas: NonSendMut<TtfAtlasResource>,
|
mut ttf_atlas: NonSendMut<TtfAtlasResource>,
|
||||||
|
batched_lines: Res<BatchedLinesResource>,
|
||||||
debug_state: Res<DebugState>,
|
debug_state: Res<DebugState>,
|
||||||
timings: Res<SystemTimings>,
|
timings: Res<SystemTimings>,
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
@@ -131,55 +247,70 @@ pub fn debug_render_system(
|
|||||||
};
|
};
|
||||||
|
|
||||||
debug_canvas.set_draw_color(Color::GREEN);
|
debug_canvas.set_draw_color(Color::GREEN);
|
||||||
for (collider, position) in colliders.iter() {
|
{
|
||||||
let pos = position.get_pixel_position(&map.graph).unwrap();
|
let rects = colliders
|
||||||
|
.iter()
|
||||||
|
.map(|(collider, position)| {
|
||||||
|
let pos = position.get_pixel_position(&map.graph).unwrap();
|
||||||
|
|
||||||
// Transform position and size using common methods
|
// Transform position and size using common methods
|
||||||
let pos = (pos * scale).as_ivec2();
|
let pos = (pos * scale).as_ivec2();
|
||||||
let size = (collider.size * scale) as u32;
|
let size = (collider.size * scale) as u32;
|
||||||
|
|
||||||
let rect = Rect::from_center(Point::from((pos.x, pos.y)), size, size);
|
Rect::from_center(Point::from((pos.x, pos.y)), size, size)
|
||||||
debug_canvas.draw_rect(rect).unwrap();
|
})
|
||||||
|
.collect::<SmallVec<[Rect; 100]>>();
|
||||||
|
if rects.len() > rects.capacity() {
|
||||||
|
warn!(
|
||||||
|
capacity = rects.capacity(),
|
||||||
|
count = rects.len(),
|
||||||
|
"Collider rects capacity exceeded"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
debug_canvas.draw_rects(&rects).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_canvas.set_draw_color(Color {
|
debug_canvas.set_draw_color(Color {
|
||||||
a: f32_to_u8(0.4),
|
a: f32_to_u8(0.6),
|
||||||
..Color::RED
|
..Color::RED
|
||||||
});
|
});
|
||||||
debug_canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
|
debug_canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
|
||||||
for (start_node, end_node) in map.graph.edges() {
|
|
||||||
let start_node_model = map.graph.get_node(start_node).unwrap();
|
|
||||||
let end_node = map.graph.get_node(end_node.target).unwrap().position;
|
|
||||||
|
|
||||||
// Transform positions using common method
|
// Use cached batched line segments
|
||||||
let start = transform_position_with_offset(start_node_model.position, scale);
|
batched_lines.render(debug_canvas);
|
||||||
let end = transform_position_with_offset(end_node, scale);
|
|
||||||
|
|
||||||
debug_canvas
|
{
|
||||||
.draw_line(Point::from((start.x, start.y)), Point::from((end.x, end.y)))
|
let rects: Vec<_> = map
|
||||||
.unwrap();
|
.graph
|
||||||
}
|
.nodes()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(id, node)| {
|
||||||
|
let pos = transform_position_with_offset(node.position, scale);
|
||||||
|
let size = (2.0 * scale) as u32;
|
||||||
|
let rect = Rect::new(pos.x - (size as i32 / 2), pos.y - (size as i32 / 2), size, size);
|
||||||
|
|
||||||
for (id, node) in map.graph.nodes().enumerate() {
|
// If the node is the one closest to the cursor, draw it immediately
|
||||||
let pos = node.position;
|
if closest_node == Some(id) {
|
||||||
|
debug_canvas.set_draw_color(Color::YELLOW);
|
||||||
|
debug_canvas.fill_rect(rect).unwrap();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// Set color based on whether the node is the closest to the cursor
|
Some(rect)
|
||||||
debug_canvas.set_draw_color(Color {
|
|
||||||
a: f32_to_u8(if Some(id) == closest_node { 0.75 } else { 0.6 }),
|
|
||||||
..(if Some(id) == closest_node {
|
|
||||||
Color::YELLOW
|
|
||||||
} else {
|
|
||||||
Color::BLUE
|
|
||||||
})
|
})
|
||||||
});
|
.collect();
|
||||||
|
|
||||||
// Transform position using common method
|
if rects.len() > rects.capacity() {
|
||||||
let pos = transform_position_with_offset(pos, scale);
|
warn!(
|
||||||
let size = (2.0 * scale) as u32;
|
capacity = rects.capacity(),
|
||||||
|
count = rects.len(),
|
||||||
|
"Node rects capacity exceeded"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
debug_canvas
|
// Draw the non-closest nodes all at once in blue
|
||||||
.fill_rect(Rect::new(pos.x - (size as i32 / 2), pos.y - (size as i32 / 2), size, size))
|
debug_canvas.set_draw_color(Color::BLUE);
|
||||||
.unwrap();
|
debug_canvas.fill_rects(&rects).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render node ID if a node is highlighted
|
// Render node ID if a node is highlighted
|
||||||
|
|||||||
@@ -138,14 +138,12 @@ pub fn input_system(
|
|||||||
|
|
||||||
// Warn if the smallvec was heap allocated due to exceeding stack capacity
|
// Warn if the smallvec was heap allocated due to exceeding stack capacity
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
if frame_events.len() > frame_events.capacity() {
|
||||||
if frame_events.len() > frame_events.capacity() {
|
tracing::warn!(
|
||||||
tracing::warn!(
|
"More than {} events in a frame, consider adjusting stack capacity: {:?}",
|
||||||
"More than {} events in a frame, consider adjusting stack capacity: {:?}",
|
frame_events.capacity(),
|
||||||
frame_events.capacity(),
|
frame_events
|
||||||
frame_events
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle non-keyboard events inline and build a simplified keyboard event stream.
|
// Handle non-keyboard events inline and build a simplified keyboard event stream.
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use bevy_ecs::{resource::Resource, system::System};
|
|||||||
use circular_buffer::CircularBuffer;
|
use circular_buffer::CircularBuffer;
|
||||||
use micromap::Map;
|
use micromap::Map;
|
||||||
use num_width::NumberWidth;
|
use num_width::NumberWidth;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::Mutex;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use strum::EnumCount;
|
use strum::{EnumCount, IntoEnumIterator};
|
||||||
use strum_macros::{EnumCount, IntoStaticStr};
|
use strum_macros::{EnumCount, EnumIter, IntoStaticStr};
|
||||||
use thousands::Separable;
|
use thousands::Separable;
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -16,7 +16,7 @@ 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)]
|
#[derive(EnumCount, EnumIter, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
pub enum SystemId {
|
pub enum SystemId {
|
||||||
Input,
|
Input,
|
||||||
PlayerControls,
|
PlayerControls,
|
||||||
@@ -46,7 +46,7 @@ impl Display for SystemId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug)]
|
#[derive(Resource, 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.
|
||||||
///
|
///
|
||||||
@@ -55,52 +55,64 @@ 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<SystemId, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>,
|
///
|
||||||
|
/// Pre-populated with all SystemId variants during initialization to avoid runtime allocations
|
||||||
|
/// and allow systems to have default zero timings when they don't submit data.
|
||||||
|
pub timings: Map<SystemId, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SystemTimings {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut timings = Map::new();
|
||||||
|
|
||||||
|
// Pre-populate with all SystemId variants to avoid runtime allocations
|
||||||
|
// and provide default zero timings for systems that don't submit data
|
||||||
|
for id in SystemId::iter() {
|
||||||
|
timings.insert(id, Mutex::new(CircularBuffer::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { timings }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemTimings {
|
impl SystemTimings {
|
||||||
pub fn add_timing(&self, id: SystemId, duration: Duration) {
|
pub fn add_timing(&self, id: SystemId, duration: Duration) {
|
||||||
// acquire a upgradable read lock
|
// Since all SystemId variants are pre-populated, we can use a simple read lock
|
||||||
let mut timings = self.timings.upgradable_read();
|
let queue = self
|
||||||
|
.timings
|
||||||
// happy path, the name is already in the map (no need to mutate the hashmap)
|
.get(&id)
|
||||||
if timings.contains_key(&id) {
|
.expect("SystemId not found in pre-populated map - this is a bug");
|
||||||
let queue = timings
|
queue.lock().push_back(duration);
|
||||||
.get(&id)
|
|
||||||
.expect("System name not found in map after contains_key check");
|
|
||||||
let mut queue = queue.lock();
|
|
||||||
|
|
||||||
queue.push_back(duration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, acquire a write lock and insert a new queue
|
|
||||||
timings.with_upgraded(|timings| {
|
|
||||||
let queue = timings.entry(id).or_insert_with(|| Mutex::new(CircularBuffer::new()));
|
|
||||||
queue.lock().push_back(duration);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_stats(&self) -> Map<SystemId, (Duration, Duration), MAX_SYSTEMS> {
|
pub fn get_stats(&self) -> Map<SystemId, (Duration, Duration), MAX_SYSTEMS> {
|
||||||
let timings = self.timings.read();
|
|
||||||
let mut stats = Map::new();
|
let mut stats = Map::new();
|
||||||
|
|
||||||
for (id, queue) in timings.iter() {
|
// Iterate over all SystemId variants to ensure every system has an entry
|
||||||
if queue.lock().is_empty() {
|
for id in SystemId::iter() {
|
||||||
|
let queue = self
|
||||||
|
.timings
|
||||||
|
.get(&id)
|
||||||
|
.expect("SystemId not found in pre-populated map - this is a bug");
|
||||||
|
|
||||||
|
let queue_guard = queue.lock();
|
||||||
|
if queue_guard.is_empty() {
|
||||||
|
// Return zero timing for systems that haven't submitted any data
|
||||||
|
stats.insert(id, (Duration::ZERO, Duration::ZERO));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let durations: Vec<f64> = queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0).collect();
|
let durations: Vec<f64> = queue_guard.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
|
||||||
let count = durations.len() as f64;
|
let count = durations.len() as f64;
|
||||||
|
|
||||||
let sum: f64 = durations.iter().sum();
|
let sum: f64 = durations.iter().sum();
|
||||||
let mean = sum / count;
|
let mean = sum / count;
|
||||||
|
|
||||||
let variance = durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
|
let variance = durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (count - 1.0).max(1.0);
|
||||||
let std_dev = variance.sqrt();
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
stats.insert(
|
stats.insert(
|
||||||
*id,
|
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),
|
||||||
@@ -113,8 +125,7 @@ impl SystemTimings {
|
|||||||
|
|
||||||
pub fn get_total_stats(&self) -> (Duration, Duration) {
|
pub fn get_total_stats(&self) -> (Duration, Duration) {
|
||||||
let duration_sums = {
|
let duration_sums = {
|
||||||
let timings = self.timings.read();
|
self.timings
|
||||||
timings
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, queue)| queue.lock().iter().sum::<Duration>())
|
.map(|(_, queue)| queue.lock().iter().sum::<Duration>())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -128,7 +139,7 @@ impl SystemTimings {
|
|||||||
diff_secs * diff_secs
|
diff_secs * diff_secs
|
||||||
})
|
})
|
||||||
.sum::<f64>()
|
.sum::<f64>()
|
||||||
/ duration_sums.len() as f64;
|
/ (duration_sums.len() - 1).max(1) as f64;
|
||||||
let std_dev_secs = variance.sqrt();
|
let std_dev_secs = variance.sqrt();
|
||||||
|
|
||||||
(mean, Duration::from_secs_f64(std_dev_secs))
|
(mean, Duration::from_secs_f64(std_dev_secs))
|
||||||
@@ -250,17 +261,22 @@ pub fn format_timing_display(
|
|||||||
})
|
})
|
||||||
.collect::<SmallVec<[Entry; 12]>>();
|
.collect::<SmallVec<[Entry; 12]>>();
|
||||||
|
|
||||||
let (max_name_width, max_avg_int_width, max_avg_decimal_width, max_std_int_width, max_std_decimal_width) = entries
|
let (max_avg_int_width, max_avg_decimal_width, max_std_int_width, max_std_decimal_width) =
|
||||||
.iter()
|
entries
|
||||||
.fold((0, 0, 3, 0, 3), |(name_w, avg_int_w, avg_dec_w, std_int_w, std_dec_w), e| {
|
.iter()
|
||||||
(
|
.fold((0, 3, 0, 3), |(avg_int_w, avg_dec_w, std_int_w, std_dec_w), e| {
|
||||||
name_w.max(e.name.len()),
|
(
|
||||||
avg_int_w.max(e.avg_int.width() as usize),
|
avg_int_w.max(e.avg_int.width() as usize),
|
||||||
avg_dec_w.max(e.avg_decimal.width() as usize),
|
avg_dec_w.max(e.avg_decimal.width() as usize),
|
||||||
std_int_w.max(e.std_int.width() as usize),
|
std_int_w.max(e.std_int.width() as usize),
|
||||||
std_dec_w.max(e.std_decimal.width() as usize),
|
std_dec_w.max(e.std_decimal.width() as usize),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let max_name_width = SystemId::iter()
|
||||||
|
.map(|id| id.to_string().len())
|
||||||
|
.max()
|
||||||
|
.expect("SystemId::iter() returned an empty iterator");
|
||||||
|
|
||||||
entries.iter().map(|e| {
|
entries.iter().map(|e| {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
use pacman::systems::profiling::{SystemId, SystemTimings};
|
use pacman::systems::profiling::{SystemId, SystemTimings};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
macro_rules! assert_close {
|
||||||
|
($actual:expr, $expected:expr, $concern:expr) => {
|
||||||
|
let tolerance = Duration::from_micros(500);
|
||||||
|
let diff = $actual.abs_diff($expected);
|
||||||
|
assert!(
|
||||||
|
diff < tolerance,
|
||||||
|
"Expected {expected:?} ± {tolerance:.0?}, got {actual:?}, off by {diff:?} ({concern})",
|
||||||
|
concern = $concern,
|
||||||
|
expected = $expected,
|
||||||
|
actual = $actual,
|
||||||
|
tolerance = tolerance,
|
||||||
|
diff = diff
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_timing_statistics() {
|
fn test_timing_statistics() {
|
||||||
@@ -14,47 +31,80 @@ fn test_timing_statistics() {
|
|||||||
timings.add_timing(SystemId::Blinking, Duration::from_millis(3));
|
timings.add_timing(SystemId::Blinking, Duration::from_millis(3));
|
||||||
timings.add_timing(SystemId::Blinking, Duration::from_millis(2));
|
timings.add_timing(SystemId::Blinking, Duration::from_millis(2));
|
||||||
timings.add_timing(SystemId::Blinking, Duration::from_millis(1));
|
timings.add_timing(SystemId::Blinking, Duration::from_millis(1));
|
||||||
fn close_enough(a: Duration, b: Duration) -> bool {
|
|
||||||
if a > b {
|
{
|
||||||
a - b < Duration::from_micros(500) // 0.1ms
|
let stats = timings.get_stats();
|
||||||
} else {
|
let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap();
|
||||||
b - a < Duration::from_micros(500)
|
|
||||||
}
|
assert_close!(*avg, Duration::from_millis(10), "PlayerControls average timing");
|
||||||
|
assert_close!(*std_dev, Duration::from_millis(2), "PlayerControls standard deviation timing");
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = timings.get_stats();
|
{
|
||||||
let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap();
|
let (total_avg, total_std) = timings.get_total_stats();
|
||||||
|
assert_close!(total_avg, Duration::from_millis(2), "Total average timing across all systems");
|
||||||
// Average should be 10ms, standard deviation should be small
|
assert_close!(
|
||||||
assert!(close_enough(*avg, Duration::from_millis(10)), "avg: {:?}", avg);
|
total_std,
|
||||||
assert!(close_enough(*std_dev, Duration::from_millis(2)), "std_dev: {:?}", std_dev);
|
Duration::from_millis(7),
|
||||||
|
"Total standard deviation timing across all systems"
|
||||||
let (total_avg, total_std) = timings.get_total_stats();
|
);
|
||||||
assert!(
|
}
|
||||||
close_enough(total_avg, Duration::from_millis(18)),
|
|
||||||
"total_avg: {:?}",
|
|
||||||
total_avg
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
close_enough(total_std, Duration::from_millis(12)),
|
|
||||||
"total_std: {:?}",
|
|
||||||
total_std
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[test]
|
#[test]
|
||||||
// fn test_window_size_limit() {
|
fn test_default_zero_timing_for_unused_systems() {
|
||||||
// let timings = SystemTimings::default();
|
let timings = SystemTimings::default();
|
||||||
|
|
||||||
// // Add more than 90 timings to test window size limit
|
// Add timing data for only one system
|
||||||
// for i in 0..100 {
|
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(5));
|
||||||
// timings.add_timing("test_system", Duration::from_millis(i));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let stats = timings.get_stats();
|
let stats = timings.get_stats();
|
||||||
// let (avg, _) = stats.get("test_system").unwrap();
|
|
||||||
|
|
||||||
// // Should only keep the last 90 values, so average should be around 55ms
|
// Verify all SystemId variants are present in the stats
|
||||||
// // (average of 10-99)
|
let expected_count = SystemId::iter().count();
|
||||||
// assert!((avg.as_millis() as f64 - 55.0).abs() < 5.0);
|
assert_eq!(stats.len(), expected_count, "All SystemId variants should be in stats");
|
||||||
// }
|
|
||||||
|
// Verify that the system with data has non-zero timing
|
||||||
|
let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap();
|
||||||
|
assert_close!(*avg, Duration::from_millis(5), "System with data should have correct timing");
|
||||||
|
assert_close!(*std_dev, Duration::ZERO, "Single measurement should have zero std dev");
|
||||||
|
|
||||||
|
// Verify that all other systems have zero timing
|
||||||
|
for id in SystemId::iter() {
|
||||||
|
if id != SystemId::PlayerControls {
|
||||||
|
let (avg, std_dev) = stats.get(&id).unwrap();
|
||||||
|
assert_close!(
|
||||||
|
*avg,
|
||||||
|
Duration::ZERO,
|
||||||
|
format!("Unused system {:?} should have zero avg timing", id)
|
||||||
|
);
|
||||||
|
assert_close!(
|
||||||
|
*std_dev,
|
||||||
|
Duration::ZERO,
|
||||||
|
format!("Unused system {:?} should have zero std dev", id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pre_populated_timing_entries() {
|
||||||
|
let timings = SystemTimings::default();
|
||||||
|
|
||||||
|
// Verify that we can add timing to any SystemId without panicking
|
||||||
|
// (this would fail with the old implementation if the entry didn't exist)
|
||||||
|
for id in SystemId::iter() {
|
||||||
|
timings.add_timing(id, Duration::from_nanos(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all systems now have non-zero timing
|
||||||
|
let stats = timings.get_stats();
|
||||||
|
for id in SystemId::iter() {
|
||||||
|
let (avg, _) = stats.get(&id).unwrap();
|
||||||
|
assert!(
|
||||||
|
*avg > Duration::ZERO,
|
||||||
|
"System {:?} should have non-zero timing after add_timing",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user