UE Hot Update: Config Overloading and Apply

UE热更新:Config的重载与应用

In the UE engine, a large number of configurations are set and controlled using ini files. For projects, understanding which of these can be updated can help establish rules for updating the project. Moreover, many functionalities in UE are realized through configurations and dynamic switches, such as CVars, as well as Device Profiles settings for specific platforms or devices, which can similarly implement dynamic dispatch and application of configurations during hot updates.

This article analyzes the loading process of ini config from the engine mechanism, how different config modules are reloaded after hot updates, and how to apply them in projects, focusing on the runtime modification and reapplication of parameters in the engine or project based on ini configuration hot updates, enhancing the ability to update games.

Before studying how to update ini files, we first need to analyze how the engine loads ini and applies it to the game. Previously, I wrote an article analyzing the loading mechanism of GConfig: Analyzing UE Code: GConfig Loading. By default, ini files are loaded when the engine starts, and they are one of the first files to be loaded; subsequent engine module startups rely on these configurations. The official documentation: Configuration Files.

The engine also has a loading phase for the order of Module loading called PostConfigInit, which ensures that modules are started only after the configuration files are loaded, avoiding cases where the configuration files cannot be read:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Phase at which this module should be loaded during startup.
namespace ELoadingPhase
{
enum Type
{
/** As soon as possible - in other words, uplugin files are loadable from a pak file (as well as right after PlatformFile is set up in case pak files aren't used) Used for plugins needed to read files (compression formats, etc) */
EarliestPossible,
/** Loaded before the engine is fully initialized, immediately after the config system has been initialized. Necessary only for very low-level hooks */
PostConfigInit,
/** The first screen to be rendered after system splash screen */
PostSplashScreen,
/** Loaded before coreUObject for setting up manual loading screens, used for our chunk patching system */
PreEarlyLoadingScreen,
/** Loaded before the engine is fully initialized for modules that need to hook into the loading screen before it triggers */
PreLoadingScreen,
/** Right before the default phase */
PreDefault,
/** Loaded at the default loading point during startup (during engine init, after game modules are loaded.) */
Default,
/** Right after the default phase */
PostDefault,
/** After the engine has been initialized */
PostEngineInit,
/** Do not automatically load this module */
None,
// NOTE: If you add a new value, make sure to update the ToString() method below!
Max
};
// ...
};

The code that starts the engine:

Runtime\Launch\Private\LaunchEngineLoop.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool FEngineLoop::AppInit()
{
// ...
{
SCOPED_BOOT_TIMING("FConfigCacheIni::InitializeConfigSystem");
LLM_SCOPE(ELLMTag::ConfigSystem);
// init config system
FConfigCacheIni::InitializeConfigSystem();
}

FDelayedAutoRegisterHelper::RunAndClearDelayedAutoRegisterDelegates(EDelayedRegisterRunPhase::IniSystemReady);

// Load "asap" plugin modules
IPluginManager& PluginManager = IPluginManager::Get();
IProjectManager& ProjectManager = IProjectManager::Get();
if (!ProjectManager.LoadModulesForProject(ELoadingPhase::EarliestPossible) || !PluginManager.LoadModulesForEnabledPlugins(ELoadingPhase::EarliestPossible))
{
return false;
}
// ...
}

At the end of the FEngineLoop::AppInit function, FCoreDelegates::OnInit.Broadcast(); is executed. This Delegate is bound to InitUObject() in FCoreUObjectModule's StartupModule, which initializes UObject in the engine. At this stage, the Config has also been fully loaded, so creating UObject can correctly read data from Config.

The article Analyzing UE Code: GConfig Loading describes the ini file loading process performed by FConfigCacheIni::InitializeConfigSystem, which we will not repeat here. It can be understood that after the execution completes, the Config is in a usable state.

Hierarchy and Priority of Ini Files

Here are several aspects to note regarding updating UE’s ini files:

  1. Configurations of the same category in UE are composed of multiple levels of ini file hierarchy.
  2. Ini files at different levels have different priorities, where higher priority values will overwrite lower priority ones.

Taking the Engine configuration as an example, there are 28 configurational files that can be configured within the engine, and other category configurations like Game/Input/DeviceProfiles, etc., are similar (UE4.25):

Engine
DeviceProfiles

These files (if they exist) can all be loaded and are arranged in ascending order of priority. This means that options in PROJECT_DIR/Config/DefaultEngine.ini can replace those in ENGINE_DIR/Config/BaseEngine.ini.

In typical project development, by default, the project attributes are usually configured in DefaultEngine.ini, while configurations for a specific platform are in Config/Windows/WindowsEngine.ini. It is rare to have so many configuration files, which can lead to a certain degree of confusion.

Thus, during updates, one can directly use Config/Default*.ini and Config/PLATFORM/PLATFORM*.ini for updates. It’s also possible to use files that originally did not exist for updates, such as if Config/UserEngine.ini did not exist in the project, one can write the needed configuration changes to this file during updates and read them during runtime to append to the engine, and it is also supported in the automatic mounting method since the priority of Config/User*.ini is the highest.

To enable ini updates, one must implement:

  1. Packing specified ini files during updates.
  2. Being able to obtain the already loaded FConfigFile instances in the engine based on the update information and append the new configurations.
  3. Updating the data in UObject objects that use the new configurations.

Loading Config in UObject

Before handling updates, it’s necessary to understand the process of loading config in UObject (only CDO of classes marked as Config will execute loading). The stack is as follows:

UObject loading data from Ini

The general process is:

  1. Check whether UClass has the CLASS_Config flag.
  2. Check whether the Property has the CPF_Config flag.
  3. Deserialize the attributes of CDO from strings.

For example:

1
2
3
4
5
6
7
8
UCLASS(config=GameUserSettings)
class ENGINE_API UGameConfig : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(Config)
FString GCloudGameID;
};

In this class, the UCLASS is appended with Config, and UHT generates the CLASS_Config flag for its UClass. Furthermore, the data member ival of this class also adds Config in UPROPERTY, increasing CPF_Config flag for the current property’s FProperty, thus achieving unity between code and loading.

The specific loading implementation can be found in the CoreUObject\Private\UObject\Obj.cpp file’s LoadConfig function.

When creating non-CDO objects, all properties are copied from CDO (after the constructor executes):

UObjectGlobals.cpp
1
2
// Binary initialize object properties to zero or defaults.
void FObjectInitializer::InitProperties(UObject* Obj, UClass* DefaultsClass, UObject* DefaultData, bool bCopyTransientsFromClassDefaults);

Therefore, the use of Config tagged by UObject affects game use in two steps:

  1. Load ini at engine startup, create CDO, and read configurations from ini.
  2. When creating Non-CDO objects, copy data from CDO.

If one wishes to override the configuration values in ini, they cannot be directly written in the constructor because the timing of LoadConfig occurs later than the constructor. However, one can override the PostInitProperties function, which is called after LoadConfig:

1
virtual void PostInitProperties()override;

Updating Configuration Values in UObject

In terms of the need for hot updates, for the Config used by UObject, we can intervene in the process before the object is created to modify the CDO, and after the CDO is created, we can also call ReloadConfig on the objects to reload from file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
bool UFlibUGConfigReloadHelper::ReloadIniFile(const FString& StrippedName,const FString& File)
{
FConfigFile* ConfigFile = GetConfigFile(StrippedName);
if(!ConfigFile)
{
return false;
}
if (!ConfigFile->Combine(File))
{
return false;
}
TArray<UObject*> Classes;
GetObjectsOfClass(UClass::StaticClass(), Classes, true, RF_NoFlags);
for (UObject* ClassObject : Classes)
{
if (UClass* const Class = Cast<UClass>(ClassObject))
{
if (Class->HasAnyClassFlags(CLASS_Config) &&
Class->ClassConfigName.IsEqual(*StrippedName))
{
TArray<UObject*> Objects;
GetObjectsOfClass(Class, Objects, true, RF_NoFlags);
for (UObject* Object : Objects)
{
if (!Object->IsPendingKill())
{
// Force a reload of the config vars
Object->ReloadConfig();
}
}
}
}
}
return true;
}

As shown in the code, first obtain all UClasses that have the Config flag in the engine, and ensure that the ini name bound to the Class matches with the ini that needs to be reloaded. Then iterate through each instance of UClass and call ReloadConfig on them.

Note: In the above code, the corresponding FConfigFile instance is modified first because the old ini file is already loaded at engine startup and stored in FConfigFile. Before calling ReloadConfig, it needs to be updated first, so that ReloadConfig can read new data from GConfig.

Console Variables

In UE, Console Variables can be used as a way to pass data between modules. In my previous notes, I mentioned ways to obtain and modify Console Variables at runtime: Various Ways to Set ConsoleVariables Values.

There are two methods for automatically applying Console Variable values during engine startup:

  1. Settings can be made in Engine/Config/ConsoleVariables.ini under [Startup]:
1
2
3
[Startup]
r.ShaderPipelineCache.Enabled=0
r.ShaderPipelineCache.LogPSO=0
  1. Specifying them in the Engine category ini hierarchy (like DefaultEngine.ini), under [ConsoleVariables]:
1
2
3
[ConsoleVariables]
r.ShaderPipelineCache.Enabled=0
r.ShaderPipelineCache.LogPSO=0

When we update the Engine category’s FConfigCache, we can call the following function:

1
::ApplyCVarSettingsFromIni(TEXT("ConsoleVariables"), *GEngineIni, ECVF_SetBySystemSettingsIni);

to apply the latest CVars values from [ConsoleVariables]. If one wishes to update both settings in [Startup] of ConsoleVariable.ini and the settings in [ConsoleVariables], one can call:

1
FConfigCacheIni::LoadConsoleVariablesFromINI();

This will first apply from [Startup], followed by applying from [ConsoleVariables].

The update process steps are:

  1. Reload the Engine category ini files.
  2. Call the ApplyCVarSettingsFromIni function.

Device Profiles

Device Profiles is a method in UE for setting parameters for specific platforms or devices. The official documentation: Setting Device Profiles. It can be used to achieve performance and parameter adaptations across different platforms and devices.

The default provided configurations of platform/device names with ini platform names in the engine: BaseDeviceProfiles.ini#L6 are stored under DeviceProfiles in *DeviceProfiles.ini, with the values of DeviceProfileNameAndTypes. The BaseDeviceProfiles.ini includes dependencies for profiles, device specifications, iOS device mappings, specific device configurations, etc., and settings in your own project can refer to these.

Moreover, UE’s Device Profiles support inheritance, like iPhoneX->IOS_High->IOS->Mobile, which conveniently allows configuration reuse.

The UDeviceProfile class is also marked as Config and is perObjectConfig, meaning each object’s configuration will be saved and loaded separately. In simple terms, the loading of Device Profiles is essentially the creation of many objects, each saving its configuration information for its platform or device. The Name passed in NewObject<UDeviceProfile> is the platform name.

When creating a UDeviceProfile, it automatically loads from the ini (stack of NewObject<UDeviceProfile>):

Since the loading of Device Profiles configurations also follows the default LoadConfig process, we can update UDeviceProfiles following the steps in Updating Configuration Values in UObject; we can obtain all UDeviceProfile instances and call ReloadConfig on them and also call it on their Parent Profiles (Parent Profiles are also UDeviceProfile objects):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void UFlibUGConfigReloadHelper::RecursiveReloadDeviceProfile(UDeviceProfile* Profile)
{
if(!Profile->IsPendingKill())
{
Profile->LoadConfig();
}
if(Profile->Parent && !Profile->IsPendingKill())
{
RecursiveReloadDeviceProfile(Cast<UDeviceProfile>(Profile->Parent));
}
};
void UFlibUGConfigReloadHelper::ReloadDeviceProfiles()
{
for (UObject* Profile : UDeviceProfileManager::Get().Profiles)
{
UDeviceProfile* const DeviceProfile = Cast<UDeviceProfile>(Profile);
if(!DeviceProfile->IsPendingKill())
{
RecursiveReloadDeviceProfile(DeviceProfile);
DeviceProfile->ValidateProfile();
}
}
// UDeviceProfileManager::Get().OnManagerUpdated().Broadcast();
}

After that, one can call ReapplyDeviceProfile from UDeviceProfileManager:

1
UDeviceProfileManager::Get().ReapplyDeviceProfile();

GameUserSettings

After overriding the configuration of GameUserSettings, one can call ApplySettings for application:

1
2
3
4
5
void UFlibUGConfigReloadHelper::ReapplyGameUserSettings()
{
GEngine->GetGameUserSettings()->LoadSettings(true);
GEngine->GetGameUserSettings()->ApplySettings(true);
}

The ApplySettings code is as follows:

GameUserSettings.cpp
1
2
3
4
5
6
7
8
void UGameUserSettings::ApplySettings(bool bCheckForCommandLineOverrides)
{
ApplyResolutionSettings(bCheckForCommandLineOverrides);
ApplyNonResolutionSettings();
RequestUIUpdate();

SaveSettings();
}

Conclusion

The loading of various configuration files in UE is actually based on the hierarchical update of UE’s ini files. First, the values in GConfig need to be updated, and then applied to different modules. This article enumerates several different configurations’ reloading and application; other unlisted situations can be handled in the same way.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE Hot Update: Config Overloading and Apply
Author:LIPENGZHA
Publish Date:2021/10/18 14:57
Word Count:8.1k Words
Link:https://en.imzlp.com/posts/9028/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!