mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 15:15:48 -06:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0133dd5329 | |||
| 635418a4da | |||
| 31193160a9 | |||
| 3086453c7b | |||
| a8b83b8e2b | |||
| 8ce2af89c8 | |||
| 5f0ee87dd9 | |||
| b88895e82f |
51
Cargo.lock
generated
51
Cargo.lock
generated
@@ -252,6 +252,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "circular-buffer"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23bdce1da528cadbac4654b5632bfcd8c6c63e25b1d42cea919a95958790b51d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -316,6 +322,12 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "disqualified"
|
name = "disqualified"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -528,6 +540,12 @@ version = "2.7.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "micromap"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18c087666f377f857b49564f8791b481260c67825d6b337e1e38ddf54a985a88"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonmax"
|
name = "nonmax"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -553,6 +571,12 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-width"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faede9396d7883a8c9c989e0b53c984bf770defb5cb8ed6c345b4c0566cf32b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -572,13 +596,17 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
|
"circular-buffer",
|
||||||
"glam 0.30.5",
|
"glam 0.30.5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
|
"micromap",
|
||||||
|
"num-width",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pathfinding",
|
"pathfinding",
|
||||||
"phf",
|
"phf",
|
||||||
|
"pretty_assertions",
|
||||||
"rand",
|
"rand",
|
||||||
"sdl2",
|
"sdl2",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -588,6 +616,7 @@ dependencies = [
|
|||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"thousands",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -701,6 +730,16 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.95"
|
||||||
@@ -992,6 +1031,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thousands"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.7"
|
version = "1.1.7"
|
||||||
@@ -1415,3 +1460,9 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ phf = { version = "0.12.1", features = ["macros"] }
|
|||||||
bevy_ecs = "0.16.1"
|
bevy_ecs = "0.16.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.1"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
|
micromap = "0.1.0"
|
||||||
|
thousands = "0.2.0"
|
||||||
|
pretty_assertions = "1.4.1"
|
||||||
|
num-width = "0.1.0"
|
||||||
|
circular-buffer = "1.1.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
13
src/app.rs
13
src/app.rs
@@ -5,14 +5,12 @@ use sdl2::render::TextureCreator;
|
|||||||
use sdl2::ttf::Sdl2TtfContext;
|
use sdl2::ttf::Sdl2TtfContext;
|
||||||
use sdl2::video::WindowContext;
|
use sdl2::video::WindowContext;
|
||||||
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
||||||
use tracing::{field, info, warn};
|
|
||||||
|
|
||||||
use crate::error::{GameError, GameResult};
|
use crate::error::{GameError, GameResult};
|
||||||
|
|
||||||
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
|
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
|
||||||
use crate::game::Game;
|
use crate::game::Game;
|
||||||
use crate::platform::get_platform;
|
use crate::platform::get_platform;
|
||||||
use crate::systems::profiling::SystemTimings;
|
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub game: Game,
|
pub game: Game,
|
||||||
@@ -110,17 +108,6 @@ impl App {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show timings if the loop took more than 25% of the loop time
|
|
||||||
let show_timings = start.elapsed() > (LOOP_TIME / 4);
|
|
||||||
if show_timings || true {
|
|
||||||
if let Some(timings) = self.game.world.get_resource::<SystemTimings>() {
|
|
||||||
let mut timings = timings.timings.lock();
|
|
||||||
let total = timings.values().sum::<Duration>();
|
|
||||||
info!("Total: {:?}, Timings: {:?}", total, field::debug(&timings));
|
|
||||||
timings.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep if we still have time left
|
// Sleep if we still have time left
|
||||||
if start.elapsed() < LOOP_TIME {
|
if start.elapsed() < LOOP_TIME {
|
||||||
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
||||||
|
|||||||
@@ -223,6 +223,19 @@ impl Graph {
|
|||||||
self.nodes.len()
|
self.nodes.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all nodes in the graph.
|
||||||
|
pub fn nodes(&self) -> impl Iterator<Item = &Node> {
|
||||||
|
self.nodes.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all edges in the graph.
|
||||||
|
pub fn edges(&self) -> impl Iterator<Item = (NodeId, Edge)> + '_ {
|
||||||
|
self.adjacency_list
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id, edge)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Finds a specific edge from a source node to a target node.
|
/// Finds a specific edge from a source node to a target node.
|
||||||
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<Edge> {
|
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<Edge> {
|
||||||
self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to)
|
self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to)
|
||||||
|
|||||||
@@ -14,17 +14,25 @@ use crate::systems::{
|
|||||||
collision::collision_system,
|
collision::collision_system,
|
||||||
components::{
|
components::{
|
||||||
Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider,
|
Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider,
|
||||||
PacmanCollider, PlayerBundle, PlayerControlled, Renderable, Score, ScoreResource,
|
PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, Score, ScoreResource,
|
||||||
},
|
},
|
||||||
control::player_system,
|
control::player_system,
|
||||||
|
debug::{debug_render_system, DebugState, DebugTextureResource},
|
||||||
input::input_system,
|
input::input_system,
|
||||||
movement::movement_system,
|
movement::movement_system,
|
||||||
profiling::{profile, SystemTimings},
|
profiling::{profile, SystemTimings},
|
||||||
render::{directional_render_system, render_system, BackbufferResource, MapTextureResource},
|
render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource},
|
||||||
};
|
};
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use bevy_ecs::schedule::IntoScheduleConfigs;
|
use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||||
use bevy_ecs::{event::EventRegistry, observer::Trigger, schedule::Schedule, system::ResMut, world::World};
|
use bevy_ecs::system::NonSendMut;
|
||||||
|
use bevy_ecs::{
|
||||||
|
event::EventRegistry,
|
||||||
|
observer::Trigger,
|
||||||
|
schedule::Schedule,
|
||||||
|
system::{Res, ResMut},
|
||||||
|
world::World,
|
||||||
|
};
|
||||||
use sdl2::image::LoadTexture;
|
use sdl2::image::LoadTexture;
|
||||||
use sdl2::render::{Canvas, ScaleMode, TextureCreator};
|
use sdl2::render::{Canvas, ScaleMode, TextureCreator};
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::video::{Window, WindowContext};
|
||||||
@@ -72,6 +80,13 @@ impl Game {
|
|||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
map_texture.set_scale_mode(ScaleMode::Nearest);
|
map_texture.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
|
// Create debug texture at output resolution for crisp debug rendering
|
||||||
|
let output_size = canvas.output_size().unwrap();
|
||||||
|
let mut debug_texture = texture_creator
|
||||||
|
.create_texture_target(None, output_size.0, output_size.1)
|
||||||
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
debug_texture.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
// Load atlas and create map texture
|
// Load atlas and create map texture
|
||||||
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
||||||
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
||||||
@@ -157,7 +172,7 @@ impl Game {
|
|||||||
},
|
},
|
||||||
entity_type: EntityType::Player,
|
entity_type: EntityType::Player,
|
||||||
collider: Collider {
|
collider: Collider {
|
||||||
size: constants::CELL_SIZE as f32 * 1.25,
|
size: constants::CELL_SIZE as f32 * 1.375,
|
||||||
layer: CollisionLayer::PACMAN,
|
layer: CollisionLayer::PACMAN,
|
||||||
},
|
},
|
||||||
pacman_collider: PacmanCollider,
|
pacman_collider: PacmanCollider,
|
||||||
@@ -168,6 +183,7 @@ impl Game {
|
|||||||
world.insert_non_send_resource(canvas);
|
world.insert_non_send_resource(canvas);
|
||||||
world.insert_non_send_resource(BackbufferResource(backbuffer));
|
world.insert_non_send_resource(BackbufferResource(backbuffer));
|
||||||
world.insert_non_send_resource(MapTextureResource(map_texture));
|
world.insert_non_send_resource(MapTextureResource(map_texture));
|
||||||
|
world.insert_non_send_resource(DebugTextureResource(debug_texture));
|
||||||
|
|
||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
@@ -175,6 +191,8 @@ impl Game {
|
|||||||
world.insert_resource(SystemTimings::default());
|
world.insert_resource(SystemTimings::default());
|
||||||
world.insert_resource(Bindings::default());
|
world.insert_resource(Bindings::default());
|
||||||
world.insert_resource(DeltaTime(0f32));
|
world.insert_resource(DeltaTime(0f32));
|
||||||
|
world.insert_resource(RenderDirty::default());
|
||||||
|
world.insert_resource(DebugState::default());
|
||||||
|
|
||||||
world.add_observer(
|
world.add_observer(
|
||||||
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| match *event {
|
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| match *event {
|
||||||
@@ -187,7 +205,6 @@ impl Game {
|
|||||||
GameEvent::Collision(_a, _b) => {}
|
GameEvent::Collision(_a, _b) => {}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
schedule.add_systems(
|
schedule.add_systems(
|
||||||
(
|
(
|
||||||
profile("input", input_system),
|
profile("input", input_system),
|
||||||
@@ -196,11 +213,29 @@ impl Game {
|
|||||||
profile("collision", collision_system),
|
profile("collision", collision_system),
|
||||||
profile("blinking", blinking_system),
|
profile("blinking", blinking_system),
|
||||||
profile("directional_render", directional_render_system),
|
profile("directional_render", directional_render_system),
|
||||||
|
profile("dirty_render", dirty_render_system),
|
||||||
|
profile("render", render_system),
|
||||||
|
profile("debug_render", debug_render_system),
|
||||||
|
profile(
|
||||||
|
"present",
|
||||||
|
|mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
|
backbuffer: NonSendMut<BackbufferResource>,
|
||||||
|
debug_state: Res<DebugState>,
|
||||||
|
mut dirty: ResMut<RenderDirty>| {
|
||||||
|
if dirty.0 || *debug_state != DebugState::Off {
|
||||||
|
// Only copy backbuffer to main canvas if debug rendering is off
|
||||||
|
// (debug rendering draws directly to main canvas)
|
||||||
|
if *debug_state == DebugState::Off {
|
||||||
|
canvas.copy(&backbuffer.0, None, None).unwrap();
|
||||||
|
}
|
||||||
|
dirty.0 = false;
|
||||||
|
canvas.present();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain(),
|
||||||
);
|
);
|
||||||
schedule.add_systems(profile("render", render_system));
|
|
||||||
|
|
||||||
// Spawn player
|
// Spawn player
|
||||||
world.spawn(player);
|
world.spawn(player);
|
||||||
|
|
||||||
@@ -214,12 +249,12 @@ impl Game {
|
|||||||
|
|
||||||
for (node_id, tile) in nodes {
|
for (node_id, tile) in nodes {
|
||||||
let (item_type, score, sprite, size) = match tile {
|
let (item_type, score, sprite, size) = match tile {
|
||||||
crate::constants::MapTile::Pellet => (EntityType::Pellet, 10, pellet_sprite, constants::CELL_SIZE as f32 * 0.2),
|
crate::constants::MapTile::Pellet => (EntityType::Pellet, 10, pellet_sprite, constants::CELL_SIZE as f32 * 0.4),
|
||||||
crate::constants::MapTile::PowerPellet => (
|
crate::constants::MapTile::PowerPellet => (
|
||||||
EntityType::PowerPellet,
|
EntityType::PowerPellet,
|
||||||
50,
|
50,
|
||||||
energizer_sprite,
|
energizer_sprite,
|
||||||
constants::CELL_SIZE as f32 * 0.9,
|
constants::CELL_SIZE as f32 * 0.95,
|
||||||
),
|
),
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,3 +109,6 @@ pub struct ScoreResource(pub u32);
|
|||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct DeltaTime(pub f32);
|
pub struct DeltaTime(pub f32);
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct RenderDirty(pub bool);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
error::GameError,
|
error::GameError,
|
||||||
events::{GameCommand, GameEvent},
|
events::{GameCommand, GameEvent},
|
||||||
systems::components::{GlobalState, PlayerControlled},
|
systems::components::{GlobalState, PlayerControlled},
|
||||||
|
systems::debug::DebugState,
|
||||||
systems::movement::Movable,
|
systems::movement::Movable,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ use crate::{
|
|||||||
pub fn player_system(
|
pub fn player_system(
|
||||||
mut events: EventReader<GameEvent>,
|
mut events: EventReader<GameEvent>,
|
||||||
mut state: ResMut<GlobalState>,
|
mut state: ResMut<GlobalState>,
|
||||||
|
mut debug_state: ResMut<DebugState>,
|
||||||
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
@@ -38,6 +40,9 @@ pub fn player_system(
|
|||||||
GameCommand::Exit => {
|
GameCommand::Exit => {
|
||||||
state.exit = true;
|
state.exit = true;
|
||||||
}
|
}
|
||||||
|
GameCommand::ToggleDebug => {
|
||||||
|
*debug_state = debug_state.next();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
217
src/systems/debug.rs
Normal file
217
src/systems/debug.rs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
//! Debug rendering system
|
||||||
|
use crate::constants::BOARD_PIXEL_OFFSET;
|
||||||
|
use crate::map::builder::Map;
|
||||||
|
use crate::systems::components::Collider;
|
||||||
|
use crate::systems::movement::Position;
|
||||||
|
use crate::systems::profiling::SystemTimings;
|
||||||
|
use crate::systems::render::BackbufferResource;
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
use sdl2::rect::Rect;
|
||||||
|
use sdl2::render::{Canvas, Texture, TextureCreator};
|
||||||
|
use sdl2::video::{Window, WindowContext};
|
||||||
|
|
||||||
|
#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum DebugState {
|
||||||
|
#[default]
|
||||||
|
Off,
|
||||||
|
Graph,
|
||||||
|
Collision,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugState {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
DebugState::Off => DebugState::Graph,
|
||||||
|
DebugState::Graph => DebugState::Collision,
|
||||||
|
DebugState::Collision => DebugState::Off,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resource to hold the debug texture for persistent rendering
|
||||||
|
pub struct DebugTextureResource(pub Texture<'static>);
|
||||||
|
|
||||||
|
/// Transforms a position from logical canvas coordinates to output canvas coordinates
|
||||||
|
fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
||||||
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
|
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
||||||
|
|
||||||
|
let x = (pos.0 * scale) as i32;
|
||||||
|
let y = (pos.1 * scale) as i32;
|
||||||
|
(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset)
|
||||||
|
fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
||||||
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
|
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
||||||
|
|
||||||
|
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32;
|
||||||
|
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32;
|
||||||
|
(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transforms a size from logical canvas coordinates to output canvas coordinates
|
||||||
|
fn transform_size(size: f32, output_size: (u32, u32), logical_size: (u32, u32)) -> u32 {
|
||||||
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
|
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
||||||
|
|
||||||
|
(size * scale) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders timing information in the top-left corner of the screen
|
||||||
|
fn render_timing_display(
|
||||||
|
canvas: &mut Canvas<Window>,
|
||||||
|
texture_creator: &mut TextureCreator<WindowContext>,
|
||||||
|
timings: &SystemTimings,
|
||||||
|
) {
|
||||||
|
// Get TTF context
|
||||||
|
let ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
|
||||||
|
// Load font
|
||||||
|
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
|
||||||
|
|
||||||
|
// Format timing information using the formatting module
|
||||||
|
let timing_text = 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 padding = 10;
|
||||||
|
|
||||||
|
// Calculate background dimensions
|
||||||
|
let max_width = lines
|
||||||
|
.iter()
|
||||||
|
.filter(|&&l| !l.is_empty()) // Don't consider empty lines for width
|
||||||
|
.map(|line| font.size_of(line).unwrap().0)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Only draw background if there is text to display
|
||||||
|
if max_width > 0 {
|
||||||
|
let total_height = (lines.len() as u32) * line_height as u32;
|
||||||
|
let bg_padding = 5;
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
let bg_rect = Rect::new(
|
||||||
|
padding - bg_padding,
|
||||||
|
padding - bg_padding,
|
||||||
|
max_width + (bg_padding * 2) as u32,
|
||||||
|
total_height + bg_padding as u32,
|
||||||
|
);
|
||||||
|
canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
|
||||||
|
canvas.set_draw_color(Color::RGBA(40, 40, 40, 180));
|
||||||
|
canvas.fill_rect(bg_rect).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render each line
|
||||||
|
let surface = font.render(line).blended(Color::RGBA(255, 255, 255, 200)).unwrap();
|
||||||
|
let texture = texture_creator.create_texture_from_surface(&surface).unwrap();
|
||||||
|
|
||||||
|
// Position each line below the previous one
|
||||||
|
let y_pos = padding + (i * line_height) as i32;
|
||||||
|
let dest = Rect::new(padding, y_pos, texture.query().width, texture.query().height);
|
||||||
|
canvas.copy(&texture, None, dest).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_render_system(
|
||||||
|
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
|
backbuffer: NonSendMut<BackbufferResource>,
|
||||||
|
mut debug_texture: NonSendMut<DebugTextureResource>,
|
||||||
|
debug_state: Res<DebugState>,
|
||||||
|
timings: Res<SystemTimings>,
|
||||||
|
map: Res<Map>,
|
||||||
|
colliders: Query<(&Collider, &Position)>,
|
||||||
|
) {
|
||||||
|
if *debug_state == DebugState::Off {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canvas sizes for coordinate transformation
|
||||||
|
let output_size = canvas.output_size().unwrap();
|
||||||
|
let logical_size = canvas.logical_size();
|
||||||
|
|
||||||
|
// Copy the current backbuffer to the debug texture
|
||||||
|
canvas
|
||||||
|
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
|
||||||
|
// Clear the debug canvas
|
||||||
|
debug_canvas.set_draw_color(Color::BLACK);
|
||||||
|
debug_canvas.clear();
|
||||||
|
|
||||||
|
// Copy the backbuffer to the debug canvas
|
||||||
|
debug_canvas.copy(&backbuffer.0, None, None).unwrap();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get texture creator before entering the closure to avoid borrowing conflicts
|
||||||
|
let mut texture_creator = canvas.texture_creator();
|
||||||
|
|
||||||
|
// Draw debug info on the high-resolution debug texture
|
||||||
|
canvas
|
||||||
|
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
|
||||||
|
match *debug_state {
|
||||||
|
DebugState::Graph => {
|
||||||
|
debug_canvas.set_draw_color(Color::RED);
|
||||||
|
for (start_node, end_node) in map.graph.edges() {
|
||||||
|
let start_node = map.graph.get_node(start_node).unwrap().position;
|
||||||
|
let end_node = map.graph.get_node(end_node.target).unwrap().position;
|
||||||
|
|
||||||
|
// Transform positions using common method
|
||||||
|
let (start_x, start_y) =
|
||||||
|
transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size);
|
||||||
|
let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size);
|
||||||
|
|
||||||
|
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_canvas.set_draw_color(Color::BLUE);
|
||||||
|
for node in map.graph.nodes() {
|
||||||
|
let pos = node.position;
|
||||||
|
|
||||||
|
// Transform position using common method
|
||||||
|
let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size);
|
||||||
|
let size = transform_size(4.0, output_size, logical_size);
|
||||||
|
|
||||||
|
debug_canvas
|
||||||
|
.fill_rect(Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebugState::Collision => {
|
||||||
|
debug_canvas.set_draw_color(Color::GREEN);
|
||||||
|
for (collider, position) in colliders.iter() {
|
||||||
|
let pos = position.get_pixel_pos(&map.graph).unwrap();
|
||||||
|
|
||||||
|
// Transform position and size using common methods
|
||||||
|
let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size);
|
||||||
|
let size = transform_size(collider.size, output_size, logical_size);
|
||||||
|
|
||||||
|
// Center the collision box on the entity
|
||||||
|
let rect = Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size);
|
||||||
|
debug_canvas.draw_rect(rect).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render timing information in the top-left corner
|
||||||
|
render_timing_display(debug_canvas, &mut texture_creator, &timings);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Draw the debug texture directly onto the main canvas at full resolution
|
||||||
|
canvas.copy(&debug_texture.0, None, None).unwrap();
|
||||||
|
}
|
||||||
147
src/systems/formatting.rs
Normal file
147
src/systems/formatting.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use num_width::NumberWidth;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Formats timing data into a vector of strings with proper alignment
|
||||||
|
pub fn format_timing_display(timing_data: Vec<(String, Duration, Duration)>) -> String {
|
||||||
|
if timing_data.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to split a duration into a integer, decimal, and unit
|
||||||
|
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
||||||
|
let (int, decimal, unit) = match duration {
|
||||||
|
// if greater than 1 second, return as seconds
|
||||||
|
n if n >= &Duration::from_secs(1) => {
|
||||||
|
let secs = n.as_secs();
|
||||||
|
let decimal = n.as_millis() as u64 % 1000;
|
||||||
|
(secs, decimal as u32, "s")
|
||||||
|
}
|
||||||
|
// if greater than 1 millisecond, return as milliseconds
|
||||||
|
n if n >= &Duration::from_millis(1) => {
|
||||||
|
let ms = n.as_millis() as u64;
|
||||||
|
let decimal = n.as_micros() as u64 % 1000;
|
||||||
|
(ms, decimal as u32, "ms")
|
||||||
|
}
|
||||||
|
// if greater than 1 microsecond, return as microseconds
|
||||||
|
n if n >= &Duration::from_micros(1) => {
|
||||||
|
let us = n.as_micros() as u64;
|
||||||
|
let decimal = n.as_nanos() as u64 % 1000;
|
||||||
|
(us, decimal as u32, "µs")
|
||||||
|
}
|
||||||
|
// otherwise, return as nanoseconds
|
||||||
|
n => {
|
||||||
|
let ns = n.as_nanos() as u64;
|
||||||
|
(ns, 0, "ns")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(int, decimal, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
name: String,
|
||||||
|
avg_int: u64,
|
||||||
|
avg_decimal: u32,
|
||||||
|
avg_unit: &'static str,
|
||||||
|
std_int: u64,
|
||||||
|
std_decimal: u32,
|
||||||
|
std_unit: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = timing_data
|
||||||
|
.iter()
|
||||||
|
.map(|(name, avg, std_dev)| {
|
||||||
|
let (avg_int, avg_decimal, avg_unit) = get_value(&avg);
|
||||||
|
let (std_int, std_decimal, std_unit) = get_value(&std_dev);
|
||||||
|
|
||||||
|
Entry {
|
||||||
|
name: name.clone(),
|
||||||
|
avg_int,
|
||||||
|
avg_decimal,
|
||||||
|
avg_unit,
|
||||||
|
std_int,
|
||||||
|
std_decimal,
|
||||||
|
std_unit,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let max_name_width = entries.iter().map(|e| e.name.len() as usize).max().unwrap_or(0);
|
||||||
|
let max_avg_int_width = entries.iter().map(|e| e.avg_int.width() as usize).max().unwrap_or(0);
|
||||||
|
let max_avg_decimal_width = entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.avg_decimal.width() as usize)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.max(3);
|
||||||
|
let max_std_int_width = entries.iter().map(|e| e.std_int.width() as usize).max().unwrap_or(0);
|
||||||
|
let max_std_decimal_width = entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.std_decimal.width() as usize)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.max(3);
|
||||||
|
|
||||||
|
let mut output_lines = Vec::new();
|
||||||
|
|
||||||
|
// Format each line using the calculated max widths for alignment
|
||||||
|
for Entry {
|
||||||
|
name,
|
||||||
|
avg_int,
|
||||||
|
avg_decimal,
|
||||||
|
avg_unit,
|
||||||
|
std_int,
|
||||||
|
std_decimal,
|
||||||
|
std_unit,
|
||||||
|
} in entries.iter()
|
||||||
|
{
|
||||||
|
output_lines.push(format!(
|
||||||
|
"{name:max_name_width$} : {avg_int:max_avg_int_width$}.{avg_decimal:<max_avg_decimal_width$}{avg_unit} ± {std_int:max_std_int_width$}.{std_decimal:<max_std_decimal_width$}{std_unit}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
output_lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_timing_display() {
|
||||||
|
let timing_data = vec![
|
||||||
|
("total".to_string(), Duration::from_micros(1234), Duration::from_micros(570)),
|
||||||
|
("input".to_string(), Duration::from_micros(120), Duration::from_micros(45)),
|
||||||
|
("player".to_string(), Duration::from_micros(456), Duration::from_micros(123)),
|
||||||
|
("movement".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
||||||
|
("render".to_string(), Duration::from_micros(12), Duration::from_micros(3)),
|
||||||
|
("debug".to_string(), Duration::from_nanos(460), Duration::from_nanos(557)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
let lines: Vec<&str> = result.lines().collect();
|
||||||
|
|
||||||
|
// Verify we have the expected number of lines
|
||||||
|
assert_eq!(lines.len(), 6);
|
||||||
|
|
||||||
|
let expected = r#"
|
||||||
|
total : 1.234ms ± 570.0 µs
|
||||||
|
input : 120.0 µs ± 45.0 µs
|
||||||
|
player : 456.0 µs ± 123.0 µs
|
||||||
|
movement : 789.0 µs ± 234.0 µs
|
||||||
|
render : 12.0 µs ± 3.0 µs
|
||||||
|
debug : 460.0 ns ± 557.0 ns
|
||||||
|
"#
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
for (line, expected_line) in lines.iter().zip(expected.lines()) {
|
||||||
|
assert_eq!(*line, expected_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the result for manual inspection
|
||||||
|
println!("Formatted output:");
|
||||||
|
println!("{}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,6 @@ pub fn input_system(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>,
|
|||||||
Event::KeyDown { keycode: Some(key), .. } => {
|
Event::KeyDown { keycode: Some(key), .. } => {
|
||||||
let command = bindings.key_bindings.get(&key).copied();
|
let command = bindings.key_bindings.get(&key).copied();
|
||||||
if let Some(command) = command {
|
if let Some(command) = command {
|
||||||
tracing::info!("triggering command: {:?}", command);
|
|
||||||
writer.write(GameEvent::Command(command));
|
writer.write(GameEvent::Command(command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ pub mod blinking;
|
|||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod control;
|
pub mod control;
|
||||||
|
pub mod debug;
|
||||||
|
pub mod formatting;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod profiling;
|
pub mod profiling;
|
||||||
|
|||||||
@@ -1,12 +1,130 @@
|
|||||||
use bevy_ecs::prelude::Resource;
|
use bevy_ecs::prelude::Resource;
|
||||||
use bevy_ecs::system::{IntoSystem, System};
|
use bevy_ecs::system::{IntoSystem, System};
|
||||||
use parking_lot::Mutex;
|
use circular_buffer::CircularBuffer;
|
||||||
use std::collections::HashMap;
|
use micromap::Map;
|
||||||
|
use parking_lot::{Mutex, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use thousands::Separable;
|
||||||
|
|
||||||
|
const TIMING_WINDOW_SIZE: usize = 30;
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug)]
|
#[derive(Resource, Default, Debug)]
|
||||||
pub struct SystemTimings {
|
pub struct SystemTimings {
|
||||||
pub timings: Mutex<HashMap<&'static str, Duration>>,
|
/// Map of system names to a queue of durations, using a circular buffer.
|
||||||
|
///
|
||||||
|
/// Uses a RwLock to allow multiple readers for the HashMap, and a Mutex on the circular buffer for exclusive access.
|
||||||
|
/// This is probably overkill, but it's fun to play with.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
pub timings: RwLock<Map<&'static str, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, 10>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemTimings {
|
||||||
|
pub fn add_timing(&self, name: &'static str, duration: Duration) {
|
||||||
|
// acquire a upgradable read lock
|
||||||
|
let mut timings = self.timings.upgradable_read();
|
||||||
|
|
||||||
|
// happy path, the name is already in the map (no need to mutate the hashmap)
|
||||||
|
if timings.contains_key(name) {
|
||||||
|
let queue = timings
|
||||||
|
.get(name)
|
||||||
|
.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(name).or_insert_with(|| Mutex::new(CircularBuffer::new()));
|
||||||
|
queue.lock().push_back(duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stats(&self) -> Map<&'static str, (Duration, Duration), 10> {
|
||||||
|
let timings = self.timings.read();
|
||||||
|
let mut stats = Map::new();
|
||||||
|
|
||||||
|
for (name, queue) in timings.iter() {
|
||||||
|
if queue.lock().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let durations: Vec<f64> = queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0).collect();
|
||||||
|
let count = durations.len() as f64;
|
||||||
|
|
||||||
|
let sum: f64 = durations.iter().sum();
|
||||||
|
let mean = sum / count;
|
||||||
|
|
||||||
|
let variance = durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
|
||||||
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
|
stats.insert(
|
||||||
|
*name,
|
||||||
|
(
|
||||||
|
Duration::from_secs_f64(mean / 1000.0),
|
||||||
|
Duration::from_secs_f64(std_dev / 1000.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_total_stats(&self) -> (Duration, Duration) {
|
||||||
|
let timings = self.timings.read();
|
||||||
|
let mut all_durations = Vec::new();
|
||||||
|
|
||||||
|
for queue in timings.values() {
|
||||||
|
all_durations.extend(queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_durations.is_empty() {
|
||||||
|
return (Duration::ZERO, Duration::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = all_durations.len() as f64;
|
||||||
|
let sum: f64 = all_durations.iter().sum();
|
||||||
|
let mean = sum / count;
|
||||||
|
|
||||||
|
let variance = all_durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
|
||||||
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
|
(
|
||||||
|
Duration::from_secs_f64(mean / 1000.0),
|
||||||
|
Duration::from_secs_f64(std_dev / 1000.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_timing_display(&self) -> String {
|
||||||
|
let stats = self.get_stats();
|
||||||
|
let (total_avg, total_std) = self.get_total_stats();
|
||||||
|
|
||||||
|
let effective_fps = match 1.0 / total_avg.as_secs_f64() {
|
||||||
|
f if f > 100.0 => (f as u32).separate_with_commas(),
|
||||||
|
f if f < 10.0 => format!("{:.1} FPS", f),
|
||||||
|
f => format!("{:.0} FPS", f),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect timing data for formatting
|
||||||
|
let mut timing_data = Vec::new();
|
||||||
|
|
||||||
|
// Add total stats
|
||||||
|
timing_data.push((effective_fps, total_avg, total_std));
|
||||||
|
|
||||||
|
// Add top 5 most expensive systems
|
||||||
|
let mut sorted_stats: Vec<_> = stats.iter().collect();
|
||||||
|
sorted_stats.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
|
||||||
|
|
||||||
|
for (name, (avg, std_dev)) in sorted_stats.iter().take(5) {
|
||||||
|
timing_data.push((name.to_string(), *avg, *std_dev));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the formatting module to format the data
|
||||||
|
crate::systems::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>(name: &'static str, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
|
||||||
@@ -25,8 +143,8 @@ where
|
|||||||
system.run((), world);
|
system.run((), world);
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
|
|
||||||
if let Some(mut timings) = world.get_resource_mut::<SystemTimings>() {
|
if let Some(timings) = world.get_resource::<SystemTimings>() {
|
||||||
timings.timings.lock().insert(name, duration);
|
timings.add_timing(name, duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
use crate::error::{GameError, TextureError};
|
use crate::error::{GameError, TextureError};
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::components::{DeltaTime, DirectionalAnimated, Renderable};
|
use crate::systems::components::{DeltaTime, DirectionalAnimated, RenderDirty, Renderable};
|
||||||
use crate::systems::movement::{Movable, MovementState, Position};
|
use crate::systems::movement::{Movable, MovementState, Position};
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use bevy_ecs::entity::Entity;
|
use bevy_ecs::entity::Entity;
|
||||||
use bevy_ecs::event::EventWriter;
|
use bevy_ecs::event::EventWriter;
|
||||||
use bevy_ecs::prelude::{Changed, Or, RemovedComponents};
|
use bevy_ecs::prelude::{Changed, Or, RemovedComponents};
|
||||||
use bevy_ecs::system::{NonSendMut, Query, Res};
|
use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
|
||||||
use sdl2::render::{Canvas, Texture};
|
use sdl2::render::{Canvas, Texture};
|
||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
|
|
||||||
|
pub fn dirty_render_system(
|
||||||
|
mut dirty: ResMut<RenderDirty>,
|
||||||
|
changed_renderables: Query<(), Or<(Changed<Renderable>, Changed<Position>)>>,
|
||||||
|
removed_renderables: RemovedComponents<Renderable>,
|
||||||
|
) {
|
||||||
|
if !changed_renderables.is_empty() || !removed_renderables.is_empty() {
|
||||||
|
dirty.0 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the directional animated texture of an entity.
|
/// Updates the directional animated texture of an entity.
|
||||||
///
|
///
|
||||||
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
|
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
|
||||||
@@ -55,18 +65,13 @@ pub fn render_system(
|
|||||||
mut backbuffer: NonSendMut<BackbufferResource>,
|
mut backbuffer: NonSendMut<BackbufferResource>,
|
||||||
mut atlas: NonSendMut<SpriteAtlas>,
|
mut atlas: NonSendMut<SpriteAtlas>,
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
|
dirty: Res<RenderDirty>,
|
||||||
renderables: Query<(Entity, &Renderable, &Position)>,
|
renderables: Query<(Entity, &Renderable, &Position)>,
|
||||||
changed_renderables: Query<(), Or<(Changed<Renderable>, Changed<Position>)>>,
|
|
||||||
removed_renderables: RemovedComponents<Renderable>,
|
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
if changed_renderables.is_empty() && removed_renderables.is_empty() {
|
if !dirty.0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Clear the main canvas first
|
|
||||||
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
|
|
||||||
canvas.clear();
|
|
||||||
|
|
||||||
// Render to backbuffer
|
// Render to backbuffer
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| {
|
.with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| {
|
||||||
@@ -81,6 +86,10 @@ pub fn render_system(
|
|||||||
|
|
||||||
// Render all entities to the backbuffer
|
// Render all entities to the backbuffer
|
||||||
for (_, renderable, position) in renderables.iter() {
|
for (_, renderable, position) in renderables.iter() {
|
||||||
|
if !renderable.visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let pos = position.get_pixel_pos(&map.graph);
|
let pos = position.get_pixel_pos(&map.graph);
|
||||||
match pos {
|
match pos {
|
||||||
Ok(pos) => {
|
Ok(pos) => {
|
||||||
@@ -103,12 +112,4 @@ pub fn render_system(
|
|||||||
})
|
})
|
||||||
.err()
|
.err()
|
||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
||||||
|
|
||||||
// Copy backbuffer to main canvas and present
|
|
||||||
canvas
|
|
||||||
.copy(&backbuffer.0, None, None)
|
|
||||||
.err()
|
|
||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
|
||||||
|
|
||||||
canvas.present();
|
|
||||||
}
|
}
|
||||||
|
|||||||
135
tests/formatting.rs
Normal file
135
tests/formatting.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use pacman::systems::formatting::format_timing_display;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_formatting() {
|
||||||
|
let timing_data = vec![
|
||||||
|
("60 FPS".to_string(), Duration::from_micros(1234), Duration::from_micros(567)),
|
||||||
|
("input".to_string(), Duration::from_micros(123), Duration::from_micros(45)),
|
||||||
|
("player".to_string(), Duration::from_micros(456), Duration::from_micros(123)),
|
||||||
|
("movement".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
||||||
|
("render".to_string(), Duration::from_micros(12), Duration::from_micros(3)),
|
||||||
|
("debug".to_string(), Duration::from_nanos(1000000), Duration::from_nanos(1000)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Basic formatting test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_desired_format() {
|
||||||
|
// This test represents the exact format you want to achieve
|
||||||
|
let timing_data = vec![
|
||||||
|
("total".to_string(), Duration::from_micros(1230), Duration::from_micros(570)),
|
||||||
|
("input".to_string(), Duration::from_micros(120), Duration::from_micros(50)),
|
||||||
|
("player".to_string(), Duration::from_micros(460), Duration::from_micros(120)),
|
||||||
|
("movement".to_string(), Duration::from_micros(790), Duration::from_micros(230)),
|
||||||
|
("render".to_string(), Duration::from_micros(10), Duration::from_micros(3)),
|
||||||
|
("debug".to_string(), Duration::from_nanos(1000000), Duration::from_nanos(1000)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Desired format test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Expected output should look like:
|
||||||
|
// total : 1.23 ms ± 0.57 ms
|
||||||
|
// input : 0.12 ms ± 0.05 ms
|
||||||
|
// player : 0.46 ms ± 0.12 ms
|
||||||
|
// movement : 0.79 ms ± 0.23 ms
|
||||||
|
// render : 0.01 ms ± 0.003ms
|
||||||
|
// debug : 0.001ms ± 0.000ms
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixed_units() {
|
||||||
|
let timing_data = vec![
|
||||||
|
("60 FPS".to_string(), Duration::from_millis(16), Duration::from_micros(500)),
|
||||||
|
(
|
||||||
|
"fast_system".to_string(),
|
||||||
|
Duration::from_nanos(500000),
|
||||||
|
Duration::from_nanos(100000),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"medium_system".to_string(),
|
||||||
|
Duration::from_micros(2500),
|
||||||
|
Duration::from_micros(500),
|
||||||
|
),
|
||||||
|
("slow_system".to_string(), Duration::from_millis(5), Duration::from_millis(1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Mixed units test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trailing_zeros() {
|
||||||
|
let timing_data = vec![
|
||||||
|
("60 FPS".to_string(), Duration::from_micros(1000), Duration::from_micros(500)),
|
||||||
|
("exact_ms".to_string(), Duration::from_millis(1), Duration::from_micros(100)),
|
||||||
|
("exact_us".to_string(), Duration::from_micros(1), Duration::from_nanos(100000)),
|
||||||
|
("exact_ns".to_string(), Duration::from_nanos(1000), Duration::from_nanos(100)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Trailing zeros test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edge_cases() {
|
||||||
|
let timing_data = vec![
|
||||||
|
("60 FPS".to_string(), Duration::from_nanos(1), Duration::from_nanos(1)),
|
||||||
|
("very_small".to_string(), Duration::from_nanos(100), Duration::from_nanos(50)),
|
||||||
|
("very_large".to_string(), Duration::from_secs(1), Duration::from_millis(100)),
|
||||||
|
("zero_time".to_string(), Duration::ZERO, Duration::ZERO),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Edge cases test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variable_name_lengths() {
|
||||||
|
let timing_data = vec![
|
||||||
|
("60 FPS".to_string(), Duration::from_micros(1234), Duration::from_micros(567)),
|
||||||
|
("a".to_string(), Duration::from_micros(123), Duration::from_micros(45)),
|
||||||
|
(
|
||||||
|
"very_long_system_name".to_string(),
|
||||||
|
Duration::from_micros(456),
|
||||||
|
Duration::from_micros(123),
|
||||||
|
),
|
||||||
|
("medium".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Variable name lengths test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_input() {
|
||||||
|
let timing_data = vec![];
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
assert_eq!(result, "");
|
||||||
|
println!("Empty input test: PASS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_entry() {
|
||||||
|
let timing_data = vec![("60 FPS".to_string(), Duration::from_micros(1234), Duration::from_micros(567))];
|
||||||
|
|
||||||
|
let result = format_timing_display(timing_data);
|
||||||
|
println!("Single entry test:");
|
||||||
|
println!("{}", result);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
40
tests/profiling.rs
Normal file
40
tests/profiling.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use pacman::systems::profiling::SystemTimings;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timing_statistics() {
|
||||||
|
let timings = SystemTimings::default();
|
||||||
|
|
||||||
|
// Add some test data
|
||||||
|
timings.add_timing("test_system", Duration::from_millis(10));
|
||||||
|
timings.add_timing("test_system", Duration::from_millis(12));
|
||||||
|
timings.add_timing("test_system", Duration::from_millis(8));
|
||||||
|
|
||||||
|
let stats = timings.get_stats();
|
||||||
|
let (avg, std_dev) = stats.get("test_system").unwrap();
|
||||||
|
|
||||||
|
// Average should be 10ms, standard deviation should be small
|
||||||
|
assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0);
|
||||||
|
assert!(std_dev.as_millis() > 0);
|
||||||
|
|
||||||
|
let (total_avg, total_std) = timings.get_total_stats();
|
||||||
|
assert!((total_avg.as_millis() as f64 - 10.0).abs() < 1.0);
|
||||||
|
assert!(total_std.as_millis() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_window_size_limit() {
|
||||||
|
// let timings = SystemTimings::default();
|
||||||
|
|
||||||
|
// // Add more than 90 timings to test window size limit
|
||||||
|
// for i in 0..100 {
|
||||||
|
// timings.add_timing("test_system", Duration::from_millis(i));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// // (average of 10-99)
|
||||||
|
// assert!((avg.as_millis() as f64 - 55.0).abs() < 5.0);
|
||||||
|
// }
|
||||||
Reference in New Issue
Block a user