Effect Benchmark — A/B GPU Cost Measurement¶
Outil de mesure automatisé du coût GPU individuel de chaque effet postprocess au sein du uber-shader ("Final Composite"). Les effets multi-pass (Bloom, DoF, Auto Exposure, Motion Blur) disposent déjà de leur propre stage GPU Profiler et ne sont pas concernés.
Vue d'ensemble¶
Utilisation¶
| Touche | Action |
|---|---|
| 8 | Démarre le sweep (ou affiche "Already running" si en cours) |
Le sweep dure environ 22 secondes à 60 fps (8 phases × (30 + 120) frames ÷ 60).
- Lancer l'application
- Stabiliser la scène (ne pas bouger la caméra pendant le bench)
- Appuyer sur 8
- Attendre la notification "FX Benchmark: Done (see log)"
- Lire les résultats dans la sortie log
⚠️ Important : Ne pas interagir avec la scène ni toggle d'effets pendant le benchmark. Le système sauvegarde/restaure
active_effectsmais tout changement externe invaliderait les mesures.
Lire les résultats¶
Exemple de sortie réelle (Intel Iris Xe, 1920×1080, scène IBL + 20 sphères) :
╔══════════════════════════════════════════════════════╗
║ POSTPROCESS EFFECT BENCHMARK RESULTS ║
╠══════════════════════════════════════════════════════╣
║ Baseline (all ON): 1.1308 ms (±0.0222 ms) ║
╠════════════════════╦═══════════╦═══════════╦════════╣
║ Effect ║ Cost(ms) ║ StdDev ║ Status ║
╠════════════════════╬═══════════╬═══════════╬════════╣
║ FXAA ║ +0.0110 ║ ±0.0042 ║ ON ║
║ Chromatic Aberration ║ — ║ — ║ OFF ║
║ Vignette ║ +0.0109 ║ ±0.0045 ║ ON ║
║ Grain ║ -0.0014 ║ ±0.0242 ║ ON ║
║ Color Grading ║ -0.0289 ║ ±0.0342 ║ ON ║
║ Banding ║ — ║ — ║ OFF ║
║ Exposure ║ — ║ — ║ OFF ║
╠════════════════════╬═══════════╬═══════════╬════════╣
║ Sum of costs ║ -0.0083 ║ ║ ║
╚════════════════════╩═══════════╩═══════════╩════════╝
Colonnes¶
| Colonne | Signification |
|---|---|
| Effect | Nom de l'effet postprocess |
| Cost(ms) | baseline_mean - mean_with_effect_OFF. Positif = l'effet coûte du temps GPU |
| StdDev | Écart-type sur 120 échantillons. Indique la stabilité de la mesure |
| Status | ON = testé (était actif), OFF = sauté (était déjà désactivé) |
Interpréter les valeurs¶
Coût positif (+0.0110 ms)¶
L'effet ajoute du temps GPU. C'est le cas attendu. Plus la valeur est grande, plus l'effet est coûteux.
Coût négatif (-0.0014 ms, -0.0289 ms)¶
Un coût négatif signifie que désactiver l'effet ralentit le composite. C'est contre-intuitif mais normal sur un iGPU. Causes possibles :
-
Bruit de mesure — Si
|cost| < stddev, la mesure est dans le bruit. Exemple : Grain coûte -0.0014 ms ± 0.0242 → le vrai coût est indistinguable de zéro. -
Divergence de branches — Le uber-shader utilise des
if (effect_enabled). Sur les GPUs à exécution SIMD (wavefronts/warps), le coût d'une branche dépend de la cohérence au sein du warp. Désactiver un seul effet peut modifier le pattern de divergence et paradoxalement ralentir les warps voisins. -
Pression registres/cache — Le compilateur GLSL peut réorganiser les registres quand le code mort est éliminé. Une configuration différente peut avoir une pression mémoire légèrement différente.
-
Ordonnancement ALU/TEX — Sur iGPU Intel, les ALUs partagent la bande passante mémoire avec le CPU. Un calcul en moins peut laisser les TEX units en attente sans recouvrement ALU.
Somme ≠ baseline¶
La ligne "Sum of costs" sera rarement égale à baseline_mean. C'est attendu :
les effets ne sont pas additifs car ils partagent les mêmes unités
d'exécution (ALU, caches texture, bande passante). L'interaction entre effets
crée des effets de masquage (latency hiding).
Règles pratiques¶
| Observation | Conclusion |
|---|---|
cost > 0 et cost > 2 × stddev |
L'effet a un coût significatif et mesurable |
cost > 0 mais cost < stddev |
Coût probable mais non significatif statistiquement |
cost ≈ 0 (positif ou négatif) et stddev élevé |
Bruit — relancer le bench en stabilisant la scène |
cost < 0 et |cost| > stddev |
Effet de divergence/cache — pas alarmant, inhérent au uber-shader |
| Tous les coûts très faibles (<0.05 ms) | Le postprocess n'est pas le goulot — chercher ailleurs (geometry, lighting) |
Effets benchmarkés¶
Seuls les effets fragment-shader exécutés dans le draw call "Final Composite" sont mesurés par A/B toggle :
| Effet | Bit | Macro |
|---|---|---|
| FXAA | 1 << 12 |
POSTFX_FXAA |
| Chromatic Aberration | 1 << 3 |
POSTFX_CHROM_ABBR |
| Vignette | 1 << 0 |
POSTFX_VIGNETTE |
| Grain | 1 << 1 |
POSTFX_GRAIN |
| Color Grading | 1 << 5 |
POSTFX_COLOR_GRADING |
| Banding | 1 << 14 |
POSTFX_BANDING |
| Exposure | 1 << 2 |
POSTFX_EXPOSURE |
Les effets multi-pass (Bloom, DoF, Auto Exposure, Motion Blur) ont déjà
leur propre stage dans le GPU Profiler (F1 pour afficher l'overlay) et ne
nécessitent pas d'A/B testing.
Architecture interne¶
Pourquoi l'A/B ?¶
Les GPU timer queries (GL_TIMESTAMP) mesurent le temps entre deux draw calls.
Or, tous les effets fragment-shader s'exécutent dans un unique fullscreen
quad draw call ("Final Composite"). Il est impossible de placer des timers
à l'intérieur d'un draw call.
La méthode A/B contourne cette limitation :
Machine à états¶
Flux par frame¶
effect_benchmark_update() est appelée après gpu_profiler_begin_frame()
pour lire les résultats du frame N-1 (double-buffered timer queries) :
-
Warmup (30 frames) — Les résultats sont ignorés. Laisse le driver/GPU stabiliser les caches et le pipeline après le changement d'état.
-
Accumulation (120 frames) — Accumule
sum_msetsum_sq_mspour calculer la moyenne et l'écart-type :
- Transition — Calcule les stats, stocke le résultat, désactive l'effet suivant, remet le compteur à zéro.
Fichiers¶
| Fichier | Rôle |
|---|---|
include/effect_benchmark.h |
Types (EffectBenchmark, BenchPhase, EffectBenchResult), constantes, API |
src/effect_benchmark.c |
Machine à états, accumulation, table d'effets, affichage résultats |
include/app.h |
Champ EffectBenchmark effect_bench dans App |
src/app.c |
effect_benchmark_init() au démarrage, effect_benchmark_update() par frame |
src/app_input.c |
Binding touche 8 → effect_benchmark_start() |
API¶
// Initialisation (une fois au démarrage)
void effect_benchmark_init(EffectBenchmark* bench,
PostProcess* postprocess,
GPUProfiler* profiler);
// Démarrer un sweep (retourne false si déjà en cours)
bool effect_benchmark_start(EffectBenchmark* bench);
// Appeler chaque frame après gpu_profiler_begin_frame()
// Retourne true quand le sweep vient de se terminer
bool effect_benchmark_update(EffectBenchmark* bench);
// Vérifier si un bench est en cours
bool effect_benchmark_is_running(const EffectBenchmark* bench);
// Afficher les résultats (appelé automatiquement à la fin)
void effect_benchmark_log_results(const EffectBenchmark* bench);
Paramètres de mesure¶
| Constante | Valeur | Rôle |
|---|---|---|
BENCH_WARMUP_FRAMES |
30 | Frames ignorées après chaque changement d'état (stabilisation pipeline) |
BENCH_MEASURE_FRAMES |
120 | Frames échantillonnées par phase (≈2s à 60fps) |
BENCH_MAX_EFFECTS |
16 | Capacité maximale de la table d'effets |
Limites¶
-
Précision iGPU — Sur GPU intégré (Intel Iris Xe), la résolution des timer queries est de l'ordre de 80 ns. Les effets très légers (< 0.01 ms) sont souvent dans le bruit.
-
Non-additivité — Le coût d'un effet dépend des autres effets actifs (latency hiding, pression registres). La somme des coûts individuels ne sera pas égale au coût total.
-
Stabilité scène requise — Bouger la caméra pendant le bench modifie la charge fragment (overdraw, fill rate) et fausse les mesures.
-
Divergence GPU — Les branches
ifdu uber-shader ont un coût qui dépend de la cohérence spatiale des pixels. L'A/B ne capture pas le coût de divergence additionnel quand plusieurs effets sont actifs simultanément.
Changelog¶
| Date | Changement |
|---|---|
| 2026-02-07 | Création du module effect_benchmark (header, implémentation, intégration) |
| 2026-02-08 | Ajout de la phase BENCH_STABILIZE et du Timeout (2s) pour la fiabilité |