// Voir LPVideoQualitySubsystem.h. #include "LPVideoQualitySubsystem.h" #include "Blueprint/UserWidget.h" #include "Components/Button.h" #include "Components/ButtonSlot.h" #include "Components/HorizontalBox.h" #include "Components/HorizontalBoxSlot.h" #include "Components/PanelWidget.h" #include "Components/ScrollBox.h" #include "Components/ScrollBoxSlot.h" #include "Components/SizeBox.h" #include "Components/Slider.h" #include "Components/TextBlock.h" #include "Engine/GameInstance.h" #include "Engine/World.h" #include "HAL/IConsoleManager.h" #include "LifePlay.h" #include "Misc/ConfigCacheIni.h" #include "Misc/Paths.h" #include "DynamicRHI.h" #include "RHI.h" #include "RHIStats.h" #include "Scalability.h" #include "Styling/CoreStyle.h" #include "Styling/SlateTypes.h" namespace LPVideoQuality { constexpr int32 MaxLevel = 3; // Niveau retenu quand la VRAM est inconnue (0 ou -1) : High. constexpr int32 RecommendedUnknownLevel = 2; // VRAM dediee en octets, 0 si inconnue (RHI absent, -nullrhi, valeur non renseignee). int64 QueryDedicatedVideoMemoryBytes() { if (!GDynamicRHI) { return 0; } FTextureMemoryStats Stats; RHIGetTextureMemoryStats(Stats); return FMath::Max(Stats.DedicatedVideoMemory, 0); } // <4 Go Low, <6 Med, <10 High, sinon Epic ; 0 Go (inconnu) = High. int32 LevelFromVramGB(double VramGB) { if (VramGB <= 0.0) { return RecommendedUnknownLevel; } if (VramGB < 4.0) { return 0; } if (VramGB < 6.0) { return 1; } if (VramGB < 10.0) { return 2; } return 3; } FString IniPath() { return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config/Windows/LPVideoQuality.ini")); } FSlateFontInfo MenuFont() { FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Regular", 20); Font.OutlineSettings.OutlineSize = 2; Font.OutlineSettings.bApplyOutlineToDropShadows = true; Font.OutlineSettings.OutlineColor = FLinearColor(0.f, 0.f, 0.f, 0.8f); return Font; } void FindLook(UWidget* Widget, UTextBlock*& OutText, USlider*& OutSlider) { if (!Widget || (OutText && OutSlider)) { return; } if (!OutText) { OutText = Cast(Widget); } if (!OutSlider) { OutSlider = Cast(Widget); } if (UUserWidget* User = Cast(Widget)) { FindLook(User->GetRootWidget(), OutText, OutSlider); } if (UPanelWidget* Panel = Cast(Widget)) { for (int32 Index = 0; Index < Panel->GetChildrenCount() && !(OutText && OutSlider); ++Index) { FindLook(Panel->GetChildAt(Index), OutText, OutSlider); } } } int32 ClampLevel(int32 Level) { return FMath::Clamp(Level, 0, MaxLevel); } } FLPVideoLevels FLPVideoLevels::FromOverall(int32 Level) { const int32 Clamped = LPVideoQuality::ClampLevel(Level); FLPVideoLevels Result; Result.Overall = Clamped; Result.Shadows = Clamped; Result.Textures = Clamped; Result.Effects = Clamped; Result.AA = Clamped; return Result; } void FLPVideoLevels::Normalize() { Shadows = LPVideoQuality::ClampLevel(Shadows); Textures = LPVideoQuality::ClampLevel(Textures); Effects = LPVideoQuality::ClampLevel(Effects); AA = LPVideoQuality::ClampLevel(AA); Overall = (Shadows == Textures && Textures == Effects && Effects == AA) ? Shadows : -1; } bool FLPVideoLevels::operator==(const FLPVideoLevels& Other) const { return Overall == Other.Overall && Shadows == Other.Shadows && Textures == Other.Textures && Effects == Other.Effects && AA == Other.AA; } FString ULPVideoQualitySubsystem::LevelLabel(int32 Level) { switch (Level) { case 0: return TEXT("Low"); case 1: return TEXT("Med"); case 2: return TEXT("High"); case 3: return TEXT("Epic"); default: return TEXT("Custom"); } } void ULPVideoQualitySubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); Load(); ApplyToScalability(); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPVideoQualitySubsystem::Tick), 0.5f); } void ULPVideoQualitySubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); Super::Deinitialize(); } bool ULPVideoQualitySubsystem::Tick(float DeltaTime) { (void)DeltaTime; UWorld* World = GetWorld(); if (World && World->IsGameWorld() && LastAppliedWorld.Get() != World) { // Le Blueprint peut reappliquer ses valeurs au chargement d'un niveau. LastAppliedWorld = World; ApplyToScalability(); } EnsureControls(); return true; } void ULPVideoQualitySubsystem::ReadFromScalability() { const Scalability::FQualityLevels Current = Scalability::GetQualityLevels(); Levels.Shadows = Current.ShadowQuality; Levels.Textures = Current.TextureQuality; Levels.Effects = Current.EffectsQuality; Levels.AA = Current.AntiAliasingQuality; Levels.Normalize(); } void ULPVideoQualitySubsystem::Load() { // Valeurs de repli : etat Scalability courant (premier lancement). ReadFromScalability(); const FString Ini = LPVideoQuality::IniPath(); FConfigFile File; File.Read(Ini); bool bAnyKey = false; auto Read = [&File, &bAnyKey](const TCHAR* Key, int32& Out) { FString Text; if (File.GetString(TEXT("LPVideoQuality"), Key, Text)) { Out = FCString::Atoi(*Text); bAnyKey = true; } }; // Overall est relu pour la trace mais ignore : Normalize() le recalcule. int32 StoredOverall = Levels.Overall; Read(TEXT("Overall"), StoredOverall); Read(TEXT("Shadows"), Levels.Shadows); Read(TEXT("Textures"), Levels.Textures); Read(TEXT("Effects"), Levels.Effects); Read(TEXT("AA"), Levels.AA); Levels.Normalize(); if (bAnyKey) { bConfigured = true; UE_LOG(LogLifePlay, Display, TEXT("Video : relu Overall %d, Shadows %d, Textures %d, Effects %d, AA %d depuis %s"), Levels.Overall, Levels.Shadows, Levels.Textures, Levels.Effects, Levels.AA, *Ini); } else { bConfigured = false; UE_LOG(LogLifePlay, Display, TEXT("Video : pas de %s, niveaux lus depuis Scalability (Overall %d, Shadows %d, Textures %d, Effects %d, AA %d)"), *Ini, Levels.Overall, Levels.Shadows, Levels.Textures, Levels.Effects, Levels.AA); } } void ULPVideoQualitySubsystem::Save() const { const FString Ini = LPVideoQuality::IniPath(); FConfigFile File; File.Read(Ini); auto Write = [&File](const TCHAR* Key, int32 Value) { File.SetString(TEXT("LPVideoQuality"), Key, *FString::FromInt(Value)); }; Write(TEXT("Version"), 1); Write(TEXT("Overall"), Levels.Overall); Write(TEXT("Shadows"), Levels.Shadows); Write(TEXT("Textures"), Levels.Textures); Write(TEXT("Effects"), Levels.Effects); Write(TEXT("AA"), Levels.AA); File.Write(Ini); UE_LOG(LogLifePlay, Display, TEXT("Video : Overall %d, Shadows %d, Textures %d, Effects %d, AA %d enregistres dans %s"), Levels.Overall, Levels.Shadows, Levels.Textures, Levels.Effects, Levels.AA, *Ini); } void ULPVideoQualitySubsystem::ApplyToScalability() { if (!bConfigured) { return; } // Ecrire uniquement les 4 sg.* en SetByConsole : SetQualityLevels appliquerait aussi // r.ScreenPercentage / r.Streaming.PoolSize / r.SSS.Scale (souvent deja poses par le BP // en SetByConsole) et spamait des warnings "SetByScalability ignored". auto SetSg = [](const TCHAR* Name, int32 Value) { if (IConsoleVariable* Variable = IConsoleManager::Get().FindConsoleVariable(Name)) { Variable->Set(Value, ECVF_SetByConsole); } }; SetSg(TEXT("sg.ShadowQuality"), Levels.Shadows); SetSg(TEXT("sg.TextureQuality"), Levels.Textures); SetSg(TEXT("sg.EffectsQuality"), Levels.Effects); SetSg(TEXT("sg.AntiAliasingQuality"), Levels.AA); UE_LOG(LogLifePlay, Display, TEXT("Video : qualite appliquee (sg.ShadowQuality %d, sg.TextureQuality %d, sg.EffectsQuality %d, sg.AntiAliasingQuality %d)"), Levels.Shadows, Levels.Textures, Levels.Effects, Levels.AA); } void ULPVideoQualitySubsystem::SetLevels(FLPVideoLevels NewLevels, bool bFromOverall) { if (bFromOverall && NewLevels.Overall >= 0) { NewLevels = FLPVideoLevels::FromOverall(NewLevels.Overall); } else { NewLevels.Normalize(); } Levels = NewLevels; bConfigured = true; ApplyToScalability(); RefreshWidgets(); Save(); } void ULPVideoQualitySubsystem::RefreshWidgets() { if (bRefreshing) { return; } bRefreshing = true; const int32 Values[NumRows] = { Levels.Overall >= 0 ? Levels.Overall : LPVideoQuality::ClampLevel(FMath::RoundToInt((Levels.Shadows + Levels.Textures + Levels.Effects + Levels.AA) * 0.25f)), Levels.Shadows, Levels.Textures, Levels.Effects, Levels.AA }; for (int32 Index = 0; Index < NumRows; ++Index) { if (USlider* Slider = Rows[Index].Slider.Get()) { Slider->SetValue(static_cast(Values[Index])); } if (UTextBlock* Readout = Rows[Index].Readout.Get()) { Readout->SetText(FText::FromString(LevelLabel(Index == RowOverall ? Levels.Overall : Values[Index]))); } } bRefreshing = false; } void ULPVideoQualitySubsystem::OnOverallChanged(float Value) { if (bRefreshing) { return; } SetLevels(FLPVideoLevels::FromOverall(FMath::RoundToInt(Value)), true); } void ULPVideoQualitySubsystem::OnShadowsChanged(float Value) { if (bRefreshing) { return; } FLPVideoLevels L = Levels; L.Shadows = LPVideoQuality::ClampLevel(FMath::RoundToInt(Value)); SetLevels(L, false); } void ULPVideoQualitySubsystem::OnTexturesChanged(float Value) { if (bRefreshing) { return; } FLPVideoLevels L = Levels; L.Textures = LPVideoQuality::ClampLevel(FMath::RoundToInt(Value)); SetLevels(L, false); } void ULPVideoQualitySubsystem::OnEffectsChanged(float Value) { if (bRefreshing) { return; } FLPVideoLevels L = Levels; L.Effects = LPVideoQuality::ClampLevel(FMath::RoundToInt(Value)); SetLevels(L, false); } void ULPVideoQualitySubsystem::OnAAChanged(float Value) { if (bRefreshing) { return; } FLPVideoLevels L = Levels; L.AA = LPVideoQuality::ClampLevel(FMath::RoundToInt(Value)); SetLevels(L, false); } void ULPVideoQualitySubsystem::OnRecommendedClicked() { const int64 VramBytes = LPVideoQuality::QueryDedicatedVideoMemoryBytes(); const double VramGB = static_cast(VramBytes) / (1024.0 * 1024.0 * 1024.0); const int32 Level = LPVideoQuality::LevelFromVramGB(VramGB); if (VramBytes > 0) { UE_LOG(LogLifePlay, Display, TEXT("Video : Recommended = %s (niveau %d), VRAM dediee detectee %.1f Go"), *LevelLabel(Level), Level, VramGB); } else { UE_LOG(LogLifePlay, Display, TEXT("Video : Recommended = %s (niveau %d), VRAM dediee inconnue (0 Go), defaut High"), *LevelLabel(Level), Level); } SetLevels(FLPVideoLevels::FromOverall(Level), true); } USlider* ULPVideoQualitySubsystem::AddRow(ERow Row, UScrollBox* Scroll, UWidget*& OutRow, const TCHAR* Name, const TCHAR* Label, FName Handler, const TCHAR* Tip, const FSlateFontInfo& Font, const FSlateColor& Color, const FSliderStyle* SliderStyle) { UHorizontalBox* Box = NewObject(Scroll, Name); UTextBlock* Caption = NewObject(Box); Caption->SetText(FText::FromString(Label)); Caption->SetFont(Font); Caption->SetColorAndOpacity(Color); USizeBox* CaptionBox = NewObject(Box); CaptionBox->SetWidthOverride(220.f); CaptionBox->AddChild(Caption); USlider* Slider = NewObject(Box); if (SliderStyle) { Slider->SetWidgetStyle(*SliderStyle); } Slider->SetMinValue(0.f); Slider->SetMaxValue(static_cast(LPVideoQuality::MaxLevel)); Slider->SetStepSize(1.f); Slider->SetLocked(false); Slider->SetValue(0.f); UTextBlock* Readout = NewObject(Box); Readout->SetFont(Font); Readout->SetColorAndOpacity(Color); if (UHorizontalBoxSlot* Slot = Box->AddChildToHorizontalBox(CaptionBox)) { Slot->SetVerticalAlignment(VAlign_Center); Slot->SetPadding(FMargin(10.f, 0.f, 20.f, 0.f)); } if (UHorizontalBoxSlot* Slot = Box->AddChildToHorizontalBox(Slider)) { Slot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); Slot->SetVerticalAlignment(VAlign_Center); Slot->SetPadding(FMargin(0.f, 0.f, 20.f, 0.f)); } if (UHorizontalBoxSlot* Slot = Box->AddChildToHorizontalBox(Readout)) { Slot->SetVerticalAlignment(VAlign_Center); Slot->SetPadding(FMargin(0.f, 0.f, 10.f, 0.f)); } Box->SetToolTipText(FText::FromString(Tip)); FScriptDelegate Delegate; Delegate.BindUFunction(this, Handler); Slider->OnValueChanged.Add(Delegate); Rows[Row].Slider = Slider; Rows[Row].Readout = Readout; OutRow = Box; return Slider; } UWidget* ULPVideoQualitySubsystem::MakeRecommendedRow(UScrollBox* Scroll, const FSlateFontInfo& Font, const FSlateColor& Color) { UHorizontalBox* Box = NewObject(Scroll, TEXT("LPVideoRecommended")); UButton* Button = NewObject(Box); UTextBlock* Caption = NewObject(Button); Caption->SetText(FText::FromString(TEXT("Recommended"))); Caption->SetFont(Font); Caption->SetColorAndOpacity(Color); Button->SetContent(Caption); if (UButtonSlot* ContentSlot = Cast(Caption->Slot)) { ContentSlot->SetHorizontalAlignment(HAlign_Left); ContentSlot->SetVerticalAlignment(VAlign_Center); ContentSlot->SetPadding(FMargin(10.f)); } FScriptDelegate Delegate; Delegate.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(ULPVideoQualitySubsystem, OnRecommendedClicked)); Button->OnClicked.Add(Delegate); if (UHorizontalBoxSlot* Slot = Box->AddChildToHorizontalBox(Button)) { Slot->SetVerticalAlignment(VAlign_Center); Slot->SetHorizontalAlignment(HAlign_Left); Slot->SetPadding(FMargin(10.f, 0.f, 0.f, 0.f)); } Box->SetToolTipText(FText::FromString(TEXT("Apply the recommended preset for this PC."))); RecommendedButton = Button; return Box; } void ULPVideoQualitySubsystem::EnsureControls() { UScrollBox* Scroll = nullptr; for (TObjectIterator It; It; ++It) { UScrollBox* Candidate = *It; if (!Candidate || Candidate->GetFName() != TEXT("SettingsScrollBox") || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { continue; } const UUserWidget* Owner = Candidate->GetTypedOuter(); if (!Owner || Owner->HasAnyFlags(RF_ClassDefaultObject) || Owner->GetGameInstance() != GetGameInstance()) { continue; } Scroll = Candidate; break; } if (!Scroll) { return; } int32 InsertAt = INDEX_NONE; for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { if (UWidget* Child = Scroll->GetChildAt(Index)) { if (Child->GetFName() == TEXT("LPVideoOverall")) { return; } if (InsertAt == INDEX_NONE && Child->GetName().StartsWith(TEXT("LPColor"))) { InsertAt = Index; } } } UTextBlock* SampleText = nullptr; USlider* SampleSlider = nullptr; for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { LPVideoQuality::FindLook(Scroll->GetChildAt(Index), SampleText, SampleSlider); } const FSlateFontInfo Font = SampleText ? SampleText->GetFont() : LPVideoQuality::MenuFont(); const FSlateColor Color = SampleText ? SampleText->GetColorAndOpacity() : FSlateColor(FLinearColor(1.f, 1.f, 1.f, 0.95f)); const FSliderStyle* SliderStyle = SampleSlider ? &SampleSlider->GetWidgetStyle() : nullptr; // Nouvelle instance du menu : on repart de l'etat moteur tant que le joueur n'a rien choisi. if (!bConfigured) { ReadFromScalability(); } UWidget* Built[NumRows + 1] = { nullptr }; AddRow(RowOverall, Scroll, Built[0], TEXT("LPVideoOverall"), TEXT("Overall Quality"), GET_FUNCTION_NAME_CHECKED(ULPVideoQualitySubsystem, OnOverallChanged), TEXT("Preset applied to shadows, textures, effects and anti-aliasing. Custom when they differ."), Font, Color, SliderStyle); AddRow(RowShadows, Scroll, Built[1], TEXT("LPVideoShadows"), TEXT("Shadows"), GET_FUNCTION_NAME_CHECKED(ULPVideoQualitySubsystem, OnShadowsChanged), TEXT("Shadow resolution and distance."), Font, Color, SliderStyle); AddRow(RowTextures, Scroll, Built[2], TEXT("LPVideoTextures"), TEXT("Textures"), GET_FUNCTION_NAME_CHECKED(ULPVideoQualitySubsystem, OnTexturesChanged), TEXT("Texture filtering and streaming budget."), Font, Color, SliderStyle); AddRow(RowEffects, Scroll, Built[3], TEXT("LPVideoEffects"), TEXT("Effects"), GET_FUNCTION_NAME_CHECKED(ULPVideoQualitySubsystem, OnEffectsChanged), TEXT("Translucency, refraction, subsurface scattering and particles."), Font, Color, SliderStyle); AddRow(RowAA, Scroll, Built[4], TEXT("LPVideoAA"), TEXT("Anti-Aliasing"), GET_FUNCTION_NAME_CHECKED(ULPVideoQualitySubsystem, OnAAChanged), TEXT("Anti-aliasing quality. The method itself is fixed."), Font, Color, SliderStyle); Built[5] = MakeRecommendedRow(Scroll, Font, Color); bool bInserted = false; if (InsertAt != INDEX_NONE) { bInserted = true; for (int32 Index = 0; Index < NumRows + 1; ++Index) { UPanelSlot* PanelSlot = Scroll->InsertChildAt(InsertAt + Index, Built[Index]); if (!PanelSlot) { bInserted = false; break; } if (UScrollBoxSlot* Slot = Cast(PanelSlot)) { Slot->SetPadding(FMargin(0.f, 0.f, 0.f, 10.f)); } } } if (!bInserted) { if (InsertAt != INDEX_NONE) { // Repli : InsertChildAt a echoue. On retire ce qui a pu etre insere, on ajoute en fin // puis on remet les LPColor* derriere en conservant leurs objets. UE_LOG(LogLifePlay, Warning, TEXT("Video : InsertChildAt a echoue, repli par AddChild et reordonnancement des reglages de couleur")); for (UWidget* Row : Built) { if (Row && Row->GetParent() == Scroll) { Scroll->RemoveChild(Row); } } } for (UWidget* Row : Built) { if (UScrollBoxSlot* Slot = Cast(Scroll->AddChild(Row))) { Slot->SetPadding(FMargin(0.f, 0.f, 0.f, 10.f)); } } if (InsertAt != INDEX_NONE) { TArray Colors; for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { UWidget* Child = Scroll->GetChildAt(Index); if (Child && Child->GetName().StartsWith(TEXT("LPColor"))) { Colors.Add(Child); } } for (UWidget* Child : Colors) { Scroll->RemoveChild(Child); } for (UWidget* Child : Colors) { if (UScrollBoxSlot* Slot = Cast(Scroll->AddChild(Child))) { Slot->SetPadding(FMargin(0.f, 0.f, 0.f, 10.f)); } } } } // Les curseurs Blueprint remplaces par LPVideo* sont replies tout de suite ; // LPSettingsSubsystem les garde replies au changement d'onglet. for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { if (UWidget* Child = Scroll->GetChildAt(Index)) { const FName ChildName = Child->GetFName(); if (ChildName == TEXT("ShadowQuality") || ChildName == TEXT("PostProcessQuality") || ChildName == TEXT("PostProcessAAQuality")) { Child->SetVisibility(ESlateVisibility::Collapsed); } } } RefreshWidgets(); // Nouvelle instance du menu : le Blueprint a pu reappliquer ses propres valeurs. ApplyToScalability(); UE_LOG(LogLifePlay, Display, TEXT("Video : controles ajoutes avant les reglages de couleur")); }