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:
Jérôme Bousquié
2026-09-16 14:17:11 +02:00
parent bb7fab4911
commit 8eec38e55c
6 changed files with 748 additions and 457 deletions
+2 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
_ => {}
}
}
}
+6
View File
@@ -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.