We were encountering this issue, and I believe I’ve found an appropriate solution.
The root cause
All of this is discussing the code in Runtime/Engine/Private/GameUserSettings.cpp
. When UGameUserSettings
is first constructed, code in UGameUserSettings::SetToDefaults()
sets the properties ResolutionSizeX
and ResolutionSizeY
to 0
.
void UGameUserSettings::SetToDefaults()
{
ResolutionSizeX = LastUserConfirmedResolutionSizeX = GetDefaultResolution().X;
ResolutionSizeY = LastUserConfirmedResolutionSizeY = GetDefaultResolution().Y;
...
if (FApp::CanEverRender())
{
UpdateResolutionQuality();
}
}
FIntPoint UGameUserSettings::GetDefaultResolution()
{
return FIntPoint::ZeroValue;
}
UpdateResolutionQuality()
then uses ResolutionSizeX
and ResolutionSizeY
to calculate the MinResolutionScale
which is used throughout the rest of the settings code to calculate the lower bound for resolution ratios.
void UGameUserSettings::UpdateResolutionQuality()
{
const int32 MinHeight = UKismetSystemLibrary::GetMinYResolutionFor3DView();
const int32 ScreenWidth = (FullscreenMode == EWindowMode::WindowedFullscreen) ? GetDesktopResolution().X : ResolutionSizeX;
const int32 ScreenHeight = (FullscreenMode == EWindowMode::WindowedFullscreen) ? GetDesktopResolution().Y : ResolutionSizeY;
MinResolutionScale = FMath::Max<float>(Scalability::MinResolutionScale, ((float)MinHeight / (float)ScreenHeight) * 100.0f);
...
}
If FullscreenMode
is not WindowedFullscreen
(in my testing, I used Fullscreen
and Windowed
), then the ResolutionSize*
properties are used to calculate the Screen*
dimensions. When the game or editor is first loaded, because the ResolutionSize*
properties are initialized to zero, ScreenWidth
and ScreenHeight
are both set to zero. This results in ((float)MinHeight / (float)ScreenHeight) * 100.0f
evaluating to infinity because of the division by zero. Consequently, FMath::Max
returns inf
instead of the default Scalability::MinResolutionScale
, thus, MinResolutionScale
gets set to positive infinity.
Further down in UpdateResolutionQuality()
, this value is assigned to ScalabilityQuality.ResolutionQuality
:
void UGameUserSettings::UpdateResolutionQuality()
{
...
if (bUseDesiredScreenHeight)
{
ScalabilityQuality.ResolutionQuality = UGameUserSettings::GetDefaultResolutionScale();
}
else
{
ScalabilityQuality.ResolutionQuality = FMath::Max(ScalabilityQuality.ResolutionQuality, MinResolutionScale);
}
}
Note: bUseDesiredScreenHeight
is initialized to false
, and is only true
if manually changed in GameUserSettings.ini
.
Thus the results are obvious when examining the implementation of GetResolutionScaleInformationEx
:
void UGameUserSettings::GetResolutionScaleInformationEx(float& CurrentScaleNormalized, float& CurrentScaleValue, float& MinScaleValue, float& MaxScaleValue) const
{
CurrentScaleValue = ScalabilityQuality.ResolutionQuality;
MinScaleValue = MinResolutionScale;
MaxScaleValue = Scalability::MaxResolutionScale;
CurrentScaleNormalized = ((float)CurrentScaleValue - (float)MinScaleValue) / (float)(MaxScaleValue - MinScaleValue);
}
Albeit, slightly different from the original post: CurrentScaleValue
will be inf
or sometimes a valid value, MinScaleValue
is infinity, MaxScaleValue
is 100.f
, and CurrentScaleNormalized
becomes -nan(ind)
.
So, essentially, the problem is ResolutionSizeX
and ResolutionSizeY
being zero at startup, when first querying Game User Settings.
Potential/Attempted Solutions
I searched through and attempted several different solutions, in the end I found only two which actually worked. However, for the sake of completeness, I’ll attempt to enumerate all of the ideas I attempted
Changing the default value for GetDefaultResolution()
Since GetDefaultResolution()
was returning (0,0), I figured I’d try setting it to some extremes that were actual resolutions. If it’s set small, eg 640x480, then MinResolutionScale
becomes to large for any screen (around 0.75), resulting in invalid bounds when first queried. If it were set to a very large default, then the MinResolutionScale
value would be too small for smaller screens. Basically, it fixes the invalid numbers, but results in bad values elsewhere. Not a good option.
Detect zero values in UpdateResolutionQuality()
I attempted to replace the ScreenWidth
and ScreenHeight
calculations with additional conditions:
const int32 ScreenWidth = (FullscreenMode == EWindowMode::WindowedFullscreen && ResolutionSizeX != 0) ? GetDesktopResolution().X : ResolutionSizeX;
const int32 ScreenHeight = (FullscreenMode == EWindowMode::WindowedFullscreen && ResolutionSizeY != 0) ? GetDesktopResolution().Y : ResolutionSizeY;
This resulted in weird behavior on Windowed games, as it set the default resolution too high for the actual requested dimensions. Not a good option.
Utilize ValidateSettings()
ValidateSettings()
is one of the two methods which actually set the values of ResolutionSizeX
and ResolutionSizeY
. I considered two places where a to this could be logically placed: Inside LoadSettings()
or SetToDefaults()
.
LoadSettings()
I never actually tried this. ValidateSettings()
itself includes multiple settings to LoadSettings()
and this seemed like an invitation for recursion, so nixed that idea.
SetToDefaults()
I placed a to ValidateSettings()
in SetToDefaults()
right before the to UpdateResolutionQuality()
.
void UGameUserSettings::SetToDefaults()
{
...
if (FApp::CanEverRender())
{
ValidateSettings();
UpdateResolutionQuality();
}
}
This was the first solution that worked well. It solved all problems, and never results in the zero values for ResolutionSizeX
and ResolutionSizeY
, even during editor startup. The potential drawback I saw is that ValidateSettings()
includes multiple calls to LoadSettings()
and itself could SetToDefaults()
which might result in recursion—to be honest, I didn’t even notice the SetToDefaults until I was typing this out. Semantically, it also doesn’t seem like a good fit, including non-deterministic code in SetToDefaults().
Call UpdateResolutionQuality() from LoadSettings()
This is the best solution I’ve found. ResolutionSizeX
and ResolutionSizeY
are loaded from config values within LoadSettings()
, which itself is called in the UEngine
singleton access method. By placing UpdateResolutionQuality()
in LoadSettings()
, it ensures that MinResolutionScale
has is valid as soon as the singleton is loaded. It also seems quite logical to perform the update after loading the config values, so it seems like a good logical location for such a.
The only potential issue I see is that MinResolutionScale
does still receive infinity during editor startup when it’s loading class defaults. I doubt this will pose a significant issue, though.
That said, it seems to work well, so this is the fix I’m going with for now.
My Solution
void UGameUserSettings::LoadSettings( bool bForceReload/*=false*/ )
{
...
// Ensure that MinResolutionScale is updated with the new values of ResolutionSizeX/Y
UpdateResolutionQuality();
}
Edit: I’ve submitted a pull request for this on Github.