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).
27 KiB
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:
// `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 (Rgba16Floatoffscreen) + 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 pastfar > 1000), IBL / env map (SH9 diffuse + GGX-prefilteredRgba16Floatcubemap), 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_indirectwith GPU-writteninstanceCount, 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, customGeometry, LOD by quadric edge collapse with seam-aware welding - Textures — PNG / JPEG / HDR files, procedural checkerboard / gradient / value noise, explicit
colour-vs-data formats (
Rgba8UnormSrgbvsRgba8Unorm, and a lock that shows the two agree at mip 0 and differ at mip 1), sampler choice (uploadlinear /upload_nearestfor atlases) - Transparency & sprites —
Alpha/AlphaTest/Additiveblending 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,Framewraps each frame's surface texture + view. - Executor layer (
Renderer) — binds aMaterialpipeline +Meshbuffers into a RenderPass and submits.render_scenebatches 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
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
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 · User guide: 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)
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(())
}
Scenemethods returnResult<_, String>— typed-error unification is on the roadmap.
Manual workflow (advanced)
Bypass the App facade and drive Context, Renderer and PipelineCache yourself:
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
# 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/ |
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/ |
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/ |
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/ |
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
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,
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:
[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 for the per-step state |
| 6.x — Material/render extensions (detail map, opacity map, parallax, repetition-breaking, log depth) | ✅ — see 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/ | User guide |
| docs/tech/ | Internal architecture |
| docs/ROADMAP.md | Roadmap — the status reference, verified against the code by roadmap_consistency |
| 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) |