Guaranteed startup crash in packaged builds when the PSO precache option KeepInMemoryUntilUsed is enabled on NVIDIA / Qualcomm GPUs

Summary

Enabling the PSO precache optimization CVar r.PSOPrecache.KeepInMemoryUntilUsed=1 (documented as recommended for NVIDIA and Qualcomm drivers) causes a 100% reproducible assertion failure during engine boot in packaged builds, before anything is rendered.

Root cause (verified against 5.8 sources):

  1. TPrecachePipelineCacheBase::ReleasePrecachedPSO() (PipelineStateCache.cpp:2796-2799) begins with check(!ShouldKeepPrecachedPSOsInMemory()) — by design the refcounted-release lifecycle and the keep-in-memory lifecycle are mutually exclusive (see “Refcounted and max in memory PSOs can’t be used together” at PipelineStateCache.cpp:2561, and PrecacheTask.RefCount = ShouldKeepPrecachedPSOsInMemory() ? 0 : 1 at line 2651).
  2. However, during UGameEngine::Init, a FGlobalComponentRecreateRenderStateContext is created/destroyed (e.g. while applying startup settings). Its destructor calls FMaterialPSORequestManager::ClearMaterialPSORequests(), which unconditionally calls ReleasePrecachedPSO() for in-flight material PSO precache requests — hitting the assert.
  3. ShouldKeepPrecachedPSOsInMemory() (PipelineStateCache.cpp:464-467) is GPSOPrecacheKeepInMemoryUntilUsed && (IsRHIDeviceNVIDIA() || IsRHIDeviceQualcomm()), so the crash only manifests on the exact GPU vendors the optimization targets. On AMD/Intel the CVar is a silent no-op, easy to miss in testing. It also cannot reproduce in editor/PIE/-game because PSO precaching is compile-time disabled there — only packaged builds are affected.

Suggested fix: make FMaterialPSORequestManager::ClearMaterialPSORequests() (and any other global-recreate cleanup path) aware of the keep-in-memory mode — either skip the refcounted release and let the in-memory LRU handle the PSOs, or make ReleasePrecachedPSO() handle the mode gracefully instead of asserting.

What Type of Bug are you experiencing?

Rendering (Graphics / Niagara)

Steps to Reproduce

  1. Take any project (ours: Third Person C++ template; the trigger path is project-agnostic — UGameEngine::Init + material PSO precache requests in flight).
  2. Add to Config/DefaultEngine.ini:
    [SystemSettings]
    r.PSOPrecache.KeepInMemoryUntilUsed=1
  3. Package for Windows, Development configuration, DX12/SM6.
  4. Run the packaged executable on a machine with an NVIDIA GPU.

Expected Result

The optimization works as documented (precached PSOs kept in memory until first use), or is safely ignored.

Observed Result

Assertion failure + crash reporter during engine boot, every launch, before anything is rendered. Reproduced 2/2 simultaneous game instances, multiple runs. Removing the single CVar line fixes it.

Affects Versions

5.8

Platform(s)

Windows

For crash reports, include your callstack

LogWindows: Error: appError called: Assertion failed: !ShouldKeepPrecachedPSOsInMemory() [File:D:\build++UE5\Sync\Engine\Source\Runtime\RHI\Private\PipelineStateCache.cpp] [Line: 2799]
[Callstack] EOS_OSS_Tutorial.exe!FDebug::CheckVerifyFailedImpl2()
[Callstack] EOS_OSS_Tutorial.exe!TPrecachePipelineCacheBase<FPrecacheGraphicsPipelineCache,FGraphicsPipelineStateInitializer,FGraphicsPipelineState>::ReleasePrecachedPSO()
[Callstack] EOS_OSS_Tutorial.exe!FMaterialPSORequestManager::ClearMaterialPSORequests()
[Callstack] EOS_OSS_Tutorial.exe!FGlobalComponentRecreateRenderStateContext::~FGlobalComponentRecreateRenderStateContext()
[Callstack] EOS_OSS_Tutorial.exe!UGameEngine::Init()
[Callstack] EOS_OSS_Tutorial.exe!FEngineLoop::Init()
[Callstack] EOS_OSS_Tutorial.exe!GuardedMain()
[Callstack] EOS_OSS_Tutorial.exe!GuardedMainWrapper()
[Callstack] EOS_OSS_Tutorial.exe!LaunchWindowsStartup()
[Callstack] EOS_OSS_Tutorial.exe!WinMain()

Additional Notes

System: UE 5.8.1 binary (Epic Games Launcher), Windows 11 Pro (10.0.26200), NVIDIA GeForce RTX 4090 (driver 610.88), DX12 SM6, Development packaged build (BuildCookRun -pak -iostore -compressed).

Extra note: since check() compiles out in Shipping, a Shipping build would not assert but would instead silently run the release path that the keep-in-memory bookkeeping assumes impossible (RefCount 0 objects being released) — i.e. potential silent state corruption instead of a clean crash.

Disclosure: the crash was found by the account owner while testing their project; the root-cause diagnosis and this write-up were done by Claude (Anthropic’s AI assistant) from engine-source analysis, and the account owner reviewed and authorized this submission.