GPU culling

This commit is contained in:
Jérôme Bousquié
2026-09-22 15:48:15 +02:00
parent 3dd372410f
commit 3a424afe8c
25 changed files with 2155 additions and 177 deletions
+7 -6
View File
@@ -2,7 +2,7 @@
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 **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](#status) and [Roadmap](#roadmap).
> **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** (Compute Pass deriving world matrices + frustum culling → indirect draws) is **implemented** (Step 15, Phase 3): `render_scene` and the shadow pass are 100 % indirect, and frustum culling is opt-in (`AppBuilder::with_culling(true)`, off by default) — see [Status](#status), [docs/user/gpu-driven.md](docs/user/gpu-driven.md) and [Roadmap](#roadmap).
## Status
@@ -11,7 +11,7 @@ WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) and [win
| 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](docs/tech/ARCHI_CPU_GPU.md) |
| GPU-driven two-pass pipeline (Compute → indirect draw) | ✅ Working (Step 15, Phase 3) — `render_scene` + shadow pass are 100 % indirect; opt-in frustum culling (bug « fenêtre noire » fixed 2026-09-22 — WGSL `select` argument order — and verified by GPU readback; see DRAFT D14). User doc: [gpu-driven.md](docs/user/gpu-driven.md) · spec: [ARCHI_CPU_GPU.md](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).
@@ -145,7 +145,7 @@ fn main() {
- **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](docs/tech/ARCHI_APP.md) and [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) but is **not implemented yet**.
The **GPU-driven two-pass pipeline** (Step 15, Phase 3) is implemented: a Compute Pass derives each entity's world matrix and fills per-entity indirect draw arguments (with opt-in frustum culling), then the main and shadow render passes issue one indirect draw per active slot. 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); user-facing guide in [docs/user/gpu-driven.md](docs/user/gpu-driven.md).
## Quick reference
@@ -201,8 +201,8 @@ Three layers (user docs and API reference in **English**; technical docs in **Fr
- [Shadows](docs/user/shadows.md) · [Camera & input](docs/user/camera-input.md) · [Examples](docs/user/examples.md)
**Technical documentation — `docs/tech/`** (internal architecture; each document states whether it describes the **current** or the **target** architecture):
- [ARCHI_APP](docs/tech/ARCHI_APP.md) — engine architecture. ✅ Current facade (`App`/`AppHandler`) / 🎯 **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_APP](docs/tech/ARCHI_APP.md) — engine architecture. ✅ **Current** — facade (`App`/`AppHandler`) and GPU-driven two-pass pipeline (implemented in Phase 3, 2026-07-20, with the documented deviations); only the future double-buffering notes remain target.
- [ARCHI_CPU_GPU](docs/tech/ARCHI_CPU_GPU.md) — CPU/GPU workload split specification. ✅ **Current** — implemented in ROADMAP Phase 3 (2026-07-20, DRAFT Étape 17, decisions D1–D14); deviations from the original spec are noted in the document.
- [ARCHI_RENDU](docs/tech/ARCHI_RENDU.md) — update/render mutability model. ✅ Current dichotomy (auto scene render) / 🎯 **Target** — material batching.
- [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.
@@ -212,7 +212,7 @@ Three layers (user docs and API reference in **English**; technical docs in **Fr
## 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).
2. ✅ **GPU-driven two-pass pipeline** — Compute Pass (world matrices + frustum culling) filling an indirect draw buffer, then indirect draws (see ARCHI_CPU_GPU). *(Done 2026-07-20 — see item 17. Deviation from the original spec: one indirect draw **per slot** rather than a single fused draw, DRAFT D1.)*
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.
@@ -227,3 +227,4 @@ Three layers (user docs and API reference in **English**; technical docs in **Fr
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 `WindowEvent`s 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.)
17. ✅ **GPU-driven rendering (Step 15, Phase 3.1/3.2/3.3)** — world matrices and indirect draw args move from CPU to GPU. `shaders/gpu_driven.wgsl` (two compute entry points, `compute_matrices` + `cull`, one module, explicit 3-group layout) runs before the render passes over a fixed 256-slot table (the world-matrix buffer is bound to the `uniform` object slot; WebGPU caps a `uniform` binding at 64 KB and a `uniform` offset at 256 B, so each matrix slot is padded to 256 B and 256 × 256 B = 64 KB is the max); `render_scene` and the shadow pass become **100 % indirect** (one indirect draw per active slot, culled/inactive slots are no-ops), and the per-entity CPU draw loop is gone. New `math::Frustum` (Gribb–Hartmann, WebGPU `[0,1]` z) + `BBox` on `Geometry`; `TransformSlot`/`MatSlot`/`BBoxSlot`/`DrawSlot`/`CullUniforms` Pod mirrors of the WGSL structs. Frustum **culling is off by default** (non-regression) and opt-in via `AppBuilder::with_culling(true)` / `Renderer::set_culling(bool)`; the `demo` enables it. The object bind-group layout is now dynamic so every entity shares one GPU matrix buffer via per-slot offsets. (Done 2026-07-20; WGSL + frustum + scene-slot tests, 56 lib / 3 WGSL / 3 doctests all green. **Culling fix 2026-09-22**: the WGSL `select` arguments had been written HLSL-style, silently zeroing the draw count of every *visible* entity — a black window; fixed and verified by GPU readback, see DRAFT D14.)