- 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).
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
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.
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_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. 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
- Scene auto-rendering —
App/Rendereriterate registered entities and draw them in one encoder/submit per frame; expose the frame view toAppHandler::renderfor custom draws. - 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.