f10e249898
- 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).
166 lines
11 KiB
Markdown
166 lines
11 KiB
Markdown
# 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, 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](#status) and [Roadmap](#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](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
|
|
|
|
### 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.
|
|
// 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](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 | ✅ (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:
|
|
|
|
```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 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](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::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.
|