//! # App Facade Module — High-Level Application Orchestration //! //! Defines the `App` facade type and `AppHandler` trait that provide the high-level user-facing API. //! `App` encapsulates window lifecycle, event loop, frame acquisition, and rendering automation. //! Users implement `AppHandler` to inject their game logic into the render loop without touching wgpu directly. //! The `AppBuilder` provides a builder-style constructor for creating configured `App` instances. //! //! ## Interaction with Other Modules //! - **core::context**: Consumes GPU hardware lifecycle via Context; acquires frames for rendering. //! - **core::renderer**: Delegates draw call execution to Renderer per frame. //! - **scene::scene**: Exposes Scene as mutable field so users can register resources and entities. //! Since Step 7 the `PipelineCache` lives *inside* the Scene (via `Scene::init_gpu`), owned and //! used for material building there. //! - **utils::conf**: Provides default window title, dimensions, and embedded WGSL source. //! - **handler**: Defines the AppHandler trait that users implement for custom logic. //! //! ## Architecture Note (winit 0.30) //! winit 0.30 removed the synchronous window-creation API (`WindowBuilder`) and the closure-based //! `EventLoop::run`, replacing them with the [`ApplicationHandler`] model driven by `EventLoop::run_app`. //! Windows can only be created inside `ApplicationHandler::resumed()`. Consequently this module builds //! the window and GPU context lazily inside `AppRunner`'s `resumed()` callback, and exposes the //! user-facing `AppBuilder::build → App::run` flow over that model. `AppHandler::setup()` is invoked //! once right after GPU initialization so users can register shaders/meshes/materials/entities. use crate::AppHandler; use crate::core::{Context, InputState, Renderer}; use crate::scene::Scene; use crate::utils::WsgError; use crate::utils::conf::{APP_DEFAULT_HEIGHT, APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH}; use std::sync::Arc; use winit::application::ApplicationHandler; use winit::dpi::LogicalSize; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::{Window, WindowAttributes}; /// High-level application facade that orchestrates window lifecycle, event loop, and rendering automation. /// Encapsulates all five WGPU objects (Instance, Surface, Adapter, Device, Queue) plus the render loop. /// Users create an App via AppBuilder, then run it with their implementation of AppHandler. /// /// The GPU-facing fields (`context`, `renderer`, `window`) are created lazily when the application is /// resumed (see `AppRunner`); they are only populated after `App::run` has started. The `PipelineCache` /// is not a field here: since Step 7 it lives in the `Scene`'s pipeline context (via `Scene::init_gpu`). /// Access GPU resources through the `context()`, `renderer()` and `window()` accessors, which are /// guaranteed to work inside `AppHandler::setup`, `update` and `render`. pub struct App { /// Resource depot and entity graph — users register Meshes/Materials here during `AppHandler::setup`. pub scene: Scene, /// Unified input state (keyboard/mouse/scroll, DRAFT Step 15). Fed by the winit window events /// and rotated each frame by `begin_frame`/`end_frame` around `AppHandler::update`. Read it in /// `update` via `app.input` (e.g. `app.input.key_held(KeyCode::KeyW)`). pub input: InputState, /// Window title, read by the runner when the window is created in `resumed`. pub(crate) title: String, /// Window width, read by the runner when the window is created in `resumed`. pub(crate) width: u32, /// Window height, read by the runner when the window is created in `resumed`. pub(crate) height: u32, /// GPU frustum culling (Step 15, D8); applied to the renderer in `resumed`. pub(crate) culling: bool, /// Winit event loop for window management. Set to None after run() consumes it. event_loop: Option>, // On met en Option pour pouvoir faire .take() facilement /// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle. context: Option, /// Executor layer — binds Materials and Meshes into RenderPasses during draw calls. renderer: Option, /// The OS-level window backing this application. Shared via Arc for multi-owner access. window: Option>, } impl App { /// Returns a reference to the GPU renderer. /// Panics if called before `App::run` has created the renderer (i.e. before `resumed` fires). pub fn renderer(&self) -> &Renderer { self.renderer .as_ref() .expect("renderer not initialized yet — call app.run(handler) first") } /// Returns a mutable reference to the GPU renderer. /// Panics if called before `App::run` has created the renderer (i.e. before `resumed` fires). /// Callers can configure the renderer here, e.g. `app.renderer_mut().set_unlit(true)` in `setup` /// to select flat 2D rendering (DRAFT Step 5). pub fn renderer_mut(&mut self) -> &mut Renderer { self.renderer .as_mut() .expect("renderer not initialized yet — call app.run(handler) first") } /// Returns a reference to the GPU hardware context. /// Panics if called before `App::run` has created the context (i.e. before `resumed` fires). pub fn context(&self) -> &Context { self.context .as_ref() .expect("context not initialized yet — call app.run(handler) first") } /// Returns a reference to the window backing this application. /// Panics if called before `App::run` has created the window (i.e. before `resumed` fires). pub fn window(&self) -> &Window { self.window .as_ref() .expect("window not initialized yet — call app.run(handler) first") .as_ref() } /// Runs the application's main loop: processes events, updates logic per frame, renders, and presents. /// Inputs: handler — user-provided AppHandler implementation containing game logic. /// Returns Ok(()) on success or Err(WsgError::WindowSystem) if the event loop exits abnormally. /// Called once at application entry point; runs until the window is closed or an error occurs. /// Internal steps: 1) take EventLoop from Option → 2) build an `AppRunner` around the handler → /// 3) on resumed: create window/context/renderer/cache and call handler.setup() → /// 4) on about_to_wait: call handler.update() + request_redraw → /// 5) on RedrawRequested: acquire frame → call handler.render() → present frame → /// 6) on CloseRequested: exit the event loop. pub fn run(mut self, handler: H) -> Result<(), WsgError> { // Extract the event_loop safely via Option let event_loop = self.event_loop.take().ok_or(WsgError::WindowSystem)?; // error if already taken let mut runner = AppRunner { title: self.title.clone(), width: self.width, height: self.height, culling: self.culling, handler, app: None, }; event_loop .run_app(&mut runner) .map_err(|_| WsgError::WindowSystem) } /// Renders every entity in `self.scene` into the given color view in a single batched render pass. /// Called automatically each frame by the default `AppHandler::render`, or manually by users /// who override `render` to control drawing themselves. /// Inputs: view — the frame's texture view acting as the color attachment target. /// /// The viewport aspect ratio (needed for the active camera's perspective projection, Step 4.3) /// is derived here from the window's current inner size, so the `Renderer` stays independent of /// the windowing backend. pub fn render_scene(&self, view: &wgpu::TextureView) { let size = self.window().inner_size(); let aspect = size.width as f32 / size.height.max(1) as f32; self.renderer().render_scene(view, &self.scene, aspect); } /// Resizes the surface and depth texture to a new window size (ROADMAP Phase 4.4). /// Reconfigures the surface via `Context::configure` (which returns the chosen format) and /// recreates the depth texture via `Renderer::resize_depth` so the color and depth attachments /// stay the same size. If the surface format changes (rare, deterministic per window), the /// Scene's GPU context is re-initialized to the new format; otherwise the swap alone suffices. /// Inputs: width/height — the new surface dimensions in pixels. /// Returns Ok(()) on success or a `WsgError` if the surface cannot be reconfigured. pub fn resize(&mut self, width: u32, height: u32) -> Result<(), WsgError> { let context = self.context.as_ref().ok_or(WsgError::SurfaceIncompatible)?; let old_format = self.renderer().format(); let new_format = context.configure(&context.adapter, width, height)?; self.renderer_mut().resize_depth(width, height); self.renderer_mut().set_format(new_format); if new_format != old_format { // Surface format changed: re-wire the Scene's GPU context (device + queue + format) // so its PipelineCache/pipelines match the new surface format. let device = std::sync::Arc::new(self.renderer_mut().device().clone()); self.scene .init_gpu(device, self.context().queue.clone(), new_format); } Ok(()) } } /// Builder for constructing a configured `App` instance with custom title and dimensions. /// Provides a fluent API for setting window properties before building the full application context. pub struct AppBuilder { /// Window title displayed in the OS taskbar/window decorations. title: String, /// Window width in pixels. width: u32, /// Window height in pixels. height: u32, /// GPU frustum culling enabled (Step 15, D8). Defaults to false (non-regression). culling: bool, } impl AppBuilder { /// Creates an AppBuilder with default values: "WSG App" title, 800x600 resolution. /// Called as the entry point of the builder pattern — always start here. pub fn new() -> Self { Self { title: APP_DEFAULT_TITLE.to_string(), width: APP_DEFAULT_WIDTH, height: APP_DEFAULT_HEIGHT, culling: false, } } /// Sets the window title to display in the OS taskbar/window decorations. /// Inputs: title (string reference). Returns Self for method chaining. pub fn title(mut self, title: &str) -> Self { self.title = title.to_string(); self } /// Sets the window dimensions in pixels. /// Inputs: width (pixel count), height (pixel count). Returns Self for method chaining. pub fn size(mut self, width: u32, height: u32) -> Self { self.width = width; self.height = height; self } /// Enables GPU frustum culling (Step 15, D8). When true, entities whose bounding sphere is /// fully outside the camera frustum are skipped (their indirect draw args are zeroed on the /// GPU). Defaults to **off** (non-regression): the culling compute pass still runs but marks /// every active entity visible, so the rendered image is identical to culling-off. pub fn with_culling(mut self, enabled: bool) -> Self { self.culling = enabled; self } /// Builds the configured `App` instance: creates the event loop and stores the window /// configuration. The GPU context, window and renderer are created later, when the event loop /// is resumed (inside `App::run`), because winit 0.30 only allows window creation in that phase. /// Returns Ok(App) on success or Err(WsgError) if the event loop cannot be created. /// Called after setting desired properties via the builder pattern before `App::run`. pub async fn build(self) -> Result { let event_loop = EventLoop::new().map_err(|_| WsgError::WindowSystem)?; Ok(App { scene: Scene::new(), input: InputState::new(), title: self.title, width: self.width, height: self.height, culling: self.culling, event_loop: Some(event_loop), context: None, renderer: None, window: None, }) } } /// Internal runner that adapts a user `AppHandler` to winit's 0.30 `ApplicationHandler` model. /// It owns the window/GPU lifecycle: everything is created lazily inside `resumed()`, then the /// user's `setup`, `update` and `render` hooks are driven from the corresponding winit events. struct AppRunner { /// Window title, applied when the window is created in `resumed`. title: String, /// Window width in pixels, applied when the window is created in `resumed`. width: u32, /// Window height in pixels, applied when the window is created in `resumed`. height: u32, /// GPU frustum culling (Step 15, D8); applied to the renderer in `resumed`. culling: bool, /// The user-provided game logic. handler: H, /// The fully-built App facade, populated on the first `resumed` event. app: Option, } impl ApplicationHandler for AppRunner { /// Builds the window, GPU context, renderer and shader cache, then invokes the user's `setup`. /// Guarded so redundant back-to-back `resumed` events do not re-initialize the GPU. /// Inputs: event_loop — the active event loop used to create the window and control redrawing. fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.app.is_some() { return; } event_loop.set_control_flow(ControlFlow::Poll); let attrs = WindowAttributes::default() .with_title(&self.title) .with_inner_size(LogicalSize::new(self.width as f64, self.height as f64)); let window = Arc::new( event_loop .create_window(attrs) .map_err(|_| WsgError::WindowSystem) .expect("failed to create window"), ); // GPU initialization (blocking, kept as simple as possible) let context = pollster::block_on(Context::new(window.clone())).expect("GPU init failed"); let format = context .configure(&context.adapter, self.width, self.height) .expect("surface configuration failed"); let device = Arc::new(context.device.clone()); let renderer = Renderer::new(&context, format, self.width, self.height); // Step 15, D8: apply the culling flag (off by default — non-regression). renderer.set_culling(self.culling); // Step 7 (DRAFT 7.1): the PipelineCache now lives in the Scene. We wire the GPU context // (device + queue + format + cache) into the Scene before setup so it can build materials/meshes. let mut scene = Scene::new(); scene.init_gpu(device, context.queue.clone(), format); let mut app = App { scene, input: InputState::new(), title: self.title.clone(), width: self.width, height: self.height, culling: self.culling, event_loop: None, context: Some(context), renderer: Some(renderer), window: Some(window), }; // Let the user register shaders/meshes/materials/entities once the GPU is ready. self.handler.setup(&mut app); self.app = Some(app); } /// Drives the user's per-frame update and requests a redraw so the window renders continuously. /// Inputs: _event_loop — active event loop (unused here). fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { let Some(app) = self.app.as_mut() else { return; }; // Poll the device each frame: wgpu only fires async callbacks (queue.on_submitted_work_done, // buffer/texture map_async) when the device is polled, and the event loop never does it on // our behalf. `Wait` with no timeout = block until the most recent submission completes // (i.e. once per frame on a live GPU, which is what we want for the windowed loop). // A failed poll (e.g. a device-lost error) is logged, not fatal: the next frame's poll // will retry, and wgpu surfaces the loss through the device's error handler anyway. if let Err(e) = app.context().device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None, }) { eprintln!("WSG: device.poll() failed ({e:?})"); } // Step 15 (input): start the input frame (rotate pressed/released + reset deltas), // run the user logic, then close (clear the transient states). app.input.begin_frame(); self.handler.update(app); app.input.end_frame(); app.window().request_redraw(); } /// Dispatches window events: RedrawRequested renders/presents a frame, CloseRequested exits. /// Inputs: event_loop — active event loop, used to exit on close; event — the window event. fn window_event( &mut self, event_loop: &ActiveEventLoop, _window_id: winit::window::WindowId, event: WindowEvent, ) { let Some(app) = self.app.as_mut() else { return; }; // Step 15 (input): feed the unified state from winit events (keyboard/mouse/wheel). app.input.handle_window_event(&event); match event { WindowEvent::Resized(size) => { // Guard (D3): minimizing the window sends Resized(0x0); never reconfigure at 0. let w = size.width as u32; let h = size.height as u32; if w == 0 || h == 0 { return; } // Step 11: reconfigure surface + depth to the new size, then re-render. if let Err(e) = app.resize(w, h) { eprintln!("WSG: resize error ({e:?})"); } app.window().request_redraw(); } WindowEvent::RedrawRequested => { // Guard (D6): do not render on a zero-sized surface (minimized window). let size = app.window().inner_size(); if size.width == 0 || size.height == 0 { return; } // Rendering logic let frame = app.context().get_next_frame(); // Call the user's render() (receives the current frame) self.handler.render(app, &frame); // Present automatically app.renderer().present(frame); } WindowEvent::CloseRequested => { event_loop.exit(); } _ => {} } } }