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
+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);
}
}