# WSG - WGPU Simple Graphics Library WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) and [winit](https://crates.io/crates/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](#status) and [Roadmap](#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](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): ```rust 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: ```rust 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](docs/tech/ARCHI_APP.md) and [docs/tech/ARCHI_CPU_GPU.md](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: ```toml [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, pixel-rendering workflow. The `simple` example (App facade) now compiles and opens a window with a running update → render → present loop, but it does not draw a scene yet (see [Roadmap](#roadmap)). ## 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](docs/tech/ARCHI_APP.md) — engine architecture. 🎯 **Target** — the GPU-driven two-pass pipeline parts are not implemented yet. - [ARCHI_CPU_GPU](docs/tech/ARCHI_CPU_GPU.md) — CPU/GPU workload split specification. 🎯 **Target** — GPU-driven pipeline, ROADMAP Phase 3. - [ARCHI_RENDU](docs/tech/ARCHI_RENDU.md) — update/render mutability model. 🎯 **Target** — model for the future scene auto-render. - [ARCHI_ARENES](docs/tech/ARCHI_ARENES.md) — 🎯 **Target/deferred** — slotmap generational handles; String IDs are used today. - [FRAME_LOOP](docs/tech/FRAME_LOOP.md) — frame lifetime and resource persistence. ✅ **Current** — implemented. ## 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.