Files
wsg/README_DETAILS.md
Jérôme Bousquié 997e2d7b66 18 · le contrat lu par un agent : README §4, deux verrous, une campagne, 21 → 0
L'assertion « cette bibliothèque est utilisable par un agent » est maintenant une
propriété testable, et le README la porte en quatrième propriété avec ce qui la
garantit, énoncé par côté du défaut qu'elle empêche (README §4).

Ce que la mesure a trouvé, puis corrigé — les trois trous identifiés :

1. TROUS DE COMMANDE. README_DETAILS conseillait `cargo test --workspace` sans jamais
   nommer `./run-tests-honest.sh` : le document envoyait le lecteur (donc un agent) vers
   la commande qui compte `passed` un test GPU revenu tôt faute de device (gotcha 13.1).
   Corrigé dans les deux README ; verrou `whole_suite_advice_names_the_honest_harness`
   (périmètre 4 fichiers, AGENTS.md exclu, non-vacuité 4 occurrences mesurées / plancher 3).
   Trouvé ROUGE sur l'arbre d'ouverture par son propre verrou.

2. BUILD BRUYANT CHEZ L'APPELANT. 21 avertissements fuyaient dans le build d'une crate
   consommatrice (19 `missing_docs`, `unused_mut`, 3 champs `never read`) → 0.
   - les 19 doc-comments ajoutés (chiffres et oracles cités vérifiés : `FIELD_TOWER_SCALE`
     lu par `pick_tests.rs:2520`, `BLEND_WINDOW` par 4244, le 64/0 de `TexelFilter` mesuré 6.12) ;
   - `missing_docs` passe à `deny` **hors `cfg(test)`** : brut, il faisait échouer le build
     de test (7 erreurs sur `testutil`/`alloc_counter`/`geometry::count`, auxiliaires `pub`
     qui ne sont pas l'API). Porte justifiée par mesure avant pose : 0 warning sous le jeu
     de features du harnais et sous `--all-features`. Prouvée rouge en retirant la doc d'un
     variant (`error: missing documentation for a variant`) ; la porte est épinglée par le
     compilateur, pas par un parseur à faux positifs (14-F2) ;
   - `sort_hist_buf`, `sort_keys_alt_buf`, `sort_digit_buf` supprimés du Renderer : liés aux
     bind groups du tri (stockés), donc gardiens redondants — un bind group wgpu garde ce
     qu'il lie vivant (précédent E4, F4-G7). Commentaires de conception déplacés au site de
     construction. Preuve = harnais rejoué, pas argument.

3. AUCUNE PORTE POUR LE CONTRAT D'ÉCHEC. Créé `docs/user/errors.md` (plafonds, ce que
   l'appelant reçoit, ce qui n'arrive PAS) + index. Verrou dérivé dans les deux sens :
   `the_error_contract_names_every_refused_ceiling` relit les plafonds dans le code expédié
   (gardes `> MAX_` + messages `Err(format!(`) et les confronte à la page : garde non
   documenté → rouge, plafond inventé → rouge, `MAX_CASCADES` réel mais non refusé → VERT
   consigné. Ensemble dérivé mesuré : 4 (`MAX_BATCHES`, `MAX_INSTANCES`, `MAX_LIGHTS`,
   `MAX_LOD_LEVELS`). Index utilisateur : 50 exemples annoncés (il en annonçait 24 ; mesure :
   50 `[[example]]`).

Campagne `campaigns/mutate-agent-contract.py` : T0 double (verrous de doc + compilation
verbatim du bloc quickstart dans une crate à deux dépendances) + 6 cas, 0 écart, arbre rendu.
A1 rend l'API du bloc fictive → ROUGE : c'est le seul cas qui prouve que la jambe « vitrine »
lit le README et non un souvenir.

Trois fautes d'instrument trouvées en construisant l'instrument (toutes consignées) :
scanner qui tronquait `MAX_U16_VERTICES` en `MAX_U` ; cas A4 bâti sur `MAX_SPRITES` cru
inventé alors que c'est une constante réelle (`renderer.rs:2070`) ; mutations A3 / sonde de
gate qui n'appliquaient pas l'effet annoncé (une colonne d'un tableau, la première ligne
d'un doc-comment à trois lignes, une erreur de syntaxe au lieu du lint).

Faute commise et consignée : `git checkout --` sur un fichier non commité a détruit mes
propres doc-comments de la séance (gotcha 6.19 / 12.13, que ce dépôt documente) ; réappliqués.

Harnais honnête après tout cela : 1010 exécutés, 0 échec, 0 skip, 19 doc-tests joués.
Paquet et artefact non re-jeués ici (aucun fichier d'artefact de contrat touché hors doc).
2026-11-06 09:19:38 +01:00

453 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WSG — Detailed Documentation
> Technical content from the main README: status, architecture, API reference, workflows, 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, automatic scene rendering |
| `Scene` resource/entity registry | ✅ Working — auto-rendered in one batched pass (`App::render_scene`) |
| GPU-driven two-pass pipeline (Compute → indirect draw) | ✅ Working (Phase 3) — `render_scene` + shadow pass 100% indirect; opt-in frustum culling |
| 3D infrastructure (uniform bind groups, MVP + camera) | ✅ Working — per-frame camera + per-entity world matrices in shared uniforms |
| Shadows (shadow mapping) | ✅ Working — directional/spot, slope-scaled bias, PCF 3×3 |
| HDR + Tone Mapping | ✅ Working — offscreen Rgba16Float, ACES/Reinhard, opt-in |
| LOD (Level of Detail) | ✅ Working — quadric decimation, hysteresis, multi-level buffer |
| Mesh module (primitives + import) | ✅ Working — feature-gated primitives, OBJ + STL + glTF parsers |
| Bloom | ✅ Working — threshold + separable blur + composite, HDR required |
| Fog | ✅ Working — 3 modes (linear, exp, exp²), runtime switchable |
| MSAA | ✅ Working — 4× multisample, resolve pass |
| DoF | ✅ Working — CoC + disc blur, focus presets |
| PBR (metallic/roughness + normal maps) | ✅ Working — Cook-Torrance, GGX, IBL, screen-derivative TBN |
| Parallax mapping (6.25) | ✅ Working — opt-in per material, height read from the normal map's alpha channel, `Basic` / `Occlusion` |
| Logarithmic depth buffer (6.31) | ✅ Working — opt-in global, kills z-fighting beyond `far > 1000` |
| IBL / Env map (6.22) | ✅ Working — opt-in per scene; diffuse from SH9, specular from a GGX-prefiltered `Rgba16Float` cubemap |
| Skybox (6.36) | ✅ Working — opt-in (`with_skybox`); the same HDR environment as background, drawn from six real cube faces: never parallaxes, never fogged |
| Cascaded shadow maps (6.6) | ✅ Working — opt-in, 1–4 cascades, `split_blend`, texel-grid alignment; only worth it on large scenes (a single map is already dense at a 5 m radius) |
| Depth prepass (6.35) | ✅ Working — opt-in; must be armed **before** any material is created (`set_depth_prepass` afterwards refuses and logs) |
| SSAO (6.7) | ✅ Working — reads the prepass normals (`Rg8Unorm`, octahedral), bounded in view space, applied in both fragment entry points; `with_ssao(cfg)` implies the prepass |
| Area lights (6.14, 6.37) | ✅ Working — rect emitters (`add_area_light`): analytic irradiance over the clipped half-rectangle, plus a mirrored specular highlight bounded by `F · L` |
| Instanced batches (Phase 12) | ✅ Working — one `draw_indexed_indirect` per batch, `instanceCount` written by the GPU, per-instance frustum culling + compaction, caller-owned GPU-resident matrix buffers; `MAX_BATCHES = 256` is a **batch** ceiling, not an entity ceiling |
| Instance picking (12.13) | ✅ Working — GPU id-buffer picking (`pick_instance_at`), exact to the silhouette; the per-entity raycast (9.2) stays the better path for entities |
| Back-to-front sort (Phase 14) | ✅ Working — GPU radix sort of `compact_index` inside each transparent batch; a fixed ~0.25 ms per frame whenever a sortable batch exists (measured, bench 14‑F7) |
| Publishing (`wsg-lib`, registry gates) | ✅ Gates closed and measured — metadata complete, MIT licence (manifest field and `lib/LICENSE` text cross-checked), demo media and repository instruments excluded, package contract locked (`lib/tests/package_contract.rs`: the facade is closed under the types of its own fields; `pub use wgpu;` at crate level, never in the prelude). **Not published yet** — pushing to the registry is a decision, not remaining work (draft 16) |
Licence: MIT — `lib/LICENSE`, shipped inside the package.
Note: `standard_shader.wgsl` (Phong + PBR, with an explicit **unlit** mode) is the **single** shader the library ships. Flat 2D drawing is its unlit variant (`Renderer::set_unlit(true)`).
## Do you need `wgpu` in your own code?
No. The façade (`App` + `AppBuilder` + `Scene`) never asks you to name a `wgpu` type — measured, the
quickstart compiles in a crate whose only dependencies are `wsg-lib` and `pollster` (no `wgpu`, no
`winit`, no `glam` declared). That is not a claim about style: a consumer crate living in this
repository (`consumer/`) writes the façade gestures and is recompiled against the built library on
demand, and a lock derives the set of types the façade can hand back from the façade itself — public
fields and method returns of every prelude type — and requires each to be nameable through
`wsg_lib::*` (13 names across 3 roots, 42 occurrences, 0 unknown).
If you deliberately leave the façade — a compute pass, or a texture constructor that takes back the
`Device` and `Queue` WSG created — name those types through `wsg_lib::wgpu` instead of adding `wgpu`
to your own dependencies:
```rust
// `app` is your `wsg_lib::App`
let device: &wsg_lib::wgpu::Device = app.scene.device();
let buffer = device.create_buffer(&wsg_lib::wgpu::BufferDescriptor {
label: Some("my compute buffer"),
size: 64,
usage: wsg_lib::wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
```
Adding `wgpu` yourself means guessing which version WSG pins; measured, a wrong guess (`wgpu = "22"`)
fails with `E0308 expected BufferDescriptor<Option<&str>>, found a different
BufferDescriptor<Option<&str>>` plus a note about two copies of `wgpu_types` — diagnosable, but only
after reading our manifest. Through the re-export it cannot happen. `wgpu` is deliberately **not** in
`wsg_lib::prelude`: the façade stays the default path, the re-export is an emergency exit. The same
reasoning put `glam` (`Vec3`, `Vec4`, `Quat`, `Mat4`) in the prelude: `Transform`'s own fields are
`glam` types, so a façade-only consumer had no way to name what the API handed them.
## Feature catalogue
The detailed list, grouped the way the modules are:
- **Rendering** — Phong + PBR (metallic/roughness, normal maps via a screen-derivative TBN), unlit
mode (`set_unlit`), HDR (`Rgba16Float` offscreen) + ACES/Reinhard tone mapping with exposure,
**parallax mapping** (height read from the normal map's alpha, `Basic` / `Occlusion`),
**logarithmic depth** (opt-in, kills z-fighting past `far > 1000`), **IBL / env map** (SH9 diffuse +
GGX-prefiltered `Rgba16Float` cubemap), **skybox** (the same environment drawn from six real cube
faces: never parallaxes, never fogged)
- **Lighting** — directional / point / spot (8 lights), **rect area lights** (analytic irradiance over
the clipped half-rectangle, mirrored specular bounded by `F · L`), hemisphere ambient, opacity maps,
**detail maps** with LOD fade-out by camera distance, **UV repetition-breaking** (hex-grid warp +
noise), PCF shadows and **cascaded shadow maps** (1–4, `split_blend`, texel-grid aligned)
- **GPU-driven** — compute matrices, per-instance culling, compaction, `draw_indexed_indirect` with
GPU-written `instanceCount`, caller-owned GPU-resident matrix buffers, GPU radix sort of transparent
batches, GPU picking id-buffer, GPU timestamp queries per pass
- **Post-process** — bloom (threshold + separable blur + composite, HDR required), fog (linear /
exponential / exponential²), depth of field (CoC + disc blur), MSAA (4×, resolve pass), SSAO (reads
the prepass normals, octahedral `Rg8Unorm`, applied in both fragment entry points)
- **Geometry** — 6 procedural primitives (`cube`, `plane`, `uv_sphere` / `icosphere`, `cylinder`,
`cone`, `torus`), OBJ / STL / glTF import, custom `Geometry`, LOD by quadric edge collapse with
seam-aware welding
- **Textures** — PNG / JPEG / HDR files, procedural checkerboard / gradient / value noise, explicit
colour-vs-data formats (`Rgba8UnormSrgb` vs `Rgba8Unorm`, and a lock that shows the two agree at mip
0 and differ at mip 1), sampler choice (`upload` linear / `upload_nearest` for atlases)
- **Transparency & sprites** — `Alpha` / `AlphaTest` / `Additive` blending modes, alpha-test cutoff,
opacity maps, GPU back-to-front sort per batch, billboarded sprites with atlas UV animation
- **Particles** — instanced pools with three drivers (GPU compute, CPU emitter, manual), GPU sort
- **Scene interaction** — orbital camera, perspective/ortho, unified keyboard/mouse/scroll input,
raycasting with BVH, mesh-mesh collision, in-place `update_geometry`, per-instance and per-entity
picking
- **Debug** — BBox / BSphere overlays, debug lines / points / strips, wireframe, stats (FPS, draws,
instances, per-pass GPU milliseconds)
## Architecture
### Layer model
- **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. `render_scene` batches all entities into one encoder + one submit per frame.
- **Supporting pieces** — `PipelineCache` (shader → compiled RenderPipeline, `Arc`-shared), `Material`, `Geometry`/`Mesh`/`Vertex`, `Scene` (generational arena registry), `Camera`/`Transform`.
### Resource management (generational arenas)
The `Scene` stores all resources in **generational arenas** (`slotmap` crate) with typed handles (`MeshId`, `MaterialId`, `TextureId`, `EntityId`, `ParticlePoolId`, `LinePointId`). A label→ID index map provides the user-facing string API. Benefits: stale-handle detection, O(1) removal with slot reuse, cache-friendly iteration.
Spec: [docs/tech/ARCHI_ARENES.md](docs/tech/ARCHI_ARENES.md)
### In-place geometry update (6.9)
`Scene::update_geometry(id, geometry)` automatically uses a **fast path** when the vertex count is unchanged: a single `queue.write_buffer` on the existing GPU buffer (zero allocation, zero arena churn). A full rebuild (new buffer + arena insert) only occurs when the vertex count changes. BBox is auto-recomputed from the updated `Arc<Geometry>`.
### Double-buffered CPU→GPU buffers (6.10)
All 6 per-frame CPU-written GPU buffers use **double-buffering** (`Dbuf`/`Dbg` pairs alternating by frame parity). This is the standard game-engine pattern: reduces queue depth, future-proofs for Vulkan/D3D12 migration, and enables inspecting the previous frame's buffers in debug tools. The GPU-written `matrix_buffer` remains single-buffered (no CPU/GPU race within a command buffer). Total overhead: ~1.5 MB.
Spec: [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md)
### GPU-driven pipeline (Phase 3)
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.
Spec: [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) · User guide: [docs/user/cameras/gpu-driven.md](docs/user/cameras/gpu-driven.md)
### Module layout
```
lib/src/
├── lib.rs # crate root, re-exports
├── prelude.rs # glob re-exports
├── app.rs # App + AppBuilder
├── handler.rs # AppHandler trait
├── camera.rs # Camera, CameraController
├── input.rs # InputState
├── lights.rs # Lights, Light, LightType, directional_light, …
├── core/
│ ├── context.rs # GPU lifecycle (Instance/Surface/Adapter/Device/Queue)
│ ├── renderer.rs # RenderPass execution, shadow pass, HDR/TM pass
│ ├── frame.rs # Per-frame RAII (surface texture + view)
│ ├── geometry.rs # Geometry (positions/normals/UVs/indices) + BBox
│ ├── transform.rs # Transform (translation/rotation/scale)
│ ├── frustum.rs # Frustum (6 planes, sphere/box culling)
│ ├── lod.rs # LOD decimation (quadric edge collapse)
│ ├── shadow.rs # ShadowConfig (map size, bias, PCF)
│ ├── hdr.rs # ToneMapper enum (Aces/Reinhard)
│ ├── bloom.rs # BloomConfig + BloomPipeline
│ ├── msaa.rs # MsaaConfig
│ ├── fog.rs # FogConfig + FogMode
│ └── dof.rs # DoFConfig + DoFPipeline
├── mesh/
│ ├── mod.rs # Re-exports flat
│ ├── primitives/ # 6 feature-gated generators
│ └── import/ # OBJ + STL + glTF parsers
├── pipeline/ # PipelineCache (shader → RenderPipeline)
├── resources/ # Mesh, Material, Texture, Uniform, Vertex
├── scene/ # Scene (registry), Entity
└── utils/ # Conf constants, WsgError
```
## Quick reference (types)
| Concept | Type | Responsibility |
|---------|------|---------------|
| App / AppBuilder | Facade | Window + event loop + frame + auto scene render |
| AppHandler | Trait | `setup()` / `update()` / `render()` callbacks |
| Scene | Struct | Registry: shaders, materials, meshes, entities, lights, camera |
| Context | Struct | GPU hardware (Instance, Surface, Adapter, Device, Queue) |
| Renderer | Struct | RenderPass execution (scene, shadow, HDR/TM, bloom, DoF) |
| PipelineCache | Struct | Shader → compiled RenderPipeline cache |
| Material | Struct | Shader ID + texture + pipeline + PBR params |
| Geometry | Struct | CPU vertex data (positions/normals/UVs/colors/indices) |
| Mesh / Vertex | Struct | GPU geometry / interleaved upload tuple |
| Frame | Struct | Per-frame RAII (surface texture + view) |
| Camera / Transform | Struct | Camera math + per-entity transform |
| CameraController | Struct | Orbital camera (orbit/zoom/reset/apply_to) |
| InputState | Struct | Unified keyboard/mouse (pressed/held/released, delta, scroll) |
| Texture | Struct | GPU image (Rgba8UnormSrgb) + sampler; file (PNG/JPEG) + procedural (checkerboard, gradient, noise) |
| Lights / Light | Struct | Light list (directional/point/spot, MAX=8) + ambient |
| ShadowConfig | Struct | Shadow map size, bias, PCF taps, scene radius |
| ToneMapper | Enum | ACES Filmic / Reinhard |
| BloomConfig | Struct | Threshold, intensity, H/V passes |
| MsaaConfig | Struct | Sample count (1 = disabled) |
| FogConfig | Struct | Mode, near/far, density, color |
| DoFConfig | Struct | Focus distance, aperture, max blur |
| BBox | Struct | Axis-aligned bounding box (min/max) |
| Frustum | Struct | 6 planes, sphere/box culling |
## Declarative workflow (recommended)
```rust
use wsg_lib::prelude::*;
use wsg_lib::app::AppBuilder;
use wsg_lib::utils::WsgError;
struct MyScene;
impl AppHandler for MyScene {
fn setup(&mut self, app: &mut wsg_lib::App) {
// "standard" is a built-in shader id resolved from the embedded source — no registration
// is needed, and registering it under a path used to be the repo's own mistake (13.15).
app.scene.add_material_shader("mat", "standard").unwrap();
app.scene.create_mesh("cube", cube(1.0), Some("mat")).unwrap();
app.scene.add_entity("my_cube", "cube").unwrap();
}
fn update(&mut self, app: &mut wsg_lib::App) {
// your per-frame logic
}
// render() default: app.render_scene(frame.view()) — auto-draws everything
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
let app = AppBuilder::new().title("WSG").build().await?;
app.run(MyScene);
Ok(())
}
```
> `Scene` methods return `Result<_, String>` — typed-error unification is on the roadmap.
## Manual workflow (advanced)
Bypass the `App` facade and drive `Context`, `Renderer` and `PipelineCache` yourself:
```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::{Geometry, Material, Mesh};
use wsg_lib::utils;
fn main() {
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");
let format = context.configure(&context.adapter, 800, 600).expect("surface config");
let mut renderer = Renderer::new(&context, format, 800, 600);
let mut cache = PipelineCache::new(Arc::new(context.device.clone()));
// "standard" is a built-in id: Material::new resolves it from the embedded source.
// register_shader(id, path) exists for a WGSL file that is NOT one of the built-ins, and
// refuses a path that is not a readable file (13.15).
let material = Material::new(renderer.format(), "standard", &mut cache);
let geometry = Geometry::new(vec![-0.5f32, 0.5, 0.0, 0.5, 0.5, 0.0, 0.5, -0.5, 0.0, -0.5, -0.5, 0.0])
.with_indices(vec![0, 1, 2, 0, 2, 3]);
let mesh = Mesh::from_geometry(renderer.device(), Arc::new(geometry), None);
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();
}
```
## Features
| Feature | Default | Provides |
|---------|---------|----------|
| `prim-cube` | ✅ | `cube(size)` |
| `prim-plane` | ✅ | `plane(w, d, seg_x, seg_z)` |
| `prim-sphere` | ✅ | `uv_sphere(…)`, `icosphere(…)` |
| `prim-cylinder` | ✅ | `cylinder(…)` |
| `prim-cone` | ✅ | `cone(…)` |
| `prim-torus` | ✅ | `torus(…)` |
| `all-prims` | ✅ (default) | All 6 primitives |
| `import-obj` | ✅ | `load_obj(path)`, `parse_obj(str)` |
| `import-stl` | ✅ | `load_stl(path)`, `parse_stl(&[u8])` |
| `import-gltf` | ✅ | `load_gltf(path, mesh_index)` |
## Design principle: opt-in = zero cost
| Feature | How to enable | If NOT enabled |
|---------|--------------|----------------|
| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map, no depth pass, no PCF |
| HDR + TM | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass |
| Bloom | `AppBuilder::with_bloom(BloomConfig::…)` | No bloom textures, no passes |
| MSAA | `AppBuilder::with_msaa(4)` (a sample count, not a config struct) | Single sample, no resolve |
| Fog | `AppBuilder::with_fog(FogConfig::…)` | No fog uniforms |
| DoF | `AppBuilder::with_dof(DoFConfig::…)` | No CoC/blur textures |
| GPU-driven culling | `AppBuilder::with_culling(true)` | No compute pipeline, no indirect buffers |
| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh |
| Primitives | Cargo feature `prim-*` | Not compiled |
| File import | Cargo feature `import-*` | Not compiled |
| Stats overlay | Cargo feature `stats` | No metrics collection |
| Debug BBox/BSphere | Cargo feature `debug-bbox` | No overlay geometry |
| Parallax mapping (6.25) | `scene.set_material_parallax(id, mode, scale, steps, bias)` | One `u32` compare — **no height-map fetch at all** |
| Log depth buffer (6.31) | `AppBuilder::with_log_depth(true)` | One `log2` per vertex, identical depth encoding |
| IBL / env map (6.22) | `scene.set_environment(env)` | Falls back to the analytic 6.21 hemisphere — zero cost |
## Cargo Features
```toml
# Default: all primitives
wsg-lib = { path = "../lib" }
# Minimal: just the cube
wsg-lib = { path = "../lib", default-features = false, features = ["prim-cube"] }
# With OBJ import
wsg-lib = { path = "../lib", features = ["import-obj"] }
# With all file importers
wsg-lib = { path = "../lib", features = ["import-obj", "import-stl", "import-gltf"] }
```
| Feature | Default | Enables |
|---------|---------|---------|
| `prim-cube` | ✅ | `cube(size)` |
| `prim-plane` | ✅ | `plane(w, d, seg_x, seg_z)` |
| `prim-sphere` | ✅ | `uv_sphere(…)`, `icosphere(…)` |
| `prim-cylinder` | ✅ | `cylinder(…)` |
| `prim-cone` | ✅ | `cone(…)` |
| `prim-torus` | ✅ | `torus(…)` |
| `all-prims` | ✅ (default) | All 6 primitives |
| `import-obj` | ✅ | `load_obj(path)`, `parse_obj(str)` |
| `import-stl` | ✅ | `load_stl(path)`, `parse_stl(&[u8])` |
| `import-gltf` | ✅ | `load_gltf(path, mesh_index)` |
| `stats` | ⬜ | Real-time metrics on stdout (FPS, draws, resources) |
| `debug-bbox` | ⬜ | Debug BBox/BSphere overlay visualization |
## Examples
50 runnable examples in 4 category folders. All run from the repo root with
`cargo run -p wsg-lib --example <name>` (feature-gated ones need `--features`).
| Folder | Example | What it shows |
|--------|---------|---------------|
| [`meshes/`](lib/examples/meshes/README.md) | `simple` | Minimal declarative workflow (flat unlit quad, ~15 lines) |
| | `cube` | Textured, lit, spinning cube (the 3D MVP) |
| | `pbr` | PBR metallic/roughness + normal mapping |
| | `import` | File import: OBJ / STL / glTF (feature `import-*`) |
| | `deformable` | Deformable mesh: per-frame vertex ripple |
| | `collision` | Mesh-mesh collision (AABB + precise) |
| | `manual` | Low-level workflow (Context/Renderer/PipelineCache, no App) |
| [`lights/`](lib/examples/lights/README.md) | `shadow` | Shadow mapping in isolation |
| | `shadow_instances` | 100 instanced cubes casting individual shadows (12.12) |
| | `shadow_test` | Dedicated shadow test (directional caster + PCF) |
| | `spot_test` | Isolated spot light (beam, penumbra) |
| | `emissive` | Emissive materials + runtime exposure control |
| [`cameras/`](lib/examples/cameras/README.md) | `blend_order` | A transparent batch is sorted by what it draws — hiding a far cube repaints the centre (13.10) |
| | `culling` | GPU-driven frustum culling (15×15 grid) |
| | `instances` | 100 000 cubes in 1 draw call; `L` toggles the per-entity path (Phase 12) |
| | `instances_sim` | 100 000 GPU-resident instances driven by a caller-owned compute pass (12.15) |
| | `ortho` | Orthographic camera with perspective/ortho toggle |
| | `pick_instance` | Which of 1 000 instances is under the cursor (12.13) |
| | `picking` | Click-to-select raycasting |
| | `visibility` | Hiding 40 000 instances one band at a time — the visible side of 13.1/13.2/13.3/13.7 |
| [`effects/`](lib/examples/effects/README.md) | `demo` | Full showcase: 6 primitives, 3 lights, shadows, HDR, LOD |
| | `bloom` | HDR bloom post-process |
| | `hdr` | HDR + tone mapping (ACES/Reinhard) |
| | `msaa` | 4× multisample anti-aliasing |
| | `fog` | 3 fog modes (linear, exponential, exponential²) |
| | `dof` | Depth of field with focus presets |
| | `ssao` | Screen-space ambient occlusion — dark shading hugging the base of every volume; `S` toggles cast shadows to isolate the AO |
| | `particles` | Particle system: 3 pools × 3 drivers |
| | `debug_lines` | Debug lines/points + BBox/BSphere overlays |
| | `transparency` | Mesh transparency: glass, additive, animated alpha |
| | `sprites` | Billboard sprites with atlas UV animation |
| | `procedural_textures` | Procedural textures: checkerboard, gradient, noise |
| | `parallax` | Parallax mapping: two identical brick walls, POM reveals real depth (6.25) |
| | `log_depth` | Logarithmic depth: 1 cm-apart coplanar quads at 1 km, `L` toggles (6.31) |
| | `env_map` | IBL: six spheres lit by a procedural HDR sky, `E` toggles the environment (6.22) |
| | `skybox` | The same HDR environment drawn as the background from six real cube faces: never parallaxes, never fogged; `S` hides the sky without changing reflections, `F` cycles fog (6.36) |
## Build
```sh
cargo build --workspace # everything
./run-tests-honest.sh # THE verification that counts: it refuses to report success
# when a GPU test skipped for want of a headless device
cargo test --workspace # the same suite, but a test that returns early for want of a
# GPU is counted "passed" by the harness — the honest script is
# the one whose verdict you may repeat to someone else
cargo check --all-targets # quick check; its feature set is NOT the harness feature set
cargo run -p wsg-lib --example demo # run the showcase
```
Totals are not written in prose anywhere in this repository — recopied counts have been wrong five
times across this project's history. The generated table is
[`docs/generated/TEST-SUITE.md`](https://git.iut-rodez.fr/jerome/wsg/src/branch/main/docs/generated/TEST-SUITE.md),
written by the honest script itself, and the script refuses to write it when its two independent
counters disagree.
## Release profile
The workspace `Cargo.toml` configures an aggressive release profile for
maximum performance and minimum binary size:
```toml
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
panic = "abort"
overflow-checks = false
```
## Roadmap
| Phase | Status |
|-------|--------|
| 1 — Foundations (window, render loop, Context) | ✅ |
| 2 — 3D infrastructure (Geometry, Mesh, Material, Pipeline) | ✅ |
| 3 — GPU-driven (compute pass, indirect draws, culling) | ✅ |
| 4 — Advanced rendering (shadows, HDR/TM, lights) | ✅ |
| 5 — Polish (LOD, camera controller, input, demo) | ✅ |
| 6 — Post-MVP (bloom, PBR, fog, DoF, MSAA, cascaded shadows, SSAO) | 🔶 — what is left is per-feature module work (`6.12` texture module, `6.15` textured area lights, `6.16` god rays, `6.27` voronoi); see [docs/ROADMAP.md](docs/ROADMAP.md) for the per-step state |
| 6.x — Material/render extensions (detail map, opacity map, parallax, repetition-breaking, log depth) | ✅ — see [docs/ROADMAP.md](docs/ROADMAP.md) for the per-step state |
| 7 — Particles | ✅ |
| 8 — Developer tools (debug lines, points, overlays) | ✅ |
| 9 — Scene interaction (raycasting, BVH, collision, deformable) | ✅ |
| 10 — Lines and points (visualisation) | ✅ |
| 11 — Mesh transparency | ✅ |
| 12 — Scaling: GPU instancing (1 draw per batch, per-instance culling + compaction) | 🔶 — delivered except `12.9` (GPU-resident SPS) |
| 13 — Entity lifecycle (visibility, resize, picking stability) | ✅ |
| 14 — Intra-batch back-to-front sort | ✅ |
| 15 — Repository hygiene (draft index, banner claims, doc locks) | ✅ |
## Documentation
| Where | What |
|-------|------|
| [docs/user/](docs/user/README.md) | User guide |
| [docs/tech/](docs/tech/ARCHI_APP.md) | Internal architecture |
| [docs/ROADMAP.md](docs/ROADMAP.md) | Roadmap — **the status reference**, verified against the code by `roadmap_consistency` |
| [docs/PLAN.md](docs/PLAN.md) | Dated snapshot of the engine state (2026‑09‑30) plus open questions — not the status reference |
| `cargo doc -p wsg-lib --no-deps` | API reference (rustdoc) |