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).
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_bufferrun 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_consistencyfails; - the behaviour is locked by
au_dela_du_budget_de_slots_un_sprite_est_refuse_et_le_rendu_ne_panique_pasand its neighbours inlib/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.