LifePlay-Revival/Source/LifePlay/LPVideoQualitySubsystem.cpp

606 lines
19 KiB
C++
Raw Normal View History

// 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<int64>(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<UTextBlock>(Widget);
}
if (!OutSlider)
{
OutSlider = Cast<USlider>(Widget);
}
if (UUserWidget* User = Cast<UUserWidget>(Widget))
{
FindLook(User->GetRootWidget(), OutText, OutSlider);
}
if (UPanelWidget* Panel = Cast<UPanelWidget>(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<float>(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<double>(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<UHorizontalBox>(Scroll, Name);
UTextBlock* Caption = NewObject<UTextBlock>(Box);
Caption->SetText(FText::FromString(Label));
Caption->SetFont(Font);
Caption->SetColorAndOpacity(Color);
USizeBox* CaptionBox = NewObject<USizeBox>(Box);
CaptionBox->SetWidthOverride(220.f);
CaptionBox->AddChild(Caption);
USlider* Slider = NewObject<USlider>(Box);
if (SliderStyle)
{
Slider->SetWidgetStyle(*SliderStyle);
}
Slider->SetMinValue(0.f);
Slider->SetMaxValue(static_cast<float>(LPVideoQuality::MaxLevel));
Slider->SetStepSize(1.f);
Slider->SetLocked(false);
Slider->SetValue(0.f);
UTextBlock* Readout = NewObject<UTextBlock>(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<UHorizontalBox>(Scroll, TEXT("LPVideoRecommended"));
UButton* Button = NewObject<UButton>(Box);
UTextBlock* Caption = NewObject<UTextBlock>(Button);
Caption->SetText(FText::FromString(TEXT("Recommended")));
Caption->SetFont(Font);
Caption->SetColorAndOpacity(Color);
Button->SetContent(Caption);
if (UButtonSlot* ContentSlot = Cast<UButtonSlot>(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<UScrollBox> It; It; ++It)
{
UScrollBox* Candidate = *It;
if (!Candidate || Candidate->GetFName() != TEXT("SettingsScrollBox") || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
{
continue;
}
const UUserWidget* Owner = Candidate->GetTypedOuter<UUserWidget>();
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<UScrollBoxSlot>(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<UScrollBoxSlot>(Scroll->AddChild(Row)))
{
Slot->SetPadding(FMargin(0.f, 0.f, 0.f, 10.f));
}
}
if (InsertAt != INDEX_NONE)
{
TArray<UWidget*> 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<UScrollBoxSlot>(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"));
}