# VQ2-SPEC — Spécification `ULPVideoQualitySubsystem` **Fiche :** VQ2-T1 **Rôle :** A-ARCH **Modèle attendu :** claude-sonnet-5-5-high (M2) **Date :** 07 octobre 2026 **Workspace :** `d:\Game\LifePlay-master 5.5` **Statut :** Spécification — prête pour implémentation (VQ2-T2) et branchement onglet (VQ2-T3) **Dépend de :** `VQ0-CART.md` (§5–§7), `VQ1-SPEC.md` (§4–§6, `Config/DefaultScalability.ini`) **Patron :** `Source/LifePlay/LPColorGradeSubsystem.h/.cpp` (CHG-161 / CHG-163) --- ## 1. Objectif Ajouter, dans l'onglet **Video** de `SettingsScrollBox`, un bloc de six lignes C++ (`LPVideo*`) qui pilote les **4 groupes Scalability** définis en VQ1 (ombres, textures, effets, anti-aliasing) par un curseur **Overall** (préréglage Low → Epic) et quatre curseurs individuels. Le bloc est inséré **avant** `LPColorBrightness` (VQ0-CART §7). Hors périmètre VQ2 : calcul VRAM du bouton Recommended (VQ3), tout changement de `DefaultScalability.ini` / `DefaultEngine.ini`, tout changement Blueprint, GI / VSM / méthode d'AA / reflets (verrous L7c, VQ1 §2.2). ## 2. Fichiers | Fichier | Action | Fiche | |---|---|---| | `Source/LifePlay/LPVideoQualitySubsystem.h` | Nouveau | VQ2-T2 | | `Source/LifePlay/LPVideoQualitySubsystem.cpp` | Nouveau | VQ2-T2 | | `Source/LifePlay/LPSettingsSubsystem.cpp` (`CategoryOf`, `ApplyRows`) | Modifié (§9) | VQ2-T3 | | `Source/LifePlay/LifePlay.Build.cs` | **Inchangé** : `Engine` (Scalability), `Slate`, `SlateCore`, `UMG` déjà présents | — | | `Saved/Config/Windows/LPVideoQuality.ini` | Créé à l'exécution | — | Includes attendus (.cpp) : `Scalability.h`, `Components/Button.h`, `Components/ScrollBox.h`, `Components/ScrollBoxSlot.h`, `Components/HorizontalBox.h`, `Components/HorizontalBoxSlot.h`, `Components/SizeBox.h`, `Components/Slider.h`, `Components/TextBlock.h`, `Blueprint/UserWidget.h`, `HAL/IConsoleManager.h`, `Misc/ConfigCacheIni.h`, `Misc/Paths.h`, `LifePlay.h` (`LogLifePlay`). ## 3. Types ### 3.1 `FLPVideoLevels` ```cpp USTRUCT() struct FLPVideoLevels { GENERATED_BODY() UPROPERTY() int32 Overall = 2; // 0..3, ou -1 = Custom UPROPERTY() int32 Shadows = 2; // 0..3 -> sg.ShadowQuality UPROPERTY() int32 Textures = 2; // 0..3 -> sg.TextureQuality UPROPERTY() int32 Effects = 2; // 0..3 -> sg.EffectsQuality UPROPERTY() int32 AA = 2; // 0..3 -> sg.AntiAliasingQuality static FLPVideoLevels FromOverall(int32 Level); // 4 groupes = Level (clampé 0..3), Overall = Level void Normalize(); // clamp 0..3 des 4 groupes ; Overall := (4 groupes égaux ? valeur : -1) bool operator==(const FLPVideoLevels& Other) const; }; ``` Les valeurs par défaut du struct ne servent que de repli ; la première exécution dérive les niveaux de l'état Scalability courant (§6.2). ### 3.2 Invariant Après `Normalize()` : `Overall == -1` **si et seulement si** les 4 groupes ne sont pas tous égaux. Si `Overall >= 0`, les 4 groupes valent `Overall`. L'invariant est ré-imposé à chaque `Load()` : une valeur `Overall` incohérente lue dans l'INI est ignorée et recalculée. ### 3.3 Libellés de niveaux | Niveau | Libellé (readout) | |---|---| | 0 | `Low` | | 1 | `Med` | | 2 | `High` | | 3 | `Epic` | | Overall = -1 | `Custom` | ## 4. Classe ```cpp UCLASS() class LIFEPLAY_API ULPVideoQualitySubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; // Point d'entrée unique de modification (UI, Recommended, VQ3). void SetLevels(FLPVideoLevels NewLevels, bool bFromOverall); FLPVideoLevels GetLevels() const { return Levels; } void RefreshWidgets(); UFUNCTION() void OnOverallChanged(float Value); UFUNCTION() void OnShadowsChanged(float Value); UFUNCTION() void OnTexturesChanged(float Value); UFUNCTION() void OnEffectsChanged(float Value); UFUNCTION() void OnAAChanged(float Value); UFUNCTION() void OnRecommendedClicked(); private: enum ERow { Overall, Shadows, Textures, Effects, AA, NumRows }; struct FRowRefs { TWeakObjectPtr Slider; TWeakObjectPtr Readout; }; bool Tick(float DeltaTime); // 0.5 s, comme ColorGrade void EnsureControls(); void Load(); void Save() const; void ApplyToScalability(); // §6 USlider* AddRow(...); // même signature que ColorGrade::AddRow (+ ERow) UButton* AddButtonRow(...); // ligne Recommended static FString LevelLabel(int32 Level); FTSTicker::FDelegateHandle TickHandle; FRowRefs Rows[NumRows]; TWeakObjectPtr RecommendedButton; TWeakObjectPtr LastAppliedWorld; FLPVideoLevels Levels; bool bRefreshing = false; // garde anti-réentrance }; ``` `Initialize` : `Load()` → `ApplyToScalability()` → enregistre le ticker 0.5 s (même patron que `ULPColorGradeSubsystem`). `Deinitialize` : retire le ticker. ## 5. API ### 5.1 `SetLevels(FLPVideoLevels NewLevels, bool bFromOverall)` Ordre strict : 1. **Normaliser.** - `bFromOverall == true` : `Overall` clampé 0..3 → `NewLevels = FromOverall(Overall)`. Si `Overall < 0`, la valeur est ignorée et on retombe sur le cas `false` (pas de fan-out). - `bFromOverall == false` : clamp des 4 groupes ; `Overall` recalculé par `Normalize()` (égaux → valeur, sinon `-1`). Le champ `Overall` fourni est **ignoré**. 2. `Levels = NewLevels`. 3. **`ApplyToScalability()`** (§6). 4. **`RefreshWidgets()`** (§5.2). 5. **`Save()`** (§7). Aucune étape ne doit dépendre de la présence des widgets : `SetLevels` est appelable menu fermé (Initialize, VQ3, console de debug). ### 5.2 `RefreshWidgets()` Garde `bRefreshing` (set/reset autour du corps). Pour chaque `FRowRefs` valide : | Ligne | Position du curseur | Readout | |---|---|---| | Overall | `Overall >= 0` ? `Overall` : `clamp(round(moyenne des 4 groupes), 0, 3)` | `LevelLabel(Overall)` ou **`Custom`** | | Shadows / Textures / Effects / AA | valeur du groupe | `LevelLabel(valeur)` | `USlider::SetValue` ne diffuse pas `OnValueChanged` (seul l'événement Slate utilisateur le fait) ; la garde `bRefreshing` protège néanmoins contre un changement de comportement. Les handlers `On*Changed` retournent immédiatement si `bRefreshing`. ### 5.3 Handlers | Handler | Action | |---|---| | `OnOverallChanged(V)` | `Overall = clamp(RoundToInt(V),0,3)` → `SetLevels(FromOverall(Overall), true)` | | `OnShadowsChanged(V)` | `L = Levels; L.Shadows = clamp(RoundToInt(V),0,3)` → `SetLevels(L, false)` | | `OnTexturesChanged` / `OnEffectsChanged` / `OnAAChanged` | idem sur leur champ | | `OnRecommendedClicked()` | Stub VQ2 : `SetLevels(FromOverall(2), true)` + `UE_LOG(... "Video : Recommended = stub (High) ; VRAM prevue en VQ3")`. VQ3 remplace uniquement le corps de cette fonction. | ### 5.4 `Load()` / `Save()` — voir §7. ### 5.5 `EnsureControls()` — voir §8. ## 6. Application Scalability ### 6.1 `ApplyToScalability()` ```cpp Scalability::FQualityLevels Q = Scalability::GetQualityLevels(); // préserve les champs non gérés Q.ShadowQuality = Levels.Shadows; Q.TextureQuality = Levels.Textures; Q.EffectsQuality = Levels.Effects; Q.AntiAliasingQuality = Levels.AA; Scalability::SetQualityLevels(Q, /*bForce=*/true); ``` Règles : - **Seuls 4 champs sont écrits** (VQ1 §5). `ResolutionQuality`, `ViewDistanceQuality`, `PostProcessQuality`, `GlobalIlluminationQuality`, `ReflectionQuality`, `ShadingQuality`, `FoliageQuality`, etc. restent ceux de l'état courant. Aucun appel à `SetFromSingleQualityLevel`. - Niveau 4 (Cinematic) non exposé : pas de valeur > 3 écrite. - Pas d'appel à `Scalability::SaveState(GGameUserSettingsIni)` : la source de vérité est `LPVideoQuality.ini` (§7). `GameUserSettings.ini` peut contenir des `sg.*` périmés ; ils sont appliqués par le moteur **avant** `Initialize`, puis écrasés par `ApplyToScalability()`. - **Vérification de relecture** (log `Display`, une ligne) : après `SetQualityLevels`, relire `Scalability::GetQualityLevels()` et comparer aux 4 niveaux ; en cas d'écart, `UE_LOG(Warning)` + repli (R1, §11) : pour chaque groupe divergent, `IConsoleManager::Get().FindConsoleVariable(TEXT("sg."))->Set(Level, ECVF_SetByConsole)` puis un second `SetQualityLevels`. ### 6.2 Premier lancement (pas d'INI) Si `LPVideoQuality.ini` n'existe pas : `Levels` = niveaux 4 groupes lus dans `Scalability::GetQualityLevels()` (clampés 0..3 ; un niveau 4/Cine est ramené à 3), puis `Normalize()` ; **aucun `Save()` à l'initialisation** (le fichier n'est écrit qu'au premier changement de l'utilisateur). Ainsi un joueur qui n'ouvre jamais l'onglet conserve exactement son comportement actuel. ### 6.3 Déclencheurs de ré-application (idempotents) | # | Déclencheur | Raison | |---|---|---| | a | `Initialize` | Appliquer l'INI sur l'état moteur chargé | | b | Premier `Tick` après changement de monde de jeu (`LastAppliedWorld != GetWorld()` et `IsGameWorld()`) | Le Blueprint `MainMenu` / `GameSettings` peut réappliquer ses valeurs `.sav` au chargement d'un niveau | | c | À chaque injection réussie de `EnsureControls()` (nouvelle instance `MainMenu`) | Idem, à l'ouverture du menu | Pas de ré-application continue à 0.5 s : cela écraserait les commandes console `sg.*` de debug. ## 7. Persistance — `Saved/Config/Windows/LPVideoQuality.ini` Chemin : `FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config/Windows/LPVideoQuality.ini"))` (patron `LPColorGrade::IniPath`). Lecture/écriture via `FConfigFile` (`Read` → `SetString` → `Write`). ```ini [LPVideoQuality] Version=1 Overall=2 Shadows=2 Textures=2 Effects=2 AA=2 ``` - `Load()` : lit les 5 clés (`FCString::Atoi`), clamp 0..3 (`Overall` : -1..3), **`Normalize()` recalcule `Overall`**. Une clé manquante conserve la valeur dérivée du §6.2 ; si **aucune** clé n'est présente, comportement §6.2. Log `Display` en français, style ColorGrade : `Video : relu Overall %d, Shadows %d, Textures %d, Effects %d, AA %d depuis %s`. - `Save()` : écrit les 6 clés + log `... enregistres dans %s`. Appelé uniquement par `SetLevels`. - `Version` réservé aux migrations futures ; une version > 1 est lue en mode best-effort (clés connues seulement). ## 8. `EnsureControls()` — injection UI ### 8.1 Recherche du `SettingsScrollBox` Identique à `ULPColorGradeSubsystem::EnsureControls` : `TObjectIterator`, nom `SettingsScrollBox`, hors CDO/archétype, `GetTypedOuter()` valide, non CDO, `GetGameInstance() == GetGameInstance()`. Sortie silencieuse si introuvable. ### 8.2 Garde Sortie si un enfant direct du scroll s'appelle `LPVideoOverall` (déjà injecté pour cette instance de menu). Une nouvelle instance `MainMenu` n'a pas ce widget → réinjection ; les `TWeakObjectPtr` précédents sont remplacés. ### 8.3 Lignes injectées (ordre d'affichage) | # | Nom widget | Libellé | Type | Min–Max | Pas | Handler | Info-bulle | |---|---|---|---|---|---|---|---| | 1 | `LPVideoOverall` | `Overall Quality` | Slider + readout | 0–3 | 1 | `OnOverallChanged` | `Preset applied to shadows, textures, effects and anti-aliasing. Custom when they differ.` | | 2 | `LPVideoShadows` | `Shadows` | Slider + readout | 0–3 | 1 | `OnShadowsChanged` | `Shadow resolution and distance.` | | 3 | `LPVideoTextures` | `Textures` | Slider + readout | 0–3 | 1 | `OnTexturesChanged` | `Texture filtering and streaming budget.` | | 4 | `LPVideoEffects` | `Effects` | Slider + readout | 0–3 | 1 | `OnEffectsChanged` | `Translucency, refraction, subsurface scattering and particles.` | | 5 | `LPVideoAA` | `Anti-Aliasing` | Slider + readout | 0–3 | 1 | `OnAAChanged` | `Anti-aliasing quality. The method itself is fixed.` | | 6 | `LPVideoRecommended` | `Recommended` | `UButton` + `UTextBlock` | — | — | `OnRecommendedClicked` | `Apply the recommended preset for this PC.` | Les libellés contiennent les mots-clés `shadow` (ligne 2) / `quality` (ligne 1) ; sans importance, `CategoryOf` classe sur le préfixe `LPVideo` avant le texte (§9). ### 8.4 Structure d'une ligne Copie du patron `ColorGrade::AddRow` : `UHorizontalBox` nommé (`NewObject(Scroll, Name)`) contenant `USizeBox` 220 px + `UTextBlock` (légende), `USlider` (Fill), `UTextBlock` (readout). Mêmes paddings (10/0/20/0, 0/0/20/0, 0/0/10/0 ; bas 10 px côté scroll). Style : police/couleur/`FSliderStyle` copiés d'une ligne existante via `FindLook` (repli `MenuFont()` : `Regular 20`, contour noir 2 px, opacité 0.95). Particularités sliders : `SetMinValue(0)`, `SetMaxValue(3)`, `SetStepSize(1)`, `SetLocked(false)`. Branchement `OnValueChanged` via `FScriptDelegate::BindUFunction` (patron ColorGrade). La ligne Recommended : `UHorizontalBox` → `UButton` (`OnClicked` via `FScriptDelegate`) contenant un `UTextBlock` `Recommended`, même police/couleur, alignement gauche, padding 10 px. Les helpers `FindLook` / `MenuFont` sont **dupliqués** dans un `namespace LPVideoQuality` local au `.cpp` (V1 : aucune modification de `LPColorGradeSubsystem`). Refactor commun éventuel hors périmètre. ### 8.5 Position d'insertion : avant le premier `LPColor*` Algorithme : 1. `InsertAt = INDEX_NONE` ; parcourir les enfants directs du scroll, `InsertAt` = index du **premier** enfant dont `GetName().StartsWith(TEXT("LPColor"))`. 2. Créer les 6 lignes (jamais ajoutées directement). 3. Si `InsertAt != INDEX_NONE` : pour `i = 0..5`, `Scroll->InsertChildAt(InsertAt + i, Row[i])` (`UPanelWidget::InsertChildAt`) ; configurer ensuite le `UScrollBoxSlot` (padding bas 10) via `Cast(Row->Slot)`. 4. Sinon (`LPColor*` pas encore injecté) : `Scroll->AddChild(Row[i])` dans l'ordre → les lignes seront suivies des `LPColor*` quand ColorGrade s'injectera (il ajoute en fin). 5. **Repli** si `InsertChildAt` retourne `nullptr` : `AddChild` des 6 lignes, puis réordonner : collecter les enfants `LPColor*`, `RemoveChild` de chacun, et les `AddChild` de nouveau dans leur ordre d'origine (même objets ; `ColorGrade` garde ses `TWeakObjectPtr` valides). Le garde de `ColorGrade::EnsureControls` (`LPColorBrightness` présent) n'est pas affecté par l'ordre. Les deux sous-systèmes sont indépendants (aucune référence croisée). ### 8.6 Masquage des widgets BP redondants Widgets dirigés vers **Collapsed** : `ShadowQuality`, `PostProcessQuality`, `PostProcessAAQuality` (noms UMG, VQ0-CART §5). Conservés visibles : `ScreenPercentage`, `FullscreenButton`, `ShowFPS`, cases et SpinBoxes (VQ0-CART §8). Deux niveaux de mise en œuvre : 1. **`EnsureControls()` (VQ2-T2)** : après injection, `SetVisibility(Collapsed)` sur ces trois enfants directs (recherche par `GetFName()`). Sert d'effet immédiat. 2. **`LPSettingsSubsystem` (VQ2-T3)** : **obligatoire** pour la durabilité — `ApplyRows()` remet `Visible` toute ligne dont `Category == ActiveTab` à chaque changement d'onglet ; sans la modification du §9, le masquage du point 1 est annulé au premier clic d'onglet. Conséquences V1 (pragmatique, documentées VQ0-CART §6.1/§6.2) : - Le curseur `PP Quality (FPS intensive)` disparaît ; `sg.PostProcessQuality` n'est plus piloté par l'utilisateur et reste à sa dernière valeur / valeur moteur (VQ1 §2.2 : groupe hors périmètre). Voir question Q1. - Le curseur `AA Quality` BP (écriture directe de `r.PostProcessAAQuality`) est remplacé par `LPVideoAA` (`sg.AntiAliasingQuality`, VQ1 §4.4 : 0/2/4/4). - Aucune synchronisation BP ↔ LPVideo : les widgets masqués ne sont plus lus ni écrits par le joueur. ## 9. Intégration `LPSettingsSubsystem` (fiche VQ2-T3, rappel de contrat) Dans `LPSettingsUi::CategoryOf`, **avant** `RowText` (même endroit que la règle `LPColor`) : ```cpp if (Widget && (Widget->GetName().StartsWith(TEXT("LPColor")) || Widget->GetName().StartsWith(TEXT("LPVideo")))) { return TabVideo; } ``` Et, pour les trois widgets BP masqués, une catégorie « toujours masquée » : ```cpp constexpr int32 TabHidden = -2; // dans CategoryOf, après BackButton : if (Widget && (Widget->GetFName() == TEXT("ShadowQuality") || Widget->GetFName() == TEXT("PostProcessQuality") || Widget->GetFName() == TEXT("PostProcessAAQuality"))) { return TabHidden; } ``` `ApplyRows()` : `Category == -1` reste « toujours visible » (`BackButton`) ; `Category == TabHidden` → `Collapsed` quel que soit l'onglet. **Attention :** l'actuel `bShow = Category < 0 || ...` rendrait `-2` visible ; la condition doit devenir `Category == -1 || (...)`. Aucun autre appelant de `CategoryOf` ne doit traiter `< 0` comme « visible » (à vérifier par Grep en T3). Ordre dans le `ScrollBox` : `ApplyRows()` ne change que la visibilité, pas l'ordre ; l'ordre « BP, LPVideo*, LPColor* » est donc stable. Après injection tardive de `LPVideo*`, la visibilité initiale des nouvelles lignes est celle de `SWidget` par défaut (visible) tant que `ApplyRows()` n'a pas été rappelé : **T3 doit déclencher `ApplyRows()` après injection** (ou `LPVideo` ne peut pas l'appeler : voir Q2). ## 10. Journalisation Style ColorGrade, `LogLifePlay`, `Display`, français sans accents dans les littéraux (cohérence existante) : - `Video : relu ... depuis ` (Load) - `Video : Overall %d, Shadows %d, Textures %d, Effects %d, AA %d enregistres dans ` (Save) - `Video : qualite appliquee (sg.ShadowQuality %d, sg.TextureQuality %d, sg.EffectsQuality %d, sg.AntiAliasingQuality %d)` (Apply) - `Video : controles ajoutes avant les reglages de couleur` (EnsureControls, une fois par injection) - `Warning` : écart de relecture Scalability (§6.1), `InsertChildAt` en repli (§8.5). ## 11. Risques | ID | Risque | Mitigation | |---|---|---| | R1 | Le Blueprint (`SettingSlider`/`GameSettings`) a pu écrire `sg.*` ou `r.PostProcessAAQuality` avec une priorité **Console/Code**, supérieure à `SetByScalability` : `SetQualityLevels` serait alors ignoré silencieusement. | Relecture + repli `ECVF_SetByConsole` (§6.1) ; ré-application aux déclencheurs b/c (§6.3). À confirmer en VQ2-VAL (valeurs `r.Shadow.MaxResolution`, `r.Streaming.PoolSize`, `r.PostProcessAAQuality` conformes à VQ1 §4). | | R2 | `ApplyRows()` annule le masquage des widgets BP. | §9 obligatoire (T3). | | R3 | `InsertChildAt` indisponible/inopérant sur `UScrollBox`. | Repli §8.5.5. | | R4 | Perte de `PP Quality` : joueur ne peut plus baisser le coût post-process. | Décision V1 assumée ; voir Q1. | | R5 | `GameUserSettings.ini` contient des `sg.*` différents de `LPVideoQuality.ini`. | `LPVideoQuality.ini` est prioritaire dès qu'il existe (§6.1) ; sans INI, état moteur conservé (§6.2). | | R6 | Écart d'ordre d'initialisation ColorGrade/Video. | Insertion avant premier `LPColor*` si présent, sinon append ; les deux ordres donnent « Video avant Color » (§8.5). | | R7 | Interdits VQ1 (GI, VSM, méthode AA, reflets). | Seuls 4 champs de `FQualityLevels` écrits ; aucune cvar écrite directement hors repli R1 (`sg.*` uniquement). | ## 12. Questions ouvertes | ID | Question | Proposition par défaut | |---|---|---| | Q1 | Faut-il lier `sg.PostProcessQuality` à `Effects` (ou `Overall`) pour ne pas figer un groupe devenu sans contrôle ? | **Non** en V1 (VQ1 §5 : 4 groupes seulement). À trancher avec A-VAL après mesure de coût. | | Q2 | Comment T3 est-il notifié d'une injection pour relancer `ApplyRows()` ? | `ULPSettingsSubsystem` a son propre ticker : `ApplyRows()` est déjà réexécuté après détection de nouvelles lignes (à vérifier en T3) ; sinon un delegate `FSimpleMulticastDelegate OnControlsInjected` exposé par `ULPVideoQualitySubsystem`. | | Q3 | Niveau recommandé du stub. | `High` (2) ; VQ3 le remplace par un calcul VRAM. | ## 13. Critères d'acceptation (VQ2-T2 / VQ2-T3 / VQ2-VAL) - [ ] `LPVideoQualitySubsystem.h/.cpp` compilent sans toucher `LifePlay.Build.cs` ni `LPColorGradeSubsystem.*`. - [ ] Ordre dans l'onglet Video : lignes BP visibles → `LPVideoOverall`, `Shadows`, `Textures`, `Effects`, `AA`, `Recommended` → `LPColorBrightness` … `LPColorTint` (quel que soit l'ordre d'initialisation ColorGrade/Video). - [ ] `ShadowQuality`, `PostProcessQuality`, `PostProcessAAQuality` restent masqués après changement d'onglet aller-retour. - [ ] Overall 0→3 : les 4 curseurs suivent, readouts `Low/Med/High/Epic` ; `sg.ShadowQuality`, `sg.TextureQuality`, `sg.EffectsQuality`, `sg.AntiAliasingQuality` valent le niveau choisi (console `sg.X` en lecture) ; `sg.GlobalIlluminationQuality`, `sg.ReflectionQuality`, `sg.PostProcessQuality`, `sg.ViewDistanceQuality` inchangés. - [ ] Modifier un curseur individuel → Overall affiche `Custom` ; ramener les 4 au même niveau → Overall réaffiche ce niveau. - [ ] Redémarrage du jeu : niveaux restaurés depuis `Saved/Config/Windows/LPVideoQuality.ini` ; sans INI, aucun changement par rapport à l'état actuel. - [ ] Fermer/rouvrir le menu : lignes réinjectées avec les bonnes valeurs. - [ ] `Recommended` : applique High, ne plante pas, log stub. - [ ] Niveau 3 : GI dynamique, VSM et méthode d'AA inchangés (Lumen/VSM off, TAA) — rappel VQ1 §8. - [ ] Aucune occurrence de `Virtual`, `Lumen`, `AntiAliasingMethod`, `ReflectionMethod` dans le code ajouté.