PSO Caching in Unreal Engine

Pipeline State Object (PSO) caching in Unreal Engine 5 for Windows (DirectX 12 SM5/6) and Linux (Vulkan SM5). PSO caching enables developer control over shader precompilation and reduces stutter linked to just-in-time shader compilation.

Banner Image
🚩
Unreal 5 introduced changes to PSO caching and broke my plugin. Expect an update post in the coming months

Pipeline State Object (PSO) caching in both UE4 and UE5 relates to the pipeline objects in Vulkan and Direct3D that hold shader and state information. In Unreal, these objects can be preloaded either before the game launches or in menus to cut down on hitches during gameplay caused by a new object not having a ready shader.

These stutters can be quite detracting from the gameplay experience, and are relatively easy to cut down on. At least, once you enable the PSO caching system in Unreal.

Epic have some good guidelines on the periodicity of PSO capture in their FAQ on PSO caching.

I'm going to cover some of the basics of enabling PSO in Unreal Engine 5 below. You will need to jump back to the Epic documentation for one part of the setup as that part is still fully relevant, such as C++ docs for programmatic control.

Configuring PSO

I highly recommend reading the documentation on Unreal Engine's website, but I don't find those complete in the current state.

In short, we need the follow two settings in Project Settings > Project > Packaging. The Deterministic Shader Code Order may be unnecessary, though.

Visual Aid for wanted settings

Next, we want to enable the cache. This is done in Device Profiles. The menu has moved a bit in UE5, and is now located under Tools > Platforms > Device Profiles.

Visual aid for path in Unreal

In here, we want to edit our target platforms. I've only done Windows, as that's my current build target, but you can edit a lot in here. To access platform specific targets, just click the three dots and a second editor window will pop up.

Visual aid for platform specific settings
Visual example of some rendering settings
Example Settings

Let's cover the some settings below. Caveat Emptor, though. I don't use all of these settings.

⚠️
If you set a precompile time limit with MaxPrecompileTime, you need to manual call r.ShaderPipelineCache.Opento start the process. I'm not sure why, but you can do this via BP or C++.

r.ShaderPipelineCache.Enabled

This one enables the pipeline cache. We want this one enabled; otherwise, nothing we do here will have any impact.

r.ShaderPipelineCache.PreOptimizeEnabled

Whether we should enable the pre-optimisation stage. This is done at the first launch of the game and attempts to compile some of the PSOs.

You can manually flip the cache into precompile mode in C++, but there's probably not much point. As far as I can tell, it's mostly for first-launch compilation.

r.ShaderPipelineCache.MaxPrecompileTime

The maximum wall time before the PSO compilation switches from "fast" to "background", and we enter the game. I set this to 60 seconds as an example. You could set it to zero (0) to disable it. Disabling this seems to immediately skip precompilation.

r.ShaderPipelineCache.AutoSaveTimeBoundPSO

Time before we save logged PSOs to disk.

r.ShaderPipelineCache.BatchSize & Family

The batch size is the batch size when programmatically doing compilation. Unreal supports programmer definition of when we want PSO to compile.

For example, I kick PSO into "fast" mode in loading screens, and into "background" mode in a menu. In game, I disable PSO compilation since we now have a more important use of our CPU. PSO compilation doesn't use excessive amounts of disk, so it can be done at load time without significant negative repercussions.

The precompiled batch size is the size of the batch during precompilation at start up, and background batch size is used in "background" mode.

Mode Default Max per Frame
BatchSize 50
BackgroundBatchSize 1
PreCompileBatchSize 50

r.ShaderPipelineCache.BatchTime & Family

The target time per frame in milliseconds dedicated to compiling PSOs.

Mode Default Time
BatchTime 16ms
BackgroundBatchTime 0.0
PreCompileBatchTime 10ms

Interestingly, background is set to 0 by default. This means you get 1 PSO per frame at most in background mode. While this is a reasonable default, you could increase it to enable a more adaptive compilation strategy.

Stable Keys

Now, we're almost configured to follow the documentation on capturing and building with PSO data, but there's one omission in the documentation. This might be a change from UE4 to UE5, but even with -logPSO in my launch settings, I couldn't get either the static PSO or the runtime PSO to generate.

A little hunting through the documentation and the code revealed that the Android example option below is not Android specific. The only difference is that it goes in Config/DefaultEngine.ini rather than Config/Android/AndroidEngine.ini.

[DevOptions.Shaders]
NeedsShaderStableKeys=true

With the above lines in the DefaultEngine.ini, restart Unreal, and now you should be properly configured to follow the rest of the documentation about capturing PSO as laid out on this UE4 page.

Combining PSO Data

UE5 has slightly different syntax

In <ProjectName>\Saved\Cooked\<Platform>\<ProjectName>\Metadata\PipelineCaches, you should find a few files. These don't match quite what the Building the PSO Cache page says, though.

Firstly, the .scl.csv files referenced in the docs have become .shk files, and there's a .pipelinecache in there.

Secondly, T5 is the name of the test project I've used for this post. Replace it with your project name.

Visual aid with example files
T5 is the project name here

When you run a project with -logPSO on, the runtime PSO will be placed in <ProjectName>/Saved/CollectedPSOs from the exe of the captured build. This may seem a bit counterintuitive as it actually ends up being <Project>/Saved/StagedBuilds/<Platform>/<Project>/Saved/CollectedPSOs when run via the project launcher.

"C:\Program Files\Epic Games\UE_5.0\Engine\Binaries\
Win64\UnrealEditor-Cmd.exe" "Z:\T5\T5.uproject" -run=ShaderPipelineCacheTools Expand Z:/Pipelines/T5/*.upipelinecache Z:/Pipelines/T5/*.shk Z:/Pipelines/T5/T5.spc

move T5.spc "Z:\T5\Build\Windows\PipelineCaches\"

Note that Expand is capitalised for UE5.

There is a chance that the .spc needs to be named depending on the project and 3D API in use. For example, instead of T5.spc it will need to be T5_PCD3D_SM5.spc for Direct3D using Shader Model 5.

... build "Z:/T5/Build/Windows/PipelineCaches/*T5_PCD3D_SM5.spc" ...
Commandlet executed by Unreal Engine during the Cook process for Direct3D with SM5

As seen above, the Unreal Engine will invoke a build of the PSO data placed in PipelineCaches during the cooking process. The engine is using a wildcard in front of our *.spc files, so we can version them by prepending some information.

Just a note: you can set the r.ShaderPipelineCache.LogPSO cvar instead of using -logPSO, but I prefer specifically launching PSO logging builds.

At least for my own use, that is. If you need a build for playtesting that logs PSOs, consider building with the cvar set in the target platforms window instead.

As an aside, you can also place the following in DefaultGame.ini or GameUserSettings.ini to specify which cache to use. The PCD3D_SM5 or SF_VULKAN_SM5 will be added by the engine.

[ShaderPipelineCache.CacheFile]
LastOpened=<CacheName>

Caveat in Unreal 5 (2022-Apr-04)

In Unreal 5, and possibly UE4, PSO caching is disabled for raytracing shaders due to an issue.

Visual aid of line 724 of PIpelineFileCache.cpp
PIpelineFileCache.cpp