Files
Jérôme Bousquié b0235f343d packaging: le paquet publie s'explique tout seul (medias de demo, assets du depot)
Deux defauts reels, tous deux rencontres par un telechargeur de crates.io,
aucun par un lecteur du depot.

1) Les medias de demo. `exclude` (decision D9) retire 18 Mio de medias que
   14 exemples sur 50 chargent. Depuis un telechargement, l'exemple echouait sur
   un chemin CORRECT sans que rien, dans aucun fichier du paquet, ne dise que les
   medias vivent dans le depot : un chemin correct sans explication se lit comme
   une bibliotheque cassee.

   - `example::DEMO_ASSETS_NOTE`, URL lue par env!("CARGO_PKG_REPOSITORY") : le
     message ne peut pas derive du champ `repository` qu'il decrit (6.31), et le
     retrait du champ fait cesser de compiler plutot que publier une phrase sans
     adresse ;
   - `ExampleInfo::uses_demo_assets(dir)` n'affiche la note QUE si le dossier est
     absent. Une note vraie dans les deux cas apprend a sauter le bandeau : c'est
     le defaut que les regles `.expects()` combattent, applique au bandeau
     lui-meme. Deux tests, une jambe par cas, fixture « present » = `src/` (present
     dans le depot ET dans le paquet, contrairement a `examples/assets/`) ;
   - README racine + les 5 README d'exemples (tous expodies) + une section de
     docs/user/errors.md ;
   - verrous `examples_that_load_demo_media_say_where_to_get_them` (liste derivee,
     non-vacuite 14, et l'appel doit nommer le MEME chemin que celui charge) et
     `the_packaged_docs_explain_the_excluded_demo_media` (l'URL est confrontee au
     champ `repository`, pas au document : « l'URL apparait quelque part » etait
     satisfait par les neuf liens du README et laissait passer une commande de
     recuperation pointant ailleurs - trouve par D4) ;
   - campagne campaigns/mutate-demo-media.py : T0 + 6 cas, 0 ecart. D6 est un
     VERT consigne : un appel reformatte par rustfmt ne doit pas rougir, et la
     premiere version de la jambe lisait une seule ligne - elle cassait sous le
     formateur.

2) Trois verrous rouges sur un artefact sain. Ils lisent `examples/assets/` et
   `testassets/gltf/`, sont derriere `depot-assets` - et `--all-features` active
   ce feature aussi chez l'appelant, qui n'a pas les medias. Mesure sur le tarball
   depaquete : 3 echecs dont les messages accusaient les assets d'avoir « bouge ».
   Marqueur de checkout partage (`testutil::checkout_du_depot`,
   campaigns/skipdetect.py au parent du manifeste : jamais expédie, toujours
   present dans un checkout) + `depot_absent_alors_skip`, qui emet la forme
   canonique lue par le harnais. Dans le depot le marqueur est la, donc aucun skip
   n'est emis et le harnais refuserait la suite s'il en sortait un. Verrou
   `les_verrous_d_assets_du_depot_ne_sont_pas_rouges_sur_le_paquet`.

Mesures : artefact depaquete en --all-features, 963 passed / 0 failed, avec trois
lignes « skip : » qui nomment la raison (visibles sous --nocapture ; sans lui le
test qui skippe a reussi et sa stderr est capturee - gotcha 13.1 re-frappe).
Corrections d'exemples au passage : 4 avertissements supprimes (unused import x2,
un champ lu seulement sous debug-bbox, un mut de trop).
2026-11-06 14:12:15 +01:00

5.2 KiB

Errors and refusals — what the API does when you exceed it

Every budget in WSG is pre-sized: tables are allocated before the render loop, so going past a ceiling is not something that can be discovered halfway through a frame. It is refused at the call that exceeds it, with a message naming what was called, and the scene is left exactly as it was.

That last part is the contract, and it is locked: a refusal is not “accepted then panicked inside the first frame”. A test builds the scene one entry past each ceiling, asserts the call returns Err, and then renders — the render must not panic.

Two shapes

Shape Who returns it How to read it
Result<_, String> Scene authoring calls (create_mesh, add_sprite, add_material_shader, add_lod_level, add_light, register_shader) the string is written for a human: it names the ceiling and, where it matters, the counts that got there
Result<_, BatchError> instance-batch calls (add_instance_batch and friends) a typed enum, so you can match on it: CapacityReached { batches, entities }, InstanceCapacityReached { used, requested }, UnknownMesh, DuplicateLabel

Both are refusals, never a panic and never a log line. If you are calling from a scripted or generated client: check the Err, do not grep the log for a symptom — there is none.

A media file is missing (examples only)

Fourteen examples load demo media — glTF models, HDR environments, the six skybox JPEGs — that are not in the published package (lib/Cargo.toml, exclude; 18 MB measured, and the reason the .crate weighs 2.3 MB). From a crates.io download those examples print a Demo media banner naming the missing directory, then return a load error naming the same path.

That is the intended shape of the failure. The fix is not in the library: clone the repository and run the example from the clone (the root README's Demo media section gives the two commands). Nothing in wsg-lib itself reads those files — every test that reads media reads fixtures that ship inside the crate (lib/assets/tests/), which is why the test suite is green from a package download while those examples are not.

Ceilings you can hit

Ceiling Constant What you get back What does not happen
Transform slots — entities and instance batches share one table MAX_BATCHES Cannot add <entry>: MAX_BATCHES (<ceiling>) reached (<n> entities + <m> instance batches). no half-inserted entity, no orphan batch
GPU mesh tables (one row per mesh, indexed positionally by the GPU) MAX_BATCHES Cannot add <entry>: the GPU mesh tables hold at most <ceiling> entries (got <n>). Reduce the mesh count, or grow MAX_BATCHES together with the bbox/LOD buffers and their bind groups. the table is not lengthened, the frame is not killed
Instances across all batches MAX_INSTANCES BatchError::InstanceCapacityReached { used, requested } no truncated batch, no silently dropped instances
Lights MAX_LIGHTS Cannot add another light: MAX_LIGHTS (<ceiling>) reached. no light list corruption
LOD levels of one mesh MAX_LOD_LEVELS Mesh '<mesh>' already has the maximum of <ceiling> LOD levels. no new level appended
Registering a shader by path — Shader file '<path>' for id '<id>' is not a readable file… — checked at registration, not at first draw no silent fallback to the built-in shader (that fallback existed, and it hid 31 examples pointing at a folder that does not exist)

Two of these are worth internalising, because both were once silent failures:

  • Entities and batches share MAX_BATCHES. A scene of 250 entities leaves 6 batch slots, not 256. The message prints both counts precisely so you do not have to guess which side filled the table.
  • A mesh row is a GPU index. The row index is what the bounding-box and LOD tables address, so a refusal is not an optimisation — accepting a 257th row would make the next write_buffer run past the end of a fixed buffer. That is the failure the ceiling exists to prevent.

Removing things

Removal is O(1) and leaves holes rather than shifting rows: a freed mesh row is marked empty and its index is reused, so the rows around it do not move — an index you kept stays valid, and a stale handle is refused rather than made to refer to something else. remove_sprite releases the entity, its slot, its generated material and its generated mesh; a mesh you passed in yourself survives removal of a sprite that used it.

Machine-checked

The rows above are not prose you have to trust:

  • every ceiling named here is read back out of the shipped source — add a budget guard without documenting it, or invent a ceiling in this page, and doc_consistency fails;
  • the behaviour is locked by au_dela_du_budget_de_slots_un_sprite_est_refuse_et_le_rendu_ne_panique_pas and its neighbours in lib/src/scene/sprite_tests.rs;
  • removing a guard is a case in campaigns/mutate-sprite-arena.py, seen red.

See also scene-interaction.md for raycasting/picking APIs and stats.md for what the stats feature prints when a frame is slow.