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
//! update → render → present. Le rendu automatisé de la scène n'est pas encore en place
//! (README, Roadmap étape 1) : `render()` est donc vide pour l'instant.
//! 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é.
use std::sync::Arc;
use wsg_lib::AppHandler;
use wsg_lib::app::AppBuilder;
use wsg_lib::resources::{Material, Mesh, Vertex};
use wsg_lib::utils::WsgError;
use wsg_lib::{App, AppHandler};
struct MonQuad;
impl AppHandler for MonQuad {
fn render(&mut self, _app: &mut App) {}
}
impl AppHandler for MonQuad {}
#[pollster::main]
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)
}
+10 -2
View File
@@ -68,8 +68,8 @@ impl App {
// Rendering logic
let frame = self.context.get_next_frame();
// On appelle le render() de l'utilisateur
handler.render(&mut self);
// 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);
}
@@ -84,6 +84,14 @@ impl App {
})
.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.
+54 -12
View File
@@ -21,6 +21,7 @@
use crate::core::Context;
use crate::core::Frame;
use crate::resources::{Material, Mesh};
use crate::scene::Scene;
/// 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
@@ -80,18 +81,40 @@ impl Renderer {
..Default::default()
});
render_pass.set_pipeline(&material.pipeline);
if mesh.num_vertices > 0 {
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
} else {
// If no vertices, skip drawing entirely (nothing to render)
return;
}
if let Some(index_buffer) = &mesh.index_buffer {
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..mesh.num_indices, 0, 0..1);
} else {
render_pass.draw(0..mesh.num_vertices, 0..1);
draw_entity(&mut render_pass, mesh, material);
}
self.queue.submit(std::iter::once(encoder.finish()));
}
/// 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
/// `render` does), minimizing GPU submissions. Called automatically each frame by the default
/// `AppHandler::render` through `App::render_scene`.
/// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are drawn.
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene) {
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()));
@@ -116,3 +139,22 @@ impl Renderer {
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.
use crate::app::App;
use crate::core::Frame;
/// 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
/// 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 {
/// 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.
fn update(&mut self, _app: &mut App) {}
/// Called during each RedrawRequested event after frame acquisition. Used for executing draw calls
/// by iterating Scene entities and calling app.renderer.render(view, mesh, material) per entity.
/// Must be implemented — called every frame that needs rendering.
/// Inputs: app — mutable reference to the App facade providing access to all subsystems.
fn render(&mut self, app: &mut App);
/// Called during each RedrawRequested event after frame acquisition, receiving the current frame.
/// Used for custom draw call execution. Default implementation renders the whole scene
/// automatically (`app.render_scene(frame.view())`), so most users don't need to override it.
/// Advanced users override this method to control drawing manually.
/// 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 core;
pub mod handler;
pub mod math;
pub mod pipeline;
pub mod resources;
pub mod scene;
pub mod utils;
pub mod math;
/// 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.
+2 -2
View File
@@ -15,9 +15,9 @@
//! - `geometry.rs`: Defines the `Geometry` struct for mesh data storage
//! - `camera.rs`: Defines the `Camera` struct and view/projection matrix calculations
pub mod transform;
pub mod geometry;
pub mod transform;
// Re-exports
pub use transform::Transform;
pub use geometry::Geometry;
pub use transform::Transform;
+1 -1
View File
@@ -12,7 +12,7 @@
//! - `Transform`: Core struct for position/rotation/scale
//! - `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.
#[derive(Debug, Clone, Copy, PartialEq)]