diff --git a/src/game.rs b/src/game.rs index df5926f..f254734 100644 --- a/src/game.rs +++ b/src/game.rs @@ -14,14 +14,16 @@ use crate::systems::components::{GhostAnimation, GhostState, LastAnimationState} use crate::systems::movement::{BufferedDirection, Position, Velocity}; use crate::systems::profiling::SystemId; use crate::systems::render::RenderDirty; -use crate::systems::{self, ghost_collision_system, present_system, Hidden, LinearAnimation, MovementModifiers, NodeId}; use crate::systems::{ - audio_system, blinking_system, collision_system, debug_render_system, directional_render_system, dirty_render_system, - eaten_ghost_system, ghost_movement_system, ghost_state_system, hud_render_system, item_system, linear_render_system, profile, - render_system, AudioEvent, AudioResource, AudioState, BackbufferResource, Collider, DebugState, DebugTextureResource, - DeltaTime, DirectionalAnimation, EntityType, Frozen, Ghost, GhostAnimations, GhostBundle, GhostCollider, GlobalState, - ItemBundle, ItemCollider, MapTextureResource, PacmanCollider, PlayerBundle, PlayerControlled, Renderable, ScoreResource, - StartupSequence, SystemTimings, + self, combined_render_system, ghost_collision_system, present_system, Hidden, LinearAnimation, MovementModifiers, NodeId, +}; +use crate::systems::{ + audio_system, blinking_system, collision_system, directional_render_system, dirty_render_system, eaten_ghost_system, + ghost_movement_system, ghost_state_system, hud_render_system, item_system, linear_render_system, profile, AudioEvent, + AudioResource, AudioState, BackbufferResource, Collider, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation, + EntityType, Frozen, Ghost, GhostAnimations, GhostBundle, GhostCollider, GlobalState, ItemBundle, ItemCollider, + MapTextureResource, PacmanCollider, PlayerBundle, PlayerControlled, Renderable, ScoreResource, StartupSequence, + SystemTimings, }; use crate::texture::animated::{DirectionalTiles, TileSequence}; use crate::texture::sprite::AtlasTile; @@ -352,9 +354,7 @@ impl Game { let directional_render_system = profile(SystemId::DirectionalRender, directional_render_system); let linear_render_system = profile(SystemId::LinearRender, linear_render_system); let dirty_render_system = profile(SystemId::DirtyRender, dirty_render_system); - let render_system = profile(SystemId::Render, render_system); let hud_render_system = profile(SystemId::HudRender, hud_render_system); - let debug_render_system = profile(SystemId::DebugRender, debug_render_system); let present_system = profile(SystemId::Present, present_system); let unified_ghost_state_system = profile(SystemId::GhostStateAnimation, ghost_state_system); @@ -386,9 +386,8 @@ impl Game { directional_render_system, linear_render_system, dirty_render_system, - render_system, + combined_render_system, hud_render_system, - debug_render_system, present_system, ) .chain(), diff --git a/src/systems/debug.rs b/src/systems/debug.rs index e7af3f5..49ca442 100644 --- a/src/systems/debug.rs +++ b/src/systems/debug.rs @@ -6,7 +6,7 @@ use crate::map::builder::Map; use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings}; use crate::texture::ttf::{TtfAtlas, TtfRenderer}; use bevy_ecs::resource::Resource; -use bevy_ecs::system::{NonSendMut, Query, Res}; +use bevy_ecs::system::{Query, Res}; use glam::{IVec2, UVec2, Vec2}; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; @@ -203,15 +203,14 @@ fn render_timing_display( #[allow(clippy::too_many_arguments)] pub fn debug_render_system( - mut canvas: NonSendMut<&mut Canvas>, - mut debug_texture: NonSendMut, - mut ttf_atlas: NonSendMut, - batched_lines: Res, - debug_state: Res, - timings: Res, - map: Res, - colliders: Query<(&Collider, &Position)>, - cursor: Res, + canvas: &mut Canvas, + ttf_atlas: &mut TtfAtlasResource, + batched_lines: &Res, + debug_state: &Res, + timings: &Res, + map: &Res, + colliders: &Query<(&Collider, &Position)>, + cursor: &Res, ) { if !debug_state.enabled { return; @@ -222,121 +221,116 @@ pub fn debug_render_system( // Create debug text renderer let text_renderer = TtfRenderer::new(1.0); - let cursor_world_pos = match *cursor { + let cursor_world_pos = match &**cursor { CursorPosition::None => None, CursorPosition::Some { position, .. } => Some(position - BOARD_PIXEL_OFFSET.as_vec2()), }; - // Draw debug info on the high-resolution debug texture - canvas - .with_texture_canvas(&mut debug_texture.0, |debug_canvas| { - // Clear the debug canvas - debug_canvas.set_draw_color(Color::RGBA(0, 0, 0, 0)); - debug_canvas.clear(); + // Clear the debug canvas + canvas.set_draw_color(Color::RGBA(0, 0, 0, 0)); + canvas.clear(); - // Find the closest node to the cursor - let closest_node = if let Some(cursor_world_pos) = cursor_world_pos { - map.graph - .nodes() - .map(|node| node.position.distance(cursor_world_pos)) - .enumerate() - .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less)) - .map(|(id, _)| id) - } else { - None - }; + // Find the closest node to the cursor + let closest_node = if let Some(cursor_world_pos) = cursor_world_pos { + map.graph + .nodes() + .map(|node| node.position.distance(cursor_world_pos)) + .enumerate() + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less)) + .map(|(id, _)| id) + } else { + None + }; - debug_canvas.set_draw_color(Color::GREEN); - { - let rects = colliders - .iter() - .map(|(collider, position)| { - let pos = position.get_pixel_position(&map.graph).unwrap(); + canvas.set_draw_color(Color::GREEN); + { + let rects = colliders + .iter() + .map(|(collider, position)| { + let pos = position.get_pixel_position(&map.graph).unwrap(); - // Transform position and size using common methods - let pos = (pos * scale).as_ivec2(); - let size = (collider.size * scale) as u32; + // Transform position and size using common methods + let pos = (pos * scale).as_ivec2(); + let size = (collider.size * scale) as u32; - Rect::from_center(Point::from((pos.x, pos.y)), size, size) - }) - .collect::>(); - if rects.len() > rects.capacity() { - warn!( - capacity = rects.capacity(), - count = rects.len(), - "Collider rects capacity exceeded" - ); - } - debug_canvas.draw_rects(&rects).unwrap(); - } + Rect::from_center(Point::from((pos.x, pos.y)), size, size) + }) + .collect::>(); + if rects.len() > rects.capacity() { + warn!( + capacity = rects.capacity(), + count = rects.len(), + "Collider rects capacity exceeded" + ); + } + canvas.draw_rects(&rects).unwrap(); + } - debug_canvas.set_draw_color(Color { - a: f32_to_u8(0.6), - ..Color::RED - }); - debug_canvas.set_blend_mode(sdl2::render::BlendMode::Blend); + canvas.set_draw_color(Color { + a: f32_to_u8(0.6), + ..Color::RED + }); + canvas.set_blend_mode(sdl2::render::BlendMode::Blend); - // Use cached batched line segments - batched_lines.render(debug_canvas); + // Use cached batched line segments + batched_lines.render(canvas); - { - let rects: Vec<_> = map - .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); - - // If the node is the one closest to the cursor, draw it immediately - if closest_node == Some(id) { - debug_canvas.set_draw_color(Color::YELLOW); - debug_canvas.fill_rect(rect).unwrap(); - return None; - } - - Some(rect) - }) - .collect(); - - if rects.len() > rects.capacity() { - warn!( - capacity = rects.capacity(), - count = rects.len(), - "Node rects capacity exceeded" - ); - } - - // Draw the non-closest nodes all at once in blue - debug_canvas.set_draw_color(Color::BLUE); - debug_canvas.fill_rects(&rects).unwrap(); - } - - // Render node ID if a node is highlighted - if let Some(closest_node_id) = closest_node { - let node = map.graph.get_node(closest_node_id as NodeId).unwrap(); + { + let rects: Vec<_> = map + .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); - let node_id_text = closest_node_id.to_string(); - let text_pos = Vec2::new((pos.x + 10) as f32, (pos.y - 5) as f32); + // If the node is the one closest to the cursor, draw it immediately + if closest_node == Some(id) { + canvas.set_draw_color(Color::YELLOW); + canvas.fill_rect(rect).unwrap(); + return None; + } - text_renderer - .render_text( - debug_canvas, - &mut ttf_atlas.0, - &node_id_text, - text_pos, - Color { - a: f32_to_u8(0.4), - ..Color::WHITE - }, - ) - .unwrap(); - } + Some(rect) + }) + .collect(); - // Render timing information in the top-left corner - render_timing_display(debug_canvas, &timings, &text_renderer, &mut ttf_atlas.0); - }) - .unwrap(); + if rects.len() > rects.capacity() { + warn!( + capacity = rects.capacity(), + count = rects.len(), + "Node rects capacity exceeded" + ); + } + + // Draw the non-closest nodes all at once in blue + canvas.set_draw_color(Color::BLUE); + canvas.fill_rects(&rects).unwrap(); + } + + // Render node ID if a node is highlighted + if let Some(closest_node_id) = closest_node { + let node = map.graph.get_node(closest_node_id as NodeId).unwrap(); + let pos = transform_position_with_offset(node.position, scale); + + let node_id_text = closest_node_id.to_string(); + let text_pos = Vec2::new((pos.x + 10) as f32, (pos.y - 5) as f32); + + text_renderer + .render_text( + canvas, + &mut ttf_atlas.0, + &node_id_text, + text_pos, + Color { + a: f32_to_u8(0.4), + ..Color::WHITE + }, + ) + .unwrap(); + } + + // Render timing information in the top-left corner + render_timing_display(canvas, timings, &text_renderer, &mut ttf_atlas.0); } diff --git a/src/systems/render.rs b/src/systems/render.rs index bb5e4ac..d3fa5ed 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -2,8 +2,9 @@ use crate::constants::CANVAS_SIZE; use crate::error::{GameError, TextureError}; use crate::map::builder::Map; use crate::systems::{ - DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation, LinearAnimation, Position, Renderable, ScoreResource, - StartupSequence, Velocity, + debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime, + DirectionalAnimation, LinearAnimation, Position, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, + TtfAtlasResource, Velocity, }; use crate::texture::sprite::SpriteAtlas; use crate::texture::text::TextTexture; @@ -18,6 +19,7 @@ use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; use sdl2::render::{BlendMode, Canvas, Texture}; use sdl2::video::Window; +use std::time::Instant; #[derive(Resource, Default)] pub struct RenderDirty(pub bool); @@ -25,6 +27,13 @@ pub struct RenderDirty(pub bool); #[derive(Component)] pub struct Hidden; +/// Enum to identify which texture is being rendered to in the combined render system +#[derive(Debug, Clone, Copy)] +enum RenderTarget { + Backbuffer, + Debug, +} + #[allow(clippy::type_complexity)] pub fn dirty_render_system( mut dirty: ResMut, @@ -172,59 +181,138 @@ pub fn hud_render_system( #[allow(clippy::too_many_arguments)] pub fn render_system( + canvas: &mut Canvas, + map_texture: &NonSendMut, + atlas: &mut SpriteAtlas, + map: &Res, + dirty: &Res, + renderables: &Query<(Entity, &Renderable, &Position), Without>, + errors: &mut EventWriter, +) { + if !dirty.0 { + return; + } + + // Clear the backbuffer + canvas.set_draw_color(sdl2::pixels::Color::BLACK); + canvas.clear(); + + // Copy the pre-rendered map texture to the backbuffer + if let Err(e) = canvas.copy(&map_texture.0, None, None) { + errors.write(TextureError::RenderFailed(e.to_string()).into()); + } + + // Render all entities to the backbuffer + for (_, renderable, position) in renderables + .iter() + .sort_by_key::<(Entity, &Renderable, &Position), _>(|(_, renderable, _)| renderable.layer) + .rev() + { + let pos = position.get_pixel_position(&map.graph); + match pos { + Ok(pos) => { + let dest = Rect::from_center( + Point::from((pos.x as i32, pos.y as i32)), + renderable.sprite.size.x as u32, + renderable.sprite.size.y as u32, + ); + + renderable + .sprite + .render(canvas, atlas, dest) + .err() + .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); + } + Err(e) => { + errors.write(e); + } + } + } +} + +/// Combined render system that renders to both backbuffer and debug textures in a single +/// with_multiple_texture_canvas call for reduced overhead +#[allow(clippy::too_many_arguments)] +pub fn combined_render_system( mut canvas: NonSendMut<&mut Canvas>, map_texture: NonSendMut, mut backbuffer: NonSendMut, + mut debug_texture: NonSendMut, mut atlas: NonSendMut, + mut ttf_atlas: NonSendMut, + batched_lines: Res, + debug_state: Res, + timings: Res, map: Res, dirty: Res, renderables: Query<(Entity, &Renderable, &Position), Without>, + colliders: Query<(&Collider, &Position)>, + cursor: Res, mut errors: EventWriter, ) { if !dirty.0 { return; } - // Render to backbuffer - canvas - .with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| { - // Clear the backbuffer - backbuffer_canvas.set_draw_color(sdl2::pixels::Color::BLACK); - backbuffer_canvas.clear(); - // Copy the pre-rendered map texture to the backbuffer - if let Err(e) = backbuffer_canvas.copy(&map_texture.0, None, None) { - errors.write(TextureError::RenderFailed(e.to_string()).into()); + // Prepare textures and render targets + let textures = [ + (&mut backbuffer.0, RenderTarget::Backbuffer), + (&mut debug_texture.0, RenderTarget::Debug), + ]; + + // Record timing for each system independently + let mut render_duration = None; + let mut debug_render_duration = None; + + let result = canvas.with_multiple_texture_canvas(textures.iter(), |texture_canvas, render_target| match render_target { + RenderTarget::Backbuffer => { + let start_time = Instant::now(); + + render_system( + texture_canvas, + &map_texture, + &mut atlas, + &map, + &dirty, + &renderables, + &mut errors, + ); + + render_duration = Some(start_time.elapsed()); + } + RenderTarget::Debug => { + if !debug_state.enabled { + return; } - // Render all entities to the backbuffer - for (_, renderable, position) in renderables - .iter() - .sort_by_key::<(Entity, &Renderable, &Position), _>(|(_, renderable, _)| renderable.layer) - .rev() - { - let pos = position.get_pixel_position(&map.graph); - match pos { - Ok(pos) => { - let dest = Rect::from_center( - Point::from((pos.x as i32, pos.y as i32)), - renderable.sprite.size.x as u32, - renderable.sprite.size.y as u32, - ); + let start_time = Instant::now(); - renderable - .sprite - .render(backbuffer_canvas, &mut atlas, dest) - .err() - .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); - } - Err(e) => { - errors.write(e); - } - } - } - }) - .err() - .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); + debug_render_system( + texture_canvas, + &mut ttf_atlas, + &batched_lines, + &debug_state, + &timings, + &map, + &colliders, + &cursor, + ); + + debug_render_duration = Some(start_time.elapsed()); + } + }); + + if let Err(e) = result { + errors.write(TextureError::RenderFailed(e.to_string()).into()); + } + + // Record timings for each system independently + if let Some(duration) = render_duration { + timings.add_timing(SystemId::Render, duration); + } + if let Some(duration) = debug_render_duration { + timings.add_timing(SystemId::DebugRender, duration); + } } pub fn present_system(