- DRAFT.md : cases 9.1-9.4 cochées (terminées et vérifiées 2026-09-18), point d'étape validé, bilan de fin d'étape rédigé. - README.md : Renderer::new prend désormais width/height (signature Étape 9). - ROADMAP.md : décisions D1-D4 actées + resize avec recréation de la depth texture planifié en Phase 4.4 (acté D3, 2026-09-18).
WSG - WGPU Simple Graphics Library
WSG is a Rust library that wraps wgpu and winit for simple GPU drawing. It groups the five core wgpu objects (Instance, Surface, Adapter, Device, Queue) behind a single Context, adds small building blocks (Mesh, Material, PipelineCache, Frame), and exposes the low-level primitives for advanced users.
Status: unstable development version. The manual workflow below is fully working, the high-level "declarative" workflow (automatic
Appscene rendering) works for flat/NDC drawing, and the 3D MVP is reached : thecubeexample (Étape 5) renders a rotating, Phong-lit cube throughApp::render_scene. Since Étape 8, meshes are declared from a CPUGeometry(retained asArc<Geometry>on the Mesh) instead of raw vertex arrays. The GPU-driven two-pass pipeline described in the architecture docs is not implemented yet — see Status and Roadmap.
Status
| Area | State |
|---|---|
Manual workflow (Context + Renderer + PipelineCache) |
✅ Working |
App / AppBuilder / AppHandler event-loop facade |
✅ Working — window, events, frame presentation, and automatic scene rendering (the per-frame view is exposed via Frame::view()) |
Scene resource/entity registry |
✅ Working — the engine renders every registered entity automatically in one batched render pass (App::render_scene) |
| GPU-driven two-pass pipeline (Compute → indirect draw) | 📋 Roadmap — spec in docs/tech/ARCHI_CPU_GPU.md |
| 3D infrastructure (uniform bind groups, MVP + camera in the pipeline) | ✅ Working — the Renderer uploads per-frame camera matrices (active Camera) and per-entity world matrices to shared uniform buffers every frame; the MVP is reached (Étape 5) : the cube example renders a rotating Phong-lit cube via the standard shader |
Note: the standard_shader.wgsl (Phong, with an explicit unlit mode) is now the single shader the library ships. The old basic_shader.wgsl was removed as a separate pipeline family (Étape 5) : flat 2D drawing is the unlit variant of standard (Renderer::set_unlit(true) or app.renderer_mut().set_unlit(true), DRAFT « 2D ⊂ 3D »). See the cube example (3D, lit) and the simple example (2D, unlit).
What it does
Manual workflow (working — recommended today)
Bypass the App facade and drive Context, Renderer and PipelineCache yourself. This is the only workflow that renders pixels today (same code as the manual example):
use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use wsg_lib::core::{Context, Frame, Renderer};
use wsg_lib::pipeline::PipelineCache;
use wsg_lib::resources::{Geometry, Material, Mesh};
use wsg_lib::utils;
fn main() {
// Window + async GPU init
let event_loop = EventLoop::new().unwrap();
let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap());
let context = pollster::block_on(Context::new(window.clone())).expect("GPU init failed");
let format = context.configure(&context.adapter, 800, 600).expect("surface config failed");
// Renderer + shader cache (falls back to the embedded shader if the file is missing)
// `set_unlit(true)` selects flat 2D rendering (the quad below is drawn in NDC space, unlit).
// Étape 9 : width/height size the depth buffer allocated inside the Renderer.
let mut renderer = Renderer::new(&context, format, 800, 600);
renderer.set_unlit(true);
let mut cache = PipelineCache::new(Arc::new(context.device.clone()));
cache.register_shader("standard", utils::STANDARD_SHADER_PATH).unwrap();
// Material + mesh (Étape 8 : le mesh est construit depuis une `Geometry` — positions,
// attributs optionnels en builder, défauts blancs via `to_vertices`).
let material = Material::new(renderer.format(), "standard", &mut cache);
let geometry = Geometry::new(vec![
[-0.5, 0.5, 0.0], // Haut-Gauche
[ 0.5, 0.5, 0.0], // Haut-Droite
[ 0.5, -0.5, 0.0], // Bas-Droite
[-0.5, -0.5, 0.0], // Bas-Gauche
])
.with_colors(vec![
[1.0, 0.0, 0.0, 1.0], // Rouge
[0.0, 1.0, 0.0, 1.0], // Vert
[0.0, 0.0, 1.0, 1.0], // Bleu
[1.0, 1.0, 0.0, 1.0], // Jaune
])
.with_indices(vec![0, 1, 2, 0, 2, 3]);
let mesh = Mesh::from_geometry(renderer.device(), Arc::new(geometry), None);
// 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) {
renderer.render(frame.view(), &mesh, &material);
renderer.present(frame);
}
}
winit::event::Event::WindowEvent { event: winit::event::WindowEvent::CloseRequested, .. } => elwt.exit(),
_ => {}
}
}).unwrap();
}
Declarative workflow (work in progress)
The intended API: register your scene's resources and entities once, then let App handle the window lifecycle, event processing and frame presentation. Users implement the AppHandler trait to inject per-frame logic:
use wsg_lib::app::AppBuilder;
use wsg_lib::{App, AppHandler};
struct MyGame;
impl AppHandler for MyGame {
// update() has an empty default — implement it to mutate scene state each frame.
// render(app, frame) has a default that draws the whole scene automatically via
// app.render_scene(frame.view()). You don't need to implement it for the common case.
}
#[pollster::main]
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
let app = AppBuilder::new().build().await?;
// Register your scene once (string IDs), then App renders it automatically each frame.
// Since Étape 7 the Scene owns the pipeline cache: build materials/meshes through it and
// link the material to the mesh (no material_id on the entity anymore).
// Since Étape 8 meshes are declared from a `Geometry` (positions + optional attributes).
// app.renderer_mut().set_unlit(true); // select flat 2D rendering (optional)
// app.scene.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)?;
// app.scene.add_material_shader("mat", "standard")?; // build via the Scene's cache
// let geometry = wsg_lib::resources::Geometry::new(vec![[-0.5,0.5,0.0],[0.5,0.5,0.0]])
// .with_colors(vec![[1.0,0.0,0.0,1.0],[0.0,1.0,0.0,1.0]]);
// app.scene.create_mesh("quad", geometry, Some("mat"))?; // mesh links its Material
// app.scene.add_entity("my_quad", "quad")?;
app.run(MyGame)
}
API note:
Scene::register_shader/add_material_shader/create_mesh/add_entityandPipelineCache::register_shadercurrently returnResult<_, String>— typed error unification is on the roadmap. Since Étape 8,Scene::create_mesh(id, geometry, material)takes a CPUGeometry(source of truth, retained on the Mesh) instead of raw&[Vertex].
Architecture overview
- Manager layer (
Context) — owns the GPU hardware lifecycle (Instance → Surface → Adapter → Device → Queue). Created once at startup;configure()sets up the swapchain,Framewraps each frame's surface texture + view. - Executor layer (
Renderer) — binds aMaterialpipeline +Meshbuffers into a RenderPass and submits the commands. Rendering a wholeScene(render_scene) batches all entities into one encoder + one submit per frame; the low-levelrenderstill allocates one per object. - Supporting pieces —
PipelineCache(shader → compiled RenderPipeline,Arc-shared),Material,Geometry/Mesh/Vertex,Scene(string-ID registry),Camera/Transform(active camera wired to the frame uniforms, Étape 4.3).Geometryis the CPU source of truth (positions/normals/UVs/colors),Meshuploads it to GPU buffers and retains theArc<Geometry>,Vertexis the interleaved upload contract (Étape 8).
The planned target architecture — a GPU-driven two-pass pipeline (Compute Pass: world matrices + frustum culling → Indirect Draw Buffer, then a single draw_indexed_indirect per frame) — is specified in docs/tech/ARCHI_APP.md and docs/tech/ARCHI_CPU_GPU.md but is not implemented yet.
Quick reference
| Concept | Type | Responsibility | Status |
|---|---|---|---|
| App / AppBuilder | Facade | Window lifecycle + winit event loop + frame presentation | ✅ (auto scene rendering via App::render_scene) |
| AppHandler | Trait | User-defined update() / render() callbacks |
✅ (Frame::view() exposed; default render draws the scene) |
| Scene | Struct | String-ID registry: meshes, materials, entities | ✅ (registry auto-rendered by the facade) |
| Context | Struct | GPU hardware lifecycle (Instance, Surface, Adapter, Device, Queue) | ✅ |
| Renderer | Struct | Binds Material + Mesh into a RenderPass, submits | ✅ (render_scene batches one pass/frame) |
| PipelineCache | Struct | Shader → compiled RenderPipeline cache | ✅ |
| Material | Struct | Shader ID → RenderPipeline | ✅ |
| Geometry | Struct | CPU-side scattered vertex data (positions/normals/UVs/colors/indices), source of truth | ✅ (Étape 8 — retained Arc<Geometry> on Mesh) |
| Mesh / Vertex | Struct | GPU geometry container / CPU-side interleaved upload tuple | ✅ |
| Frame | Struct | Per-frame RAII wrapper (surface texture + view) | ✅ |
| Camera / Transform | Struct | Camera & transform math | ✅ Active camera + transform wired to per-frame uniforms (Étape 4.3) |
Getting started
WSG is not published on crates.io — depend on it by path:
[dependencies]
wsg-lib = { path = "/path/to/wsg/lib" }
pollster = "0.4" # only if you use the async AppBuilder
| Action | Command |
|---|---|
| Build everything | cargo build --workspace |
| Run the 3D MVP example | cargo run -p wsg-lib --example cube |
| Run the working example | cargo run -p wsg-lib --example manual |
| Check everything (incl. examples) | cargo check --all-targets |
The manual example is the reference for the low-level workflow. The simple example (App facade) registers a colored quad and renders it automatically through the declarative path — it draws a scene without importing wgpu. The cube example (Étape 5) demonstrates the 3D MVP: a rotating Phong-lit cube, also through the declarative path and without importing wgpu.
Documentation
The architecture docs live in docs/tech/ and are written in French. Each document states whether it describes the current (implemented) state or the target (planned, not yet implemented) architecture:
- ARCHI_APP — engine architecture. 🎯 Target — the GPU-driven two-pass pipeline parts are not implemented yet.
- ARCHI_CPU_GPU — CPU/GPU workload split specification. 🎯 Target — GPU-driven pipeline, ROADMAP Phase 3.
- ARCHI_RENDU — update/render mutability model. 🎯 Target — model for the future scene auto-render.
- ARCHI_ARENES — 🎯 Target/deferred — slotmap generational handles; String IDs are used today.
- FRAME_LOOP — frame lifetime and resource persistence. ✅ Current — implemented.
Roadmap
- ✅ Scene auto-rendering —
App::render_sceneiterates registered entities and draws them in one encoder/submit per frame; the frame view is exposed toAppHandler::renderfor custom draws. (Done 2026-09-16.) - GPU-driven two-pass pipeline — Compute Pass (world matrices + frustum culling) filling an indirect draw buffer, single
draw_indexed_indirect(see ARCHI_CPU_GPU). - CPU→GPU transform sync — persistent transform buffers with ring (triple) buffering.
- ✅ Real 3D pipeline (MVP atteint) — MVP uniforms + camera support in the vertex shader. (Engine plumbing done 2026-09-16 ; Étape 5, 2026-09-17 :
standardbranché sur l'exemplecube— un cube unitaire éclairé (Phong) qui tourne, rendu automatiquement parApp::render_scene. Retrait debasic: le 2D plat = variante unlit destandardviaRenderer::set_unlit.) - Typed resource handles — keep String IDs for the MVP (current design, source of truth in
Scene); slotmap-based generational handles (ARCHI_ARENES.md) are deferred to a later performance pass. - Error unification — replace
Result<_, String>inScene/PipelineCachewith typed errors. - ✅ CPU geometry storage (Étape 8) —
Meshretains a sharedArc<Geometry>(CPU source of truth with colors) alongside its GPU buffers; meshes are declared from aGeometryviaMesh::from_geometry/Scene::create_mesh(id, geometry, material)instead of raw&[Vertex]arrays. (Done 2026-09-18;transformstays onEntity— deviation D3.)