LifePlay-Revival/Docs/Migration/etat/VQ/VQ2/VQ2-SPEC.md
edujiro5972 0f1a61c9fe VQ : qualite video (sous-systeme LPVideoQuality, onglet Reglages, DefaultScalability), lots VQ0 a VQ5 et correctifs (CHG-237 a 242)
LifePlay.uproject : SupportedTargetPlatforms Win64 sur LPAnimCore, origine non revendiquee, range ici.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 01:40:47 +02:00

21 KiB
Raw Blame History

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

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

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<USlider> Slider; TWeakObjectPtr<UTextBlock> 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<UButton> RecommendedButton;
    TWeakObjectPtr<UWorld> 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()

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.<Groupe>"))->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).

[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<UScrollBox>, nom SettingsScrollBox, hors CDO/archétype, GetTypedOuter<UUserWidget>() 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<UHorizontalBox>(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<UScrollBoxSlot>(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) :

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 » :

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 <ini> (Load)
  • Video : Overall %d, Shadows %d, Textures %d, Effects %d, AA %d enregistres dans <ini> (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é.