fix(lib): migrate to winit 0.30 ApplicationHandler model
winit 0.30.13 removed WindowBuilder and deprecated EventLoop::run. Move window/GPU creation into ApplicationHandler::resumed, expose AppHandler::setup hook, switch App::run to run_app, and migrate both examples. pollster becomes a regular dependency (used by app.rs).
This commit is contained in:
+2
-4
@@ -8,10 +8,8 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
wgpu = "30.0.0" # Vérifiez la version la plus récente
|
||||
winit = "0.29" # For window management — pinned to match examples
|
||||
winit = "0.30.13" # For window management — pinned to match examples
|
||||
thiserror = "2"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
glam = "0.33"
|
||||
|
||||
[dev-dependencies]
|
||||
pollster = { version="0.4.0", features = ["macro"] }
|
||||
pollster = { version="1.0.1", features = ["macro"] }
|
||||
|
||||
+143
-84
@@ -1,6 +1,13 @@
|
||||
//! Workflow bas-niveau : utilisation directe du `Context`, `Renderer`, `PipelineCache`, `Mesh` et
|
||||
//! `Material`, contournant la façade `App`. Rendu d'un quad plat éclairé via la boucle winit 0.30
|
||||
//! (`EventLoop::run_app` + `ApplicationHandler`). La fenêtre et le GPU sont créés dans `resumed()`,
|
||||
//! comme l'exigent winit 0.30 et la migration faite dans `app.rs`.
|
||||
use std::sync::Arc;
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::window::WindowBuilder;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowAttributes};
|
||||
use wsg_lib::core::Context;
|
||||
use wsg_lib::core::Frame;
|
||||
use wsg_lib::core::Renderer;
|
||||
@@ -10,92 +17,144 @@ use wsg_lib::resources::Mesh;
|
||||
use wsg_lib::resources::Vertex;
|
||||
use wsg_lib::utils;
|
||||
|
||||
/// Application bas-niveau : détient les objets GPU + window, tous créés dans `resumed`.
|
||||
struct App {
|
||||
/// Fenêtre système, partagée via Arc (comme dans app.rs).
|
||||
window: Option<Arc<Window>>,
|
||||
/// Contexte GPU (Instance, Surface, Adapter, Device, Queue).
|
||||
context: Option<Context>,
|
||||
/// Couche d'exécution qui soumet les draw calls.
|
||||
renderer: Option<Renderer>,
|
||||
/// Cache de shaders/pipelines.
|
||||
cache: Option<PipelineCache>,
|
||||
/// Matériau (pipeline) du quad.
|
||||
material: Option<Material>,
|
||||
/// Mesh du quad (sommets + indices).
|
||||
mesh: Option<Mesh>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
/// Crée la fenêtre puis le GPU, et construit le mesh/matériau. Exécuté une fois au démarrage.
|
||||
/// Redondant `resumed` pour créer à nouveau ? double protection par `self.context.is_some()`.
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.context.is_some() {
|
||||
return;
|
||||
}
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
let attrs = WindowAttributes::default()
|
||||
.with_title("WSG Manual")
|
||||
.with_inner_size(LogicalSize::new(800.0, 600.0));
|
||||
let window = Arc::new(event_loop.create_window(attrs).unwrap());
|
||||
|
||||
// 1. Initialisation
|
||||
let context = pollster::block_on(Context::new(window.clone())).expect("Échec init GPU");
|
||||
|
||||
// Configuration de la surface et récupération du format
|
||||
let format = context
|
||||
.configure(&context.adapter, 800, 600)
|
||||
.expect("Échec configuration");
|
||||
|
||||
// 2. Initialisation du Renderer (Il récupère tout ce dont il a besoin)
|
||||
let device = Arc::new(context.device.clone());
|
||||
let mut cache = PipelineCache::new(device);
|
||||
cache
|
||||
.register_shader("basic", utils::BASIC_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
let renderer = Renderer::new(&context, format);
|
||||
|
||||
// 3. Material : On utilise renderer.device() et renderer.format()
|
||||
let material = Material::new(renderer.format(), "basic", &mut cache);
|
||||
|
||||
// Mesh : On utilise le device du renderer
|
||||
let vertices = [
|
||||
// Position (x,y,z) | Normale (x,y,z) | UV (u,v) | Couleur (r,g,b,a)
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 0.0],
|
||||
color: [1.0, 0.0, 0.0, 1.0],
|
||||
}, // Haut-Gauche (Rouge)
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
color: [0.0, 1.0, 0.0, 1.0],
|
||||
}, // Haut-Droite (Vert)
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 1.0],
|
||||
color: [0.0, 0.0, 1.0, 1.0],
|
||||
}, // Bas-Droite (Bleu)
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 1.0],
|
||||
color: [1.0, 1.0, 0.0, 1.0],
|
||||
}, // Bas-Gauche (Jaune)
|
||||
];
|
||||
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
|
||||
let mesh = Mesh::new(renderer.device(), &vertices, Some(&indices));
|
||||
|
||||
self.window = Some(window);
|
||||
self.context = Some(context);
|
||||
self.renderer = Some(renderer);
|
||||
self.cache = Some(cache);
|
||||
self.material = Some(material);
|
||||
self.mesh = Some(mesh);
|
||||
}
|
||||
|
||||
/// À chaque frame, demande un redessin pour un rendu continu (animation).
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch des événements de fenêtre : RedrawRequested rend puis présente, CloseRequested quitte.
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: winit::window::WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
match event {
|
||||
winit::event::WindowEvent::RedrawRequested => {
|
||||
if let (Some(context), Some(renderer), Some(mesh), Some(material)) =
|
||||
(&self.context, &self.renderer, &self.mesh, &self.material)
|
||||
{
|
||||
if let Some(frame) = Frame::try_new(&context.surface) {
|
||||
// 1. Rendu (plus d'arguments device/queue inutiles)
|
||||
renderer.render(frame.view(), mesh, material);
|
||||
|
||||
// 2. Présentation
|
||||
renderer.present(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
winit::event::WindowEvent::CloseRequested => {
|
||||
event_loop.exit(); // C'est ici que tu demandes à la boucle de s'arrêter
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!(
|
||||
"Répertoire courant : {:?}",
|
||||
std::env::current_dir().unwrap()
|
||||
);
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap());
|
||||
|
||||
// 1. Initialisation
|
||||
let context = pollster::block_on(Context::new(window.clone())).expect("Échec init GPU");
|
||||
|
||||
// Configuration de la surface et récupération du format
|
||||
let format = context
|
||||
.configure(&context.adapter, 800, 600)
|
||||
.expect("Échec configuration");
|
||||
|
||||
// 2. Initialisation du Renderer (Il récupère tout ce dont il a besoin)
|
||||
let device = Arc::new(context.device.clone());
|
||||
let mut cache = PipelineCache::new(device);
|
||||
cache
|
||||
.register_shader("basic", utils::BASIC_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
let renderer = Renderer::new(&context, format);
|
||||
|
||||
// 3. Material : On utilise renderer.device() et renderer.format()
|
||||
let material = Material::new(renderer.format(), "basic", &mut cache);
|
||||
|
||||
// Mesh : On utilise le device du renderer
|
||||
let vertices = [
|
||||
// Position (x,y,z) | Normale (x,y,z) | UV (u,v) | Couleur (r,g,b,a)
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 0.0],
|
||||
color: [1.0, 0.0, 0.0, 1.0],
|
||||
}, // Haut-Gauche (Rouge)
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
color: [0.0, 1.0, 0.0, 1.0],
|
||||
}, // Haut-Droite (Vert)
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 1.0],
|
||||
color: [0.0, 0.0, 1.0, 1.0],
|
||||
}, // Bas-Droite (Bleu)
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 1.0],
|
||||
color: [1.0, 1.0, 0.0, 1.0],
|
||||
}, // Bas-Gauche (Jaune)
|
||||
];
|
||||
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
|
||||
let mesh = Mesh::new(renderer.device(), &vertices, Some(&indices));
|
||||
|
||||
// Render loop
|
||||
event_loop
|
||||
.run(|event, elwt| {
|
||||
match event {
|
||||
winit::event::Event::AboutToWait => {
|
||||
window.request_redraw();
|
||||
}
|
||||
winit::event::Event::WindowEvent {
|
||||
event: winit::event::WindowEvent::RedrawRequested,
|
||||
..
|
||||
} => {
|
||||
if let Some(frame) = Frame::try_new(&context.surface) {
|
||||
// 1. Rendu (plus d'arguments device/queue inutiles)
|
||||
renderer.render(frame.view(), &mesh, &material);
|
||||
|
||||
// 2. Présentation
|
||||
renderer.present(frame);
|
||||
}
|
||||
}
|
||||
winit::event::Event::WindowEvent {
|
||||
event: winit::event::WindowEvent::CloseRequested,
|
||||
..
|
||||
} => {
|
||||
elwt.exit(); // C'est ici que tu demandes à la boucle de s'arrêter
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let mut app = App {
|
||||
window: None,
|
||||
context: None,
|
||||
renderer: None,
|
||||
cache: None,
|
||||
material: None,
|
||||
mesh: None,
|
||||
};
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
}
|
||||
|
||||
+56
-52
@@ -1,8 +1,9 @@
|
||||
//! Workflow déclaratif minimal, sans manipulation WGPU explicite dans ce fichier.
|
||||
//! `AppBuilder` ouvre la fenêtre, construit le `Context`/`Renderer` et fait tourner la boucle
|
||||
//! update → render → present. La scène se rend automatiquement : la méthode `render()` par défaut
|
||||
//! du trait `AppHandler` appelle `app.render_scene(frame.view())`, donc l'utilisateur n'implémente
|
||||
//! même pas `render` ici — il ne fait que remplir `app.scene` avec un mesh, un matériau et une entité.
|
||||
//! `AppBuilder` crée l'event loop puis `App::run` ouvre la fenêtre, construit le `Context`/`Renderer`
|
||||
//! et fait tourner la boucle update → render → present. Depuis la migration winit 0.30, le GPU n'existe
|
||||
//! qu'après `resumed` : c'est pourquoi l'enregistrement shader + la création mesh/matériau/entité vivent
|
||||
//! dans le hook `AppHandler::setup`, appelé une fois le contexte prêt. La scène se rend automatiquement :
|
||||
//! la méthode `render()` par défaut appelle `app.render_scene(frame.view())`.
|
||||
use std::sync::Arc;
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
@@ -11,56 +12,59 @@ use wsg_lib::utils::WsgError;
|
||||
|
||||
struct MonQuad;
|
||||
|
||||
impl AppHandler for MonQuad {}
|
||||
impl AppHandler for MonQuad {
|
||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||
let format = app.renderer().format();
|
||||
|
||||
// Enregistrement du shader, création du matériau et du mesh du quad (sans importer wgpu).
|
||||
app.cache()
|
||||
.register_shader("basic", wsg_lib::utils::BASIC_SHADER_PATH)
|
||||
.unwrap();
|
||||
let vertices = [
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 0.0],
|
||||
color: [1.0, 0.0, 0.0, 1.0],
|
||||
}, // Haut-Gauche (Rouge)
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
color: [0.0, 1.0, 0.0, 1.0],
|
||||
}, // Haut-Droite (Vert)
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 1.0],
|
||||
color: [0.0, 0.0, 1.0, 1.0],
|
||||
}, // Bas-Droite (Bleu)
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 1.0],
|
||||
color: [1.0, 1.0, 0.0, 1.0],
|
||||
}, // Bas-Gauche (Jaune)
|
||||
];
|
||||
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
|
||||
|
||||
let mesh = Arc::new(Mesh::new(
|
||||
app.renderer().device(),
|
||||
&vertices,
|
||||
Some(&indices),
|
||||
));
|
||||
let material = Arc::new(Material::new(format, "basic", app.cache()));
|
||||
|
||||
app.scene.add_mesh("quad_mesh", mesh).unwrap();
|
||||
app.scene.add_material("basic_material", material).unwrap();
|
||||
app.scene
|
||||
.add_entity("quad", "quad_mesh", "basic_material")
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), WsgError> {
|
||||
let mut app = AppBuilder::new().title("WSG Simple").build().await?;
|
||||
|
||||
// Enregistrement du shader, création du matériau et du mesh du quad (sans importer wgpu).
|
||||
app.cache
|
||||
.register_shader("basic", wsg_lib::utils::BASIC_SHADER_PATH)
|
||||
.unwrap();
|
||||
let vertices = [
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 0.0],
|
||||
color: [1.0, 0.0, 0.0, 1.0],
|
||||
}, // Haut-Gauche (Rouge)
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
color: [0.0, 1.0, 0.0, 1.0],
|
||||
}, // Haut-Droite (Vert)
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 1.0],
|
||||
color: [0.0, 0.0, 1.0, 1.0],
|
||||
}, // Bas-Droite (Bleu)
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 1.0],
|
||||
color: [1.0, 1.0, 0.0, 1.0],
|
||||
}, // Bas-Gauche (Jaune)
|
||||
];
|
||||
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
|
||||
|
||||
let mesh = Arc::new(Mesh::new(app.renderer.device(), &vertices, Some(&indices)));
|
||||
let material = Arc::new(Material::new(
|
||||
app.renderer.format(),
|
||||
"basic",
|
||||
&mut app.cache,
|
||||
));
|
||||
|
||||
app.scene.add_mesh("quad_mesh", mesh).unwrap();
|
||||
app.scene.add_material("basic_material", material).unwrap();
|
||||
app.scene
|
||||
.add_entity("quad", "quad_mesh", "basic_material")
|
||||
.unwrap();
|
||||
|
||||
let app = AppBuilder::new().title("WSG Simple").build().await?;
|
||||
app.run(MonQuad)
|
||||
}
|
||||
|
||||
+196
-71
@@ -12,6 +12,14 @@
|
||||
//! - **scene::scene**: Exposes Scene as mutable field so users can register resources and entities.
|
||||
//! - **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, Renderer};
|
||||
@@ -20,68 +28,96 @@ 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::event_loop::EventLoop;
|
||||
use winit::window::Window;
|
||||
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`, `cache`) are created lazily when the
|
||||
/// application is resumed (see `AppRunner`); they are only populated after `App::run` has started.
|
||||
/// Access them through the `context()`, `renderer()`, `window()` and `cache()` accessors, which is
|
||||
/// guaranteed to work inside `AppHandler::setup`, `update` and `render`.
|
||||
pub struct App {
|
||||
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
|
||||
pub context: Context,
|
||||
/// Executor layer — binds Materials and Meshes into RenderPasses during draw calls.
|
||||
pub renderer: Renderer,
|
||||
/// Winit event loop for window management. Set to None after run() consumes it.
|
||||
pub event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
|
||||
/// Shader compilation cache — manages RenderPipelines keyed by shader_id.
|
||||
pub cache: PipelineCache,
|
||||
/// Resource depot and entity graph — users register Meshes/Materials here before the render loop begins.
|
||||
/// Resource depot and entity graph — users register Meshes/Materials here during `AppHandler::setup`.
|
||||
pub scene: Scene,
|
||||
/// 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,
|
||||
/// Winit event loop for window management. Set to None after run() consumes it.
|
||||
event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
|
||||
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
|
||||
context: Option<Context>,
|
||||
/// Executor layer — binds Materials and Meshes into RenderPasses during draw calls.
|
||||
renderer: Option<Renderer>,
|
||||
/// The OS-level window backing this application. Shared via Arc for multi-owner access.
|
||||
pub window: Arc<Window>,
|
||||
window: Option<Arc<Window>>,
|
||||
/// Shader compilation cache — manages RenderPipelines keyed by shader_id.
|
||||
cache: Option<PipelineCache>,
|
||||
}
|
||||
|
||||
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 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 mutable reference to the shader compilation cache.
|
||||
/// Panics if called before `App::run` has created the cache (i.e. before `resumed` fires).
|
||||
pub fn cache(&mut self) -> &mut PipelineCache {
|
||||
self.cache
|
||||
.as_mut()
|
||||
.expect("cache 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) enter winit event loop →
|
||||
/// 3a) on AboutToWait: call handler.update() + request_redraw →
|
||||
/// 3b) on RedrawRequested: acquire frame → call handler.render() → present frame →
|
||||
/// 3c) on CloseRequested: exit event loop.
|
||||
pub fn run<H: AppHandler + 'static>(mut self, mut handler: H) -> Result<(), WsgError> {
|
||||
/// 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<H: AppHandler + 'static>(mut self, handler: H) -> Result<(), WsgError> {
|
||||
// On extrait l'event_loop de manière sûre grâce au Option
|
||||
let event_loop = self.event_loop.take().ok_or(WsgError::WindowSystem)?; // Erreur si déjà pris
|
||||
let mut runner = AppRunner {
|
||||
title: self.title.clone(),
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
handler,
|
||||
app: None,
|
||||
};
|
||||
event_loop
|
||||
.run(move |event, elwt| {
|
||||
match event {
|
||||
winit::event::Event::AboutToWait => {
|
||||
// update logic
|
||||
handler.update(&mut self);
|
||||
self.window.request_redraw();
|
||||
}
|
||||
winit::event::Event::WindowEvent {
|
||||
event: winit::event::WindowEvent::RedrawRequested,
|
||||
..
|
||||
} => {
|
||||
// Rendering logic
|
||||
let frame = self.context.get_next_frame();
|
||||
|
||||
// On appelle le render() de l'utilisateur (reçoit la frame courante)
|
||||
handler.render(&mut self, &frame);
|
||||
// On présente automatiquement
|
||||
self.renderer.present(frame);
|
||||
}
|
||||
winit::event::Event::WindowEvent {
|
||||
event: winit::event::WindowEvent::CloseRequested,
|
||||
..
|
||||
} => {
|
||||
elwt.exit();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.run_app(&mut runner)
|
||||
.map_err(|_| WsgError::WindowSystem)
|
||||
}
|
||||
|
||||
@@ -90,7 +126,7 @@ impl App {
|
||||
/// who override `render` to control drawing themselves.
|
||||
/// Inputs: view — the frame's texture view acting as the color attachment target.
|
||||
pub fn render_scene(&self, view: &wgpu::TextureView) {
|
||||
self.renderer.render_scene(view, &self.scene);
|
||||
self.renderer().render_scene(view, &self.scene);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,34 +164,123 @@ impl AppBuilder {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
/// Builds the configured `App` instance by creating all required components in order:
|
||||
/// EventLoop → Window → Context → Renderer → PipelineCache → Scene.
|
||||
/// Returns Ok(App) on success or Err(WsgError) if any component fails during creation.
|
||||
/// Called after setting desired properties via the builder pattern; triggers async GPU initialization.
|
||||
/// 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<App, WsgError> {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let window = Arc::new(
|
||||
winit::window::WindowBuilder::new()
|
||||
.with_title(&self.title)
|
||||
.build(&event_loop)
|
||||
.map_err(|_| WsgError::WindowSystem)?,
|
||||
);
|
||||
let context = Context::new(window.clone()).await?;
|
||||
let device = Arc::new(context.device.clone());
|
||||
let format = context
|
||||
.configure(&context.adapter, self.width, self.height)
|
||||
.map_err(|_| WsgError::SurfaceIncompatible)?;
|
||||
let renderer = Renderer::new(&context, format);
|
||||
let cache = PipelineCache::new(device);
|
||||
let scene = Scene::new();
|
||||
|
||||
let event_loop = EventLoop::new().map_err(|_| WsgError::WindowSystem)?;
|
||||
Ok(App {
|
||||
context,
|
||||
renderer,
|
||||
cache,
|
||||
scene,
|
||||
scene: Scene::new(),
|
||||
title: self.title,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
event_loop: Some(event_loop),
|
||||
window,
|
||||
context: None,
|
||||
renderer: None,
|
||||
window: None,
|
||||
cache: 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<H: AppHandler> {
|
||||
/// 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,
|
||||
/// The user-provided game logic.
|
||||
handler: H,
|
||||
/// The fully-built App facade, populated on the first `resumed` event.
|
||||
app: Option<App>,
|
||||
}
|
||||
|
||||
impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
/// 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"),
|
||||
);
|
||||
|
||||
// Initialization GPU (bloquant, simplifié au max)
|
||||
let context = pollster::block_on(Context::new(window.clone())).expect("Échec init GPU");
|
||||
let format = context
|
||||
.configure(&context.adapter, self.width, self.height)
|
||||
.expect("Échec configuration surface");
|
||||
let device = Arc::new(context.device.clone());
|
||||
let cache = PipelineCache::new(device);
|
||||
let renderer = Renderer::new(&context, format);
|
||||
|
||||
let mut app = App {
|
||||
scene: Scene::new(),
|
||||
title: self.title.clone(),
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
event_loop: None,
|
||||
context: Some(context),
|
||||
renderer: Some(renderer),
|
||||
window: Some(window),
|
||||
cache: Some(cache),
|
||||
};
|
||||
// On laisse l'utilisateur enregistrer shaders/meshes/matériaux/entités une fois le GPU prêt.
|
||||
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;
|
||||
};
|
||||
self.handler.update(app);
|
||||
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;
|
||||
};
|
||||
match event {
|
||||
WindowEvent::RedrawRequested => {
|
||||
// Rendering logic
|
||||
let frame = app.context().get_next_frame();
|
||||
|
||||
// On appelle le render() de l'utilisateur (reçoit la frame courante)
|
||||
self.handler.render(app, &frame);
|
||||
// On présente automatiquement
|
||||
app.renderer().present(frame);
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
event_loop.exit();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ use crate::core::Frame;
|
||||
/// render (draw call execution). Default implementations provide empty update and automatic
|
||||
/// scene rendering for convenience.
|
||||
pub trait AppHandler {
|
||||
/// Called once by `App::run`, right after the window/GPU context are created (winit `resumed`).
|
||||
/// Use it to register shaders, build Meshes/Materials, and populate `app.scene` before the loop
|
||||
/// starts. This replaces the pre-`run` setup that was possible before the winit 0.30 migration.
|
||||
/// Default implementation does nothing.
|
||||
/// Inputs: app — mutable reference to the fully-initialized App facade.
|
||||
fn setup(&mut self, _app: &mut App) {}
|
||||
/// Called once per frame before rendering begins. Used for physics updates, input processing,
|
||||
/// entity management, and any other pre-render logic. Default implementation does nothing.
|
||||
/// Inputs: _app — mutable reference to the App facade providing access to all subsystems.
|
||||
|
||||
Reference in New Issue
Block a user