modif appbuilder

This commit is contained in:
Jérôme Bousquié
2026-07-08 12:29:18 +02:00
parent 82ea16d118
commit 724b658896
14 changed files with 205 additions and 2447 deletions
+3
View File
@@ -11,3 +11,6 @@ wgpu = "30.0.0" # Vérifiez la version la plus récente
winit = "0.29" # For window management — pinned to match examples
thiserror = "2"
bytemuck = { version = "1.25.0", features = ["derive"] }
[dev-dependencies]
pollster = "0.4.0"
+101
View File
@@ -0,0 +1,101 @@
use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use wsg_lib::core::Context;
use wsg_lib::core::Frame;
use wsg_lib::core::Renderer;
use wsg_lib::pipeline::PipelineCache;
use wsg_lib::resources::Material;
use wsg_lib::resources::Mesh;
use wsg_lib::resources::Vertex;
use wsg_lib::utils;
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();
}
+70
View File
@@ -0,0 +1,70 @@
use wsg_lib::app::{App, AppHandler};
use wsg_lib::resources::{Material, Mesh, Vertex};
use wsg_lib::utils;
// 1. On définit notre "Jeu" qui implémente le comportement
struct MonQuad {
mesh: Mesh,
material: Material,
}
impl AppHandler for MonQuad {
fn render(&mut self, app: &mut App) {
// Le rendu devient simple : on accède aux outils via &mut app
if let Some(frame) = app.context.get_next_frame() {
app.renderer
.render(frame.view(), &self.mesh, &self.material);
app.renderer.present(frame);
}
}
}
#[pollster::main]
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
// 2. Initialisation via le Builder
let mut app = App::builder()
.title("Exemple Simple")
.size(800, 600)
.build()
.await?;
// 3. Setup des ressources (déclaration)
app.cache
.register_shader("basic", utils::BASIC_SHADER_PATH)
.unwrap();
let vertices: [Vertex; 4] = [
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],
},
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],
},
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],
},
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],
},
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let mesh = Mesh::new(app.context.device(), &vertices, Some(&indices));
let material = Material::new(app.renderer.format(), "basic", &mut app.cache);
// 4. Lancement de la boucle (la magie opère ici)
let handler = MonQuad { mesh, material };
app.run(handler)
}
+101
View File
@@ -0,0 +1,101 @@
use crate::AppHandler;
use crate::core::{Context, Renderer};
use crate::pipeline::PipelineCache;
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;
pub struct App {
pub context: Context,
pub renderer: Renderer,
pub event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
pub cache: PipelineCache,
pub scene: Scene,
pub window: Arc<Window>,
}
impl App {
pub fn run<H: AppHandler + 'static>(mut self, mut 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
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,
..
} => {
// render logic
handler.render(&mut self);
}
winit::event::Event::WindowEvent {
event: winit::event::WindowEvent::CloseRequested,
..
} => {
elwt.exit();
}
_ => {}
}
})
.map_err(|_| WsgError::WindowSystem)
}
}
pub struct AppBuilder {
title: String,
width: u32,
height: u32,
}
impl AppBuilder {
pub fn new() -> Self {
Self {
title: APP_DEFAULT_TITLE.to_string(),
width: APP_DEFAULT_WIDTH,
height: APP_DEFAULT_HEIGHT,
}
}
pub fn title(mut self, title: &str) -> Self {
self.title = title.to_string();
self
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.width = width;
self.height = height;
self
}
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();
Ok(App {
context,
renderer,
cache,
scene,
event_loop: Some(event_loop),
window,
})
}
}
+6
View File
@@ -0,0 +1,6 @@
use crate::app::App;
pub trait AppHandler {
fn update(&mut self, _app: &mut App) {}
fn render(&mut self, app: &mut App);
}
+4
View File
@@ -16,8 +16,12 @@
//! use wsg_lib::resources::{Mesh, Material, Vertex};
//! use wsg_lib::utils::BASIC_SHADER;
//! ```
pub mod app;
pub mod core;
pub mod handler;
pub mod pipeline;
pub mod resources;
pub mod scene;
pub mod utils;
pub use handler::AppHandler;
+6 -5
View File
@@ -15,8 +15,8 @@
//! - wgpu 30 requires `compilation_options` in VertexState/FragmentState and `depth_slice` in color attachments.
//! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer.
use crate::utils::BASIC_SHADER;
use crate::resources::Vertex;
use crate::utils::BASIC_SHADER;
use std::collections::HashMap;
use std::sync::Arc;
@@ -24,6 +24,7 @@ use std::sync::Arc;
/// Shader pipeline cache: maps (shader_id, format) keys to compiled RenderPipelines.
/// Ensures each unique shader+format combination is compiled at most once; subsequent requests return cached instances.
pub struct PipelineCache {
device: Arc<wgpu::Device>,
/// Cached pipelines keyed by their shader identifier string. Multiple Materials sharing the same ID share one Arc-wrapped pipeline.
pipelines: HashMap<String, Arc<wgpu::RenderPipeline>>,
/// Maps shader IDs to file paths on disk for WGSL loading in `load_shader()`.
@@ -33,8 +34,9 @@ pub struct PipelineCache {
impl PipelineCache {
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines.
/// Called at application startup before any Material creation. Shader paths must be registered via register_shader() first.
pub fn new() -> Self {
pub fn new(device: Arc<wgpu::Device>) -> Self {
Self {
device,
pipelines: HashMap::new(),
// Maps shader IDs to file paths on disk for WGSL loading in load_shader().
// When a path exists, it reads from it; otherwise falls back to BASIC_SHADER constant.
@@ -70,7 +72,6 @@ impl PipelineCache {
/// Returns an Arc-wrapped RenderPipeline ready for rendering. Called by Material::new().
pub fn get_or_create(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader_id: &str,
) -> Arc<wgpu::RenderPipeline> {
@@ -85,8 +86,8 @@ impl PipelineCache {
.get(shader_id)
.map(|s| s.as_str())
.unwrap_or(shader_id);
let shader = self.load_shader(device, path);
let pipeline = Self::build_pipeline(device, format, &shader);
let shader = self.load_shader(&self.device, path);
let pipeline = Self::build_pipeline(&self.device, format, &shader);
// Step 3: Cache the new pipeline behind Arc and return it
let pipeline_arc = Arc::new(pipeline);
+2 -7
View File
@@ -26,14 +26,9 @@ impl Material {
/// Inputs: device (GPU command source for pipeline creation), format (surface texture format),
/// shader_id (unique key into PipelineCache), cache (mutable ref for potential insertion).
/// Returns a Material holding the Arc-wrapped pipeline. Called at scene initialization time.
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader_id: &str,
cache: &mut PipelineCache,
) -> Self {
pub fn new(format: wgpu::TextureFormat, shader_id: &str, cache: &mut PipelineCache) -> Self {
// Request pipeline from cache — returns cached instance if already exists, creates new otherwise
let pipeline = cache.get_or_create(device, format, shader_id);
let pipeline = cache.get_or_create(format, shader_id);
Self {
shader_id: shader_id.to_string(),
pipeline,
+4
View File
@@ -14,3 +14,7 @@ pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
/// The basic WGSL shader source code, embedded at compile time via `include_str!`.
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
pub const APP_DEFAULT_TITLE: &str = "WSG App";
pub const APP_DEFAULT_WIDTH: u32 = 800;
pub const APP_DEFAULT_HEIGHT: u32 = 600;