Étape 3 (infrastructure uniforms) + le câblage minimal d'Étape 4 pour garder les exemples exécutables (wgpu requiert que tous les bind groups du layout pipeline soient posés au draw) : - resources/uniform.rs : types bytemuck Pod FrameUniforms (192 B) et ObjectUniform (64 B), alignés 16 octets sans padding; offsets vérifiés par un test unitaire contre le contrat du shader. glam feature bytemuck activé. - pipeline_cache: create_uniform_bind_group_layouts() expose les 2 layouts (frame @0 Vertex|Fragment + object @1 Vertex); build_pipeline les attache à TOUT pipeline (un seul layout pour tous, décision actée). - Renderer: alloue le buffer frame partagé + BindGroup(0) (défaut identité, mode lit) et un object identité partagé pour le chemin bas-niveau; cache RefCell<HashMap<label,(buffer,bindgroup)>> par entité, model réécrit chaque frame depuis transform.to_matrix(); draw_entity pose groupes 0+1. 4.3 (caméra active + aspect) non implémenté: simple/manual restent exécutables car basic ignore ces uniforms. Documentation DRAFT mise à jour. Validation: check workspace+examples 0 warning, doc 0 warning, test (Pod+wgsl) OK, fmt propre.
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, and the high-level "declarative" workflow (automatic
Appscene rendering) works for flat/NDC drawing. 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 transforms (MVP uniforms, camera in the pipeline) | 📋 Roadmap — the bundled shader draws positions straight to NDC today |
Note: the bundled basic_shader.wgsl treats vertex positions as already in NDC space, so what you can see today is flat, untransformed drawing (e.g. a colored quad) — not a 3D scene.
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::{Material, Mesh, Vertex};
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)
let renderer = Renderer::new(&context, format);
let mut cache = PipelineCache::new(Arc::new(context.device.clone()));
cache.register_shader("basic", utils::BASIC_SHADER_PATH).unwrap();
// Material + mesh
let material = Material::new(renderer.format(), "basic", &mut cache);
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(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) {
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:
// app.cache.register_shader("basic", wsg_lib::utils::BASIC_SHADER_PATH)?;
// app.scene.add_mesh("quad", Arc::new(mesh))?;
// app.scene.add_material("mat", Arc::new(Material::new(app.renderer.format(), "basic", &mut app.cache)))?;
// app.scene.add_entity("my_quad", "quad", "mat")?;
app.run(MyGame)
}
API note:
Scene::add_mesh/add_material/add_entityandPipelineCache::register_shadercurrently returnResult<_, String>— typed error unification is on the roadmap.
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,Mesh/Vertex,Scene(string-ID registry),Camera/Transform(types only, not yet used by the pipeline).
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 | ✅ |
| Mesh / Vertex | Struct | GPU geometry container / CPU-side vertex tuple | ✅ |
| Frame | Struct | Per-frame RAII wrapper (surface texture + view) | ✅ |
| Camera / Transform | Struct | Camera & transform math | 📋 Types only, not in the pipeline |
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 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.
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 uniforms + camera support in the vertex shader.
- 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.