15.5 bandeaux non falsifiables, 9 familles sur 9 : cliquet BANDEAUX_SANS_PREDICTION_RATCHET reposé de 9 a 0 (mesure). Hemisphere (6.21) : l ambiante hemispherique n etait rendue par AUCUN test -- trois verrous neufs dans core::hemisphere_tests (scene sans lumiere, camera a plomb, cle du mix = n.y). Mesures propres : dessus b-g = +27, dessous g-b = +27, 3 364 texels par vue ; ciel/sol permutes = dominances inversees ; ambiante plate = 0 texel de difference. Campagne campaigns/mutate-hemisphere.py : 4 cas, 0 ecart (M1 mix PBR a l envers ROUGE, M2 t force a 0,5 ROUGE, M3 copie non-PBR a l envers VERT consigne = le mix vit a DEUX endroits, aucun des trois verrous ne traverse la copie hemisphere). Mon premier seuil de dominance (60) etait invente et serait tombe rouge sur un moteur sain : reposé a 10 sur les deux bornes mesurees +27 / -27 (regle 12.15). Verrous de source neufs : l_exemple_particles_declare_quatre_pools_aux_trois_drivers_et_deux_mixages, l_exemple_import_detecte_le_format_par_l_extension_et_refuse_le_reste, l_exemple_detail_map_oppose_autolod_et_fixed_always_visible. Defaut de document corrige : docs/user/examples.md repetait le faux du bandeau log_depth (the ground stretching to the horizon - l exemple ne construit aucun sol). Harnais : 960 tests, 0 skip, 9 doc-tests ignores.
This commit is contained in:
1 parent
1280539df1
commit
e2019dc04f
14 files changed
+535
-28
No files matched your search
@@ -844,6 +844,13 @@ decoder_can_read`, dont la liste d'extensions est **lue dans `lib/Cargo.toml`**
|
||||
|
||||
- **`Scene::set_depth_prepass` PANIQUE si elle est appelée avant `Scene::init_gpu` — et la fenêtre où elle est correcte est étroite (15.5, mesuré au premier essai d'un harnais DoF)** : le setter (`scene.rs:466`) fait `self.gpu().cache.borrow().set_depth_prepass(enabled)` — donc il exige le GPU — alors que sa documentation et le gotcha 13.2 imposent de l'armer **avant toute création de matériau**. Les deux contraintes se referment sur un intervalle : **après `init_gpu`, avant `add_material`/`create_mesh`**. Un appel juste après `Scene::new()` meurt sur « scene pipeline not initialized yet », sans rapport avec ce que l'appelant essayait de faire. Le setter rend un `bool` (refus du cache) : l'asserter, sinon la passe est désarmée en silence (13.2). Vérifier ce qu'une API **exige** avant d'écrire un harnais — le nom de l'API ne le dit pas (famille 13.5 « il n'existe pas de traversée d'entités sans GPU »).
|
||||
|
||||
- **Un bandeau purement descriptif est invisible à toute la suite de couverture — cliquet neuf (15.5, mesuré 2026‑11‑03)** : `UNCOVERED_EXAMPLES_RATCHET` ne compte que les phrases **falsifiables** (chiffre, token de touche en mot entier, `not`/`never`/`unchanged`). Un exemple dont chaque `.expects()` est descriptive sort donc du décompte **des deux côtés** : aucune prédiction → rien à confronter → 0 offense. Mesuré : **9** bandeaux dans cet état, dont celui du `cube()` dont le faux (15.5) avait survécu précisément parce que la phrase n'était pas falsifiable. Cliquet `BANDEAUX_SANS_PREDICTION_RATCHET` posé à la mesure (9 → **0** après réécriture des neuf familles : `simple`, `alpha_test`, `opacity_map`, `ambient_color`, `log_depth`, `particles`, `import`, `detail_map`, `hemisphere_light`). Règle : une promesse de bandeau est une **prédiction chiffrée ou négative**, sinon aucun verrou ne peut la prendre en défaut — et le cliquet de couverture ne la voit pas.
|
||||
*Et ce que la réécriture a trouvé, trouvé en mesurant et non en lisant* : le bandeau de `log_depth` promettait « the ground stretching to the horizon ». Inventaire de la scène : **3** `create_mesh`, tous des cubes — l'exemple ne construit **aucun** sol. Les dalles 900×900 à 1 km sont des `cube(1.0)` mis à l'échelle, jamais un `plane()`, et c'est délibéré (gotcha 6.31 : l'encodage logarithmique écarte les triangles couvrant une grande étendue de profondeur). Verrou de source neuf `l_exemple_log_depth_ne_construit_pas_sa_dalle_avec_plane` (`FAR` 5 000 / `START_DIST` 1 000 / `GAP` centimétrique, constantes **lues** dans l'exemple).
|
||||
⚠ **Premier jet ROUGE sur un arbre propre, deux fois dans la même séance** : l'unique occurrence de `plane(` était la **phrase du bandeau** qui nomme l'appel interdit ; même faute sur `particles` (le 3ᵉ `BlendingMode::Additive` compté était ma phrase). Règle : un verrou de source qui interdit un motif doit exclure la portion bandeau (`hors_bandeau`, `ExampleInfo::new` … `.print();`) — sinon il punit celui qui explique la règle (14‑F6, troisième occurrence relevée).
|
||||
|
||||
- **L'ambiante hémisphérique (6.21) n'était rendue par AUCUN test, et son `mix` vit à DEUX endroits (15.5, mesuré)** : `standard_shader.wgsl` porte `mix(frame.ambient_ground.rgb, frame.ambient.rgb, n.y * 0.5 + 0.5)` en deux copies — `hemisphere` (ligne ~611, chemin non‑PBR) et `ibl_diffuse` (ligne ~1235, chemin PBR). Les verrous de 6.21 portaient sur le préfiltrage IBL et l'exclusivité IBL/ambiante ; le gradient lui‑même n'était exercé que par des yeux. Trois verrous neufs dans `lib/src/core/hemisphere_tests.rs` : une scène **sans aucune lumière** (`clear_lights()` — `Scene::new()` en installe une, gotcha 13.1) pour que chaque texel vale l'ambiante de sa normale, caméra à plomb pour ne voir que la face +Y ou que la face −Y. Mesures propres : dessus `b−g = +27`, dessous `g−b = +27`, 3 364 texels par vue ; ciel/sol permutés → les deux dominances s'inversent ; ambiante **plate** → **0** texel de différence (identité, pas seuil — 13.1). Campagne `campaigns/mutate-hemisphere.py` : 4 cas, 0 écart — M1 (`mix` PBR à l'envers) ROUGE, M2 (`t` forcé à 0,5) ROUGE, **M3 (copie non‑PBR à l'envers) VERT consigné** : la copie `hemisphere` n'est traversée par aucun des trois verrous, limite écrite plutôt que cachée (famille du verrou PBR du spot).
|
||||
*Le seuil, pas la physique, était faux au premier essai* : j'avais prédit un écart de ~200 octets (les couleurs sont séparées de 0,8) et posé 60. Mesure propre : **27** — l'ambiante est multipliée par l'albédo et les facteurs du lobe, pas recopiée. Un verrou à 60 serait tombé rouge sur un moteur **sain**. Seuil reposé à **10** sur les deux bornes mesurées **+27 / −27** (règle 12.15 : un seuil se calibre sur la faute qu'il doit attraper **et** sur l'état propre qu'il doit laisser passer).
|
||||
|
||||
<!-- lean-ctx -->
|
||||
## lean-ctx
|
||||
|
||||
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Campagne 15.5 — famille `hemisphere_light` : les trois verrous neufs de `core::hemisphere_tests`.
|
||||
|
||||
Verrous confrontés (mesures propres consignées dans les messages d'échec) :
|
||||
* `la_face_cele_prend_la_couleur_du_ciel_et_la_face_sol_celle_du_sol` — dessus `b-g = +27`,
|
||||
dessous `g-b = +27`, 3 364 texels par vue ;
|
||||
* `permuter_ciel_et_sol_inverse_les_dominances` — ciel et sol échangés, les deux dominances
|
||||
s'inversent (c'est LA jambe qui distingue `mix(ground, sky, t)` de `mix(sky, ground, t)`) ;
|
||||
* `une_ambiante_plate_ne_distingue_plus_les_deux_vues` — ambiante blanche : **0** texel de
|
||||
différence entre les deux vues (identité, pas seuil — 13.1).
|
||||
|
||||
Le `mix` hémisphérique vit à **deux endroits** de `standard_shader.wgsl` : la copie `hemisphere`
|
||||
(ligne ~611, chemin non-PBR) et la copie `ibl_diffuse` (ligne ~1235, chemin PBR). Le verrou rend un
|
||||
matériau PBR, donc seule la seconde est traversée. M3 mute la première : **consigné VERT** si mesuré
|
||||
tel — c'est la limite de couverture, et elle doit être écrite plutôt que cachée (famille du verrou
|
||||
PBR du spot, 13.x).
|
||||
|
||||
Discipline (gotchas 9.4 / 12.13 / 13.3 / 13.6 / 14-F4 / 6.14) : verdict dérivé du **compteur**
|
||||
`test result:` ; compilation ou validation GPU qui refuse = **REFUSÉE**, jamais comptée comme
|
||||
détection ; skip = REFUSÉ (`campaigns/skipdetect.py` reconnaît les trois formes) ; la restauration
|
||||
réécrit les octets d'avant, pas `git checkout` (6.19 / 12.13).
|
||||
|
||||
Usage : ./campaigns/mutate-hemisphere.py
|
||||
"""
|
||||
import subprocess, sys, json, re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SHADER = ROOT / "lib/src/shaders/standard_shader.wgsl"
|
||||
|
||||
LOCKS = [
|
||||
"la_face_cele_prend_la_couleur_du_ciel_et_la_face_sol_celle_du_sol",
|
||||
"permuter_ciel_et_sol_inverse_les_dominances",
|
||||
"une_ambiante_plate_ne_distingue_plus_les_deux_vues",
|
||||
]
|
||||
CMDS = [f"cargo test -q -p wsg-lib --lib {n} -- --test-threads=1 2>&1" for n in LOCKS]
|
||||
|
||||
PBR = "let ibl_diffuse = mix(frame.ambient_ground.rgb, frame.ambient.rgb, n.y * 0.5 + 0.5)"
|
||||
AUTRE = "let hemisphere = mix(frame.ambient_ground.rgb, frame.ambient.rgb, n.y * 0.5 + 0.5)"
|
||||
|
||||
CASES = [
|
||||
dict(
|
||||
id="T0",
|
||||
attente="VERT",
|
||||
description="arbre propre — l'instrument doit savoir dire « vrai » (gotcha 6.6)",
|
||||
mutations=[],
|
||||
),
|
||||
dict(
|
||||
id="M1",
|
||||
attente="ROUGE",
|
||||
description="le `mix` du chemin PBR est écrit À L'ENVERS (ciel et sol permutés) — c'est le "
|
||||
"défaut que la jambe « permuter » existe à attraper (gotcha 6.25 : un argument de `mix` "
|
||||
"inversé rend une image plausible)",
|
||||
mutations=[dict(old=PBR, new="let ibl_diffuse = mix(frame.ambient.rgb, frame.ambient_ground.rgb, n.y * 0.5 + 0.5)")],
|
||||
),
|
||||
dict(
|
||||
id="M2",
|
||||
attente="ROUGE",
|
||||
description="le facteur de mélange devient une constante 0,5 : le gradient ne dépend plus de "
|
||||
"la normale, les deux faces prennent la même couleur",
|
||||
mutations=[dict(old=PBR, new="let ibl_diffuse = mix(frame.ambient_ground.rgb, frame.ambient.rgb, 0.5)")],
|
||||
),
|
||||
dict(
|
||||
id="M3",
|
||||
attente="VERT",
|
||||
description="seule la COPIE non-PBR (`hemisphere`, ligne ~611) est mise à l'envers. Le verrou "
|
||||
"rend un matériau PBR, donc il ne traverse pas ce chemin : attendu et mesuré **VERT**, c'est "
|
||||
"la limite de couverture des trois verrous — elle est écrite ici plutôt que cachée (famille "
|
||||
"du verrou PBR du spot).",
|
||||
mutations=[dict(old=AUTRE, new="let hemisphere = mix(frame.ambient.rgb, frame.ambient_ground.rgb, n.y * 0.5 + 0.5)")],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def run(cmd: str, timeout: int = 2400):
|
||||
return subprocess.run(cmd, shell=True, cwd=ROOT, capture_output=True, text=True, timeout=timeout).stdout
|
||||
|
||||
|
||||
def noms_rouges(out: str):
|
||||
bloc = re.findall(r"(?m)^failures:\n((?:[ ]{4}\S+\n)+)", out)
|
||||
return sorted({l.strip() for b in bloc for l in b.strip().splitlines()})
|
||||
|
||||
|
||||
def count(out: str):
|
||||
res = re.findall(r"(?m)^test result: \w+\. (\d+) passed; (\d+) failed", out)
|
||||
if len(res) < len(CMDS):
|
||||
return None
|
||||
return sum(int(a) for a, b in res), sum(int(b) for a, b in res)
|
||||
|
||||
|
||||
def refused(out: str):
|
||||
if re.search(r"(?m)^(error\[E\d+\]|error: could not compile)", out):
|
||||
return "compilation"
|
||||
if re.search(r"wgpu error: Validation Error|parsing error|create_shader_module", out):
|
||||
return "validation GPU / shader refusé (artefact, pas comportement)"
|
||||
if re.search(r"(?m)^\s*(skip|\[skip\])", out, re.I):
|
||||
return "test muet (skip)"
|
||||
if count(out) is None:
|
||||
return "aucun test result: — l'instrument n'a pas observé"
|
||||
return None
|
||||
|
||||
|
||||
def apply_mutations(mutations):
|
||||
orig = SHADER.read_bytes()
|
||||
s = orig.decode()
|
||||
for m in mutations:
|
||||
c = s.count(m["old"])
|
||||
assert c == 1, f"{m['old']!r} : {c} occurrences, attendu 1 — ancre non trouvée (gotcha 13.6)"
|
||||
s = s.replace(m["old"], m["new"])
|
||||
SHADER.write_text(s)
|
||||
return orig
|
||||
|
||||
|
||||
def restore(orig):
|
||||
SHADER.write_bytes(orig)
|
||||
assert SHADER.read_bytes() == orig, "restauration non exacte (gotcha 14-F4)"
|
||||
|
||||
|
||||
def main():
|
||||
rows = []
|
||||
for c in CASES:
|
||||
if not c["mutations"]:
|
||||
out = run_all()
|
||||
else:
|
||||
orig = apply_mutations(c["mutations"])
|
||||
try:
|
||||
out = run_all()
|
||||
finally:
|
||||
restore(orig)
|
||||
assert subprocess.run(f"git diff --quiet -- {SHADER}", shell=True, cwd=ROOT).returncode == 0, (
|
||||
f"{c['id']} : standard_shader.wgsl encore muté après restauration"
|
||||
)
|
||||
why = refused(out)
|
||||
if why:
|
||||
rows.append((c["id"], c["attente"], "REFUSÉ", "-", "-", why))
|
||||
continue
|
||||
passed, failed = count(out)
|
||||
rows.append((c["id"], c["attente"], "ROUGE" if failed else "VERT", passed, failed, "; ".join(noms_rouges(out))[:300]))
|
||||
|
||||
cle = ("id", "attendu", "rendu", "passed", "failed", "note")
|
||||
print(json.dumps([dict(zip(cle, r)) for r in rows], ensure_ascii=False, indent=1))
|
||||
ecart = sum(1 for r in rows if r[1] != r[2])
|
||||
print(f"\n{len(rows)} cas, {ecart} écart(s)")
|
||||
return 1 if ecart else 0
|
||||
|
||||
|
||||
def run_all():
|
||||
return "".join(run(c) + "\n" for c in CMDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -276,3 +276,19 @@ de carte : c'est une étape, pas un verrou.
|
||||
| log_depth | a 1x1 ground quad would vanish without a word | log_depth_drops_triangles_spanning_a_large_depth_range |
|
||||
| log_depth | the 1 cm gap between those plates | t5_log_depth_separates_coplanar_quads_at_one_km |
|
||||
| log_depth | that is where z-fighting appears first | t5_log_depth_separates_coplanar_quads_at_one_km |
|
||||
| particles | 4 pools at once | l_exemple_particles_declare_quatre_pools_aux_trois_drivers_et_deux_mixages |
|
||||
| particles | 2 blending modes | l_exemple_particles_declare_quatre_pools_aux_trois_drivers_et_deux_mixages |
|
||||
| particles | it is not locked by any test | l_exemple_particles_declare_quatre_pools_aux_trois_drivers_et_deux_mixages |
|
||||
| import | .obj, .stl, .gltf, .glb are recognized | l_exemple_import_detecte_le_format_par_l_extension_et_refuse_le_reste |
|
||||
| import | refused by name and not silently | l_exemple_import_detecte_le_format_par_l_extension_et_refuse_le_reste |
|
||||
| import | names the format actually detected | l_exemple_import_detecte_le_format_par_l_extension_et_refuse_le_reste |
|
||||
| import | not compiled in is refused by name | l_exemple_import_detecte_le_format_par_l_extension_et_refuse_le_reste |
|
||||
| detail_map | SAME range (15.0) | l_exemple_detail_map_oppose_autolod_et_fixed_always_visible |
|
||||
| detail_map | That difference of mode, and nothing else | l_exemple_detail_map_oppose_autolod_et_fixed_always_visible |
|
||||
| detail_map | not locked by any test | l_exemple_detail_map_oppose_autolod_et_fixed_always_visible |
|
||||
| detail_map | beyond 15.0 the middle cube should converge | l_exemple_detail_map_oppose_autolod_et_fixed_always_visible |
|
||||
| hemisphere_light | blue-minus-green +27 | la_face_cele_prend_la_couleur_du_ciel_et_la_face_sol_celle_du_sol |
|
||||
| hemisphere_light | Swap sky and ground and both flip | permuter_ciel_et_sol_inverse_les_dominances |
|
||||
| hemisphere_light | a 0 there would mean there is no gradient | permuter_ciel_et_sol_inverse_les_dominances, la_face_cele_prend_la_couleur_du_ciel_et_la_face_sol_celle_du_sol |
|
||||
| hemisphere_light | 0 texel of difference, measured | une_ambiante_plate_ne_distingue_plus_les_deux_vues |
|
||||
| hemisphere_light | came from n.y and not from the | une_ambiante_plate_ne_distingue_plus_les_deux_vues |
|
||||
@@ -143,3 +143,4 @@ recomptement, pas comme un fait, et la liste des 4 est **nommée** (une liste se
|
||||
| `campaigns/mutate-envmap.py` | le verrou de démonstration `les_six_spherees_denv_map_ne_different_que_par_metallic_et_roughness` (la phrase « ONLY in metallic and roughness » porte sur l'exemple livré) | `lib/src/resources/material_tint_tests.rs` (4 cas, 0 écart — M1 `tf.scale` posée dans la boucle : ROUGE ; M2 la table `SPHERES` ramenée à 5 paires : ROUGE ; M3 une seconde géométrie `uv_sphere` dans la boucle : ROUGE. Le verrou a été vu ROUGE quatre fois **pendant sa propre écriture** : une tranche d'annotation de type prise pour la table, un offset **relatif** traité comme absolu, et deux assertions qui rougissaient sur des faits légitimes — le `create_mesh` et l'`add_material_pbr` du sol) |
|
||||
| `campaigns/mutate-dof.py` | le verrou `le_flou_interieur_est_degrade_et_ne_deborde_pas_de_la_silhouette` (dégradé interne, confinement à la silhouette) | `lib/src/core/dof_tests.rs` (5 cas, 0 écart — M1 retirer le court-circuit `coc < 0.5` : **VERT, mutation inerte**, le rayon vaut `coc * texel` donc 0 hors de l'objet ; M2 noyau replié sur l'échantillon central : **ROUGE**, le verrou nommé tombe ; M3 plancher de rayon `max(coc, 6.0)` : **VERT** — la garde redondante porte, le fond sort avant le noyau ; M4 **les deux gardes retirées** : **ROUGE**, quatre verrous dont le verrou nommé. Mesure à retenir : les deux gardes (`coc < 0.5` et `rayon = coc · texel`) sont **redondantes**, chacune seule suffit à confiner le flou — famille 6.14 / 6.37 « deux gardes redondantes sur le seul cas exercé ») |
|
||||
| `campaigns/mutate-deform.py` | les trois gardes de la correction `update_geometry` (défaut 15.5 : le remplacement de la géométrie CPU était sous `Arc::get_mut`, qui ratait toujours dès qu'une entité utilisait le maillage) | `lib/src/core/deform_tests.rs` (4 cas, 0 écart — T0 VERT ; M1 remplacement CPU retiré : **ROUGE** ; M2 condition `mesh.bvh().is_none()` retirée : **ROUGE** ; M3 `write_buffer` qui ne récrit rien : **ROUGE**, mais seulement après avoir changé la scène du verrou GPU — avec une sphère de 360 triangles, la correction faisait emprunter la voie lente, et M3 laissait les quatre verrous **VERTS** : une correction peut déplacer le chemin qu'un verrou traverse. Deux tentatives de M3 étaient des non-mutations (E0283) et ont été **REFUSÉES**.) |
|
||||
| `campaigns/mutate-hemisphere.py` | les trois verrous neufs `core::hemisphere_tests` — l'ambiante hémisphérique (6.21) n'était rendue par **aucun** test, seulement par la vitrine | `lib/src/core/hemisphere_tests.rs` (4 cas, 0 écart — M1 le `mix` du chemin PBR écrit à l'envers : **ROUGE** sur les deux verrous de dominance ; M2 le facteur de mélange forcé à `0.5` : **ROUGE** pareil ; M3 seule la copie **non-PBR** (`hemisphere`, ligne ~611) mise à l'envers : **VERT consigné** — les trois verrous rendent un matériau PBR, donc la seconde copie du `mix` n'est traversée par aucun test (famille du verrou PBR du spot, gotcha 6.7). Mesures propres : dessus `b−g = +27`, dessous `g−b = +27`, 3 364 texels par vue ; ciel/sol permutés → les deux dominances s'inversent ; ambiante plate → **0** texel de différence entre les deux vues. Seuil de dominance 10, calibré sur les deux bornes mesurées +27 / −27 (règle 12.15) : mon premier seuil, 60, était inventé et serait tombé rouge sur un moteur sain.) |
|
||||
@@ -4,23 +4,23 @@
|
||||
|
||||
# Suite de tests — mesurée, pas souvenir
|
||||
|
||||
- Généré le : 2026-11-04 12:21 +0100
|
||||
- Généré le : 2026-11-04 12:40 +0100
|
||||
- Périmètre : `cargo test --workspace --no-fail-fast --features debug-bbox,debug-wireframe,stats,import-obj,import-stl -- --nocapture --test-threads=2`
|
||||
- Verdict : **VERT HONNÊTE : 954 tests exécutés, 0 skip, 9 doc-tests ignorés.**
|
||||
- Verdict : **VERT HONNÊTE : 960 tests exécutés, 0 skip, 9 doc-tests ignorés.**
|
||||
- Lignes de skip (les trois formes `skip …`, `[skip] …`, `SKIP …`, lues par `campaigns/skipdetect.py` : tests GPU qui n'ont pas observé, donc comptés `passed` par le harnais) : **0**
|
||||
|
||||
| Binaire de test | passed | failed | ignored |
|
||||
|---|---:|---:|---:|
|
||||
| `unittests src/lib.rs` | 838 | 0 | 0 |
|
||||
| `unittests src/lib.rs` | 841 | 0 | 0 |
|
||||
| `tests/doc_consistency.rs` | 24 | 0 | 0 |
|
||||
| `tests/example_banners.rs` | 10 | 0 | 0 |
|
||||
| `tests/example_banners.rs` | 13 | 0 | 0 |
|
||||
| `tests/manual_vs_facade.rs` | 4 | 0 | 0 |
|
||||
| `tests/roadmap_consistency.rs` | 14 | 0 | 0 |
|
||||
| `tests/roadmap_reduction.rs` | 2 | 0 | 0 |
|
||||
| `tests/texture_procedural.rs` | 4 | 0 | 0 |
|
||||
| `tests/wgsl_validate.rs` | 49 | 0 | 0 |
|
||||
| `Doc-tests wsg_lib` | 9 | 0 | 9 |
|
||||
| **Total (somme des lignes ci-dessus)** | **954** | **0** | **9** |
|
||||
| **Total (somme des lignes ci-dessus)** | **960** | **0** | **9** |
|
||||
|
||||
Ce fichier est **régénéré** à chaque exécution de `./run-tests-honest.sh` : un nombre qui y
|
||||
figure a été produit par les compteurs du harnais, pas recopié d'un souvenir. Ailleurs dans les
|
||||
|
||||
@@ -59,7 +59,7 @@ README (description + how to run): [`meshes/`](../../lib/examples/meshes/README.
|
||||
| `procedural_textures` | effects | A checkerboard, a smooth colour ramp and a noisy surface, all three generated in code with no asset loaded | `cargo run -p wsg-lib --example procedural_textures` | [Procedural textures](effects/procedural_textures.md) |
|
||||
| `detail_map` | effects | Micro-detail with an LOD fade: from far away the middle cube looks identical to the left one | `cargo run -p wsg-lib --example detail_map` | [Detail map](effects/detail-map.md) |
|
||||
| `repetition_break` | effects | The same texture repeated across many quads with the **seams between tiles invisible** — the grid reads as one continuous surface | `cargo run -p wsg-lib --example repetition_break` | [Repetition breaking](effects/repetition-break.md) |
|
||||
| `log_depth` | effects | No shimmer or crawling anywhere in a huge scene, including the ground stretching to the horizon | `cargo run -p wsg-lib --example log_depth` | [Logarithmic depth](effects/log-depth.md) |
|
||||
| `log_depth` | effects | No shimmer or crawling with `FAR` at 5,000 and the plates starting at 1,000 — and the scene is three 900x900 plates 1 cm apart, never a ground `plane()` | `cargo run -p wsg-lib --example log_depth` | [Logarithmic depth](effects/log-depth.md) |
|
||||
| `parallax` | effects | Two identical brick walls side by side: the right one has relief, and the mortar recesses shift sideways as you orbit | `cargo run -p wsg-lib --example parallax` | [Parallax](effects/parallax.md) |
|
||||
| `env_map` | effects | Six spheres differing only in metallic and roughness, reflecting the same HDR sky — sharp sun on the near-mirror, smeared on the rough ones | `cargo run -p wsg-lib --example env_map` | [Environment map](effects/env-map.md) |
|
||||
| `skybox` | effects | The background **is** the environment image: the same sun and gradient the metal sphere reflects | `cargo run -p wsg-lib --example skybox` | [Skybox](effects/skybox.md) |
|
||||
|
||||
@@ -58,16 +58,18 @@ impl AppHandler for DetailMapDemo {
|
||||
// three-cube comparison where the textures differed would prove nothing.
|
||||
ExampleInfo::new("Detail map — micro-detail with LOD fade")
|
||||
.expects(
|
||||
"From FAR away, the middle cube looks identical to the left one. The detail is \
|
||||
invisible at distance by design — that is the LOD fade doing its job.",
|
||||
"Two cubes carry the SAME detail map at the SAME UV scale (4) and the SAME range (15.0): the \
|
||||
middle one uses `DetailMode::AutoLOD`, the right one `DetailMode::FixedAlwaysVisible`. \
|
||||
That difference of mode, and nothing else, is what you are comparing.",
|
||||
)
|
||||
.expects(
|
||||
"Zoom IN and the middle cube gains fine concrete grain. Zoom back out and the \
|
||||
grain fades smoothly rather than popping.",
|
||||
"What the fade does to the pixels is not locked by any test: `add_material_pbr_detail` is called \
|
||||
by no test in this repository — the source lock \
|
||||
`l_exemple_detail_map_oppose_autolod_et_fixed_always_visible` only holds the two modes apart.",
|
||||
)
|
||||
.expects(
|
||||
"The right cube keeps its detail at EVERY distance. Comparing it against the \
|
||||
middle one at range is what shows the fade working.",
|
||||
"So: beyond 15.0 the middle cube should converge on the left one (0 grain difference at range), \
|
||||
while the right cube keeps its grain at EVERY distance. If both converge, the fade broke.",
|
||||
)
|
||||
.key("D", "Toggle the detail map on the middle cube")
|
||||
.key("F", "Toggle LOD fade — the middle cube stops fading")
|
||||
|
||||
@@ -59,13 +59,14 @@ impl AppHandler for HemisphereDemo {
|
||||
// the improvement legible — a gradient seen alone just looks like a colour.
|
||||
ExampleInfo::new("Hemisphere ambient — sky above, ground below")
|
||||
.expects(
|
||||
"Faces pointing UP tinted sky-blue, faces pointing DOWN tinted dark green, vertical \
|
||||
faces mixed. The gradient follows each fragment's own normal, so the transition \
|
||||
across a single cube is smooth and continuous.",
|
||||
"Faces pointing UP take the SKY colour, faces pointing DOWN the GROUND colour: with sky blue and \
|
||||
ground green, the up-facing cube reads blue-minus-green +27 and the down-facing one +27 the other \
|
||||
way. Swap sky and ground and both flip — a 0 there would mean there is no gradient at all.",
|
||||
)
|
||||
.expects(
|
||||
"The ground plane almost entirely in the GROUND colour — its normal points straight \
|
||||
down, so it sits at the far end of the gradient.",
|
||||
"Press 2 (flat white ambient) and the two views become identical — 0 texel of difference, measured. \
|
||||
The gradient is then a constant, so what differed before came from n.y and not from the \
|
||||
lights: the scene the lock renders has none.",
|
||||
)
|
||||
.key("1", "Hemisphere ambient — blue sky, green ground")
|
||||
.key("2", "Flat white ambient — the pre-6.21 behaviour, for comparison")
|
||||
|
||||
@@ -124,14 +124,14 @@ impl AppHandler for ParticleDemo {
|
||||
// runs could never establish.
|
||||
ExampleInfo::new("Particle systems — GPU and CPU drivers")
|
||||
.expects(
|
||||
"Four distinct effects at once: an additive flame cone, a smoke plume, a cloud of \
|
||||
debris, and a ring — all running simultaneously, each with its own texture and \
|
||||
blending mode.",
|
||||
"4 pools at once — 2 integrated by the compute pass, 1 simulated in Rust, 1 driven by \
|
||||
hand — and 2 blending modes: Additive for `fire` and `ring`, Alpha for `smoke` and \
|
||||
`debris`.",
|
||||
)
|
||||
.expects(
|
||||
"Flames BRIGHTENING where they overlap (additive) while smoke OCCLUDES what is behind \
|
||||
it (alpha). That difference in how the same quad combines is the whole distinction \
|
||||
between the two blend modes.",
|
||||
"Additive pools accumulate where they overlap; Alpha pools replace what is behind them. That is the \
|
||||
whole distinction between the two modes — and it is not locked by any test: no test in this \
|
||||
repository uses `BlendingMode::Additive`. If the contrast vanishes, look at the pipeline chosen per mode.",
|
||||
)
|
||||
.key("1 / 2 / 3 / 4", "Solo one pool — fire / debris / smoke / ring")
|
||||
.key("B", "Emit a burst of 200 sparks from the fire pool")
|
||||
|
||||
@@ -40,12 +40,13 @@ fn main() {
|
||||
ExampleInfo::new("Model import — OBJ / STL / glTF")
|
||||
.expects(
|
||||
"The model given on the command line, centred and framed, with its materials \
|
||||
intact. Nothing is hard-coded: the same program handles any file of the \
|
||||
requested format.",
|
||||
intact. The format is detected from the extension — .obj, .stl, .gltf, .glb are recognized, any \
|
||||
other is refused by name and not silently.",
|
||||
)
|
||||
.expects(
|
||||
"A console line naming the format actually detected. A model that loads but \
|
||||
renders black usually means the importer found geometry and no usable material.",
|
||||
"A console line names the format actually detected — `(format: ...)` — and each of the 3 importers \
|
||||
sits behind its own cargo feature, so a format that is not compiled in is refused by name. \
|
||||
A model that loads but renders black usually means geometry without a usable material.",
|
||||
)
|
||||
.key("Command line", "Pass the model path AFTER `--`")
|
||||
.note(
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
//! Verrous 15.5 de l'ambiante hémisphérique (6.21) — la famille `effects/hemisphere_light`.
|
||||
//!
|
||||
//! Constat mesuré avant d'écrire ce fichier : **aucun** test du dépôt ne rendait l'ambiante
|
||||
//! hémisphérique. Les verrous de 6.21 portaient sur le préfiltrage IBL et l'exclusivité
|
||||
//! IBL/ambiante hémisphérique ; le **gradient** lui-même —
|
||||
//! `mix(ground, sky, n.y * 0.5 + 0.5)` — n'était exercé que par l'exemple, c'est-à-dire par des
|
||||
//! yeux. C'est la famille du gotcha 6.6 : « la preuve d'un chemin de rendu est le nombre de tests
|
||||
//! headless qui le traversent, pas l'exemple qui le montre ».
|
||||
//!
|
||||
//! Trois verrous, dans l'ordre où ils se contredisent :
|
||||
//! 1. la face tournée vers le ciel prend la couleur du **ciel**, celle tournée vers le sol la
|
||||
//! couleur du **sol** ;
|
||||
//! 2. permuter ciel et sol **inverser** les dominances — si le `mix` est écrit à l'envers, c'est
|
||||
//! cette jambe qui rougit (une seule vue ne peut pas le voir : un gradient inversé reste
|
||||
//! plausible) ;
|
||||
//! 3. une ambiante **plate** rend les deux vues identiques à 0 texel près — la touche `2` de
|
||||
//! l'exemple, et la preuve que la différence observée vient bien du gradient.
|
||||
|
||||
use crate::core::testutil;
|
||||
|
||||
const W: u32 = 160;
|
||||
const H: u32 = 120;
|
||||
|
||||
/// Seuil de dominance, calibré sur **les deux bornes mesurées** (règle 12.15) : comportement propre
|
||||
/// `b - g = +27` (et `g - b = +27` en face inférieure), comportement permuté `−27`. Le milieu est 0 ;
|
||||
/// 10 laisse une marge au-dessus du bruit de quantification et en dessous de moitié de l'écart.
|
||||
const SEUIL_DOMINANCE: i32 = 10;
|
||||
|
||||
const CIEL: [f32; 3] = [0.1, 0.2, 1.0];
|
||||
const SOL: [f32; 3] = [0.1, 1.0, 0.2];
|
||||
|
||||
/// Rend un cube sans **aucune** lumière, avec l'ambiante demandée, vu de dessus ou de dessous.
|
||||
///
|
||||
/// Aucune lumière : le terme direct est à zéro, donc chaque texel vaut l'ambiante hémisphérique de
|
||||
/// sa normale — la mesure est lisible sans soustraction. `clear_lights()` est obligatoire,
|
||||
/// `Scene::new()` installe une directionnelle par défaut (gotcha 13.1). La caméra est à plomb : de
|
||||
/// dessus on ne voit QUE la face +Y, de dessous QUE la face −Y.
|
||||
fn rendre(
|
||||
context: &crate::core::Context,
|
||||
sky: [f32; 3],
|
||||
ground: [f32; 3],
|
||||
vue_de_dessus: bool,
|
||||
) -> Vec<u8> {
|
||||
let cible = context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("hemisphere cible"),
|
||||
size: wgpu::Extent3d {
|
||||
width: W,
|
||||
height: H,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let mut renderer = crate::core::Renderer::new(
|
||||
context,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
W,
|
||||
H,
|
||||
&crate::core::shadow::ShadowConfig::default(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false, // log_depth
|
||||
false, // depth_prepass — ne pas dépendre de l'ordre matériaux/renderer (13.2)
|
||||
);
|
||||
|
||||
let mut scene = crate::scene::Scene::new();
|
||||
scene.init_gpu(
|
||||
std::sync::Arc::new(context.device.clone()),
|
||||
context.queue.clone(),
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
1,
|
||||
);
|
||||
scene.clear_lights();
|
||||
scene.set_ambient_sky(sky);
|
||||
scene.set_ambient_ground(ground);
|
||||
scene
|
||||
.add_material_pbr("mat", "standard", 0.0, 0.9)
|
||||
.expect("matériau diélectrique rugueux");
|
||||
scene
|
||||
.create_mesh("cube", crate::mesh::primitives::cube(2.0), Some("mat"))
|
||||
.expect("maillage du cube");
|
||||
scene
|
||||
.add_entity("cube", "cube")
|
||||
.expect("entité du cube");
|
||||
|
||||
let y = if vue_de_dessus { 6.0 } else { -6.0 };
|
||||
scene.set_camera(
|
||||
crate::camera::Camera::perspective(
|
||||
glam::Vec3::new(0.0, y, 0.001),
|
||||
glam::Vec3::ZERO,
|
||||
glam::Vec3::new(0.0, 0.0, -1.0),
|
||||
)
|
||||
.with_perspective(std::f32::consts::FRAC_PI_4, 0.1, 200.0),
|
||||
);
|
||||
|
||||
// Trois frames : la première construit pipelines et buffers sans dessiner (uv_scroll_tests).
|
||||
for _ in 0..3 {
|
||||
renderer.render_scene(
|
||||
&cible.create_view(&Default::default()),
|
||||
&mut scene,
|
||||
W as f32 / H as f32,
|
||||
1.0,
|
||||
0.5,
|
||||
);
|
||||
}
|
||||
testutil::readback_texture_rgba8(&context.device, &context.queue, &cible, W, H)
|
||||
}
|
||||
|
||||
/// Moyenne des canaux sur les texels qui ne sont **pas** la couleur de clear — jamais « non nul » :
|
||||
/// le fond est une constante, pas une statistique (14‑F8).
|
||||
fn moyennes(img: &[u8]) -> (f64, f64, f64, usize) {
|
||||
let fond = testutil::clear_color_bytes();
|
||||
let (mut r, mut g, mut b) = (0.0f64, 0.0f64, 0.0f64);
|
||||
let mut n = 0usize;
|
||||
for px in img.chunks(4) {
|
||||
if px[0] == fond[0] && px[1] == fond[1] && px[2] == fond[2] {
|
||||
continue;
|
||||
}
|
||||
r += px[0] as f64;
|
||||
g += px[1] as f64;
|
||||
b += px[2] as f64;
|
||||
n += 1;
|
||||
}
|
||||
if n == 0 {
|
||||
return (0.0, 0.0, 0.0, 0);
|
||||
}
|
||||
(r / n as f64, g / n as f64, b / n as f64, n)
|
||||
}
|
||||
|
||||
/// Deux rendus identiques à 0 texel près ?
|
||||
fn texels_differents(a: &[u8], b: &[u8]) -> usize {
|
||||
a.chunks(4)
|
||||
.zip(b.chunks(4))
|
||||
.filter(|(x, y)| x != y)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn la_face_cele_prend_la_couleur_du_ciel_et_la_face_sol_celle_du_sol() {
|
||||
let Ok(context) = pollster::block_on(crate::core::Context::new_headless()) else {
|
||||
eprintln!("skip : aucun GPU headless");
|
||||
return;
|
||||
};
|
||||
let dessus = rendre(&context, CIEL, SOL, true);
|
||||
let dessous = rendre(&context, CIEL, SOL, false);
|
||||
let (r_h, g_h, b_h, n_h) = moyennes(&dessus);
|
||||
let (r_b, g_b, b_b, n_b) = moyennes(&dessous);
|
||||
eprintln!(
|
||||
"6.21 hemisphere : dessus r={r_h:.1} g={g_h:.1} b={b_h:.1} sur {n_h} texels | dessous r={r_b:.1} g={g_b:.1} b={b_b:.1} sur {n_b}"
|
||||
);
|
||||
|
||||
// Non-vacuité dimensionnée : **mesuré 3 364 texels** (le cube à plomb occupe 17,5 % du cadre),
|
||||
// 0 si la scène ne dessine pas. Un simple « non nul » serait satisfait par le fond (13.1 / 12.15).
|
||||
assert!(n_h > 2_000, "vue de dessus : seulement {n_h} texels (mesure propre attendue ~3 364) — la scène ne remplit pas le cadre, la moyenne ne dit rien");
|
||||
assert!(n_b > 2_000, "vue de dessous : seulement {n_b} texels (mesure propre attendue ~3 364)");
|
||||
|
||||
let ecart_bleu_dessus = (b_h - g_h) as i32;
|
||||
let ecart_vert_dessous = (g_b - b_b) as i32;
|
||||
assert!(
|
||||
ecart_bleu_dessus > SEUIL_DOMINANCE,
|
||||
"la face tournée vers le ciel n'est PAS dominante bleue : b-g = {ecart_bleu_dessus} (seuil {SEUIL_DOMINANCE}) — l'ambiante `sky` n'atteint pas la normale +Y"
|
||||
);
|
||||
assert!(
|
||||
ecart_vert_dessous > SEUIL_DOMINANCE,
|
||||
"la face tournée vers le sol n'est PAS dominante verte : g-b = {ecart_vert_dessous} (seuil {SEUIL_DOMINANCE}) — l'ambiante `ground` n'atteint pas la normale −Y"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permuter_ciel_et_sol_inverse_les_dominances() {
|
||||
let Ok(context) = pollster::block_on(crate::core::Context::new_headless()) else {
|
||||
eprintln!("skip : aucun GPU headless");
|
||||
return;
|
||||
};
|
||||
// Même scène, ciel et sol échangés : la face +Y doit devenir verte, la −Y bleue. C'est LA jambe
|
||||
// qui distingue `mix(ground, sky, t)` de `mix(sky, ground, t)` — une seule vue ne le peut pas.
|
||||
let dessus = rendre(&context, SOL, CIEL, true);
|
||||
let dessous = rendre(&context, SOL, CIEL, false);
|
||||
let (r_h, g_h, b_h, n_h) = moyennes(&dessus);
|
||||
let (r_b, g_b, b_b, n_b) = moyennes(&dessous);
|
||||
eprintln!(
|
||||
"6.21 hemisphere permute : dessus r={r_h:.1} g={g_h:.1} b={b_h:.1} sur {n_h} | dessous r={r_b:.1} g={g_b:.1} b={b_b:.1} sur {n_b}"
|
||||
);
|
||||
assert!(n_h > 2_000 && n_b > 2_000, "{n_h}/{n_b} texels : scène vide (mesure propre attendue ~3 364)");
|
||||
let vert_dessus = (g_h - b_h) as i32;
|
||||
let bleu_dessous = (b_b - g_b) as i32;
|
||||
assert!(
|
||||
vert_dessus > SEUIL_DOMINANCE,
|
||||
"ciel et sol permutés, la face +Y n'est pas devenue verte : g-b = {vert_dessus}. Le `mix` est probablement écrit à l'envers (gotcha 6.25 : un argument de `mix`/`select` inversé rend une image plausible)."
|
||||
);
|
||||
assert!(
|
||||
bleu_dessous > SEUIL_DOMINANCE,
|
||||
"ciel et sol permutés, la face −Y n'est pas devenue bleue : b-g = {bleu_dessous}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn une_ambiante_plate_ne_distingue_plus_les_deux_vues() {
|
||||
let Ok(context) = pollster::block_on(crate::core::Context::new_headless()) else {
|
||||
eprintln!("skip : aucun GPU headless");
|
||||
return;
|
||||
};
|
||||
// La touche `2` de l'exemple : blanche/blanche, le comportement pré-6.21. Le gradient est alors
|
||||
// une constante, donc les deux vues sont les MÊMES — identité, pas seuil (13.1).
|
||||
let dessus = rendre(&context, [1.0, 1.0, 1.0], [1.0, 1.0, 1.0], true);
|
||||
let dessous = rendre(&context, [1.0, 1.0, 1.0], [1.0, 1.0, 1.0], false);
|
||||
let (_, _, _, n_h) = moyennes(&dessus);
|
||||
let (_, _, _, n_b) = moyennes(&dessous);
|
||||
let d = texels_differents(&dessus, &dessous);
|
||||
eprintln!("6.21 ambiante plate : dessus {n_h} texels, dessous {n_b}, texels différents {d}");
|
||||
assert!(n_h > 2_000 && n_b > 2_000, "{n_h}/{n_b} texels : la scène ne dessine pas (mesure propre attendue ~3 364), l'égalité ne prouverait rien");
|
||||
assert_eq!(
|
||||
d, 0,
|
||||
"{d} texels différents entre les deux vues sous ambiante plate : le gradient ne dépend pas uniquement de n.y — ou l'ambiante plate ne rend pas les deux faces identiques comme la touche `2` de l'exemple le promet"
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ mod emissive_tests;
|
||||
#[cfg(test)]
|
||||
mod hdr_tests;
|
||||
#[cfg(test)]
|
||||
mod hemisphere_tests;
|
||||
#[cfg(test)]
|
||||
mod msaa_tests;
|
||||
/// 15.5 — famille « ombres » de la file des bandeaux : pénombre PCF, allongement selon la lumière,
|
||||
/// rectangle en vue de dessus, ombre propre par casteur et bascule `set_shadow_caster`. `cfg(test)` only.
|
||||
|
||||
@@ -68,7 +68,7 @@ const UNCOVERED_EXAMPLES_RATCHET: usize = 0; // mesuré 2026‑11‑03 après `d
|
||||
/// falsifiables). C'est la famille `cube()` de 15.5 : la promesse était fausse depuis des semaines.
|
||||
/// Valeur **mesurée** ; la liste est affichée par le message d'échec et par `WSG_MEASURE_RATCHET=1`,
|
||||
/// jamais recopiée dans la prose.
|
||||
const BANDEAUX_SANS_PREDICTION_RATCHET: usize = 4; // reposé à la mesure à chaque bandeau réécrit ; la chaîne des repos vit dans le message d'échec, pas ici
|
||||
const BANDEAUX_SANS_PREDICTION_RATCHET: usize = 0; // reposé à la mesure à chaque bandeau réécrit ; la chaîne des repos vit dans le message d'échec, pas ici
|
||||
// Chaîne des mesures : 37 → **36** (`fog` est passé du côté couvert — trois lignes de manifeste, dont
|
||||
// la phrase falsifiable « the fog VANISHES », verrouillée par
|
||||
// `the_fog_example_is_measurable_at_its_own_framing`) → **35** (`procedural_textures`, 5 lignes de
|
||||
|
||||
@@ -1011,3 +1011,104 @@ fn le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme() {
|
||||
fn occurrences(src: &str, aiguille: &str) -> usize {
|
||||
src.matches(aiguille).count()
|
||||
}
|
||||
|
||||
/// Retire la portion bandeau (`ExampleInfo::new` … `.print();`) d'une source d'exemple. Un verrou qui
|
||||
/// cherche un motif interdit dans le fichier **entier** rougit sur la phrase qui énonce la règle — ce
|
||||
/// qui apprend à la contourner plutôt qu'à la lire (gotcha 14‑F6, et déjà appliqué à
|
||||
/// `l_exemple_log_depth_ne_construit_pas_sa_dalle_avec_plane`, dans un autre binaire de test).
|
||||
fn hors_bandeau(src: &str) -> String {
|
||||
let debut = src.find("ExampleInfo::new").unwrap_or(0);
|
||||
let fin = src
|
||||
.find(".print();")
|
||||
.map(|f| f + ".print();".len())
|
||||
.unwrap_or(debut);
|
||||
let mut s = String::new();
|
||||
s.push_str(&src[..debut]);
|
||||
s.push_str(&src[fin.min(src.len())..]);
|
||||
s
|
||||
}
|
||||
|
||||
/// Verrou 15.5 — le bandeau de `particles` annonce « four distinct effects … each with its own texture
|
||||
/// and blending mode ». Mesure : **aucun** test du dépôt n'emploie `BlendingMode::Additive` — la
|
||||
/// distinction additive/alpha n'est verrouillée nulle part. Ce que ce verrou porte, c'est la
|
||||
/// déclaration : quatre lots, trois pilotes, deux mixages. La conséquence visuelle (les flammes qui
|
||||
/// éclairent davantage là où elles se recouvrent) n'est **pas** verrouillée par les pixels, et le
|
||||
/// bandeau le dit.
|
||||
#[test]
|
||||
fn l_exemple_particles_declare_quatre_pools_aux_trois_drivers_et_deux_mixages() {
|
||||
let src = hors_bandeau(&strip_comments(
|
||||
&std::fs::read_to_string(crate_root().join("examples/effects/particles.rs"))
|
||||
.expect("examples/effects/particles.rs"),
|
||||
));
|
||||
let gpu = src.matches("attach_gpu_emitter(").count();
|
||||
let cpu = src.matches("attach_cpu_emitter(").count();
|
||||
let manual = src.matches("attach_manual(").count();
|
||||
assert_eq!(gpu, 2, "`particles` attache {gpu} pilotes GPU : le bandeau annonce quatre lots dont deux intégrés par le compute pass");
|
||||
assert_eq!(cpu, 1, "`particles` attache {cpu} pilote CPU : `debris` est le lot simulé en Rust pur");
|
||||
assert_eq!(manual, 1, "`particles` attache {manual} pilote manuel : `ring` est le lot piloté à la main");
|
||||
assert_eq!(gpu + cpu + manual, 4, "{}/{}/{} : quatre lots attendus", gpu, cpu, manual);
|
||||
let add = src.matches("BlendingMode::Additive").count();
|
||||
let alpha = src.matches("BlendingMode::Alpha").count();
|
||||
assert_eq!(add, 2, "{add} lots Additive : le bandeau oppose deux mixages, `fire` et `ring` en Additive");
|
||||
assert_eq!(alpha, 2, "{alpha} lots Alpha : `smoke` et `debris` en Alpha");
|
||||
for nom in ["fire", "smoke", "debris", "ring"] {
|
||||
assert!(src.contains(&format!("\"{nom}\"")), "le lot `{nom}` n'est plus déclaré dans `particles`");
|
||||
}
|
||||
}
|
||||
|
||||
/// Verrou 15.5 — le bandeau de `import` affirme que le format est **détecté** et que rien n'est
|
||||
/// codé en dur. Le verrou porte les deux : la dispatch se fait sur l'extension, les quatre
|
||||
/// extensions connues sont nommées, une extension inconnue est **refusée par son nom**, et l'exemple
|
||||
/// annonce à l'écran le format retenu.
|
||||
#[test]
|
||||
fn l_exemple_import_detecte_le_format_par_l_extension_et_refuse_le_reste() {
|
||||
let src = strip_comments(
|
||||
&std::fs::read_to_string(crate_root().join("examples/meshes/import.rs"))
|
||||
.expect("examples/meshes/import.rs"),
|
||||
);
|
||||
// La ligne qui nomme le format réellement retenu (le bandeau la promet).
|
||||
assert!(
|
||||
src.contains("(format: {ext})"),
|
||||
"`import` n'affiche plus le format détecté : le bandeau promet une ligne de console qui nomme le format"
|
||||
);
|
||||
for ext in [".obj", ".stl", ".gltf", ".glb"] {
|
||||
assert!(src.contains(ext), "`import` ne reconnaît plus l'extension `{ext}` que le bandeau annonce");
|
||||
}
|
||||
assert!(
|
||||
src.contains("unsupported format"),
|
||||
"`import` ne refuse plus une extension inconnue par son nom : le refus silencieux est le cas que ce verrou existe à empêcher"
|
||||
);
|
||||
// Trois importeurs indépendants, chacun derrière sa feature.
|
||||
for f in ["import-obj", "import-stl", "import-gltf"] {
|
||||
assert!(src.contains(f), "`import` ne mentionne plus la feature `{f}` : un des trois formats n'est plus branché");
|
||||
}
|
||||
}
|
||||
|
||||
/// Verrou 15.5 — la démonstration de `detail_map` tient à **deux modes** posés sur des matériaux
|
||||
/// sinon identiques : `AutoLOD` (le cube du milieu, qui s'efface au-delà de 15 m) contre
|
||||
/// `FixedAlwaysVisible` (le cube de droite, qui garde son détail à toute distance). Mesure :
|
||||
/// `add_material_pbr_detail` n'est appelé par **aucun test** — c'est l'un des membres que seule la
|
||||
/// vitrine exerce. Si les deux modes convergent, la démonstration devient vide sans que rien ne
|
||||
/// rougisse ; ce verrou est ce qui reste pour le dire.
|
||||
#[test]
|
||||
fn l_exemple_detail_map_oppose_autolod_et_fixed_always_visible() {
|
||||
let src = strip_comments(
|
||||
&std::fs::read_to_string(crate_root().join("examples/effects/detail_map.rs"))
|
||||
.expect("examples/effects/detail_map.rs"),
|
||||
);
|
||||
assert_eq!(
|
||||
src.matches("add_material_pbr_detail(").count(),
|
||||
2,
|
||||
"`detail_map` appelle {} matériaux à carte de détail : la démonstration oppose le cube du milieu à celui de droite",
|
||||
src.matches("add_material_pbr_detail(").count()
|
||||
);
|
||||
assert!(src.contains("DetailMode::AutoLOD"), "`detail_map` n'utilise plus `DetailMode::AutoLOD` : le cube du milieu ne s'efface plus au-delà de la portée annoncée");
|
||||
assert!(src.contains("DetailMode::FixedAlwaysVisible"), "`detail_map` n'utilise plus `DetailMode::FixedAlwaysVisible` : le cube de droite cesse d'être le témoin, et la comparaison des deux ne veut plus rien dire");
|
||||
let fades = src.matches("15.0,").count();
|
||||
assert_eq!(fades, 2, "{fades} matériaux portent la portée de fondu de 15,0 : les deux cubes doivent partager la même portée, sinon la différence observée n'est plus le mode");
|
||||
// Les deux matériaux sinon identiques : même uv_scale (4) des deux côtés.
|
||||
// L'échelle d'UV doit être la même des deux côtés, sinon ce n'est plus le MODE qui est comparé.
|
||||
// Décompte robuste à l'indentation : une ligne dédiée « 4, » plus la variante commentée.
|
||||
let echelle = src.lines().filter(|l| l.trim() == "4,").count() + src.matches("4, // uv_scale").count();
|
||||
assert_eq!(echelle, 2, "{echelle} matériaux portent l'échelle d'UV 4 : les deux cubes doivent partager la même échelle, sinon la différence observée n'est plus le mode");
|
||||
}
|
||||
Reference in new issue
Block a user