Jérôme Bousquié 3dd372410f update demos et docs
2026-09-21 12:01:28 +02:00
2026-07-31 20:43:57 +02:00
2026-09-21 11:36:19 +02:00
2026-09-21 12:01:28 +02:00
2026-07-08 12:29:18 +02:00
2026-07-31 19:09:46 +02:00
2026-07-31 19:10:04 +02:00
2026-07-08 12:29:18 +02:00
2026-08-01 09:34:12 +02:00
2026-07-31 19:10:04 +02:00
2026-09-21 12:01:28 +02:00

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 declarative workflow (AppBuilder + App + AppHandler) is the recommended path and is fully working: scene auto-rendering (App::render_scene), 3D Phong lighting, textures, shadows, camera and unified input — the demo example is the showcase. The manual workflow (Context/Renderer/PipelineCache) coexists for fine-grained control. Meshes are declared from a CPU Geometry (retained as Arc<Geometry> on the Mesh). 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 (advanced — fine-grained control)
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 (Step 5) : the cube example renders a rotating Phong-lit cube via the standard shader

Note: standard_shader.wgsl (Phong, with an explicit unlit mode) is the single shader the library ships — flat 2D drawing is its unlit variant (Renderer::set_unlit(true) or app.renderer_mut().set_unlit(true)). See the cube example (3D, lit) and the simple example (2D, unlit).

What it does

Register your scene once in setup(), then let App handle the window lifecycle, events, input and frame presentation — without importing wgpu or winit. This is the workflow of the simple, cube, demo, shadow_test and spot_test examples (excerpt below is simple):

use wsg_lib::app::AppBuilder;
use wsg_lib::resources::Geometry;
use wsg_lib::utils::WsgError;
use wsg_lib::AppHandler;

struct MonQuad;

impl AppHandler for MonQuad {
    fn setup(&mut self, app: &mut wsg_lib::App) {
        app.renderer_mut().set_unlit(true); // 2D flat (optional)
        app.scene
            .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
            .unwrap();
        let geometry = Geometry::new(vec![
            [-0.5,  0.5, 0.0],
            [ 0.5,  0.5, 0.0],
            [ 0.5, -0.5, 0.0],
            [-0.5, -0.5, 0.0],
        ])
        .with_normals(vec![[0.0, 0.0, 1.0]; 4])
        .with_colors(vec![
            [1.0, 0.0, 0.0, 1.0],
            [0.0, 1.0, 0.0, 1.0],
            [0.0, 0.0, 1.0, 1.0],
            [1.0, 1.0, 0.0, 1.0],
        ])
        .with_indices(vec![0, 1, 2, 0, 2, 3]);
        app.scene.create_mesh("quad_mesh", geometry, None).unwrap(); // None = default material
        app.scene.add_entity("quad", "quad_mesh").unwrap();
    }
    // `update(&mut self, app)` — your per-frame logic (empty default).
    // `render(&mut self, app, frame)` — default: `app.render_scene(frame.view())`,
    // the whole scene is drawn automatically in one pass per frame.
}

#[pollster::main]
async fn main() -> Result<(), WsgError> {
    let app = AppBuilder::new().title("WSG Simple").build().await?;
    app.run(MonQuad)
}

API note: Scene methods currently return Result<_, String> — typed-error unification is on the roadmap. Scene::create_mesh(id, geometry, material) takes a CPU Geometry (source of truth, retained as Arc<Geometry> on the Mesh); material = None uses the scene's default material.

The full user documentation (meshes, materials, lights, shadows, camera & input, all examples) lives in docs/user.

Manual workflow (advanced — fine-grained control)

Bypass the App facade and drive Context, Renderer and PipelineCache yourself (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).
    // Step 9: width/height size the depth buffer allocated inside the Renderer.
    let mut renderer = Renderer::new(&context, format, 800, 600);
    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 (Step 8: the mesh is built from a `Geometry` — positions,
    // optional attributes via builder, white defaults via `to_vertices`).
    let material = Material::new(renderer.format(), "standard", &mut cache);
    let geometry = Geometry::new(vec![
        [-0.5, 0.5, 0.0],  // top-left
        [ 0.5, 0.5, 0.0],  // top-right
        [ 0.5, -0.5, 0.0], // bottom-right
        [-0.5, -0.5, 0.0], // bottom-left
    ])
    .with_colors(vec![
        [1.0, 0.0, 0.0, 1.0], // red
        [0.0, 1.0, 0.0, 1.0], // green
        [0.0, 0.0, 1.0, 1.0], // blue
        [1.0, 1.0, 0.0, 1.0], // yellow
    ])
    .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();
}

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, Step 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 (Step 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 setup() / update() / render() callbacks ✅ (default render draws the scene via App::render_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 ✅ (Step 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 (Step 4.3)
Texture Struct GPU diffuse image (device + view + sampler, Rgba8UnormSrgb) ✅ (Step 10 — from_rgba8/from_bytes/from_file/white_placeholder)
Lights / Light Struct Scene-wide light list (directional + point + spot, MAX_LIGHTS = 8) + ambient ✅ (Steps 12-13)
CameraController Struct Orbital camera (yaw/pitch/distance/target; orbit/zoom/reset/apply_to) ✅ (Step 15.C)
InputState Struct Unified keyboard/mouse state (pressed/held/released, mouse delta, scroll) ✅ (Step 15.B — app.input)
math::primitives Module Procedural Geometry generators (cube, plane, uv_sphere, icosphere, cylinder, cone, torus) ✅ (Step 15.A)

Getting started

WSG is not published on crates.io — depend on it by path:

[dependencies]
wsg-lib = { path = "/path/to/wsg/lib" }
pollster = { version = "1", features = ["macro"] }   # for #[pollster::main] (async AppBuilder)
winit = "0.30"   # only if your code mentions winit types (KeyCode, MouseButton)
Action Command
Build everything cargo build --workspace
Run the showcase (primitives, lights, shadows, orbital camera) cargo run -p wsg-lib --example demo
Run the 3D MVP example cargo run -p wsg-lib --example cube
Run the minimal example cargo run -p wsg-lib --example simple
Run the shadow / spot light showcases cargo run -p wsg-lib --example shadow_test / cargo run -p wsg-lib --example spot_test
Run the advanced (manual) example cargo run -p wsg-lib --example manual
Check everything (incl. examples) cargo check --all-targets

The demo example is the showcase: one of each primitive, procedural textures, three lights, a shadow-casting light and a live orbital camera. simple is the minimal declarative app (a colored quad, unlit); cube is the 3D MVP (a rotating Phong-lit, textured cube); shadow_test and spot_test isolate the shadow and spot-light systems; manual is the reference for the low-level workflow. All of them except manual use the declarative path and draw a scene without importing wgpu.

Documentation

Three layers (user docs and API reference in English; technical docs in French):

User documentation — docs/user (how to use the API, no wgpu knowledge needed):

Technical documentation — docs/tech/ (internal architecture; each document states whether it describes the current or the target architecture):

  • ARCHI_APP — engine architecture. ✅ Current facade (App/AppHandler) / 🎯 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. ✅ Current dichotomy (auto scene render) / 🎯 Target — material batching.
  • ARCHI_ARENES — 🎯 Target/deferred — slotmap generational handles; String IDs are used today.
  • FRAME_LOOP — frame lifetime and resource persistence. ✅ Current — implemented.

API reference — full rustdoc: cargo doc -p wsg-lib --no-deps (every public type is documented).

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 reached) — MVP uniforms + camera support in the vertex shader. (Engine plumbing done 2026-09-16; Step 5, 2026-09-17: standard wired into the cube example — a unit cube lit (Phong) and spinning, rendered automatically by App::render_scene. Removal of basic: flat 2D = unlit variant of 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 (Step 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.)
  8. ✅ Diffuse textures (Step 10, Phase 4.1) — resources::Texture (GPU image: device+view+sampler, Rgba8UnormSrgb, loaders from_rgba8/from_bytes/from_file) attached to a Material as diffuse texture. The standard shader samples it via bind group @2 (shared layout: sampler+texture); UVs are forwarded as vertex attribute location 2. Without a texture the material uses a shared 1×1 white placeholder so lit and unlit rendering are unchanged (no regression). The cube example now uses a procedural checkerboard texture. (Done 2026-09-18.)
  9. ✅ Window resize (Step 11, Phase 4.4) — App::resize reconfigures the surface (Context::configure) and recreates the depth texture (Renderer::resize_depth) together on each WindowEvent::Resized, so color and depth attachments always match. Guards against 0×0 (minimize). The surface format is re-synced to the Renderer and Scene if it ever changes. (Done 2026-09-18; verified at runtime on the cube example.)
  10. ✅ Multi-lighting (Step 12, Phase 4.2) — the scene now carries a global light list (directional + point) with a white ambient, uploaded into the per-frame FrameUniforms array each frame. Scene::add_directional_light / add_point_light / set_ambient / clear_lights configure it; FrameUniforms::default() (one white directional along +Z + white ambient) reproduces the pre-multi-light look exactly. The standard fragment accumulates ambient + all lights; the cube example adds a warm point light on top of the default directional. (Done 2026-09-18.)
  11. ✅ Spot lights (Step 13, Phase 4.2) — spot lights (oriented cone + half-angle) added on top of the multi-lighting system. Scene::add_spot_light(pos, dir, color, intensity, radius, half_angle) registers a spot light; the standard fragment accumulates a spot term with a smoothed penumbra (half-angle ± 0.1 rad) and linear attenuation. Light grew from 48 to 64 bytes (added dir_angle); FrameUniforms from 576 to 704 bytes (added num_spot). Non-regression: default scene unchanged. The cube example adds a green spot light aimed at the cube. (Done 2026-09-18.)
  12. ✅ Shadows — shadow mapping (Step 14, Phase 4.2, optional) — classic two-pass shadow mapping on a single light (directional or spot), selected by Scene::set_shadow_caster(index). A depth-only pass (shadow_shader.wgsl + dedicated shadow_pipeline) renders the scene into a 1024² Depth32Float shadow map (Renderer-owned, slope-scaled depth bias); the standard fragment re-projects each fragment into light space and applies a PCF 3×3 comparison-sampler test (bind group @3, shared). FrameUniforms grew from 704 to 784 bytes (shadow_light_index, light_view_proj, shadow_params). Shadows are off by default (shadow_caster = None) so simple/cube/manual/spot_test are unchanged. The shadow_test example casts a soft shadow from a cube onto a ground slab. (Done 2026-09-19.)
  13. ✅ Procedural primitive meshes (Step 15.A) — math::primitives provides drop-in Geometry generators (cube, plane, uv_sphere, icosphere, cylinder, cone, torus) with positions + per-face/smooth normals + UVs + indices. Re-exported at math::*. The cube and spot_test examples now reuse math::cube(1.0) (the cube_geometry helper was factored away; shadow_test keeps its generic box_geometry). (Done 2026-09-20; 6 unit tests.)
  14. ✅ Unified input (Step 15.B) — core::input::InputState gives cross-frame pressed/held/released semantics for keyboard (physical KeyCode) and mouse (buttons, position, per-frame delta, wheel scroll), rotated by begin_frame/end_frame around AppHandler::update. App exposes it as a public input field, fed from winit WindowEvents and reset each frame. Gamepad is reserved/deferred (DRAFT D7). (Done 2026-09-20; 5 unit tests; winit event handling is host-driven on the CPU, not WGSL.)
  15. ✅ Orbital camera + final demo (Step 15.C) — resources::CameraController (yaw/pitch/distance/target, apply_to writes into a Camera, drag-orbit + wheel-zoom + clamps) drives the new demo example: one of each primitive, procedural textures, standard Phong material, a shadow-casting directional light + point + spot, and live mouse-orbit / wheel-zoom / R reset / 1/2/3 view presets. Run with cargo run -p wsg-lib --example demo. (Done 2026-09-20; runtime-verified headless.)
  16. ✅ User documentation (Step 16, Phase 5) — docs/user/ (quickstart, meshes, materials, lights, shadows, camera & input, examples) written in English and cross-linked to each other, to the tech docs and to rustdoc; tech docs interlinked with their stale status banners refreshed; this README re-anchored (declarative workflow = recommended, manual = advanced, demo = showcase, pollster 1.x). (Done 2026-07-19.)
S
Description
No description provided
Readme 2.2 MiB
Languages
Rust 94.2%
WGSL 5.8%