Files
wsg/README.md
T
Jérôme Bousquié f6c0851d4f docs: reconcile README/ROADMAP/PLAN state and settle slotmap vs String IDs
- README stays the source of truth for current state; drive/item-5 now
  reflects the settled decision: String IDs for the MVP, slotmap deferred.
- ROADMAP gains a 'point de depart' (present-state) block, marks glam done,
  removes the slotmap Phase-1 mandates, and documents the String-ID decision
  plus the deferred 'typed handles' step.
- PLAN corrects stale [X] checkmarks (App render() cannot draw, Scene
  rendering not automated, simple example does not compile) and notes its
  verification checklist against current reality.
- Remove the unused slotmap direct dependency from wsg-lib (package remains
  in Cargo.lock only as a transitive dep of glow).
2026-09-14 15:28:41 +02:00

9.3 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 and the GPU-driven two-pass pipeline described in the architecture docs are not implemented yet — see Status and Roadmap.

Status

Area State
Manual workflow (Context + Renderer + PipelineCache) ✅ Working
App / AppBuilder / AppHandler event-loop facade 🚧 Scaffold — window, events and frame presentation work, but the render() callback cannot draw yet (the per-frame view is not exposed to it)
Scene resource/entity registry 🚧 Registration API works; the engine does not render the scene yet
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

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.
    fn render(&mut self, _app: &mut App) {
        // The engine acquires and presents the frame around this callback,
        // but scene rendering is not automated yet — see Roadmap.
    }
}

#[pollster::main]
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
    let app = AppBuilder::new().build().await?;

    // Scene registration is available (string IDs):
    //   app.scene.add_mesh("quad", Arc::new(mesh))?;
    //   app.scene.add_material("mat", Arc::new(material))?;
    //   app.scene.add_entity("my_quad", "quad", "mat")?;
    // ...but the engine will not draw them until the declarative pipeline lands.

    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. Today this is one encoder + one submit 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 🚧 Scaffold (no scene rendering)
AppHandler Trait User-defined update() / render() callbacks ✅ (render() has no frame access yet)
Scene Struct String-ID registry: meshes, materials, entities 🚧 Registration only
Context Struct GPU hardware lifecycle (Instance, Surface, Adapter, Device, Queue) ✅
Renderer Struct Binds Material + Mesh into a RenderPass, submits ✅ (one submit per object)
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 working workflow. The simple example (App facade) is work in progress and currently does not compile.

Documentation

The architecture docs live in docs/tech/ and are written in French:

  • ARCHI_APP — engine architecture. ⚠️ Describes the target architecture; the GPU-driven pipeline parts are not implemented yet.
  • ARCHI_CPU_GPU — CPU/GPU workload split specification.
  • ARCHI_RENDU — update/render mutability model.
  • ARCHI_ARENES — planned slotmap-based generational resource handles.
  • FRAME_LOOP — frame lifetime and resource persistence.

Roadmap

  1. Scene auto-rendering — App/Renderer iterate registered entities and draw them in one encoder/submit per frame; expose the frame view to AppHandler::render for custom draws.
  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.
  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.