feat(core): expose frame view and auto-render the scene

- AppHandler::render now receives the current &Frame; its default
  implementation renders the whole scene automatically via
  app.render_scene(frame.view()) (Option A). Users can simply not
  implement render for full auto-rendering.
- Add Renderer::render_scene: batch-renders every scene entity in a
  single render pass. Factored per-mesh draw logic into a private
  draw_entity helper shared with Renderer::render.
- Add App::render_scene(view) delegating to the Renderer.
- Fill simple.rs with a real quad (mesh/material/entity) without
  importing wgpu; the scene now auto-renders via the trait default.
This commit is contained in:
Jérôme Bousquié
2026-09-16 10:36:35 +02:00
parent 628c125925
commit 4acf1d821d
8 changed files with 136 additions and 34 deletions
+55 -8
View File
@@ -1,19 +1,66 @@
//! Workflow déclaratif minimal (~15 lignes), sans manipulation WGPU explicite. //! 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 //! `AppBuilder` ouvre la fenêtre, construit le `Context`/`Renderer` et fait tourner la boucle
//! update → render → present. Le rendu automatisé de la scène n'est pas encore en place //! update → render → present. La scène se rend automatiquement : la méthode `render()` par défaut
//! (README, Roadmap étape 1) : `render()` est donc vide pour l'instant. //! 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é.
use std::sync::Arc;
use wsg_lib::AppHandler;
use wsg_lib::app::AppBuilder; use wsg_lib::app::AppBuilder;
use wsg_lib::resources::{Material, Mesh, Vertex};
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
use wsg_lib::{App, AppHandler};
struct MonQuad; struct MonQuad;
impl AppHandler for MonQuad { impl AppHandler for MonQuad {}
fn render(&mut self, _app: &mut App) {}
}
#[pollster::main] #[pollster::main]
async fn main() -> Result<(), WsgError> { async fn main() -> Result<(), WsgError> {
let app = AppBuilder::new().title("WSG Simple").build().await?; 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();
app.run(MonQuad) app.run(MonQuad)
} }
+10 -2
View File
@@ -68,8 +68,8 @@ impl App {
// Rendering logic // Rendering logic
let frame = self.context.get_next_frame(); let frame = self.context.get_next_frame();
// On appelle le render() de l'utilisateur // On appelle le render() de l'utilisateur (reçoit la frame courante)
handler.render(&mut self); handler.render(&mut self, &frame);
// On présente automatiquement // On présente automatiquement
self.renderer.present(frame); self.renderer.present(frame);
} }
@@ -84,6 +84,14 @@ impl App {
}) })
.map_err(|_| WsgError::WindowSystem) .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.
pub fn render_scene(&self, view: &wgpu::TextureView) {
self.renderer.render_scene(view, &self.scene);
}
} }
/// Builder for constructing a configured `App` instance with custom title and dimensions. /// Builder for constructing a configured `App` instance with custom title and dimensions.
+54 -12
View File
@@ -21,6 +21,7 @@
use crate::core::Context; use crate::core::Context;
use crate::core::Frame; use crate::core::Frame;
use crate::resources::{Material, Mesh}; use crate::resources::{Material, Mesh};
use crate::scene::Scene;
/// The Executor layer of the architecture. Holds shared references to Device and Queue from Context, /// The Executor layer of the architecture. Holds shared references to Device and Queue from Context,
/// plus the surface texture format. Executes WGPU rendering commands by binding Materials and Meshes /// plus the surface texture format. Executes WGPU rendering commands by binding Materials and Meshes
@@ -80,18 +81,40 @@ impl Renderer {
..Default::default() ..Default::default()
}); });
render_pass.set_pipeline(&material.pipeline); draw_entity(&mut render_pass, mesh, material);
if mesh.num_vertices > 0 { }
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); self.queue.submit(std::iter::once(encoder.finish()));
} else { }
// If no vertices, skip drawing entirely (nothing to render)
return; /// Renders every entity in `scene` into the given color view within a single batched render pass.
} /// This avoids allocating a separate encoder and render pass per entity (which the low-level
if let Some(index_buffer) = &mesh.index_buffer { /// `render` does), minimizing GPU submissions. Called automatically each frame by the default
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16); /// `AppHandler::render` through `App::render_scene`.
render_pass.draw_indexed(0..mesh.num_indices, 0, 0..1); /// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are drawn.
} else { pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene) {
render_pass.draw(0..mesh.num_vertices, 0..1); let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("scene encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scene render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
for (_label, mesh, material) in scene.iter_entities() {
draw_entity(&mut render_pass, mesh, material);
} }
} }
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
@@ -116,3 +139,22 @@ impl Renderer {
self.format self.format
} }
} }
/// Binds a Material pipeline and Mesh buffers into an active render pass and issues the draw call.
/// Shared by `Renderer::render` and `Renderer::render_scene` to avoid duplicated draw logic.
/// Draws indexed geometry when an index buffer exists, otherwise falls back to a non-indexed draw.
/// Inputs: pass (active render pass), mesh (geometry to draw), material (pipeline to bind).
fn draw_entity(pass: &mut wgpu::RenderPass<'_>, mesh: &Mesh, material: &Material) {
if mesh.num_vertices == 0 {
// No vertices — nothing to render.
return;
}
pass.set_pipeline(&material.pipeline);
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
if let Some(index_buffer) = &mesh.index_buffer {
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
pass.draw_indexed(0..mesh.num_indices, 0, 0..1);
} else {
pass.draw(0..mesh.num_vertices, 0..1);
}
}
+11 -6
View File
@@ -17,18 +17,23 @@
//! the engine "brick by brick" through direct Context/PipelineCache/Renderer manipulation if needed. //! the engine "brick by brick" through direct Context/PipelineCache/Renderer manipulation if needed.
use crate::app::App; use crate::app::App;
use crate::core::Frame;
/// Trait defining user-provided game logic injected into the render loop at two callback points. /// Trait defining user-provided game logic injected into the render loop at two callback points.
/// Users implement this trait to define what happens per-frame: update (pre-render logic) and /// Users implement this trait to define what happens per-frame: update (pre-render logic) and
/// render (draw call execution). Default implementations provide empty update for convenience. /// render (draw call execution). Default implementations provide empty update and automatic
/// scene rendering for convenience.
pub trait AppHandler { pub trait AppHandler {
/// Called once per frame before rendering begins. Used for physics updates, input processing, /// 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. /// 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. /// Inputs: _app — mutable reference to the App facade providing access to all subsystems.
fn update(&mut self, _app: &mut App) {} fn update(&mut self, _app: &mut App) {}
/// Called during each RedrawRequested event after frame acquisition. Used for executing draw calls /// Called during each RedrawRequested event after frame acquisition, receiving the current frame.
/// by iterating Scene entities and calling app.renderer.render(view, mesh, material) per entity. /// Used for custom draw call execution. Default implementation renders the whole scene
/// Must be implemented — called every frame that needs rendering. /// automatically (`app.render_scene(frame.view())`), so most users don't need to override it.
/// Inputs: app — mutable reference to the App facade providing access to all subsystems. /// Advanced users override this method to control drawing manually.
fn render(&mut self, app: &mut App); /// Inputs: app — mutable reference to the App facade; frame — the acquired frame exposing its view.
fn render(&mut self, app: &mut App, frame: &Frame) {
app.render_scene(frame.view());
}
} }
+1 -1
View File
@@ -31,11 +31,11 @@
pub mod app; pub mod app;
pub mod core; pub mod core;
pub mod handler; pub mod handler;
pub mod math;
pub mod pipeline; pub mod pipeline;
pub mod resources; pub mod resources;
pub mod scene; pub mod scene;
pub mod utils; pub mod utils;
pub mod math;
/// Re-export of the high-level application facade for convenient top-level access. /// Re-export of the high-level application facade for convenient top-level access.
/// Users create App instances via `AppBuilder`, then call `.run(handler)` to start the application. /// Users create App instances via `AppBuilder`, then call `.run(handler)` to start the application.
+1 -1
View File
@@ -25,4 +25,4 @@ pub struct Geometry {
pub uvs: Option<Vec<[f32; 2]>>, pub uvs: Option<Vec<[f32; 2]>>,
/// Optional indices for indexed rendering /// Optional indices for indexed rendering
pub indices: Option<Vec<u16>>, pub indices: Option<Vec<u16>>,
} }
+2 -2
View File
@@ -15,9 +15,9 @@
//! - `geometry.rs`: Defines the `Geometry` struct for mesh data storage //! - `geometry.rs`: Defines the `Geometry` struct for mesh data storage
//! - `camera.rs`: Defines the `Camera` struct and view/projection matrix calculations //! - `camera.rs`: Defines the `Camera` struct and view/projection matrix calculations
pub mod transform;
pub mod geometry; pub mod geometry;
pub mod transform;
// Re-exports // Re-exports
pub use geometry::Geometry;
pub use transform::Transform; pub use transform::Transform;
pub use geometry::Geometry;
+2 -2
View File
@@ -12,7 +12,7 @@
//! - `Transform`: Core struct for position/rotation/scale //! - `Transform`: Core struct for position/rotation/scale
//! - `to_matrix()`: Converts transform to a 4x4 matrix //! - `to_matrix()`: Converts transform to a 4x4 matrix
use glam::{Vec3, Quat, Mat4}; use glam::{Mat4, Quat, Vec3};
/// Represents a 3D transformation with translation, rotation, and scale. /// Represents a 3D transformation with translation, rotation, and scale.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@@ -42,4 +42,4 @@ impl Transform {
pub fn to_matrix(&self) -> Mat4 { pub fn to_matrix(&self) -> Mat4 {
Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
} }
} }