LifePlay.uproject : SupportedTargetPlatforms Win64 sur LPAnimCore, origine non revendiquee, range ici. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 KiB
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 :
- Normaliser.
bFromOverall == true:Overallclampé 0..3 →NewLevels = FromOverall(Overall). SiOverall < 0, la valeur est ignorée et on retombe sur le casfalse(pas de fan-out).bFromOverall == false: clamp des 4 groupes ;Overallrecalculé parNormalize()(égaux → valeur, sinon-1). Le champOverallfourni est ignoré.
Levels = NewLevels.ApplyToScalability()(§6).RefreshWidgets()(§5.2).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é estLPVideoQuality.ini(§7).GameUserSettings.inipeut contenir dessg.*périmés ; ils sont appliqués par le moteur avantInitialize, puis écrasés parApplyToScalability(). - Vérification de relecture (log
Display, une ligne) : aprèsSetQualityLevels, relireScalability::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 secondSetQualityLevels.
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()recalculeOverall. Une clé manquante conserve la valeur dérivée du §6.2 ; si aucune clé n'est présente, comportement §6.2. LogDisplayen 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 parSetLevels.Versionré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 :
InsertAt = INDEX_NONE; parcourir les enfants directs du scroll,InsertAt= index du premier enfant dontGetName().StartsWith(TEXT("LPColor")).- Créer les 6 lignes (jamais ajoutées directement).
- Si
InsertAt != INDEX_NONE: pouri = 0..5,Scroll->InsertChildAt(InsertAt + i, Row[i])(UPanelWidget::InsertChildAt) ; configurer ensuite leUScrollBoxSlot(padding bas 10) viaCast<UScrollBoxSlot>(Row->Slot). - Sinon (
LPColor*pas encore injecté) :Scroll->AddChild(Row[i])dans l'ordre → les lignes seront suivies desLPColor*quand ColorGrade s'injectera (il ajoute en fin). - Repli si
InsertChildAtretournenullptr:AddChilddes 6 lignes, puis réordonner : collecter les enfantsLPColor*,RemoveChildde chacun, et lesAddChildde nouveau dans leur ordre d'origine (même objets ;ColorGradegarde sesTWeakObjectPtrvalides).
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 :
EnsureControls()(VQ2-T2) : après injection,SetVisibility(Collapsed)sur ces trois enfants directs (recherche parGetFName()). Sert d'effet immédiat.LPSettingsSubsystem(VQ2-T3) : obligatoire pour la durabilité —ApplyRows()remetVisibletoute ligne dontCategory == 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.PostProcessQualityn'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 QualityBP (écriture directe der.PostProcessAAQuality) est remplacé parLPVideoAA(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),InsertChildAten 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/.cppcompilent sans toucherLifePlay.Build.csniLPColorGradeSubsystem.*.- 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,PostProcessAAQualityrestent 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.AntiAliasingQualityvalent le niveau choisi (consolesg.Xen lecture) ;sg.GlobalIlluminationQuality,sg.ReflectionQuality,sg.PostProcessQuality,sg.ViewDistanceQualityinchangé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,ReflectionMethoddans le code ajouté.