Concept: PSO Precaching & the Grace Period

Why the plugin measures shader compilation instead of trying to predict it, and how the two engine PSO systems differ.

Two independent systems

Unreal ships two unrelated PSO mechanisms. The plugin handles both, but for different reasons and with different counters.

SystemWhat it isPlugin's use
Bundled PSO file cache (FShaderPipelineCache)A .upipelinecache recorded from playtests and shipped in Content/. One large, finite, countable batch at boot.Holds the loading screen with a progress bar.
Runtime PSO precaching (PipelineStateCache)Automatic per-component/material precache issued as things register. No recording step.Optionally drains outstanding requests before a level becomes visible.
The counters overlap
FShaderPipelineCache::NumPrecompilesRemaining() already folds the runtime precache count into its result. That is why the level-load drain reads NumActivePrecacheRequests() directly - using the combined counter mid-game would also make the level wait on the boot-time batch.

The engine cannot tell you whether a boot will be slow

The obvious design is to detect a stale cache and show the screen only then. Unreal does not expose that, and it cannot:

  • The GPU driver's own PSO cache (NVIDIA DXCache and friends) is completely opaque to Unreal. The engine cannot see whether the driver already has your pipelines.
  • The engine does validate the cache file's game version, shader platform and format - but the outcome is "ignore this file", not "tell the game to show a screen".
  • IsPrecompilationSlowTask() is the closest native hint, and it is only set by Vulkan, Metal and OpenGL. D3D12 never sets it, so on Windows it is permanently false.
  • The driver-health signal (bDriverCacheSuspectedUnhealthy) is reactive - it flips only after the player has already suffered around 100 hitches. Useful telemetry, useless as a boot-time predictor.

So the plugin measures instead

A warm driver cache returns compiles in milliseconds; a cold one does not. That difference is directly observable, so there is no need to predict it. The loading screen is already on screen at boot, so the gate simply declines to release it and watches the clock:

text
boot -> loading screen already visible
|
start precompile, poll
|
+------+------+
| |
done <2s still going
| |
release swap widget -> "Optimizing shaders 42%"
(no UI) |
OnComplete -> release

On a normal boot the state never leaves Warming and the player sees an ordinary loading screen. After a game or driver update the work is real, ShaderPrecompileGraceSecs elapses, and the progress UI reveals. Because the screen was already up, there is no flicker of a menu appearing and then being covered.

Why not a version stamp?
Persisting build + GPU + driver identity and comparing on boot was considered and rejected. It cannot see OS or D3D runtime updates, or a user-cleared driver cache, so it produces false negatives - it skips the screen and ships the stutter anyway. Measuring has no such blind spot.

Progress is advisory; completion is authoritative

Two engine constraints shape the API:

  • There is no exported total. The engine keeps it private, so the only supported source for a percentage is the count handed to the precompilation-begin delegate. If that fired before the plugin's module loaded, the plugin falls back to a high-water mark of the remaining count - hence the StartupMode=0 recommendation.
  • The remaining count is not monotonic. It is clamped to a minimum of 1 while precompiling and can increase as further cache tasks queue. The bar can legitimately stall below 100%.
Never gate on "remaining == 0"
The gate releases on the engine's completion delegate, never on the counter reaching zero - Epic notes it may never reach zero in the general case. MaxShaderPrecompileBlockSecs exists as a backstop for the case where completion never arrives.

Where the checks sit

The two checks are deliberately placed at opposite ends of the condition list:

CheckPositionWhy
Shader precompile gateCondition 2, near the topSo it can hold the screen over a main menu that has already finished loading.
Runtime precache drainCondition 16, lastSo it only ever delays the final hide and never gates anything above it.

See also: How-To: Show Shader Precompile Progress for the setup steps, and Concept: Lifecycle & Show Conditions for the full condition list.