Jérôme Bousquié 28eb1ae882 D17 bis : un fichier expédié ne peut pas pointer hors du paquet, et la langue devient une conséquence de la frontière
Mesuré avant d'écrire : sur les 167 fichiers .rs/.md que le paquet emporte,
13 liens relatifs, dont 10 sortaient de la racine du crate et 3 pointaient
`examples/assets/`, un dossier `exclude` du manifeste. Aucun n'était un lien
mort DANS LE DEPOT — cargo test vert, cargo doc vert, et c'est chez le
téléchargeur que la cible manque. Le verrou de liens existant ne marchait que
`docs/user/**` (13.9, septième occurrence : le périmètre est l'endroit où le
défaut a été trouvé la première fois).

Verrou : les_fichiers_expedies_n_ont_aucun_lien_hors_du_paquet
(package_contract, 16 verrous), deux mâchoires — cible hors racine du crate,
et cible sous une entrée `exclude` lue dans le manifeste. Non-vacuité calibrée
sur deux bornes mesurées : 9 liens avant correction, 8 après, 0 si la marche
devient muette ; plancher 4. Limite écrite : les 8 liens restants sont tous
sous examples/, donc une marche src/ muette ne ferait pas bouger le plancher.

C'est la deuxième mâchoire qui a trouvé un énoncé faux plutôt qu'un lien :
examples/README.md présentait assets/textures/ comme « shipped » alors que D9
l'exclut — vert dans le dépôt, faux chez l'appelant. Le texte nomme maintenant
que les médias ne voyagent pas, quelles démos les lisent, et ce que répond le
chargeur (NotFound, pas un placeholder).src/README.md § 4 faisait la même
affirmation pour assets/gltf/.

Corrections : les 10 liens sont nommés plutôt que liés
(Renderer::build_gpu_descriptors plutôt qu'ARCHI_APP.md,
Renderer::record_ssao_passes plutôt qu'un draft), base de résolution =
répertoire du fichier. core/particles.rs pointait ARCHI_PARTICULES.md pour
citer un fait (Particle = 112 o) ; la valeur se relit dans Particle::SIZE.

Frontière de langue (D17) : le tri se fait par visibilité rustdoc, non par
fichier. Cliquet de la surface expédiée reposé à la mesure, chaîne consignée
au draft 16 § 22 : 2375 -> 513. Traduits dans la séance : renderer.rs,
scene.rs, batch.rs, batch_sort_tests.rs, visibility_tests.rs,
pipeline_cache.rs, lib.rs, sky.rs, texture/gpu.rs, uniform.rs, viewport.rs,
frame.rs, alloc_counter.rs, particles.rs, context.rs, geometry.rs, conf.rs,
line_point.rs, shared/sources.rs, examples/cameras/README.md — plus 21 lignes
réellement affichées par les exemples (le seul endroit où l'utilisateur voyait
du français sans le savoir) et valeur_bloom -> bloom_value.

Campagne mutate-langue.py : L1 rouge, L2 fixture rouge / cliquet vert, L3 et
L4 verts par la frontière (privé, module #[cfg(test)]). L5 (bandeau d'exemple)
est consigné NON JOUÉ : trois constructions ont échoué, la quatrième rendait
VERT sans avoir muté — la famille 13.4 / 9.4, pas un succès.

Ce qui reste ouvert, écrit : les mentions en prose d'un chemin docs/... dans
un fichier expédié ne sont pas verrouillées ; docs/user/** (31 fichiers) reste
à traduire, hors paquet donc hors cliquet.

Harnais honnête : 1020 tests, 0 échec, 0 skip, 0 doc-test ignoré, 0 diagnostic
rustdoc, 0 diagnostic sur cargo check --examples.
2026-11-06 18:15:05 +01:00
2026-08-01 09:34:12 +02:00
2026-07-31 19:10:04 +02:00

WSG — a light, GPU-driven 3D library on top of wgpu

WSG wraps wgpu into a scene API: you declare meshes, materials and entities, and the library runs the window, the frame loop, the batching and the presentation. You never touch Instance / Adapter / Device / Queue, and you pay for nothing you did not ask for.

Four properties, and what each one actually means:

Light & opt-in One façade over wgpu; every effect is switched on by you, and a disabled effect allocates nothing and executes nothing.
GPU-driven The per-frame work that can live on the GPU does: instance matrices, frustum culling, compaction, draw counts, transparent sorting, particle simulation.
Built for frame budgets Everything is allocated before the render loop; resources live in memory arenas; nothing is rebuilt or re-uploaded per frame unless it changed.
Usable by an agent The library is designed to be driven by an AI coding agent, and that is a testable property, not a mood: every name, command, snippet and ceiling an agent could copy is confronted with the code by a test, and the suite refuses to report a success it did not observe. What guarantees it is in §4.

1 · Light: only what you enable is there

You don't need to know wgpu. App + AppBuilder + Scene never ask you to name a wgpu type — measured, the quickstart below compiles in a crate whose only dependencies are wsg-lib and pollster, with no wgpu, no winit and no glam declared. A consumer crate living in this repository (consumer/) writes exactly those gestures and is compiled and tested by the repository's own test harness, so the claim is re-checked, not asserted. If you deliberately leave the façade, the types you need are re-exported as wsg_lib::wgpu — details in README_DETAILS.md.

Opt-in, and the cost of not enabling is zero. Not "cheap": zero. A disabled effect allocates no texture, records no pass, and where the shader still runs it switches on a uniform flag instead of executing the effect.

Effect You switch it on with If you don't
Shadows (PCF, cascades) scene.set_shadow_caster(Some(idx)) no shadow map, no depth pass, no PCF tap
HDR + tone mapping AppBuilder::with_hdr(ToneMapper::Aces) no offscreen texture, no tone-map pass
Bloom / DoF / MSAA / fog with_bloom(..) / with_dof(..) / with_msaa(..) / with_fog(..) no texture, no pass, no uniform
SSAO (needs the depth prepass) AppBuilder::with_ssao(cfg) (implies the prepass) no normal texture, no AO passes
GPU-driven culling AppBuilder::with_culling(true) no compute pipeline, no indirect buffers
Area lights, IBL, skybox, parallax, log depth per scene or per material analytic fallback, one u32 compare, or nothing
Primitives, file importers Cargo features (prim-*, import-*) not compiled into your binary at all

The full switch list, with what each one costs when it is on, is in README_DETAILS.md.

2 · GPU-driven: the work that can move, moved

render_scene records, in one encoder and one submit: a compute stage that derives what to draw, then one indirect draw per batch. The CPU sends a small table of parameters; the GPU decides what is visible and how many instances survive.

Work Where it runs
World matrices of each instance of a batch GPU — compute pass compute_matrices; for a GPU-resident batch your own compute shader writes them
Frustum culling, per instance (not per batch) GPU — cull, and cull_shadow for the shadow pass
Compaction of surviving instances GPU — cs_count / cs_compact, and instanceCount of the indirect draw is written by the GPU
Draw counts and draw arguments GPU — draw_indexed_indirect; a culled batch costs no rasterization
Back-to-front order of transparent batches GPU — radix sort (6 dispatches) inside each sortable batch
Particle simulation and particle sorting GPU — compute update + compute sort, per pool
Shadow map, depth prepass, SSAO, bloom, DoF, tone map GPU — their own passes, timed individually
"Which instance is under the cursor?" GPU — id-buffer picking pass, exact to the silhouette
Timing of each pass GPU — timestamp queries, printed by the stats feature as milliseconds

Deliberately on the CPU, because the CPU is the cheaper place for it: the LOD level choice (one distance test with hysteresis, then applied through a GPU table), BVH raycasting and picking by entity, and authoring of transforms. The GPU is not a religion here; it is where the cost is.

3 · Built for frame budgets

  • Declare everything before the loop. setup() creates the GPU buffers: mesh vertex/index buffers, materials, textures, entity slots, batch tables. Frames then only upload and draw.
  • Tables are pre-sized, not grown. 256 batch slots, 100 000 instances, a fixed set of double-buffered CPU→GPU tables. Going past a budget is refused with a message naming the entry you called — not accepted and then panicking inside the first frame.
  • Arenas, not hash maps. Meshes, materials, textures, entities, particle pools and debug points live in generational arenas (slotmap) with free lists, including the GPU table rows: removal is O(1), the index is reused, and a stale handle is detected instead of silently referring to something else.
  • Nothing is rebuilt per frame. Pipelines are cached (a lock counts shader compilations: an instanced batch used to recompile its pipeline at every draw — 225 compilations for 9 frames); a batch's matrices are uploaded once and re-armed only when they change (dirty flag); the derived CPU quantities of a static batch are computed once per frame, not once per read.
  • Measured, not assumed. The release profile is opt-level = 3, lto = "fat", codegen-units = 1; and the stats feature prints FPS, draw calls, instance counts and per-pass GPU milliseconds, so "CPU-bound or GPU-bound?" is a reading, not a guess.

Simulations: animating thousands of objects

The same property that makes a scene cheap — one draw per batch — is what makes a simulation cheap. You get a frame budget for the simulation itself, not for the drawing.

// Illustrative — `lib/examples/cameras/instances.rs` is the runnable version.
// 100 000 cubes: one batch, one draw call, per-instance culling.
let ts: Vec<Transform> = grid_of_transforms();
app.scene.add_instance_batch("grid", "cube", &ts)?;

// Per frame, mutate the matrices in place; the upload is gated by a dirty flag.
app.scene.with_batch_matrices_mut("grid", |mats| {
    for (i, m) in mats.iter_mut().enumerate() {
        *m = transforms[i];
    }
});

What is available for a simulation:

Need API Example to run
Thousands of moving objects add_instance_batch · instance_batch · with_batch_matrices_mut cargo run -p wsg-lib --example instances
Motion computed on the GPU, with your own compute shader set_batch_gpu_resident · restore_batch_matrices cargo run -p wsg-lib --example instances_sim
What is actually on screen per-instance culling + compaction, instanceCount written by the GPU cargo run -p wsg-lib --example culling
Particle systems (pools of instances) create_particle_pool + attach_gpu_emitter / attach_cpu_emitter / attach_manual cargo run -p wsg-lib --example particles
Deformable geometry (per-frame vertices) update_geometry (fast path: one write_buffer when the vertex count is unchanged) cargo run -p wsg-lib --example deformable
Collision between scene objects collides (AABB) · collides_precise (BVH) cargo run -p wsg-lib --example collision
Selecting what is under the cursor pick_instance_at (GPU) or ray_at_ndc (CPU, exact to the triangle) cargo run -p wsg-lib --example pick_instance
Hiding part of a population set_batch_visible (zero uploads) or a per-instance visibility flag cargo run -p wsg-lib --example visibility
Correct transparent overlap GPU back-to-front sort per batch cargo run -p wsg-lib --example instance_sort
Scale + camera for a large world Camera::with_perspective(fov, near, far), log_depth, orthographic camera cargo run -p wsg-lib --example ortho

Two scales are worth knowing, both measured on this repository: 100 000 instances in one draw call per batch, and a stable frame at 720p once per-frame uploads are gated (the bench prints the GPU milliseconds of each pass). Below a few thousand instances, the plain per-entity path is cheaper than a compute pass — the crossover is printed, not asserted.


Quickstart

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 — it needs no registration (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();
    }
}

// `AppBuilder::build` is async (it awaits adapter selection), hence the async main.
#[pollster::main]
async fn main() -> Result<(), WsgError> {
    let app = AppBuilder::new()
        .title("My WSG scene")
        .with_hdr(ToneMapper::Aces)  // optional
        .build()
        .await?;
    app.run(MyScene)
}

The full showcase, with 6 primitives, 3 lights, shadows, HDR and LOD:

cargo run -p wsg-lib --example demo

Demo media: 14 examples need the repository, not the package

The published crate deliberately ships without the demo media — the glTF models, the HDR environments and the six skybox JPEGs, 18 MB measured. It is what keeps the package at 2.3 MB (lib/Cargo.toml, exclude), and it is a decision, not an oversight.

Fourteen examples load those files. Run one from a crates.io download and it prints, before the first frame:

  Demo media
    Missing: /home/you/wsg/lib/examples/assets/textures
    The media this example loads are NOT in the published package.
    They are in the repository, under `lib/examples/assets/`. To run this
    example, clone the repository and run it from the clone:
      git clone https://git.iut-rodez.fr/jerome/wsg wsg
      cd wsg/lib && cargo run --example <name>
    A missing file here is expected for a crates.io download, not a bug.

then fails on the load, with the path named. To run them, use a clone:

git clone https://git.iut-rodez.fr/jerome/wsg wsg
cd wsg/lib && cargo run --example skybox

What is not affected: the library itself reads none of those files, and every test that reads media reads fixtures shipped inside the crate (lib/assets/tests/). That is why the test suite is green from a package download while those fourteen examples are not — and why the banner above names a clone rather than a bug tracker.

What's in the box

  • Rendering — Phong + PBR (metallic/roughness, normal maps), unlit mode, HDR + tone mapping, parallax (relief from the height stored in the normal map's alpha), logarithmic depth, IBL / env map (SH9 diffuse + GGX-prefiltered cubemap), skybox
  • Lighting — directional, point, spot (8 max), rect area lights (analytic irradiance + bounded specular highlight), hemisphere ambient, opacity maps, detail maps with distance fade-out, UV repetition-breaking, PCF shadows and cascaded shadow maps (1–4, opt-in)
  • Post-process — bloom, fog (3 modes), depth of field, MSAA, SSAO reading the prepass normals
  • Geometry — 6 procedural primitives, OBJ / STL / glTF import, custom geometry, LOD by quadric decimation
  • Textures — PNG / JPEG files and procedural generation (checkerboard, gradient, noise)
  • Transparency & sprites — alpha-blended / additive materials, GPU-sorted; billboarded quads with atlas animation
  • Camera & input — orbital camera, perspective/ortho, unified keyboard/mouse/scroll, raycasting and picking, debug lines/points/BBox overlays

50 runnable examples in 4 category folders, each explained on stdout with the effect, the expected visual result and the keys: lib/examples/README.md

4 · Usable by an agent

A coding agent uses a library through the same documents you read, and it fails in three predictable ways: it invents an API when a name is ambiguous, it copies a command whose verdict lies, and it reports success from a test that never ran. WSG is arranged so that each of those fails loudly. The claim is carried by instruments, listed next to the failure it prevents:

What the agent would get wrong What guarantees it does not
A snippet that does not compile The quickstart block is extracted and compiled verbatim, in a crate whose only dependencies are wsg-lib and pollster, as case T0 of campaigns/mutate-agent-contract.py — an instrument that must know how to say “true” before its refusals mean anything. Measured again 2026‑11‑05: exit 0. The repository also keeps a consumer crate (consumer/) doing the façade gestures, compiled by the suite.
An invented method name documented_api_names_exist_in_the_code types the receiver of every call inside a rust block of the README and docs/user/** and looks it up in the real API; documented_example_commands_name_a_declared_example matches every documented --example command against the manifest.
A ceiling it walked past, then a panic mid-frame Budgets are refused at the call, with a message naming the entry and the counts that got there; the scene is unchanged. the_error_contract_names_every_refused_ceiling derives the ceilings from the shipped source and compares them with docs/user/errors.md, in both directions: an undocumented guard, or an invented ceiling, both fail.
A verification command that counts skipped GPU tests as passed ./run-tests-honest.sh exits non-zero when any test skipped, and refuses to write docs/generated/TEST-SUITE.md when its two independent counters disagree. whole_suite_advice_names_the_honest_harness fails if any document recommends the plain whole-suite command without naming the one that refuses.
A described effect that does nothing Each example prints its own falsifiable prediction (.expects()), and every_banner_claim_has_a_lock requires each printed claim to be linked to a test.
Its own build noise, read as a signal Building wsg-lib as a dependency emits no warning: missing_docs is deny-level, so an undocumented public item is a build error rather than a hint an agent learns to scroll past.
A “fixed” claim nobody re-checked The locks above are themselves mutated by campaigns (mutate-agent-contract.py, mutate-consumer.py, mutate-doc-api.py, mutate-doc-commands.py): a lock that never saw its own defect fail is scenery.

So: point an agent at this repository, and the shortest honest instruction is “change the code, then make ./run-tests-honest.sh refuse to complain”. It is not a style preference — the suite is what separates “compiles” from “does what the document says”.


Documentation

Where What
README_DETAILS.md Everything under the hood: architecture, pass pipeline, the opt-in cost table, Cargo features, the full example list, build and release profile
docs/user/ User guide — step-by-step API usage
docs/tech/ Internal architecture and design decisions
cargo doc -p wsg-lib --no-deps API reference (rustdoc)

  • Language: Rust 2024 (rust-version = "1.87").
  • Dependencies: wgpu 30, winit 0.30, glam — re-exported where the API needs them named.
  • License: MIT — see lib/LICENSE.
S
Description
No description provided
Readme
52 MiB
0 Stars 1 Watchers 0 Forks
Languages
Rust 84.8%
Python 10.5%
WGSL 3.9%
Shell 0.7%