Files
wsg/README.md
T
Jérôme Bousquié 9ad47e8790 docs(étape8): valider et documenter le refactor stockage CPU Arc<Geometry> (8.6)
- DRAFT.md : cases 8.1-8.6 cochées, état 'terminée et vérifiée 2026-09-18', bilan final.
- README.md : workflow manuel et déclaratif (extraits Mesh::new -> Geometry/from_geometry),
  note API create_mesh(Geometry), section architecture + table quick reference, roadmap +1 (CPU storage).
- ROADMAP.md (1.2) : colors + refactor Mesh/Scene cochés 'fait', déviation D3 'implémenté'.
- PLAN.md : statut réel à jour 2026-09-18 (Étape 8 effectuée).
- resources/README.md : Mesh::new() -> from_geometry() + rétention CPU.
2026-09-18 10:22:43 +02:00

13 KiB

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 App scene rendering) works for flat/NDC drawing, and the 3D MVP is reached : the cube example (Étape 5) renders a rotating, Phong-lit cube through App::render_scene. Since Étape 8, meshes are declared from a CPU Geometry (retained as Arc<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

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).
    let mut renderer = Renderer::new(&context, format);
    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_entity and PipelineCache::register_shader currently return Result<_, String> — typed error unification is on the roadmap. Since Étape 8, Scene::create_mesh(id, geometry, material) takes a CPU Geometry (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, Frame wraps each frame's surface texture + view.
  • Executor layer (Renderer) — binds a Material pipeline + Mesh buffers into a RenderPass and submits the commands. Rendering a whole Scene (render_scene) batches all entities into one encoder + one submit per frame; the low-level render still 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). Geometry is the CPU source of truth (positions/normals/UVs/colors), Mesh uploads it to GPU buffers and retains the Arc<Geometry>, Vertex is 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

  1. ✅ Scene auto-rendering — App::render_scene iterates registered entities and draws them in one encoder/submit per frame; the frame view is exposed to AppHandler::render for custom draws. (Done 2026-09-16.)
  2. GPU-driven two-pass pipeline — Compute Pass (world matrices + frustum culling) filling an indirect draw buffer, single draw_indexed_indirect (see ARCHI_CPU_GPU).
  3. CPU→GPU transform sync — persistent transform buffers with ring (triple) buffering.
  4. ✅ Real 3D pipeline (MVP atteint) — MVP uniforms + camera support in the vertex shader. (Engine plumbing done 2026-09-16 ; Étape 5, 2026-09-17 : standard branché sur l'exemple cube — un cube unitaire éclairé (Phong) qui tourne, rendu automatiquement par App::render_scene. Retrait de basic : le 2D plat = variante unlit de standard via Renderer::set_unlit.)
  5. 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.
  6. Error unification — replace Result<_, String> in Scene/PipelineCache with typed errors.
  7. ✅ CPU geometry storage (Étape 8) — Mesh retains a shared Arc<Geometry> (CPU source of truth with colors) alongside its GPU buffers; meshes are declared from a Geometry via Mesh::from_geometry/Scene::create_mesh(id, geometry, material) instead of raw &[Vertex] arrays. (Done 2026-09-18; transform stays on Entity — deviation D3.)