UE project optimization: PSO Cache

UE项目优化:PSO Cache

In UE, there is a PSO Cache mechanism, which stands for Pipeline State Object Caching. It is used to pre-record and construct the shader information dependent on the materials used at runtime. When the project first utilizes these shaders, this list can speed up the shader loading/compilation process. The PSO Cache saves rendering states, vertex declarations, primitive types, render target pixel formats, and other data to files, enhancing shader loading efficiency. This article primarily introduces the enabling and construction process of PSO Cache, and will analyze the loading process of PSO Cache in the engine, along with implementing hot-update PSO methods, error handling, etc. The principle of PSO Cache will be analyzed in detail later.

The official documentation for PSO Cache: PSO Cache.

Overview of the PSO Cache construction process:

The deployment and use of PSO Cache can be roughly divided into the following steps:

  1. Enable PSO Cache and Shader Stable Keys for the project, and after packaging, you can obtain ShaderStableInfo*.scl.csv from the Metadata/PipelineCaches directory.
  2. Add the logPSO parameter to start the game, which is used to record PSO data at runtime (*.rec.upipelinecache).
  3. Generate *.stablepc.csv using ShaderStableInfo*.scl.csv and *.rec.upipelinecache.
  4. Execute Cook again to generate the upipelinecache file using *.stablepc.csv, which will be included in the package.
  5. Start the game, and the engine will automatically load *.stable.upipelinecache, using the PSO Cache when compiling shaders.

The content order of this article also follows these steps.

Enable Shader Stable Keys

First, you need to enable ShaderStableKeys for the project to generate stable Shader Keys during Cook, serving as a record for shaders.

Add the following value in DefaultEngine.ini (or platform-specific files like AndroidEngine.ini):

1
2
[DevOptions.Shaders]
NeedsShaderStableKeys=true

After adding this, execute packaging (Cook) again, which will create the following directory:

1
Saved/Cooked/PLATFORM_NAME/PROJECT_NAME/Metadata/PipelineCaches

And it will produce two files in this directory (corresponding to the project and the engine):

1
2
ShaderStableInfo-PROJECT_NAME-GLSL_ES3_1_ANDROID.scl.csv
ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv

During the Cook process, the following log will indicate that these two files were generated:

1
2
LogCook: Display: Saved scl.csv D:/PSOExample/Saved/Cooked/Android_ASTC/PSOExample/Metadata/PipelineCaches/ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv for platform Android_ASTC
LogCook: Display: Saved scl.csv D:/PSOExample/Saved/Cooked/Android_ASTC/PSOExample/Metadata/PipelineCaches/ShaderStableInfo-PSOExample-GLSL_ES3_1_ANDROID.scl.csv for platform Android_ASTC

You can use them to generate *.stablepc.csv by running the commandlet -run=ShaderPipelineCacheTools.

The content of the *.scl.csv file:

Runtime Capture of PSO Data

When starting the game, add the -logPSO parameter or include the following configuration in DefaultEngine.ini:

Config/DefaultEngine.ini
1
2
3
4
[ConsoleVariables]
r.ShaderPipelineCache.Enabled=1
r.ShaderPipelineCache.LogPSO=1
r.ShaderPipelineCache.SaveBoundPSOLog=1

You can also set these parameters in the Devices Profile:

These two parameters are used in the following code:

Runtime/RHI/Private/PipelineFileCache.cpp
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
bool FPipelineFileCache::IsPipelineFileCacheEnabled()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("psocache"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Forcing PSO cache from command line"));
}
return FileCacheEnabled && (bCmdLineForce || CVarPSOFileCacheEnabled.GetValueOnAnyThread() == 1);
}

bool FPipelineFileCache::LogPSOtoFileCache()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("logpso"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Forcing logging of PSOs from command line"));
}
return (bCmdLineForce || CVarPSOFileCacheLogPSO.GetValueOnAnyThread() == 1);
}

During gameplay, the log will include:

1
2
3
4
5
6
7
8
9
10
11
12
LogConfig: Applying CVar settings from Section [ConsoleVariables] File [../../../FGame/Saved/Config/Android/Engine.ini]
LogConfig: Setting CVar [[r.ShaderPipelineCache.Enabled:1]]
LogConfig: Setting CVar [[r.ShaderPipelineCache.LogPSO:1]]
LogConfig: Setting CVar [[r.ShaderPipelineCache.SaveBoundPSOLog:1]]
...
LogRHI: Base name for record PSOs is ../../../FGame/Saved/CollectedPSOs/++UE4+Release-4.25-CL-0-FGame_GLSL_ES3_1_ANDROID_00084A4308D90436AC0F652223AA8D4F.rec.upipelinecache
LogRHI: Could not open FPipelineCacheFile: ../../../FGame/Content/PipelineCaches/Android/FGame_GLSL_ES3_1_ANDROID.upipelinecache
...
LogRHI: Display: Encountered a new graphics PSO: 3478445130
LogRHI: Display: New Graphics PSO (3478445130) Description: 416F798F22743F626513A16205A15ECB135C3791,0000000000000000000000000000000000000000,0000000000000000000000000000000000000000,0000000000000000000000000000000000000000,<0 1 0 0 1 0 0 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0>,<0.000000 0.000000 2 1 0 0>,<0 3 0 7 0 0 0 0 7 0 0 0 255 255>,1,11,1051148,2,2,0,0,0,4,10,1114633,0,0,18,2569,0,0,37,1051145,0,0,37,2585,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15360,15391,1055,1027,1025,0,0,0,0,1,<0 0 3 0 12 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>
LogRHI: Display: Encountered a new graphics PSO: 898510125
LogRHI: Display: New Graphics PSO (898510125) Description: 432A3E5557E9ED8328AC7E6CDA5AA6FC2F0B2439,0FED48EC019CFDD251488DE33D563DCFFD7E69DA,0000000000000000000000000000000000000000,0000000000000000000000000000000000000000,0000000000000000000000000000000000000000,<0 1 0 0 1 0 7 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0 1 0 0 1 0 15 0>,<0.000000 0.000000 2 0 0 0>,<0 7 0 7 0 0 0 0 7 0 0 0 255 255>,1,11,1051148,2,2,0,0,0,4,10,1114633,0,0,18,2569,0,0,37,1051145,0,0,37,2585,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15360,15391,1055,1027,1025,0,0,0,2,2,<0 0 4 0 32 0>,<0 16 2 1 32 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>,<0 0 0 0 0 0>

And a following file will be created in Saved/CoolectedPSOs:

Note that by default, only the current MaterialQualityLevel will be collected. If you wish to collect for other qualities, you need to switch and rerun.

Generate *.stablepc.csv

Use the following commandlet:

1
2
3
4
5
6
Engine/Binaries/Win64/UE4Editor-Cmd.exe
D:/Client/Client.uproject
-run=ShaderPipelineCacheTools expand
D:/PSOCache/*.rec.upipelinecache
D:/PSOCache/*.scl.csv
D:/PSOCache/Client_GLSL_ES3_1_ANDROID.stablepc.csv

The above command will generate the Client_GLSL_ES3_1_ANDROID.stablepc.csv file under the engine’s Binaries/Win64. Note that it must match the naming convention {PROJECTNAME}_{SHADER_FORMART_NAME}.stablepc.csv.

For Android, the naming will be: Client_GLSL_ES3_1_ANDROID.stablepc.csv
For iOS, the naming will be: Client_SF_METAL.stablepc.csv

During generation, the following logs will be produced:

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
D:\PSOCache>"C:\Program Files\Epic Games\UE_4.25\Engine\Binaries\Win64\UE4Editor-Cmd.exe" "D:\PSOExample\PSOExample.uproject" -run=ShaderPipelineCacheTools expand D:/PSOCache/*.rec.upipelinecache D:/PSOCache/*.scl.csv D:/PSOCache/PSOExample_GLSL_ES3_1_ANDROID.stablepc.csv
[2021.04.22-08.57.39:623][ 0]LogTargetPlatformManager: Display: Building Assets For Windows
[2021.04.22-08.57.39:648][ 0]LogAudioDebug: Display: Lib vorbis DLL was dynamically loaded.
[2021.04.22-08.57.39:841][ 0]LogShaderCompilers: Display: Using Local Shader Compiler.
[2021.04.22-08.57.40:492][ 0]LogDerivedDataCache: Display: Max Cache Size: 512 MB
[2021.04.22-08.57.40:523][ 0]LogDerivedDataCache: Display: Loaded Boot cache: C:/Users/lipengzha/AppData/Local/UnrealEngine/4.25/DerivedDataCache/Boot.ddc
[2021.04.22-08.57.40:533][ 0]LogDerivedDataCache: Display: Pak cache opened for reading ../../../Engine/DerivedDataCache/Compressed.ddp.
[2021.04.22-08.57.44:616][ 0]LogAudioCaptureCore: Display: No Audio Capture implementations found. Audio input will be silent.
[2021.04.22-08.57.44:616][ 0]LogAudioCaptureCore: Display: No Audio Capture implementations found. Audio input will be silent.
[2021.04.22-08.57.45:274][ 0]LogShaderPipelineCacheTools: Display: Loading D:/PSOCache/ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv...
[2021.04.22-08.57.45:275][ 0]LogShaderPipelineCacheTools: Display: Loading D:/PSOCache/ShaderStableInfo-PSOExample-GLSL_ES3_1_ANDROID.scl.csv...
[2021.04.22-08.57.45:280][ 0]LogShaderPipelineCacheTools: Display: Loaded 548 shader info lines from D:/PSOCache/ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv.
[2021.04.22-08.57.45:287][ 0]LogShaderPipelineCacheTools: Display: Loaded 5707 shader info lines from D:/PSOCache/ShaderStableInfo-PSOExample-GLSL_ES3_1_ANDROID.scl.csv.
[2021.04.22-08.57.45:287][ 0]LogShaderPipelineCacheTools: Display: Loaded 6255 unique shader info lines total.
[2021.04.22-08.57.45:289][ 0]LogShaderPipelineCacheTools: Display: Loading D:/PSOCache/++UE4+Release-4.25-CL-13942748-PSOExample_GLSL_ES3_1_ANDROID_000843BC08D905AF09E24614C6A6086F.rec.upipelinecache....
[2021.04.22-08.57.45:293][ 0]LogShaderPipelineCacheTools: Display: Loaded 105 PSOs
[2021.04.22-08.57.45:297][ 0]LogShaderPipelineCacheTools: Display: Loaded 105 PSOs total [Usage Mask Merged = 0].
[2021.04.22-08.57.45:325][ 0]LogShaderPipelineCacheTools: Display: Generated 478 stable PSOs total
[2021.04.22-08.57.45:344][ 0]LogShaderPipelineCacheTools: Display: Wrote stable PSOs, 479 lines (541.8 KB) to D:/PSOCache/PSOExample_GLSL_ES3_1_ANDROID.stablepc.csv
[2021.04.22-08.57.45:346][ 0]LogInit: Display:
[2021.04.22-08.57.45:349][ 0]LogInit: Display: Success - 0 error(s), 0 warning(s)
[2021.04.22-08.57.45:353][ 0]LogInit: Display:
Execution of commandlet took: 0.08 seconds
[2021.04.22-08.57.45:408][ 0]LogShaderCompilers: Display: Shaders left to compile 0
[2021.04.22-08.57.45:621][ 0]LogContentStreaming: Display: There are 1 unreleased StreamingManagers

All files needed for the final PSO:

1
2
3
4
5
6
7
8
D:\PSOCache>tree /a /f
Volume in drive D is Data
Volume serial number is 004B-E876
D:.
++UE4+Release-4.25-CL-13942748-PSOExample_GLSL_ES3_1_ANDROID_000843BC08D905AF09E24614C6A6086F.rec.upipelinecache
PSOExample_GLSL_ES3_1_ANDROID.stablepc.csv
ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv
ShaderStableInfo-PSOExample-GLSL_ES3_1_ANDROID.scl.csv

I backed up the files generated by the test project: PSOCache.7z, which allows you to view the contents of each file.

Generate *.stable.upipelinecache

Place the generated *stablepc.csv into the Build/Android/PipelineCaches directory, noting that the Build/PLATFORM this platform is the compilation platform, not the resource platform during Cook; for Android, the package should be Android, not Android_ASTC, etc. Then repackage.

The engine creates the PipelineCache using stablepc.csv during Cook:

Editor\UnrealEd\Private\CookOnTheFlyServer.cpp
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
void UCookOnTheFlyServer::CreatePipelineCache(const ITargetPlatform* TargetPlatform, const FString& LibraryName)
{
// make sure we have a registry generated for all the platforms
const FString TargetPlatformName = TargetPlatform->PlatformName();
TArray<FString>* SCLCSVPaths = OutSCLCSVPaths.Find(FName(TargetPlatformName));
if (SCLCSVPaths && SCLCSVPaths->Num())
{
TArray<FName> ShaderFormats;
TargetPlatform->GetAllTargetedShaderFormats(ShaderFormats);
for (FName ShaderFormat : ShaderFormats)
{
// *stablepc.csv or *stablepc.csv.compressed
const FString Filename = FString::Printf(TEXT("*%s_%s.stablepc.csv"), *LibraryName, *ShaderFormat.ToString());
const FString StablePCPath = FPaths::ProjectDir() / TEXT("Build") / TargetPlatform->IniPlatformName() / TEXT("PipelineCaches") / Filename;
const FString StablePCPathCompressed = StablePCPath + TEXT(".compressed");

TArray<FString> ExpandedFiles;
IFileManager::Get().FindFilesRecursive(ExpandedFiles, *FPaths::GetPath(StablePCPath), *FPaths::GetCleanFilename(StablePCPath), true, false, false);
IFileManager::Get().FindFilesRecursive(ExpandedFiles, *FPaths::GetPath(StablePCPathCompressed), *FPaths::GetCleanFilename(StablePCPathCompressed), true, false, false);
if (!ExpandedFiles.Num())
{
UE_LOG(LogCook, Display, TEXT("---- NOT Running UShaderPipelineCacheToolsCommandlet for platform %s shader format %s, no files found at %s"), *TargetPlatformName, *ShaderFormat.ToString(), *StablePCPath);
}
else
{
UE_LOG(LogCook, Display, TEXT("---- Running UShaderPipelineCacheToolsCommandlet for platform %s shader format %s"), *TargetPlatformName, *ShaderFormat.ToString());

const FString OutFilename = FString::Printf(TEXT("%s_%s.stable.upipelinecache"), *LibraryName, *ShaderFormat.ToString());
const FString PCUncookedPath = FPaths::ProjectDir() / TEXT("Content") / TEXT("PipelineCaches") / TargetPlatform->IniPlatformName() / OutFilename;

if (IFileManager::Get().FileExists(*PCUncookedPath))
{
UE_LOG(LogCook, Warning, TEXT("Deleting %s, cooked data doesn't belong here."), *PCUncookedPath);
IFileManager::Get().Delete(*PCUncookedPath, false, true);
}

const FString PCCookedPath = ConvertToFullSandboxPath(*PCUncookedPath, true);
const FString PCPath = PCCookedPath.Replace(TEXT("[Platform]"), *TargetPlatformName);


FString Args(TEXT("build "));
Args += TEXT("\"");
Args += StablePCPath;
Args += TEXT("\"");

int32 NumMatched = 0;
for (int32 Index = 0; Index < SCLCSVPaths->Num(); Index++)
{
if (!(*SCLCSVPaths)[Index].Contains(ShaderFormat.ToString()))
{
continue;
}
NumMatched++;
Args += TEXT(" ");
Args += TEXT("\"");
Args += (*SCLCSVPaths)[Index];
Args += TEXT("\"");
}
if (!NumMatched)
{
UE_LOG(LogCook, Warning, TEXT("Shader format %s for platform %s had this file %s, but no .scl.csv files."), *ShaderFormat.ToString(), *TargetPlatformName, *StablePCPath);
for (int32 Index = 0; Index < SCLCSVPaths->Num(); Index++)
{
UE_LOG(LogCook, Warning, TEXT(" .scl.csv file: %s"), *((*SCLCSVPaths)[Index]));
}
continue;
}

Args += TEXT(" ");
Args += TEXT("\"");
Args += PCPath;
Args += TEXT("\"");
UE_LOG(LogCook, Display, TEXT(" With Args: %s"), *Args);

int32 Result = UShaderPipelineCacheToolsCommandlet::StaticMain(Args);

if (Result)
{
LogCookerMessage(FString::Printf(TEXT("UShaderPipelineCacheToolsCommandlet failed %d"), Result), EMessageSeverity::Error);
}
else
{
UE_LOG(LogCook, Display, TEXT("---- Done running UShaderPipelineCacheToolsCommandlet for platform %s"), *TargetPlatformName);
}
}
}
}
}

The actual use of stablepc.csv is to execute the ShaderPipelineCacheTools commandlet to generate upipelinecache files and include them in the package.

The execution command for ShaderPipelineCacheToolsCommandlet is:

1
2
3
4
5
6
7
Engine\Binaries\Win64\UE4Editor-Cmd.exe
D:\PSOExample\PSOExample.uproject
-run=ShaderPipelineCacheTools build
"D:\PSOExample/Build/Android/PipelineCaches/*PSOExample_GLSL_ES3_1_ANDROID.stablepc.csv"
"D:\PSOExample/Saved/Cooked/Android_ASTC/PSOExample/Metadata/PipelineCaches/ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv"
"D:\PSOExample/Saved/Cooked/Android_ASTC/PSOExample/Metadata/PipelineCaches/ShaderStableInfo-PSOExample-GLSL_ES3_1_ANDROID.scl.csv"
"D:\PSOExample/Saved/Cooked/Android_ASTC/PSOExample/Content/PipelineCaches/Android/PSOExample_GLSL_ES3_1_ANDROID.stable.upipelinecache"

The path for the generated *.stable.upipelinecache file in the package is Content\PipelineCaches\Android:

1
2
3
4
5
6
D:\PSOExample\Saved\Cooked\Android_ASTC\PSOExample\Content\PipelineCaches>tree /a /f
Volume in drive C is Windows
Volume serial number is 0C49-9EA3
C:.
\---Android
PSOExample_GLSL_ES3_1_ANDROID.stable.upipelinecache

Since it is located under Content and will be packed into pak, it can also be updated hot.

When a package containing the upipelinecache is installed, the following log will appear at runtime:

1
2
3
4
5
6
7
8
9
10
11
LogShaderLibrary: Display: Using ../../../PSOExample/Content/ShaderArchive-PSOExample-GLSL_ES3_1_ANDROID.ushaderbytecode for material shader code. Total 3053 unique shaders.
LogShaderLibrary: Display: Cooked Context: Using Shared Shader Library PSOExample
LogRHI: Display: Opened pipeline cache after state change and enqueued 0 of 0 tasks for precompile.
LogRHI: Base name for record PSOs is ../../../PSOExample/Saved/CollectedPSOs/++UE4+Release-4.25-CL-13942748-PSOExample_GLSL_ES3_1_ANDROID_00087B4B08D905BBC5A827F40CA03A0C.rec.upipelinecache
LogRHI: FPipelineCacheFile Header Game Version: 13942748
LogRHI: FPipelineCacheFile Header Engine Data Version: 17
LogRHI: FPipelineCacheFile Header TOC Offset: 38155
LogRHI: FPipelineCacheFile File Size: 51011 Bytes
LogRHI: Opened FPipelineCacheFile: ../../../PSOExample/Content/PipelineCaches/Android/PSOExample_GLSL_ES3_1_ANDROID.stable.upipelinecache (GUID: 00000000000000000000000000000000) with 102 entries.
LogRHI: Scanning Binary program cache, using Shader Pipeline Cache version 6988202F47BA858F3F0DE483D7DB0606
LogRHI: AndroidEGL:SwapBuffers eglGetCompositorTimingANDROID EGL_COMPOSITE_DEADLINE_ANDROID=2718926192606265, EGL_COMPOSITE_INTERVAL_ANDROID=16559027, EGL_COMPOSITE_TO_PRESENT_LATENCY_ANDROID=14559027

Loading and Hot Updating PSO Cache

Similar to ShaderCode, the engine automatically loads the PSO Cache upon startup by calling FPipelineCacheFile::OpenPipelineFileCache to read *.stable.upipelinecache within FEngineLoop.

The code for loading PSO in PreInitPreStartupScreen is as follows:

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
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
// ...
{
bool bUseCodeLibrary = FPlatformProperties::RequiresCookedData() || GAllowCookedDataInEditorBuilds;
if (bUseCodeLibrary)
{
{
SCOPED_BOOT_TIMING("FShaderCodeLibrary::InitForRuntime");
// Will open material shader code storage if project was packaged with it
// This only opens the Global shader library, which is always in the content dir.
FShaderCodeLibrary::InitForRuntime(GMaxRHIShaderPlatform);
}

#if !UE_EDITOR
// Cooked data only - but also requires the code library - game only
if (FPlatformProperties::RequiresCookedData())
{
SCOPED_BOOT_TIMING("FShaderPipelineCache::Initialize");
// Initialize the pipeline cache system. Opening is deferred until the manual call to
// OpenPipelineFileCache below, after content pak's ShaderCodeLibraries are loaded.
FShaderPipelineCache::Initialize(GMaxRHIShaderPlatform);
}
#endif //!UE_EDITOR
}
}
// ...
//Handle opening shader library after our EarlyLoadScreen
{
LLM_SCOPE(ELLMTag::Shaders);
SCOPED_BOOT_TIMING("FShaderCodeLibrary::OpenLibrary");

// Open the game library which contains the material shaders.
FShaderCodeLibrary::OpenLibrary(FApp::GetProjectName(), FPaths::ProjectContentDir());
for (const FString& RootDir : FPlatformMisc::GetAdditionalRootDirectories())
{
FShaderCodeLibrary::OpenLibrary(FApp::GetProjectName(), FPaths::Combine(RootDir, FApp::GetProjectName(), TEXT("Content")));
}

// Now our shader code main library is opened, kick off the precompile, if already initialized
FShaderPipelineCache::OpenPipelineFileCache(GMaxRHIShaderPlatform);
}
// ...
}

FShaderPipelineCache::OpenPipelineFileCache has two overloaded versions:

Runtime\RenderCore\Private\ShaderPipelineCache.cpp
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
bool FShaderPipelineCache::OpenPipelineFileCache(EShaderPlatform Platform)
{
bool bFileOpen = false;
if (GConfig)
{
FString LastOpenedName;
if ((GConfig->GetString(FShaderPipelineCacheConstants::SectionHeading, FShaderPipelineCacheConstants::LastOpenedKey, LastOpenedName, *GGameUserSettingsIni) || GConfig->GetString(FShaderPipelineCacheConstants::SectionHeading, FShaderPipelineCacheConstants::LastOpenedKey, LastOpenedName, *GGameIni)) && LastOpenedName.Len())
{
bFileOpen = OpenPipelineFileCache(LastOpenedName, Platform);
}
}

if (!bFileOpen)
{
bFileOpen = OpenPipelineFileCache(FApp::GetProjectName(), Platform);
}

return bFileOpen;
}

bool FShaderPipelineCache::OpenPipelineFileCache(FString const& Name, EShaderPlatform Platform)
{
if (ShaderPipelineCache)
return ShaderPipelineCache->Open(Name, Platform);
else
return false;
}

Upon engine startup, it defaults to reading OpenPipelineFileCache(FApp::GetProjectName(), Platform), which is PSOExample_GLSL_ES3_1_ANDROID.stable.upipelinecache. The Platform parameter can be obtained by passing the global object GMaxRHIShaderPlatform.

UE also provides a console command to specify loading stable.upipelinecache: r.ShaderPipelineCache.Open, along with several other console commands that control PSO:

Runtime/RenderCore/Private/ShaderPipelineCache.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static FAutoConsoleCommand LoadPipelineCacheCmd(
TEXT("r.ShaderPipelineCache.Open"),
TEXT("Takes the desired filename to open and then loads the pipeline file cache."),
FConsoleCommandWithArgsDelegate::CreateStatic(ConsoleCommandLoadPipelineFileCache)
);

static FAutoConsoleCommand SavePipelineCacheCmd(
TEXT("r.ShaderPipelineCache.Save"),
TEXT("Save the current pipeline file cache."),
FConsoleCommandDelegate::CreateStatic(ConsoleCommandSavePipelineFileCache)
);

static FAutoConsoleCommand ClosePipelineCacheCmd(
TEXT("r.ShaderPipelineCache.Close"),
TEXT("Close the current pipeline file cache."),
FConsoleCommandDelegate::CreateStatic(ConsoleCommandClosePipelineFileCache)
);

static FAutoConsoleCommand SwitchModePipelineCacheCmd(
TEXT("r.ShaderPipelineCache.SetBatchMode"),
TEXT("Sets the compilation batch mode, which should be one of:\n\tPause: Suspend precompilation.\n\tBackground: Low priority precompilation.\n\tFast: High priority precompilation."),
FConsoleCommandWithArgsDelegate::CreateStatic(ConsoleCommandSwitchModePipelineCacheCmd)
);

This means that you only need to include the latest *.stable.upipelinecache in the hot update package, and then call OpenPipelineFileCache to load the latest PSO Cache, which can maintain consistency with the hot update process of ShaderCode.

Generating a new PSO Cache requires two critical types of data:

  1. Runtime-captured PSO data (upipelinecache)
  2. ShaderStableInfo (located in the Metadata directory)

Since ShaderCode can be hot updated, and ShaderStableInfo can be obtained by cooking the latest project, the PSO Cache can similarly be iteratively updated by hot updating shaders and continuously capturing the latest PSO data. I plan to add the PSO Cache hot update functionality to HotPatcher when I have time, thus integrating the deployment and packaging of PSO Cache into an automated hot update process, marking this down for future reference.

When r.ShaderPipelineCache.Enabled=1 is set, the engine will automatically load the project’s PSO Cache upon startup, and the engine imposes a restriction that it can only be loaded once; subsequent calls to OpenPipelineFileCache will not be loaded:

Runtime\RHI\Private\PipelineFileCache.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool FPipelineFileCache::OpenPipelineFileCache(FString const& Name, EShaderPlatform Platform, FGuid& OutGameFileGuid)
{
bool bOk = false;
OutGameFileGuid = FGuid();

if(IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);

if(FileCache == nullptr)
{
FileCache = new FPipelineCacheFile();

bOk = FileCache->OpenPipelineFileCache(Name, Platform, OutGameFileGuid);

// File Cache now exists - these caches should be empty for this file otherwise will have false positives from any previous file caching - if not something has been caching when it should not be
check(NewPSOs.Num() == 0);
check(NewPSOHashes.Num() == 0);
check(RunTimeToPSOUsage.Num() == 0);
}
}

return bOk;
}

After the engine loads by default, FileCache will no longer be nullptr, so subsequent loading calls will directly return false. The solution is to prevent the engine from automatically loading the PSO Cache at startup and wait until runtime hot updates to load it manually. I checked the code and found that this can be detected from the IsPipelineFileCacheEnabled check:

Runtime\RHI\Private\PipelineFileCache.cpp
1
2
3
4
5
6
7
8
9
10
11
12
bool FPipelineFileCache::IsPipelineFileCacheEnabled()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("psocache"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Forcing PSO cache from command line"));
}
return FileCacheEnabled && (bCmdLineForce || CVarPSOFileCacheEnabled.GetValueOnAnyThread() == 1);
}

Its return value depends on two values: FileCacheEnabled and CVarPSOFileCacheEnabled.

FileCacheEnabled is assigned in FPipelineFileCache::Initialize, and is always true for platforms other than iOS. For iOS, it depends on the result of FPipelineFileCache::ShouldEnableFileCache.

CVarPSOFileCacheEnabled is a console variable that controls the value of r.ShaderPipelineCache.Enabled:

Runtime\RHI\Private\PipelineFileCache.cpp
1
2
3
4
5
6
static TAutoConsoleVariable<int32> CVarPSOFileCacheEnabled(
TEXT("r.ShaderPipelineCache.Enabled"),
PIPELINE_CACHE_DEFAULT_ENABLED,
TEXT("1 Enables the PipelineFileCache, 0 disables it."),
ECVF_Default | ECVF_RenderThreadSafe
);

We need to follow three steps:

  1. Set the default value of CVarPSOFileCacheEnabled to false when the engine starts.
  2. Manually modify the CVarPSOFileCacheEnabled value at runtime to enable the PSO Cache.
  3. Load the PSO Cache.

The specific implementation process is as follows:

  1. Set r.ShaderPipelineCache.Enabled=0 in DefaultEngine.ini before packaging:
DefaultEngine.ini
1
2
[ConsoleVariables]
r.ShaderPipelineCache.Enabled=0

Then write two functions to enable and load PSO at runtime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "ShaderPipelineCache.h"
#include "RHIShaderFormatDefinitions.inl"
#include "HAL/IConsoleManager.h"

bool UFlibShaderPipelineCacheHelper::EnableShaderPipelineCache(bool bEnable)
{
UE_LOG(LogHotPatcher,Display,TEXT("EnableShaderPipelineCache %s"),bEnable?TEXT("true"):TEXT("false"));
auto Var = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ShaderPipelineCache.Enabled"));
if(Var)
{
Var->Set( bEnable ? 1 : 0);
}
return !!Var;
}

bool UFlibShaderPipelineCacheHelper::LoadShaderPipelineCache(const FString& Name)
{
UE_LOG(LogHotPatcher,Display,TEXT("Load Shader pipeline cache %s for platform %d"),*Name,*ShaderPlatformToShaderFormatName(GMaxRHIShaderPlatform).ToString());
return FShaderPipelineCache::OpenPipelineFileCache(Name,GMaxRHIShaderPlatform);
}

This will enable delayed loading of the PSO Cache, which can be manually loaded after a hot update.### Delayed Collection and Storage

Since collecting and storing PSO Cache has additional performance overhead, you can disable the collection and storage of PSO data and enable it at runtime as needed. In DefaultEngine.ini, disable LogPSO and SaveBoundPSOLog, so that automatic collection and storage do not occur when packaging the basic build:

DefaultEngine.ini
1
2
3
[ConsoleVariables]
r.ShaderPipelineCache.LogPSO=0
r.ShaderPipelineCache.SaveBoundPSOLog=0

Then enable it at runtime:

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
UENUM(BlueprintType)
enum class EPSOSaveMode : uint8
{
Incremental = 0, // Fast(er) approach which saves new entries incrementally at the end of the file, replacing the table-of-contents, but leaves everything else alone.
BoundPSOsOnly = 1, // Slower approach which consolidates and saves all PSOs used in this run of the program, removing any entry that wasn't seen, and sorted by the desired sort-mode.
SortedBoundPSOs = 2 // Slow save consolidates all PSOs used on this device that were never part of a cache file delivered in game-content, sorts entries into the desired order and will thus read-back from disk.
};

bool UFlibShaderPipelineCacheHelper::SavePipelineFileCache(EPSOSaveMode Mode)
{
return FShaderPipelineCache::SavePipelineFileCache((FPipelineFileCache::SaveMode)Mode);
}

bool UFlibShaderPipelineCacheHelper::EnableLogPSO(bool bEnable)
{
UE_LOG(LogHotPatcher,Display,TEXT("EnableLogPSO %s"),bEnable?TEXT("true"):TEXT("false"));
auto Var = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ShaderPipelineCache.LogPSO"));
if(Var)
{
Var->Set( bEnable ? 1 : 0);
}
return !!Var;
}

bool UFlibShaderPipelineCacheHelper::EnableSaveBoundPSOLog(bool bEnable)
{
UE_LOG(LogHotPatcher,Display,TEXT("EnableSaveBoundPSOLog %s"),bEnable?TEXT("true"):TEXT("false"));
auto Var = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ShaderPipelineCache.SaveBoundPSOLog"));
if(Var)
{
Var->Set( bEnable ? 1 : 0);
}
return !!Var;
}

Enabling SaveBoundPSOLog will automatically store the collected PSO data. You can choose not to enable automatic storage and manually store it during runtime by calling FShaderPipelineCache::SavePipelineFileCache.

Error Handling

Cook Did Not Generate .scl.csv

Make sure to enable ShaderStableKeys for the project, or the .scl.csv file will not be generated.

No upipelinecache File Generated at Runtime

Please strictly follow the steps in Runtime Capture PSO Data.

  1. Confirm that r.ShaderPipelineCache.Enabled is enabled (in DefaultEngine.ini or DeviceProfile).
  2. Add the -logPSO parameter in ue4commandline.txt.

Bad PSO

If using the official engine, the above process is complete, but sometimes the project requires modifications to the engine to support specific rendering features, such as adding Multi-subpasshint support:

Runtime\RHI\Public\PipelineFileCache.h
1
2
3
4
5
6
7
8
9
10
11
12
struct RHI_API FPipelineCacheFileFormatPSO
{
// ...
struct RHI_API GraphicsDescriptor
{
// uint8 SubpassHint; to SubpassHint[8];
uint8 SubpassHint[8];
uint8 SubpassIndex;
// ...
};
// ...
};

This change requires simultaneous modifications to both the GraphicsDescriptor::StateToString() and GraphicsDescriptor::StateFromString() functions to include serialization support for multi-subpasshint.

However, after modifying this, when using -run=ShaderPipelineCacheTools to generate *.stablepc.csv, the following log error occurs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LogShaderPipelineCacheTools: Expanding matched    1 files: D:\PipelineCaches\*.rec.upipelinecache
LogShaderPipelineCacheTools: : D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache
LogShaderPipelineCacheTools: Expanding matched 2 files: D:\PipelineCaches\*.scl.csv
LogShaderPipelineCacheTools: : D:\PipelineCaches\ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv
LogShaderPipelineCacheTools: : D:\PipelineCaches\ShaderStableInfo-PSOCaching-GLSL_ES3_1_ANDROID.scl.csv
LogShaderPipelineCacheTools: Display: Loading D:\PipelineCaches\ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv...
LogShaderPipelineCacheTools: Display: Loading D:\PipelineCaches\ShaderStableInfo-PSOCaching-GLSL_ES3_1_ANDROID.scl.csv...
LogShaderPipelineCacheTools: Display: Loaded 926 shader info lines from D:\PipelineCaches\ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv.
LogShaderPipelineCacheTools: Display: Loaded 9266 shader info lines from D:\PipelineCaches\ShaderStableInfo-PSOCaching-GLSL_ES3_1_ANDROID.scl.csv.
LogShaderPipelineCacheTools: Display: Loaded 10192 unique shader info lines total.
LogShaderPipelineCacheTools: Display: Loading D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache....
LogShaderPipelineCacheTools: Display: Loaded 115 PSOs
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]
LogShaderPipelineCacheTools: Warning: Bad PSO found discarding [Invertibility=FAIL Verify=PASS in: D:\PipelineCaches\++UE4+Release-4.25-CL-0-PSOCaching_GLSL_ES3_1_ANDROID_0008BF4D08D9069169A7B6820601243D.rec.upipelinecache]

This is because the String reversibility verification for PSO data has failed:

Editor\UnrealEd\Private\Commandlets\ShaderPipelineCacheToolsCommandlet.cpp
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
bool CheckPSOStringInveribility(const FPipelineCacheFileFormatPSO& Item)
{
FPipelineCacheFileFormatPSO TempItem(Item);
TempItem.Hash = 0;

FString StringRep;
if (Item.Type == FPipelineCacheFileFormatPSO::DescriptorType::Compute)
{
StringRep = TempItem.ComputeDesc.ToString();
}
else
{
StringRep = TempItem.GraphicsDesc.ToString();
}
FPipelineCacheFileFormatPSO DupItem;
FMemory::Memzero(DupItem.GraphicsDesc);
DupItem.Type = Item.Type;
DupItem.UsageMask = Item.UsageMask;
if (Item.Type == FPipelineCacheFileFormatPSO::DescriptorType::Compute)
{
DupItem.ComputeDesc.FromString(StringRep);
}
else
{
DupItem.GraphicsDesc.FromString(StringRep);
}
UE_LOG(LogShaderPipelineCacheTools, Verbose, TEXT("CheckPSOStringInveribility: %s"), *StringRep);

return (DupItem == TempItem) && (GetTypeHash(DupItem) == GetTypeHash(TempItem));
}

The key part is in the line DupItem.GraphicsDesc.FromString(StringRep); where the data for GraphicsDesc failed to restore successfully.

After debugging, it was found that the engine records the number of parseable elements in the string for Pipeline Cache Graphics Descriptor, which is the number of entries in the second column of the generated *.stablepc.csv. The official engine defaults to 63, recorded by FPipelineCacheGraphicsDescPartsNum:

Runtime\RHI\Private\PipelineFileCache.cpp
1
const int32  FPipelineCacheGraphicsDescPartsNum = 63; // parser will expect this number of parts in a description string

The status and data of each item in the generated *.stablepc.csv are as follows:

It consists of exactly 63 entries. Please note that in GraphicsDescriptor::StateFromString, there is a check for the expected number of parts, and the data passed to FromString must match the value of FPipelineCacheGraphicsDescPartsNum:

Runtime\RHI\Private\PipelineFileCache.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool FPipelineCacheFileFormatPSO::GraphicsDescriptor::StateFromString(const FStringView& Src)
{
constexpr int32 PartCount = FPipelineCacheGraphicsDescPartsNum;

TArray<FStringView, TInlineAllocator<PartCount>> Parts;
UE::String::ParseTokens(Src.TrimStartAndEnd(), TEXT(','), [&Parts](FStringView Part) { Parts.Add(Part); });

// check if we have expected number of parts
if (Parts.Num() != PartCount)
{
// instead of crashing let caller handle this case
return false;
}
// ...
}

Because we added multi-subpasshint support, we modified SubpassHint from uint8 to uint8[8], adding 7 more data points, so the corresponding FPipelineCacheGraphicsDescPartsNum must also be increased by 7 to become 70, otherwise the StateFromString verification will fail.

After modifying this, regenerating *.stablepc.csv using -run=ShaderPipelineCacheTools no longer results in the Bad PSO error.

Usage and Configuration

You can control the construction of PSO data at runtime through the functions of FShaderPipelineCache:

Runtime\RenderCore\Public\ShaderPipelineCache.h
1
2
3
4
5
6
7
8
9
10
/** Pauses precompilation. */
static void PauseBatching();
/** Resumes precompilation batching. */
static void ResumeBatching();
/** Returns the number of pipelines waiting for precompilation. */
static uint32 NumPrecompilesRemaining();
/** Returns the number of pipelines actively being precompiled this frame. */
static uint32 NumPrecompilesActive();
/** Sets the precompilation batching mode. */
static void SetBatchMode(BatchMode Mode);

The official recommendation is to wait for the PSO to finish building before hiding the LoadingScreen during the loading screen:

1
2
3
4
5
6
7
8
if(FShaderPipelineCache::NumPrecompilesRemaining() > 0)
{
if (OutDebugReason != nullptr)
{
*OutDebugReason = FString(TEXT("PC: PSO cache still compiling"));
}
return true;
}

You can also build during the opening of the UI, cutscenes, or pause menus by handling it through the following three function combinations:

1
2
3
4
5
6
7
8
9
10
11
12
// Pause PSO cache compilation
FShaderPipelineCache::PauseBatching();
// Set the processing mode for PSO
// enum class BatchMode
// {
// Background, // The maximum batch size is defined by r.ShaderPipelineCache.BackgroundBatchSize
// Fast, // The maximum batch size is defined by r.ShaderPipelineCache.BatchSize
// Precompile // The maximum batch size is defined by r.ShaderPipelineCache.PrecompileBatchSize
// };
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Background);
// Resume compiling PSO
static void ResumeBatching();

There are multiple CVars in the engine to control PSO capturing, loading, etc. For detailed information, refer to: UE Console Variables and Command, search for r.shaderpipelinecache to see all supported CVars.

You can also use configurations to automatically build when the game starts by modifying the [ConsoleVariables] configuration in DefaultEngine.ini. They are defined in Runtime\RenderCore\Private\ShaderPipelineCache.cpp within ConsoleVariable, which can be adjusted at runtime or in configuration files as needed:

Enabling automatic PSO data construction at startup will generate the following log:

1
2
3
4
5
6
7
LogRHI: Base name for record PSOs is ../../../FGame/Saved/CollectedPSOs/++UE4+Release-4.25-CL-0-FGame_SF_METAL_8F3222B7964FE2A89C849E90E0000736.rec.upipelinecache
LogRHI: FPipelineCacheFile Header Game Version: 0
LogRHI: FPipelineCacheFile Header Engine Data Version: 17
LogRHI: FPipelineCacheFile Header TOC Offset: 293853
LogRHI: FPipelineCacheFile File Size: 380497 Bytes
LogRHI: Opened FPipelineCacheFile: ../../../FGame/Content/PipelineCaches/IOS/FGame_SF_METAL.stable.upipelinecache (GUID: 00000000000000000000000000000000) with 690 entries.
LogRHI: Display: Opened pipeline cache and enqueued 441 of 441 tasks for precompile with BatchSize 50 and BatchTime 10.000000.

You can also set the PSO’s SortOrder in DefaultGameUserSettings.ini:

[ShaderPipelineCache.CacheFile]
;default is 0
;Default = 0, // Whatever order they are already in.
;FirstToLatestUsed = 1, // Start with the PSOs with the lowest first-frame used and work toward those with the highest.
;MostToLeastUsed = 2 // Start with the most often used PSOs working toward the least.
SortOrder=1
```### Profiling
You can use Unreal Insights to view the time taken for shader linking:
![](https://img.imzlp.com/imgs/zlp/picgo/2023/20230524160612.png)

And by using `stat pipelinestatecache`, you can check the loading and generation status of PSOs.

### Notes

#### ShaderLibrary Requirements
Using PSOs requires changing the shader serialization method to share shader library; if it is in inline shader form, Pre-Compiled PSOs cannot be used. Runtime collection is possible, but it cannot be utilized in the next package build.

#### scl.csv to shk
> In 4.27, StableExtension has been changed from `scl.csv` to `shk`. [ShaderCodeLibrary.cpp#L65](https://github.com/EpicGames/UnrealEngine/blob/4.27/Engine/Source/Runtime/RenderCore/Private/ShaderCodeLibrary.cpp#L65)

![](https://img.imzlp.com/imgs/zlp/picgo/2023/20230525192513.png)



### References

- [PSO Cache](https://docs.unrealengine.com/en-US/SharingAndReleasing/PSOCaching/index.html).
- [Unreal Engine 4: PSO Cache (Pipeline State Object) to Reduce Load Times/Hitches](https://www.youtube.com/watch?v=0tzrsEm6V9Q&ab_channel=StephenMaloney)
- [Introduction to UE4 Rendering Engine Modules (2)](https://zhuanlan.zhihu.com/p/72768828)
The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE project optimization: PSO Cache
Author:LIPENGZHA
Publish Date:2021/04/22 19:40
Word Count:12k Words
Link:https://en.imzlp.com/posts/24336/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!