PSO Caching Update: UE5

An update to the state of PSO caching and to the tools I was making to ease PSO collection headaches.

Image from an aircraft

Let's begin by saying that I didn't really intend this to be a two – or maybe more – part series on this blog. I'd wanted to create a plugin that took the pain out of PSO collection and precompilation and put it on the Epic Store, but that probably won't happen. I'd halted that when Epic themselves started to work on PSO caching more seriously in Unreal 5.0.

The current state of the plugin still doesn't reach a level of polish I would feel happy commercialising and covering the bases for every type of PSO collection feels untenable. Instead, I've been planning to open source the plugin and the server component it talks to.

♨️
Epic say PSO stutters are a thing of the past with 5.2, but that pop-in may increase: Unreal Engine 5.2 Release Overview | Inside Unreal - YouTube

With the fresh news that Epic say they have solved DX12 PSO hitches, things get a bit easier. To be clear, they are now delaying the render of an object until its PSOs are ready, which will increase pop-in in some cases. This doesn't invalidate the collecting PSOs, but it does mean that incomplete coverage is no longer a critical issue.

So, let's get into this.

Firstly, there's a few things that need configured. Namely, I now suggest setting r.ShaderPipelineCache.StartupMode to 0. By starting the cache paused, the plugin will have the chance to correctly set the internal level index before it starts. If you don't do this, the plugin will actually do this for you when it initialises the game instance.

Plugin and Cache Tools

A while ago, I started building two components for making PSOs a bit more manageable, or at least moving the complexity of the process elsewhere. They are not at a point where I would consider them polished enough to be store assets. Instead, they're destined to be GitHub projects. If you are interested in them, you can find them at the following GitHub links.

Firstly, there is the Unreal side that automates collecting PSOs from test runs and sending them to a server. Secondly, there is a server-side component that stores the PSOs and allows them to be pulled by build servers.

Let's begin with the plugin.

Plugin

The plugin has two goals. The first is provide a set of convenient functions for PSO management, and the other is to aid in PSO collection. To achieve the former, the plugin includes a series of Blueprint Library Functions for controlling the PSO state, such as enabling and setting the compilation mode or waiting for compilation to occur. For the former, the plugin bundles a game instance that can automatically set the game usage mask to a mask based on the level and will upload the PSOs from the run to the server on completion.

A notable quirk of the FShaderPipelineCache that the plugin interacts with is that PreCompileMask is actually just setting the initial UsageMask, which has the effect of doing nothing after we set UsageMask. Just be aware, since the plugin can be allowed to set the UsageMask immediately.

Also, be aware that the plugin setting either the UsageMask or the PreCompileMaskwill do nothing in shipping builds if r.ShaderPipelineCache.GameFileMaskEnabledis not set to a non-zero number; however, the collected PSOs will still receive the correct masks regardless.

Server Component

Written in Node.JS, the server component is a basic web application that exposes an API for managing PSOs. PSOs can be pushed to the server based on UUIDs and can be requested based on the push date, RHI API version, and a version string.

The goal of this server is to be a central collection for a build system to pull and combine from multiple runs of the game. The plugin provides two python scripts that can handle pulling the shader cache files at build time.

// Create Pipeline folder
bat "if not exist \"${WORKSPACE}/PipelineBuilds\" mkdir \"${WORKSPACE}/PipelineBuilds\""
bat "if not exist \"${WORKSPACE}/PipelineBuilds/PCD3D_SM5\" mkdir \"${WORKSPACE}/PipelineBuilds/PCD3D_SM5\""
bat "if not exist \"${WORKSPACE}/PipelineBuilds/PCD3D_SM6\" mkdir \"${WORKSPACE}/PipelineBuilds/PCD3D_SM6\""

// Pull Pipeline Data
bat 'python <ProjectName>/Plugins/UnrealPSOPlugin/BuildScripts/PullData.py "DirectX 12" PCD3D_SM5 "%WORKSPACE%/PipelineBuilds/PCD3D_SM5" "%PullMachineCreds%" "%ProjectCreds%"'
bat 'python <ProjectName>/Plugins/UnrealPSOPlugin/BuildScripts/PullData.py "DirectX 12" PCD3D_SM6 "%WORKSPACE%/PipelineBuilds/PCD3D_SM6" "%PullMachineCreds%" "%ProjectCreds%"'

// Combine and ready
bat "python <ProjectName>/Plugins/UnrealPSOPlugin/BuildScripts/InvokeUEShaderbuild.py \"C:/UE/UE_5.2/Engine/Binaries/Win64/UnrealEditor-Cmd.exe\" \"<ProjectName>\" \"PCD3D_SM5\" \"${WORKSPACE}/PipelineBuilds/\" \"${WORKSPACE}/<ProjectName>/\""

The above code readies the pipeline cache files for compilation and places them in the correct folder to be picked up by Unreal's build process.