Back to Mirador Blog

Walking Through the Pipeline

2025-06-28
17 min read
Development
miradorgame-developmentrust

The actual process that the program goes through to initialize the game and then update it every frame is relatively straightforward, but it has a lot of moving parts that are difficult to piece together by reading the source code. With the way the project is currently organized, walking the process by following the function calls requires jumping across many different files. To help me better internalize this process I decided to write this blog post to explain how Mirador's engine is initialized and updated.

Application Initialization

When you run cargo run, the program begins with minimal state. The event_loop and app are created, but the app starts without a window or game state. This separation allows the event loop to establish itself before any rendering resources are allocated. The app only gains its window and state when the event loop calls the resumed method. This method handles the transition from initialization to active execution by creating the window and initializing the AppState. The state initialization is where renderers and game logic are set up - this is the foundation that everything else builds upon.

The Event Loop Foundation

The event loop serves as the central nervous system of the application. It manages the lifecycle of the window and coordinates all system events. The initialization process follows a specific sequence that begins with creating the event loop and application instance, then transitions to window creation when the application is resumed.

async fn run() {

let event_loop = EventLoop::new().unwrap(); let mut app = App::new(); event_loop.run_app(&mut app).expect("Failed to run app"); }

This simple structure belies the complexity beneath. The event loop must handle multiple types of events:

  • Window events: Resize, close, focus changes
  • Input events: Mouse movement, button clicks, keyboard input
  • Redraw events: Frame timing and rendering requests
  • Device events: GPU context changes, display updates

The separation between event loop creation and window initialization is deliberate. This two-phase approach allows the system to establish its event handling infrastructure before allocating expensive rendering resources.

Window Creation and State Initialization

The resumed method represents the critical transition point where the application moves from initialization to active execution. It creates a maximized window and then calls the set_window method to handle the actual initialization of all game systems.

fn resumed(&mut self, event_loop: &ActiveEventLoop) {

// Create window with maximized state let window_attributes = Window::default_attributes().with_maximized(true); let window = event_loop.create_window(window_attributes).unwrap(); // Initialize the application state asynchronously pollster::block_on(self.set_window(window)); }

The set_window method orchestrates the creation of the WGPU surface and the initialization of the AppState. This is where the real work begins - the method creates the surface from the window, determines the appropriate dimensions based on whether the window is maximized, and then initializes the application state asynchronously. The AppState initialization is where the real work begins. This is where renderers are created, game logic is initialized, and all the systems that will drive the application are set up. The initialization process follows a specific order dictated by dependencies between systems. The WGPU renderer initialization represents the most computationally expensive part of the startup process. It involves creating the GPU adapter, device, and queue, configuring the surface, and setting up all the rendering pipelines. This step must complete before any other rendering-dependent systems can be initialized. Following the renderer initialization, the text renderer is created. This system handles all UI text rendering and requires access to the WGPU device and queue. The text renderer loads fonts and prepares the text rendering infrastructure that will be used throughout the application's lifetime. The game state is initialized next, providing the foundation for all game logic. This includes the player, enemy, maze systems, and audio manager. The game state serves as the central repository for all mutable game data and coordinates interactions between different game systems. UI systems are initialized after the core game state is established. The pause menu and upgrade menu are created with references to the WGPU device and queue, allowing them to render their own UI elements. These systems provide the interface layer that allows players to interact with the game state. Finally, benchmarking components are initialized to provide performance monitoring capabilities. The profiler tracks timing information for different pipeline stages, while the frame rate counter monitors rendering performance throughout the application's lifetime. This initialization process establishes the foundation for everything that follows. Each component must be created in the correct order, as dependencies exist between systems. The renderer needs the window, the text renderer needs the renderer for asset loading, and the UI managers need both the renderer and game state.

Event Loop Processing

Once fully initialized, the app enters its main processing loop. It waits for events from the event loop, processes them, then calls handle_redraw() to begin the next frame. This pattern continues throughout the program's lifetime. The event processing loop follows a specific pattern designed to maintain responsiveness while ensuring consistency. The system uses the ApplicationHandler trait to implement event handling, which provides a structured approach to processing different types of events.

Input Processing and State Updates

The input processing system must handle multiple types of input simultaneously while maintaining consistency. The system processes device events for mouse movement and window events for keyboard and mouse input, ensuring that all input is captured and routed to the appropriate systems. Mouse movement is handled through device events and is only processed when the game is in an active state and mouse capture is enabled. This allows the player to control the camera orientation while preventing unwanted movement during menu interactions or loading screens.

fn device_event(&mut self, _event_loop: &ActiveEventLoop, _device_id: DeviceId, event: DeviceEvent) {

if let DeviceEvent::MouseMotion { delta } = event { if let Some(state) = self.state.as_mut() { if (state.game_state.current_screen == CurrentScreen::Game || state.game_state.current_screen == CurrentScreen::ExitReached) && state.game_state.capture_mouse { // Allow mouse movement in both Game and ExitReached screens state.game_state.player.mouse_movement(delta.0, delta.1); } state.triage_mouse(window); } } }

Keyboard input is processed through window events and includes both movement controls and system commands. The system distinguishes between movement keys, which are handled continuously, and action keys, which are processed immediately when pressed. This separation allows for responsive gameplay while maintaining clean input handling. The input system must coordinate between multiple components:

  • UI Manager: Handles button clicks, menu navigation, and interface interactions
  • Game State: Processes gameplay input like movement and actions
  • Audio Manager: Triggers sound effects based on input events

This coordination requires careful state management to ensure that input events are processed in the correct order and that state changes are propagated to all relevant systems.

The Rendering Pipeline

The redraw process triggers the rendering pipeline. The handle_redraw() method orchestrates the complete rendering pipeline, handling different game screens, updating game state, and managing UI overlays.

State-Based Rendering Decisions

The rendering pipeline uses a state machine approach to determine what to render. The system examines the current game screen and routes rendering to the appropriate handler. This ensures that the correct visual elements are displayed regardless of the current game state. Loading screens are handled by a specialized renderer that displays maze generation progress and loading bars. The loading renderer updates the maze visualization in real-time as the generation algorithm progresses, providing visual feedback to the player during the initialization process. Title screens are handled by a dedicated title renderer that displays the game's branding and provides the entry point for new games. The title screen serves as the main menu and handles transitions to the loading screen when the player begins a new game. Game screens render the main 3D environment, including the maze geometry, player, enemy, and UI overlays. The game renderer manages multiple rendering passes to create the complete scene, including depth buffering, lighting, and post-processing effects. Pause and upgrade menus are rendered as overlays on top of the current game state. These menus use their own rendering systems to create semi-transparent backgrounds and interactive elements that don't interfere with the underlying game rendering.

The Rendering Process

The actual rendering process follows a specific sequence designed to maximize performance. The system creates a command encoder to batch GPU operations, updates the canvas surface with the current game state, and then executes multiple render passes to create the final frame.

// Prepare rendering commands

let mut encoder = state.wgpu_renderer.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); // Update canvas surface let (surface_view, surface_texture) = state.wgpu_renderer.update_canvas( window, &mut encoder, &state.game_state, &mut state.text_renderer, state.start_time, )?; // Submit commands and present state.wgpu_renderer.queue.submit(Some(encoder.finish())); surface_texture.present(); state.wgpu_renderer.device.poll(wgpu::Maintain::Poll);

Text rendering occurs first, ensuring that all UI text is properly positioned and styled. The text renderer prepares vertex buffers for all visible text elements and renders them using specialized shaders that handle font rendering and text layout. Game object rendering follows, drawing the 3D environment, player, and enemy. This pass uses the main rendering pipeline with depth buffering and lighting calculations to create the immersive game world. UI overlays are rendered last to ensure they appear on top of all other elements. The pause menu and upgrade menu use their own rendering systems that create semi-transparent backgrounds and interactive elements. The system submits all rendering commands to the GPU and presents the completed frame. Device polling ensures that GPU operations are properly synchronized and resources are cleaned up between frames. This rendering sequence ensures that:

  • Frame buffer is properly managed before rendering begins
  • Render commands are batched for optimal GPU performance
  • UI elements are rendered last to ensure they appear on top
  • Frame presentation is synchronized with the display refresh rate

Performance Considerations

The rendering pipeline must balance several competing concerns:

Frame Rate Consistency: The system targets 60 FPS, which requires each frame to complete within 16.67 milliseconds. This includes:
  • Event processing time
  • State update calculations
  • Rendering command generation
  • GPU command submission
Memory Management: The renderer must carefully manage GPU memory:
  • Texture uploads are batched to minimize GPU memory transfers
  • Vertex buffers are reused when possible
  • Shader programs are cached to avoid recompilation
Input Latency: The system must minimize the time between input events and visual feedback:
  • Input events are processed immediately when received
  • State updates are applied before rendering begins
  • Rendering commands are submitted as early as possible

The Complete Pipeline Flow

The pipeline can be divided into two main phases: application initialization and the per-frame event loop.

Initialization Phase (before the event loop):

1. Event Loop and App Creation (run_app in src/main.rs): The program begins by creating the event loop and the main application instance. At this stage, no window or rendering resources are allocated. 2. Window and State Initialization (resumed in src/app/mod.rs): When the event loop calls the resumed method, the application creates a maximized window and initializes all core state, including rendering and game logic. This setup is performed asynchronously to ensure all resources are ready before entering the main loop.

Event Loop Phase (repeats every frame):

1. Event Processing

  • Window Events (window_event): Handles window changes like resize or close.
  • Device Events (device_event): Captures input from mouse and keyboard.

2. Frame Preparation

  • Timing and Input Update (handle_frame_timing, key_state.update): Calculates delta time and updates accumulated input state.
  • Game Logic & UI Update (update_game_ui, audio_manager.update): Applies input to game state and updates UI and audio systems accordingly.

3. Rendering Pipeline

  • Background & Game World (clear_render_target, render_stars, render_game_objects): Clears the screen, renders the starfield, maze, and enemies.
  • Overlays & HUD (render_timer_bar_overlay, render_stamina_bar_overlay, render_compass): Draws gameplay overlays like the timer, stamina bar, and compass.
  • Text & Menus
  • Text Preparation & Rendering (handle_score_and_level_text, render_text, text_renderer.prepare, text_renderer.render): Handles layout and rendering for all UI text and debug info.
  • Menu Rendering (pause_menu.render, upgrade_menu.render): Draws pause and upgrade menu overlays when active.

4. Finalization

  • GPU Submission and Presentation (queue.submit, surface_texture.present): Submits all GPU commands and presents the final frame to the screen.
  • Frame Continuation (window.request_redraw): Requests the next frame, continuing the loop.

The pipeline described above is intentionally condensed to aid comprehension. For clarity, a more detailed and precise sequence of operations that occurs during each frame is outlined below:

Event Loop Phase (repeats every frame):

1. Window Event Processing (window_event in src/app/event_handler.rs): Handles window-related events as they arrive, such as resizing, closing, and focus changes. This step ensures the application responds promptly to changes in the window's state. 2. Device Event Processing (device_event in src/app/event_handler.rs): Processes device events, including input from the keyboard and mouse. Input state is updated immediately when events are received. 3. Frame Timing (handle_frame_timing in src/app/update.rs): Delta time is calculated and frame timing is updated before rendering begins. 4. Handle Redraw (handle_redraw in src/app/update.rs): Updates the state based on the current game screen to prepare all the information that the renderer needs to draw the next frame.

  • 5. Input State Update (key_state.update in src/app/update.rs): Input state is processed and accumulated input from the previous event processing step is applied to game state.
  • 6. Game UI Update (update_game_ui in src/app/update.rs): Game UI systems are updated based on the current game state and input.
  • 7. Audio Update (audio_manager.update in src/app/update.rs): Audio systems are updated based on game state.
  • Rendering:
  • 8. Clear Canvas (clear_render_target in src/renderer/wgpu_lib.rs): The render target is cleared with the game's background color.
  • 9. Background Rendering (render_stars in src/renderer/wgpu_lib.rs): Animated starfield background is rendered to create the game's atmosphere.
  • 10. Maze and Enemy Rendering (render_game_objects in src/renderer/wgpu_lib.rs): The core game elements are rendered:
  • 11. Maze Geometry (game_renderer.render_game in src/renderer/game_renderer/mod.rs): 3D maze walls and floor with depth testing
  • 12. Enemy Entities (enemy_renderer.render in src/renderer/game_renderer/enemy.rs): Enemy sprites that face the player
  • 13. Timer Bar Overlay (render_timer_bar_overlay in src/renderer/wgpu_lib.rs): Time remaining indicator is rendered at the top of the screen.
  • 14. Stamina Bar Overlay (render_stamina_bar_overlay in src/renderer/wgpu_lib.rs): Player stamina indicator is rendered below the timer bar.
  • 15. Compass Rendering (render_compass in src/renderer/wgpu_lib.rs): Directional compass overlay is rendered to guide the player.
  • 16. Game UI Text Positioning (handle_score_and_level_text in src/renderer/text.rs): Score and level text elements are positioned and sized.
  • 17. Game UI Text Rendering (render_text in src/renderer/wgpu_lib.rs): All game UI text elements are rendered to the screen.
  • 18. Debug Info Rendering (create_text_buffer in src/app/update.rs): Debug information is prepared and rendered when the debug panel is visible.
  • 19. Text Preparation (text_renderer.prepare in src/app/update.rs): All UI text elements are prepared for rendering with updated layouts and styles.
  • 20. Text Rendering (text_renderer.render in src/app/update.rs): All visible text elements are rendered to the screen using specialized font shaders.
  • 21. Pause Menu Overlay (pause_menu.render in src/app/update.rs): Semi-transparent overlay and pause menu buttons are rendered when the game is paused.
  • 22. Upgrade Menu Overlay (upgrade_menu.render in src/app/update.rs): Semi-transparent overlay and upgrade menu buttons are rendered when the upgrade menu is active.
  • 23. Command Submission (queue.submit in src/app/update.rs): GPU commands are submitted to the GPU for execution.
  • 24. Frame Presentation (surface_texture.present in src/app/update.rs): The completed frame is presented to the display.
  • 25. Next Frame Request (window.request_redraw in src/app/update.rs): The application requests the next redraw, continuing the cycle.

This flow repeats continuously throughout the application's lifetime, with each iteration representing one frame of the game.

Synchronization and Timing

The pipeline must maintain proper synchronization between different systems. Frame timing calculations ensure that all systems operate with consistent timing information, while FPS monitoring provides feedback on rendering performance.

fn handle_frame_timing(&mut self, current_time: Instant) {

if let Some(state) = self.state.as_mut() { let duration = current_time.duration_since(state.game_state.last_fps_time); state.elapsed_time = current_time.duration_since(state.start_time); state.game_state.frame_count += 1; if duration.as_secs_f32() >= 1.0 { state.game_state.current_fps = state.game_state.frame_count; state.game_state.frame_count = 0; state.game_state.last_fps_time = current_time; } let delta_time = current_time.duration_since(state.game_state.last_frame_time).as_secs_f32(); state.game_state.delta_time = delta_time; state.game_state.last_frame_time = current_time; } }

The system calculates delta time between frames to ensure smooth gameplay regardless of frame rate variations. This timing information is used by all game systems that need to update based on elapsed time, such as player movement, enemy AI, and animation systems. FPS monitoring tracks rendering performance and provides debugging information when performance issues arise. The system maintains a rolling average of frame times and can adapt rendering quality based on performance metrics. This synchronization ensures that:

  • Input processing happens before state updates
  • State updates are applied before rendering
  • Audio updates are synchronized with visual updates
  • Frame timing is consistent across the entire pipeline

Error Handling and Recovery

The pipeline must be robust enough to handle various failure scenarios. The system includes comprehensive error handling for GPU context loss, input system failures, and rendering errors. GPU context loss is a common issue on Windows systems and requires the renderer to reinitialize all GPU resources. The system detects context loss and automatically recreates the rendering pipeline without requiring application restart. Input system failures are handled gracefully by resetting input state and preventing cascading failures. The system logs errors for debugging while maintaining application stability. Rendering failures are handled by attempting to recover the renderer and continuing with reduced functionality if recovery fails. This ensures that the application remains responsive even when rendering encounters problems.

Performance Monitoring and Optimization

The pipeline includes built-in performance monitoring to identify bottlenecks. The system tracks timing information for each pipeline stage, allowing developers to identify performance issues and optimize specific components. Performance metrics include frame time, event processing time, state update time, render time, and GPU wait time. These metrics are collected continuously and can be analyzed to identify performance bottlenecks. The monitoring system allows the application to:

  • Identify performance bottlenecks in specific pipeline stages
  • Adapt rendering quality based on performance metrics
  • Provide debugging information for optimization efforts
  • Ensure consistent frame rates across different hardware

The Pipeline's Role in Game Architecture

The rendering pipeline serves as the backbone of Mirador's architecture. It provides the foundation for all game systems and ensures that the application remains responsive and visually consistent.

Separation of Concerns: Each pipeline stage has a specific responsibility:
  • Event processing handles input
  • State updates handle game logic
  • Rendering handles visual output
Modularity: Each component can be modified independently:
  • Input systems can be enhanced without affecting rendering
  • Rendering improvements don't require game logic changes
  • State management can evolve without breaking the pipeline
Extensibility: New features can be added by extending specific pipeline stages:
  • New input types can be added to the event processing stage
  • New game mechanics can be added to the state update stage
  • New visual effects can be added to the rendering stage
Performance: The pipeline structure enables optimization at multiple levels:
  • Event processing can be optimized for responsiveness
  • State updates can be optimized for consistency
  • Rendering can be optimized for visual quality

Conclusion

This simple flow - event processing followed by state-based rendering - provides the foundation for Mirador's real-time graphics. The separation between event handling and rendering allows the system to respond to input while maintaining consistent visual output. In the code base this process is scattered across dozens of files making it hard to keep it all in your head at once. Hopefully this blog post makes it a little easier to understand the procedure that is repeated 60 times a second to produce the experience that is Mirador.