Files
wsg/README.md
T
Jérôme Bousquié f10e249898 feat(renderer): active camera wired to frame uniforms (Étape 4.3)
- Camera enrichie: fov/near/far stockés, Default (pos (0,0,3), 45°, near 0.1,
  far 100), with_perspective(), projection_matrix(aspect) depuis les params
  stockés (au lieu de les passer en argument).
- Scene porte une caméra active: set_camera()/camera() (défaut Camera::default).
- Renderer::render_scene(view, scene, aspect) écrit chaque frame view/proj/
  cam_pos réels dans le buffer frame (write_frame_uniforms) avant de dessiner;
  le Renderer garde le handle du frame_buffer. Le chemin bas-niveau render()
  conserve les valeurs par défaut (identité).
- App::render_scene calcule l'aspect depuis window.inner_size() (le Renderer
  reste indépendant de la fenêtre).

Docs synchronisées: DRAFT (4.3 coche), README (statut 3D-infra + quick ref),
PLAN (caméras), ROADMAP (1.1/1.3/1.5/2.3).

Validation: check workspace+examples 0 warning, test (Pod + wgsl) OK, doc 0
warning, fmt propre. Le rendu 3D visible attend Étape 5 (brancher standard).
2026-09-16 17:25:50 +02:00

11 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, and the high-level "declarative" workflow (automatic App scene 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 infrastructure (uniform bind groups, MVP + camera in the pipeline) ✅ Working at the engine level — the Renderer uploads per-frame camera matrices (active Camera) and per-entity world matrices to shared uniform buffers every frame; the bundled basic shader still ignores them, so visible 3D awaits wiring standard_shader.wgsl to an example

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. The Phong-lit standard_shader.wgsl exists and validates, and the uniform plumbing (bind groups + per-frame camera + per-entity world matrices) is in place, but it is not yet bound to a visible example.

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::{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_entity and PipelineCache::register_shader currently return Result<_, 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, 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, Mesh/Vertex, Scene (string-ID registry), Camera/Transform (active camera wired to the frame uniforms, Étape 4.3).

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 ✅ 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 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

  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 uniforms + camera support in the vertex shader. (Engine-side plumbing done 2026-09-16: uniform bind groups, per-frame active camera matrices, per-entity world matrices; visible 3D awaits wiring standard_shader.wgsl to an example — Étape 5.)
  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.