campagne mutate-emissive.py : 4 cas, 0 écart (M1/M2 ROUGE, M3 VERT consigné) + gotcha « l'émissif est multiplié par l'albédo »
This commit is contained in:
1 parent
60d6a0636a
commit
e4840b4fa7
5 files changed
+125
-9
No files matched your search
@@ -808,6 +808,8 @@ decoder_can_read`, dont la liste d'extensions est **lue dans `lib/Cargo.toml`**
|
||||
- **Un instrument dont la DÉTECTION dépend de ce qu'il doit mesurer ne mesure pas — et un sol en ombre ne se détecte plus par la luminance (15.5, mesuré deux fois dans le même verrou)** : `horizon_row` trouvait le sol à « la première rangée dont le maximum dépasse le ciel de 24 de luminance ». Sur la jambe « lumière rase » (`L.y = 0,25`), le sol éclairé vaut **86** et le ciel (couleur de clear) **97** : l'horizon n'est plus détecté du tout (`y0 = 240`), la mesure rend 0, et cela se lit comme « l'effet a disparu ». Seconde faute au même endroit : un seuil **absolu** (35 % du niveau éclairé de CHAQUE image) classait **48 960 px** en ombre sur la jambe rase — c'est-à-dire TOUT le sol : la métrique se normalisait elle-même, la 6.6 dans sa forme la plus pure. Remède en deux temps, pas en commentaires : (a) `y0` est établi **une fois** sur un rendu de référence et partagé par les quatre rendus — c'est une propriété de la caméra, pas de l'éclairage ; d'où `testutil::bands_below` et `testutil::shadow_bbox_below(.., y0, ..)` qui reçoivent la rangée de départ au lieu de la deviner ; (b) quand les deux jambes **changent l'éclairage**, la grandeur partagée n'est pas une luminance absolue mais l'**assombrissement dû au casteur** : même scène avec et sans `set_shadow_caster`, et un pixel compte comme ombre s'il est assombri de plus de 25 % par rapport à sa valeur SANS casteur (garde `b > 60` : un pixel déjà noir ne peut pas être comparé à lui-même sans que le critère ne dépende du bruit de quantification — la dénominateur écrêté de 15.3). Mesures : aire d'ombre lumière haute **422 px** / rase **2 601 px**, barycentre x **167,5 → 235,3**, cube posé **185,9** / levé **227,4**, remplissage de la boîte englobante en vue de dessus **0,985**, trois régions d'ombre **[11, 89, 103]** pour trois casteurs, différence ON/OFF **4 696 / 4 697** px. Verrous : les cinq de `lib/src/core/shadow_edge_tests.rs`. Campagne `campaigns/mutate-shadows.py` : 5 cas, 0 écart — dont M4 (rayon PCF ×4) **prédit VERT de mémoire et mesuré ROUGE**, et un seul tap PCF qui fait tomber **quatre** verrous : les mesures d'aire et de barycentre dépendent de l'existence du filtrage, pas seulement la bande de pénombre.
|
||||
*Et une leçon de localité :* `assert_eq!(pixels changés hors du sol, 0)` était **faux mesuré 1** — le sommet d'un cube dépasse la rangée d'horizon détectée et son éclairage propre dépend du casteur (self-shadowing). Ce qui discrimine est une **fraction** : 95 % du changement sur le sol (mesuré 99,98 %). Une identité est plus forte qu'un seuil quand elle est vraie ; quand elle ne l'est pas, c'est une borne structurelle avec les deux valeurs citées (6.6 / 12.15).
|
||||
|
||||
- **L'émissif est MULTIPLIÉ par l'albédo — un albédo noir annule l'émission, sans erreur ni log (15.5, trouvé en mesurant, pas en relisant)** : `standard_shader.wgsl` calcule `emissive_contrib = base_for_detail * object.emissive.rgb * object.emissive.a` en **quatre** endroits (deux entrées fragment `unlit`, deux lit — la même obligation que le gotcha 6.7 « brancher dans TOUS les points d'entrée »). Le premier harnais de la 15.5 mettait `with_color([0.0,0.0,0.0,1.0])` pour « isoler l'émissif » : la jambe HDR était **totalement muette**, sans le moindre message. Même famille qu'`ObjectUniform::ambient_color = (0,0,0,0)` : *une valeur par défaut qui annule un terme entier ne se voit pas*. Verrou de l'effet : `au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_reference_ne_bouge_pas` (identité LDR **0 texel**, HDR **1 198** texels, référence **0**). ⚠ **Ce fait précis n'est pas verrouillé** : la campagne `campaigns/mutate-emissive.py` (4 cas, 0 écart — M1 émissif neutre aux quatre sites ROUGE, M2 écrêtage LDR supprimé ROUGE, **M3 retrait de la multiplication par l'albédo VERT consigné**) le dit mesuré. Un verrou d'identité+localité ne porte pas une magnitude : l'écrire comme couvert serait une fausse preuve (6.19).
|
||||
|
||||
<!-- lean-ctx -->
|
||||
## lean-ctx
|
||||
|
||||
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Campagne 15.5 — le verrou de la famille `emissive` (`core::emissive_tests`).
|
||||
|
||||
Verrou : `au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_reference_ne_bouge_pas`
|
||||
Mesures propres : identité LDR **0 texel** ; contrôle positif HDR **1 198** texels (seuil 300) ;
|
||||
empreinte de la référence `(106,44)-(143,75)` changée à **0** texel.
|
||||
|
||||
Trois cas, et les lignes mutées sont des **ancres multiples** : la formule
|
||||
`emissive_contrib = base_for_detail * object.emissive.rgb * object.emissive.a` existe en **quatre**
|
||||
endroits de `standard_shader.wgsl` (deux entrées `unlit`, deux entrées lit — la même raison que le
|
||||
gotcha 6.7, « un effet doit être branché dans TOUS les points d'entrée »). Le script **affirme le
|
||||
nombre d'occurrences mutées** avant de conclure (gotcha 13.6 : une ancre non trouvée rend VERT).
|
||||
|
||||
* **M1** l'émissif ne fait rien (`emissive.a` forcé à 0 aux quatre sites) → le contrôle positif HDR
|
||||
meurt → ROUGE.
|
||||
* **M2** l'écrêtage LDR n'a pas lieu (`final_rgb × 0.25` avant le return, donc les deux niveaux
|
||||
passent sous 1.0) → la première phrase du bandeau est contredite → ROUGE.
|
||||
* **M3** la multiplication par l'**albédo** est retirée aux quatre sites → attendu **VERT consigné** :
|
||||
le verrou porte une identité et une localité, pas cette multiplication. C'est le trou de couverture
|
||||
trouvé par le harnais (albédo noir = émissif invisible, sans erreur ni log) — le consigner vert vaut
|
||||
mieux qu'un verrou décoratif qui prétendrait la couvrir.
|
||||
|
||||
Discipline (9.4 / 12.13 / 13.3) : verdict par compteur `test result:` ; compilation ou validation GPU
|
||||
= REFUSÉ ; restauration par les octets d'avant, jamais `git checkout` (6.19 / 12.13).
|
||||
|
||||
Usage : ./campaigns/mutate-emissive.py
|
||||
"""
|
||||
import subprocess, sys, json, re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SH = ROOT / "lib/src/shaders/standard_shader.wgsl"
|
||||
LOCK = "au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_reference_ne_bouge_pas"
|
||||
CMD = f"cargo test -q -p wsg-lib --lib {LOCK} -- --test-threads=1 2>&1"
|
||||
|
||||
EMISS = " let emissive_contrib = base_for_detail * object.emissive.rgb * object.emissive.a;"
|
||||
EMISS_12 = " let emissive_contrib = base_for_detail * object.emissive.rgb * object.emissive.a;"
|
||||
RET = " return vec4<f32>(apply_fog(final_rgb, in.world_pos), alpha);"
|
||||
RET_12 = " return vec4<f32>(apply_fog(final_rgb, in.world_pos), alpha);"
|
||||
|
||||
CASES = [
|
||||
dict(id="T0", attente="VERT", n=0, ancre=None,
|
||||
description="arbre propre (6.6 : l'instrument doit savoir dire « vrai »)", edits=[]),
|
||||
dict(id="M1", attente="ROUGE", n=4, ancre="base_for_detail * object.emissive.rgb * object.emissive.a;",
|
||||
description="émissif neutre aux QUATRE sites",
|
||||
edits=[(EMISS, " let emissive_contrib = base_for_detail * object.emissive.rgb * 0.0;"),
|
||||
(EMISS_12, " let emissive_contrib = base_for_detail * object.emissive.rgb * 0.0;")]),
|
||||
dict(id="M2", attente="ROUGE", n=4, ancre="return vec4<f32>(apply_fog(final_rgb, in.world_pos), alpha);",
|
||||
description="plus d'écrêtage LDR : ×0.25 avant le return, les deux niveaux restent sous 1.0",
|
||||
edits=[(RET, " return vec4<f32>(apply_fog(final_rgb * 0.25, in.world_pos), alpha);"),
|
||||
(RET_12, " return vec4<f32>(apply_fog(final_rgb * 0.25, in.world_pos), alpha);")]),
|
||||
dict(id="M3", attente="VERT", n=4, ancre="base_for_detail * object.emissive.rgb * object.emissive.a;",
|
||||
description="albédo retiré du terme émissif aux quatre sites : le trou de couverture trouvé "
|
||||
"par le harnais n'est porté par AUCUN verrou — VERT consigné, pas masqué",
|
||||
edits=[(EMISS, " let emissive_contrib = object.emissive.rgb * object.emissive.a;"),
|
||||
(EMISS_12, " let emissive_contrib = object.emissive.rgb * object.emissive.a;")]),
|
||||
]
|
||||
|
||||
|
||||
def count(out):
|
||||
r = re.findall(r"(?m)^test result: \w+\. (\d+) passed; (\d+) failed", out)
|
||||
return None if len(r) != 1 else (int(r[0][0]), int(r[0][1]))
|
||||
|
||||
|
||||
def refused(out):
|
||||
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", out):
|
||||
return "validation GPU (artefact)"
|
||||
if re.search(r"(?m)^\s*(skip|\[skip\])", out, re.I):
|
||||
return "test muet (skip)"
|
||||
if count(out) is None:
|
||||
return "aucun test result: (le filtre n'a rien lancé — 9.4)"
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
original = SH.read_text()
|
||||
rows = []
|
||||
for c in CASES:
|
||||
txt = original
|
||||
for (old, new) in c["edits"]:
|
||||
n = txt.count(old)
|
||||
if n == 0:
|
||||
continue
|
||||
txt = txt.replace(old, new)
|
||||
# Comptage **sans double emploi** : la variante à 4 espaces est un SOUS-CHAÎNE de celle à 8,
|
||||
# donc sommer les deux comptages donnait 6 pour 4 sites (mesuré, première exécution).
|
||||
touches = original.count(c["ancre"]) if c["ancre"] else 0
|
||||
assert touches == c["n"], f"{c['id']} : ancrage vue {touches} fois, attendu {c['n']} (13.6)"
|
||||
SH.write_text(txt)
|
||||
try:
|
||||
out = subprocess.run(CMD, shell=True, cwd=ROOT, capture_output=True, text=True,
|
||||
timeout=2400).stdout
|
||||
finally:
|
||||
SH.write_text(original)
|
||||
assert SH.read_text() == original, f"{c['id']} : restauration non exacte"
|
||||
why = refused(out)
|
||||
if why:
|
||||
rows.append((c["id"], c["attente"], "REFUSÉ", "-", "-", why + f" ({touches} sites mutés)"))
|
||||
continue
|
||||
p, f = count(out)
|
||||
rows.append((c["id"], c["attente"], "ROUGE" if f else "VERT", p, f,
|
||||
f"{touches} sites mutés ; {c['description']}"))
|
||||
print(json.dumps([dict(zip(("id", "attendu", "rendu", "passed", "failed", "note"), 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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -140,17 +140,17 @@ le bloom d'un défaut qu'il n'a pas). Campagne `mutate-bloom.py` : 4 cas, 0 éca
|
||||
ne fait tomber que le verrou des coins** — l'angle non couvert du verrou géométrique est le seuil,
|
||||
et l'amplitude `intensity` n'est portée par aucun des deux (trou consigné, pas masqué).
|
||||
|
||||
### `emissive` FERMÉ (verrous + manifeste ; **campagne à jouer au prochain tour**)
|
||||
### `emissive` FERMÉ (verrous + manifeste + campagne `campaigns/mutate-emissive.py`)
|
||||
Un seul verrou de rendu, `core::emissive_tests`, pour les quatre phrases : identité **0 texel** entre
|
||||
émissif 2,0 et 4,0 en LDR ; **1 198** texels changés en HDR (contrôle positif, seuil 300) ;
|
||||
**0 texel** changé dans l'empreinte de la référence, mesurée par « présente / absente ».
|
||||
Découverte trouvée par le harnais, pas la relecture : `emissive_contrib = base_for_detail ×
|
||||
object.emissive.rgb × object.emissive.a` — l'émissif est **multiplié par l'albédo**, donc un albédo
|
||||
noir annule l'émissif sans erreur ni log (famille `ambient_color = (0,0,0,0)`). Premier essai du
|
||||
verrou, albédo noir → jambe HDR muette. Campagne non encore jouée : `mutate-emissive.py` (M1
|
||||
`emissive.a` forcé à 0 dans les DEUX entrées fragment (unlit et lit — les deux doivent être mutées,
|
||||
famille 6.7) : le contrôle positif HDR doit tomber ; attendu : M3 « retirer la multiplication par
|
||||
l'albédo » laisse le verrou VERT, ce qui consigne que le fait découvert n'est pas verrouillé).
|
||||
verrou, albédo noir → jambe HDR muette. Campagne jouée : **4 cas, 0 écart** — M1 ROUGE, M2 ROUGE,
|
||||
M3 VERT comme prévu. L'ancrage multiple était d'abord faux : la variante à 4 espaces est une
|
||||
**sous-chaîne** de celle à 8, donc sommer les deux comptages annonçait **6** pour 4 sites (mesuré dès
|
||||
la première exécution ; famille 13.6 — le comptage d'occurrences est une grandeur, pas une intuition).
|
||||
|
||||
### `hdr` — 3 phrases, **zéro** verrou d'effet de tone mapping
|
||||
Seul `the_sky_goes_through_tone_mapping_only_when_hdr_is_on` touche la courbe, pour le ciel.
|
||||
|
||||
@@ -130,6 +130,7 @@ recomptement, pas comme un fait, et la liste des 4 est **nommée** (une liste se
|
||||
| `campaigns/mutate-pbr.py` | les trois phrases du bandeau de `meshes/pbr` : normale, metallic, nombres annoncés | `lib/src/resources/material_tint_tests.rs` (6 cas, 0 écart — M1 **carte de normales ignorée** : un seul verrou ; M2 **canaux r/b permutés** : trois verrous, mesure non prévue — le **repli** `(128,128,255)` n'est pas invariant par permutation, donc la faute atteint tout matériau PBR ; M3 **`(1 − metallic)` retiré des deux lobes diffus** : le verrou de masse lumineuse tombe, et celui de teinte spéculaire avec lui ; M4 **normale multipliée dans la couleur** : seul cas à faire rougir l'assertion de chroma (l'uniformité ne le voit pas, la carte étant uniforme) ; M5 **dérive de bandeau** (rugosité 0.7 → 0.65 dans le code seul) : le verrou de bandeau seul, les verrous d'image sont insensibles à une valeur annoncée ; **M6 VERT consigné** : reformuler la prose non chiffrée n'est pas une offense (14-F6)) |
|
||||
| `campaigns/mutate-cube.py` | orientation de l'atlas sur les faces de `cube()` et attribution de l'éclairage au bandeau de `meshes/cube` | `lib/src/mesh/primitives/atlas_tests.rs` (4 cas, 0 écart — M1 **une cellule par face**, ce qu'affirmait le bandeau : les **deux** verrous tombent ; M2 **axes UV échangés** (l'atlas pivoté de 90° de plus que ne le dit la table du vertex stage) : seul le verrou d'orientation tombe, la jambe « même contenu » restant verte puisque le contenu est bien le même — mesure qui dit que les deux jambes ne se remplacent pas ; M3 **normale plate dans `vs_main`** : seul le verrou d'éclairage, c'est le cube « plat » que le bandeau nomme expressément. ⚠ une première version de la campagne refusait de conclure sur ses quatre cas parce que `cargo test` n'accepte qu'**un** filtre positionnel : les deux verrous sont désormais lancés séparément et leurs compteurs sommés (famille 9.4 / 13.3)) |
|
||||
| `campaigns/mutate-shadows.py` | famille « ombres » des bandeaux `shadow`, `shadow_test`, `shadow_instances` | `lib/src/core/shadow_edge_tests.rs` | (5 cas, 0 écart — M1 **un seul tap PCF** : **quatre** verrous tombent, donc les mesures d'aire et de barycentre dépendent de l'existence du filtrage et pas seulement la bande de pénombre ; M2 **flip V de la re-projection retiré** : trois ; M3 **facteur d'ombre forcé à 1,0** : les **cinq** ; M4 **rayon PCF ×4** : prédit VERT de mémoire, **mesuré ROUGE** (deux verrous) — l'angle non couvert par cette mutation est la largeur de la pénombre et le taux de remplissage en vue de dessus, qui tolèrent ×4) |
|
||||
| `campaigns/mutate-emissive.py` | le verrou de la famille `emissive` (identité LDR, contrôle positif HDR, localité de la référence) | `lib/src/core/emissive_tests.rs` (4 cas, 0 écart — M1 `emissive.a` forcé à 0 aux **quatre** sites du shader : ROUGE ; M2 `final_rgb × 0.25` donc plus d'écrêtage LDR : ROUGE ; **M3 retrait de la multiplication par l'albédo : VERT consigné**, le fait découvert n'est porté par aucun verrou. ⚠ l'ancrage multiple comptait **6** pour 4 sites parce que la variante à 4 espaces est une sous-chaîne de celle à 8 — comptage corrigé, famille 13.6) |
|
||||
| `campaigns/mutate-bloom.py` | les deux verrous neufs `bloom` : extraction vide, seuil forcé à 0 (le scénario « threshold far too low » du bandeau), post-process jamais construit | `lib/src/core/bloom_tests.rs` (4 cas, 0 écart — M1 et M3 font tomber **les deux** verrous ; M2 ne fait tomber **que** le verrou des coins : le verrou géométrique du halo reste vert car un seuil à 0 élargit encore le débordement — angle de couverture consigné, avec l'amplitude `intensity` qui n'est portée par aucun des deux) |
|
||||
| `campaigns/mutate-sky-msaa.py` | les trois verrous neufs `skybox` / `msaa` de la séance 15.5 | `lib/src/core/sky_tests.rs`, `lib/src/core/msaa_tests.rs` (4 cas, 0 écart — M1 `set_skybox` ignore son argument : seul le verrou du fond tombe ; M2 forcer 1 échantillon MSAA : **REFUSÉ par validation wgpu même après mutation de quatre sites** — le `sample_count` est une convention répartie sur ≥ 5 endroits, donc les deux verrous de `core::msaa_tests` n'ont pas encore de cas qui prouve qu'ils ne sont pas des décors (écart consigné) ; M3 seul `main_sample_count` muté : **VERT, mutation inerte** — ce champ ne décide pas de l'échantillonnage) |
|
||||
| `campaigns/mutate-doc-contract.py` | — | AGENTS.md |
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
# Suite de tests — mesurée, pas souvenir
|
||||
|
||||
- Généré le : 2026-11-03 17:14 +0100
|
||||
- Généré le : 2026-11-03 17:36 +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 : 924 tests exécutés, 0 skip, 9 doc-tests ignorés.**
|
||||
- Verdict : **VERT HONNÊTE : 925 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` | 813 | 0 | 0 |
|
||||
| `unittests src/lib.rs` | 814 | 0 | 0 |
|
||||
| `tests/doc_consistency.rs` | 24 | 0 | 0 |
|
||||
| `tests/example_banners.rs` | 9 | 0 | 0 |
|
||||
| `tests/roadmap_consistency.rs` | 14 | 0 | 0 |
|
||||
@@ -19,7 +19,7 @@
|
||||
| `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)** | **924** | **0** | **9** |
|
||||
| **Total (somme des lignes ci-dessus)** | **925** | **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
|
||||
|
||||
Reference in new issue
Block a user