New editable sample widgets + BP for those interested:
CreateEditableLoadingWidget.py (5.6 KB)
LoadingWidgetTemplates_README.md (2.4 KB)
New editable sample widgets + BP for those interested:
CreateEditableLoadingWidget.py (5.6 KB)
LoadingWidgetTemplates_README.md (2.4 KB)
1.3.1a, 2026-06-04
Bug Fixes
• Fixed Infinite Loading Screen: Resolved an emergent deadlock issue where the loading screen would hang indefinitely at “Finding startup assets…” when launching in Standalone Game mode. The plugin now correctly binds and listens to the Asset Registry OnFilesLoaded delegate without prematurely yielding the tick.
• UE 5.7 Compilation Fix: Fixed an “incomplete type” / “unrelated pointers” compilation error in PSOAutopilotSubsystemHelpers.cpp on Unreal Engine 5.7 by adding the explicit include “UObject/Package.h” dependency for GetTransientPackage() during NewObject construction.
FPSOAutopilotPlatformOverride) lets you replace the effective items-per-step, frame-time budget, memory-cleanup, and pipeline-cache wait on a named platform (Windows, Mac, Linux, Android, IOS). An enabled override matching the running platform is overlaid onto the runtime snapshot; disabled or non-matching entries are ignored.PSOAutopilotCoverage commandlet resolves the configured warmup content via the asset registry, prints a per-type coverage report, and returns a non-zero exit code when discovered assets fall below -MinAssets=<N>, so a content/cook regression fails the build. Run: UnrealEditor-Cmd.exe <project> -run=PSOAutopilotCoverage -MinAssets=50. Its dedup/counting/threshold accounting is extracted into FPSOAutopilotCoverageReport and unit-tested.Scripts/Run-CI.ps1 chains build → PSOAutopilot.* automation tests → coverage gate and fails fast on the first failed stage, for local pre-flight or a CI runner. The coverage floor defaults to -MinAssets 900, so losing the demo content set fails the gate.ApplySimplePresetTuning) applied identically to the persistent settings object and the runtime snapshot, so the two can never drift.CaptureFrom and ApplyTo now share one field list (PSO_SNAPSHOT_COPYABLE_FIELDS X-macro), so adding or removing a snapshot-copied setting cannot desync capture and restore.CaptureEffectiveFrom layers the Simple-mode seed and the per-platform override over the raw capture; Advanced mode passes raw content through unchanged.FPSOAutopilotSettingsSnapshot::Sanitize() trims, de-duplicates, and clamps the snapshot before warmup consumes it (wired into the live warmup start path alongside CaptureEffectiveFrom).FPSOPipelineFlushTracker.Automation RunTests PSOAutopilot, NullRHI), exit code 0: SettingsPresetTuningParity, SettingsSnapshotRoundTrip, SettingsEffectiveCapture, SettingsSanitize, SettingsPlatformOverride, PipelineFlushTracker, SettingsSimpleSetupMerge, LiveSmokeTest, CoverageReport, CoverageFolders.-MinAssets=0 → “Coverage gate passed (1001 >= required 0)”, exit 0; -MinAssets=5000 → “Coverage gate FAILED … below … 5000”, exit 1.Scripts/Run-CI.ps1 runs build + tests + coverage green end-to-end (exit 0).PS1 files are not allowed in Fab packages. I provide them here for those interested in up-to-date validation and integration scripts:
Run-CI.ps1 (9.8 KB)
ShippingValidation_README.md (1.8 KB)
Build-ShippingValidation.ps1 (11.8 KB)
This plugin seems amazing! We’d love to buy it for our project, but we need it to be compatible with UE5.8. version.
This technical update adds per-platform tuning, headless coverage/CI tooling, and a phase-aware
pipeline-flush ETA. Finally it hardens the internals with a full decomposition of the warmup engine.
The monolithic warmup subsystem (2,700+ lines) was decomposed into a thin coordinator plus
eight focused, individually unit-tested collaborators. Runtime behavior, the Blueprint API,
and the C++ public API are unchanged.
UPSOAutopilotSubsystem (coordinator: state machine + Tick)
│
┌───────────┬───────────┬───┴────────┬─────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼ ▼
Progress Fingerprint Warmup Package Batch JIT
Reporter Store Discovery Preload Mgr Loader Warmup Mgr
│ │ │ │ │ │
└───────────┴───────────┴─────┬──────┴─────────────┴─────────────┘
▼
AssetWarmupEngine + TextureStreamingStep
(shared per-asset PSO precache & texture work)
Source/PSOAutopilot/{Public,Private}/Warmup/: FPSOWarmupProgressReporter (progress bands, summary text, ETA inputs), FPSOWarmupFingerprintStore (smart-cache skip persistence), FPSOWarmupDiscovery (asset scanning + runtime-asset queueing), FPSOPackagePreloadManager (gameplay-map preloads), FPSOAssetWarmupEngine (per-asset PSO precache work), FPSOBatchLoader (time-sliced batch loop), FPSOJitWarmupManager (just-in-time actor-class warmup), FPSOTextureStreamingStep (texture residency phase). The pipeline-flush accounting extracted earlier (FPSOPipelineFlushTracker) completes the set.ResetWorkAndCompileEvents, AbandonInFlightWork, ReleaseEngineRequests, ClearStagedNames), each a 1:1 wrap of the previous inline call sites. Dead code removed (MergeRuntimeAndFilter, QueueByPath).EPSOWarmupState moved to its own thin header Public/PSOAutopilotWarmupState.h. PSOAutopilotSubsystem.h still includes it, so existing C++ user code compiles unchanged; the Blueprint enum path is identical..ini (new test-only config-path seam), so running the suite no longer clears the developer’s real smart-cache history.Scripts/Run-PackagedWarmupSmoke.ps1): cooks a Win64 build, launches it, and asserts the warmup state machine reaches Finished (Success=true) without hanging; catching the class of regression that headless NullRHI tests cannot see. The script clears the smart-cache fingerprint itself so a cached skip can never produce a vacuous pass.Scripts/Run-CI.ps1 reliability fix: stage waits use WaitForExit() instead of Start-Process -Wait, which could hang indefinitely on a lingering editor child process.Warmup Finished. Success=true, no livelock.BuildPlugin builds succeeded on UE 5.5, 5.6, and 5.7 with the decomposed module layout.UE5.8 update in this case is a challenge, but I want to do it sooner rather than later. ![]()
/Content (fixes potential sample asset issues on UE 5.5). Nothing depended on it: every path in the runtime module, the editor module, and the documentation resolves /Game/..., never the /PSOAutopilot/... mount point. The loading screen is created in your project by the setup wizard, and the demo is generated by Scripts/CreatePSOAutopilotDemo.py; both of which save through your editor, so the assets always match your engine version. This removes thePlugins/PSOAutopilot/Content/ folder — and its warnings, if present — can survive the upgrade. Delete that folder once; nothing references it.Public/Warmup/*.h headers used FSoftObjectPath, TSubclassOf,TObjectPtr, and TWeakObjectPtr while including only CoreMinimal.h, which no longerStopWarmup — which is BlueprintCallable and the Cancel path on the loading-flowUpdateResource() tearsSetForceMipLevelsToBeResident per duplicate; in a single frame, at the exact moment the loadingPSOAutopilot.WarmupMaterialDedup test that pins the de-duplicationReset.The setup wizard generates the demo for your specific setup, no more manual stuff.
Generate Demo Content. Window > PSO Autopilot Startup Setup now has a button that creates the
demo in your project: the boot flow, the stress level, 1000 material instances, the loader and the
loading screen. It is offered whether or not the demo already exists — labelled Regenerate Demo
Content when it does — because the generator is idempotent and anyone clearing a stale
Content/ folder after upgrading from 1.3.5 needs exactly this.
Before this, generating the demo required knowing that an experimental, disabled-by-default
engine plugin had to be enabled, and then building it. The plugin has shipped no bundled demo content since 1.3.6, by design.
The wizard tells you when Python is missing. Generate Demo Content needs the Python Editor
Script Plugin, which Unreal disables by default. If it is off, the wizard now names the menu path
instead of idling.
The setup wizard’s validation logic no longer lives inside the widget.
SPSOAutopilotSetupWizard.cpp drops from 1,973 to 878 lines. All 13 validation rules, their
supporting helpers and the discovery-preview scan move into FPSOAutopilotSetupValidator, which
has no Slate dependency and reaches the editor only through a small probe interface.
This is why the editor module had no tests for its entire life: the logic was private to a Slate
widget and unreachable. Runtime behaviour, the wizard’s appearance and every message string are
unchanged — rule bodies were moved verbatim.
inline/constexpr before the split. Two .cpp files in one module defining the samePSOAUTOPILOT_API andPrivate/, so a C++ caller received aPublic/; no API changed.PSOAutopilot.LoadingFlowActionLifecycle pins the async node’s contract: the factory returns aActivate(), and cancellingPSOAutopilot.StatusSnapshotParity asserts every status field equals the getter it mirrors, soRemoved a deprecated engine call that would have broken future builds.
ForEachObjectWithOuter was being passed a boolean bIncludeNestedObjects; UE 5.8 deprecates
that form in favour of an EGetObjectsFlags enum and warns that it will stop compiling in a
later release:
warning C4996: 'ForEachObjectWithOuter' with a boolean bIncludeNestedObjects has been deprecated
- please use the EGetObjectsFlags enum instead - Please update your code to the new API before
upgrading to the next release, otherwise your project will no longer compile.
Because true is the default on 5.5–5.7 and IncludeNestedObjects is the default on 5.8, the
argument was simply dropped — identical behaviour on every supported engine, and no
version-specific code.
Who this affected: the plugin ships as source, so on 5.8 this was a compiler warning. Editor work and PIE were unaffected.
BuildPlugin against every supported engine as aVersionName and Version encode each other (1.4.0 → 140).PSOAutopilot.EditorPreviewWidgetFlags pins the nested-object traversal inBuildPlugin — editor and packaged-game targets — against UE 5.5, 5.6, 5.7Scripts/CreatePSOAutopilotDemo.py produced the full setPSO Autopilot Warmup Finished. Success=true on content that engine generated itself —FingerprintMatched=false, so the smart-cache skip did not short-circuit the asset loop.UnrealGame Shipping target on all four engines, and a packaged Shipping build completesWITH_EDITOR isUE_BUILD_SHIPPING is set, and optimisation differs — so a Development-only smoke doesA fix release over 1.4.0., aimed at projects that build file-by-file; upgrading from 1.4.0 needs no project changes.
Issues in projects that do not use unity builds. Five files used symbols
whose declaring headers they never included. Two of the five files are shipped runtime and editor source; the other three are automation tests, which are not shipped.
Start PSO Autopilot Loading Flow (Async) fired On Completed when it was cancelled.
Cancelling after the node had started reported completion, telling your graph the level had been
prepared, and the On Cancelled pin could not fire.
The async node could be garbage collected in the middle of a flow. A loading screen that removes its own widget while warmup is still running could have the
flow collected out from under it.
Generate Demo Content issues. The editor’s Python console command reports
that it routed the request, not that the script ran – and it returns the same answer when the
interpreter is not ready and the command is merely queued.
BuildPlugin gates, plus the collision scan, on Unreal EngineAn attempt at a visual explainer of the Setup Wizard:
A fix release over 1.4.1.:
Upgrading needs no project changes. A Content to Prepare row that shows a path like
D:/Projects/MyGame/Content is now resolved correctly at warmup; editing that row in the settings
rewrites it to /Game for good.
The Content to Prepare row (and the Advanced Content Folders row) uses a directory picker, so choosing a folder stores an absolute path such as D:/Projects/MyGame/Content. Unreal’s asset registry only understands package paths like /Game, so warmup asked it for a path that could never match and prepared none of the content in that folder.
Both sides now share one conversion, which also understands plugin content (.../Plugins/FancyFX/Content/Emitters resolves to /FancyFX/Emitters, not /Game/...). A folder that cannot be expressed as a package path is now reported as an error in the log and left in place in the settings, rather than scanned as if it were valid.
Settings now repair the stored value. Editing a content folder rewrites it to its package path, so the value cooked into a packaged build is already /Game/... instead of a path that only resolves on the machine that set it.
New
: Docs/Setup_Wizard.html (name subject to change) – a control-by-control reference for the Setup Wizard main screen. It covers the four action buttons (including which two fixes Fix Safe Items deliberately will not run), every Simple Setup row with the preset tuning numbers, how to read Preflight Results, and what the two escape hatches – Skip Packaging Validation and Allow Custom Startup Level – actually switch off, together with what you give up by ticking them.
New
: Docs/Setup_Wizard_Visual.html (name subject to change) – the same screen as an interactive diagram. May be actually more immediately helpful than the above. Selecting a control explains it; ticking Skip Packaging Validation rewrites the two preflight rows it silences in front of you, and Allow Custom Startup Level softens the startup-level error from ERROR to WARN. A flow diagram underneath lights up the stage of the boot each setting governs. Its labels were read from the running wizard rather than transcribed.
C4996 count 0 on every run.The big 1.4 update keeps producing new hiccups, and this fix remedies another batch of them. Upgrading from 1.4.x needs no action. Upgrading from 1.3.x may need one leftover file deleted.
A cooked smoke could pass judgement on a package built without the plugin in it, and say
nothing. The smoke now tees the cook to Saved/Logs/smoke-cook.log and fails immediately if the plugin was
skipped, printing the EngineVersion the descriptor actually declares. It also says explicitly
not to repair the content: the map and the Blueprints are never damaged, only unreadable while
the plugin is absent. Pinning the descriptor and rebuilding restores them with no asset edits.
This only ever affected local validation against a scratch project. Shipped packages were never
exposed, because the packer rewrites EngineVersion per engine when it builds each zip – which
is exactly why the shipped zips worked where a raw repository copy did not.
A project could stop compiling after upgrading from 1.3.x, with an error naming two copies of
the same plugin header:
PSOAutopilotLoadingFlowHandle.h shipped in Private/ in 1.3.6, and moved to Public/
in 1.4.0 so that C++ callers could include it. An in-place upgrade installs the new copy but does
not delete a file the new version dropped, so both end up in the plugin folder.
The packages were never defective. All versions contain only the
Public/ copy, and the packer wipes its staging directory, so it cannot accumulate a stale file.
The duplicate only ever existed in an installed folder that had been upgraded over.
The fix is to delete the Private/ copy and rebuild; clearing Intermediate is not needed.
The build now diagnoses it. PSOAutopilot.Build.cs checks for the stale header and fails with
the full path to delete and the reason it is there. It fires as a rules error before Unreal
Header Tool runs, so the actionable message replaces the cryptic one rather than following it.
A guard can only help someone upgrading to a version that
contains it. Anyone moving from 1.3.x to 1.4.2 or earlier still sees the raw Unreal Header Tool
error, which is why the FAQ now carries the answer as well.
Advanced > Audio > Mute Audio During Warmup, default off. Silences the game while the
warmup runs, and hands the player back exactly what they had. It is opt-in because changing audio
behaviour by default would surprise every existing project on upgrade.
It works by pushing a sound mix, not by writing a volume, and that is the whole design. Both
global volume levers are absolute values that engine code writes without coordinating with anyone:
APlayerCameraManager sets the transient primary volume on every camera fade, and the platform
focus handler resets the app volume multiplier on every alt-tab. A fade during a loading screen is
the expected case, not an edge case, so a save-and-restore cannot be made correct against either –
and the transient primary volume cannot even be read from the game thread, because its getter
asserts it is on the audio thread. A mix push is a stack: it composes with whatever else is set,
and needs no saved state at all.
The mix is built at runtime rather than shipped as an asset, because the package deliberately
carries no /Content.
The unmute is attached to the single state transition into Idle, not to the four exits that
reach it – completion, cancel, failure and shutdown. A push with no matching pop would leave the
player silent for the rest of the session without saying so, and a fifth exit added later would be
the one to forget.
PSOAutopilot.AudioMuter pins the mix contents – including the flag that applies it to childA version banner. The version and descriptor number are read from the plugin descriptor at
runtime rather than hardcoded, so a support log always names the build that produced it.
A startup diagnostic, logged once about five seconds in. It warns when Simple Setup names a
Loading Level that the project’s startup map is not, or is but holds no Loader actor – the exact
state in which the plugin is installed, configured, and never runs. It stays silent in the editor
and in PIE, and once anything has requested a warmup.
It runs from a one-shot ticker rather than the warmup’s own tick, because that tick is disabled in
precisely the idle state being detected. Five seconds is deliberately short: a blocking level load
does not advance the core ticker, so it is measured in frames a player actually sees.
PSOAutopilot.VersionBanner asserts the descriptor lookup resolves, so a mistyped plugin nameNiagara effects are now fixed: a muzzle flash, an impact or an explosion no longer compiles its
pipelines the first time it fires. The demo is rebuilt as The Stutter Lab, which explains PSO hitches
and then measures the fix live on the machine it runs on.
Upgrading needs no action. Content to Prepare folders that hold VFX simply start contributing them.
A folder of VFX warmed its materials and none of its effects. UNiagaraSystem was not in the
discovery class filter, so an asset pack’s effects contributed only their materials – and those
were precached against the static-mesh vertex factory, never the sprite, ribbon and mesh particle
factories an effect actually draws with. The first time a weapon fired, its muzzle flash compiled on
the spot.
The fix works through the effect asset itself, which can collect every enabled renderer’s
material and vertex factory without an instance. Discovery now includes UFXSystemAsset (the
Engine-module base of both UNiagaraSystem and Cascade’s UParticleSystem), and the warmup finds
effects wherever it already looks: folders, explicit assets, and through the Blueprints, levels and
components that reference them – including a property that is only spawned later with
SpawnSystemAttached. Each one’s precache is launched and the loading screen waits for it.
No Niagara dependency was added: the plugin still requires nothing beyond the Engine module.
What differs by engine:
bCacheOnLoad = false) and, with ther.PSOPrecache.ProxyCreationStrategy=1, hides an effect until its pipelines are ready. AnUFXSystemAsset::PrecacheAssetPSOs(), new in 5.8.r.PSOPrecache.NiagaraPrecachePSOAtAssetLoadingTime=1,Not covered: effects whose materials are only chosen at runtime (a material set on the spawned
component). Nothing about those can be predicted from the asset.
Measured in a packaged Development build on UE 5.8 (RTX 5060 Ti, D3D12), with a probe that runs
the warmup over a folder of Niagara templates and then spawns each one the way gameplay spawns a
muzzle flash, reading r.PSOPrecache.DumpStats before the first spawn:
| 1.4.3 | 1.5.0 | |
|---|---|---|
| Niagara PSOs requested before the first spawn (sprite / ribbon / mesh) | 12 / 12 / 12 (engine defaults only) | 14 / 14 / 13 (the effects’ own) |
| Frames each effect stayed invisible, three partly-cold runs | 4-11 | 1-3 |
| The one fully cold run | 73-134 frames (200-360 ms) | – |
| Draws that found their pipeline still compiling, proxy delay off | 3 | 0 |
The fully cold figure could only be taken once: the GPU driver keeps its shader cache across
launches, and the fixed build’s first run inherited the baseline’s. Every later comparison was taken
from a fresh copy of the build and ordered so the baseline ran second, which biases against the
fix. The before-first-spawn PSO count does not depend on caches at all.
PSOAutopilot.DiscoveryFXSystems – a Content to Prepare folder of Niagara systems discovers everyPSOAutopilot.WarmupEngineFXSystemAsset – an effect is queued, processed and counted, both as anUParticleSystem stands in for Niagara, so the tests need no Niagara dependency either.PSOAutopilot.Discovery.FXSystemsPSOAutopilot.Discovery into a group node in the automation tree, and the existing leaf testPSOAutopilot.WarmupEngine). Caught byThe Stutter Lab (/Game/Demo/Demo_StutterLab, generated by Scripts/CreatePSOAutopilotStutterLab.py
and by the wizard’s Generate Demo Content). A self-running loop in six chapters: what a PSO is, a
cold start with four waves of never-seen materials and Niagara effects, PSO Autopilot preparing an
equivalent set behind a loading panel, a prepared start with that set, and a scoreboard. A live
frame-time graph, hitch counter and pop-in counter run throughout. Space skips a chapter, R restarts,
H hides the overlay.
The comparison is built to be fair. A pipeline can be cold only once per process, so the Cold
and Prepared sets are structurally identical but share none: every set has its own master materials
(each bakes a unique constant into its shader code) and its own copies of the effects, whose sprite
and ribbon renderers are pointed at a set-unique particle material. The generator re-materials every
renderer it can, disables any it cannot, and checks that no material is shared between sets.
Cold/Prepared pairs are consumed for good, so a restart can never pass warm content off as cold; once
they run out, the Lab replays its first measurement and says so.
It accounts for the GPU driver’s memory. A driver keeps compiled pipelines across launches –
and shares them between the editor and a packaged game. Three things follow, each learned the hard
way while building the Lab:
Saved/PSOAutopilot/StutterLab.ini), so a second launch-StutterLabReset forgets.Its materials are deliberately heavy. With simple materials only a machine’s very first launch
showed a cost: afterwards the driver built each new pipeline well inside a frame. Every Lab material
now carries an unrolled, salted noise loop – the kind of per-pixel work real materials do – so each
set’s own pipelines take real time to build on every launch.
It says where every number comes from. The engine turns PSO precaching off in the editor, so in
PIE the Lab runs as Editor preview and tells you to package; a packaged Development build measures
live and saves the result to Saved/PSOAutopilot/StutterLabResults.json. A recorded measurement can
be played back (Presentation mode, -StutterLabPresentation) for Movie Render Queue, whose fixed
timestep makes live frame times meaningless – and the overlay badges it as recorded, with the engine,
build configuration, RHI and GPU it came from.
The overlay is painted in Slate, so the plugin still ships no content, and it is laid out on a
1920x1080 reference that scales to any viewport.
The Lab is deliberately not the boot flow’s destination: the loader warms its destination level
with soft-reference expansion, which would reach the Lab’s Cold content and pre-warm it.
Measured in packaged Development builds on UE 5.7.4 (RTX 5060 Ti, D3D12), 20 items per show:
| Launch | Cold: items that popped in | Cold: longest wait | Prepared: items that popped in | Prepare time |
|---|---|---|---|---|
| First launch of the build, SM6 | 4 / 20 | 211 ms | 0 / 20 | 72 s |
First launch on SM5 (-sm5), heavy materials |
4 / 20 (avg 9.45 frames invisible) | 301 ms | 0 / 20 | 0.7 s |
| Any later launch, SM6 or SM5, a never-seen pair | 0 / 20 | 0 ms | 0 / 20 | 0.3-0.9 s |
The cost is concentrated in a machine’s first launch. A fresh pair on a later launch costs
nothing even with heavy shaders: on this GPU a new pipeline builds within a frame once the driver
is warm. The first launch is different because every pipeline – the engine’s own ~2,000 global
ones included – is new at once and they queue behind one another. That is also the moment players
meet a game, and it is why the Lab keeps its first-launch recording and says so on the scoreboard.
The fully cold figures could each be taken once; SM5 was used for the second because it is a
shader platform this machine had never run, which reproduces a first launch without clearing the
driver’s shader cache.
PSOAutopilot.StutterLabMeterFrames, StutterLabMeterPopIn, StutterLabTimeline,StutterLabResultsJson pin the scoreboard’s arithmetic: the hitch threshold is strict, items stillIs Audio Muted For Preparation (IsWarmupAudioMuted), a Blueprint query for a loading screenPSOAutopilot.AudioMuteWiringCancel and AudioMuteWiringFinish drive a real subsystem through aRun-CI.ps1, PowerShell 7): build OK, 52/52 tests, coverage 1001 >= 900, stale-header guard OK.The warmup can no longer trip the engine’s precaching crash, the setup checks itself, the setup
wizard’s choices finally reach your config files, and two new editor tools show where startup
preparation spends its time and which shader permutations it could drop: a rebuilt Bloat
Profiler and the new Shader Diet.
| Change | You notice it if… | |
|---|---|---|
| Precache Crash Guard: the warmup no longer rebuilds the shaders of materials already on screen | your warmup runs behind a menu, a lobby or a loading level | |
| Texture preheat respects the streaming pool | your project is large, or its streaming pool small | |
| Setup checks itself: Message Log page and one notification, at editor start, on a setting change, before Play | always, in the editor | |
The setup wizard’s choices reach Config/Default*.ini |
you set the plugin up with the wizard before 1.6 | |
| Bloat Profiler rebuilt: a static scan of the content you prepare, with its shader maps | you want to know what your startup warmup costs | |
| Shader Diet: permutations your project compiles but its content never uses | you want fewer pipelines to prepare, on every platform |
Upgrading needs no action. The Precache Crash Guard and automatic setup validation are on by
default; both can be turned off under Project Settings > PSO Autopilot > Safety.
If you set the plugin up with the Setup Wizard before 1.6: its changes may never have been
written toConfig/DefaultGame.ini/DefaultEngine.ini(why).
Start the editor once: the automatic check lists anything that is missing, and Fix Safe Items
now writes it to disk.
The warmup rebuilt the shaders of materials already on screen. It called
UMaterialInterface::CacheShaders() on every material it prepared. On UE 5.5-5.8 that call ends in
RecacheUniformExpressions(true): the material’s uniform buffer is recreated, but the render state
of primitives already drawing the material is not, so their cached mesh draw commands keep the old
buffer and the RHI thread later binds freed memory. A warmup that runs while a menu, a lobby or the
loading level is visible can crash that way. The call is now made only for a material that has no
shaders for rendering yet – in a cooked build, practically none. An object still waiting for
PostLoad has its load finished instead.
| The material, when the warmup reaches it | Before 1.6 | 1.6 |
|---|---|---|
CacheShaders() |
||
CacheShaders() |
||
CacheShaders() |
CacheShaders(), as before |
UE 5.7 made the feature-level GetMaterialResource overload final and always null. The guard
reads the rendering resource through the shader-platform overload on 5.7 and later; read the old
way, every material would have looked “never set up” and been rebuilt anyway.
A cancelled run dropped its loaded content while its pipelines were still compiling. Stop and
cancel released the batch’s objects at once; garbage collection could then destroy a material with
PSO precache tasks in flight. Those objects are now held until their tasks finish (at most 30 s),
and released at shutdown.
r.PSOAutopilot.CrashGuard (1 by default) to turn it off without a rebuild. Either one offGetCrashGuardStats, Blueprint): what the guard did in thePrecache Crash Guard: 16 materials already set up for rendering were left alone, 0 finished loading, 0 were set up.Warmup/PSOPrecacheCrashGuard.h/.cpp: the decision table as a pure functionDecideCacheShaders), PrepareMaterialForPrecache, which replaces the direct CacheShaders()FPSOAssetWarmupEngine::WarmMaterial, and FPSOPrecacheQuarantine. The subsystem owns theAddReferencedObjects) and ticks it while it holdsFPSOBatchLoader::GetInFlightWork hands a cancelled batch over before Reset() drops it.PSOAutopilot.CrashGuard.Decision pins the decision table, PSOAutopilot.CrashGuard.QuarantinePSOAutopilot.CrashGuard.Summary the log line.Texture preheat could pin more textures than the streaming pool holds. Every texture of every
prepared material was forced resident for 300 s, with no limit, starving video memory on a large
warmup exactly while the pipelines compile. Forced residency is now reserved against half the
streaming pool; the rest streams normally, and the log says how much was held and how many
textures were left to streaming.
texture streaming pool ████████████████████████
forced resident ████████████ ← at most half, 300 s window, refreshed on reuse
every other texture ············ ← streams as usual
FPSOTextureStreamingStep::TryForceResident reserves each texture’s size (all mips) against theFitsResidencyBudget, pure); the warmup engine reaches the step through the newFPSOWarmupRuntimeContext::TextureStreaming. An unknown pool, or streaming off, never caps.PSOAutopilot.TextureResidencyBudget: the budget stops at half the pool, remembers what itThe Startup Setup checks run automatically, and report in one place:
🚀 the editor has started (once the asset registry has scanned) ─┐
⚙️ a PSO Autopilot setting changes (2 s after the last edit) ├─▶ 📋 Message Log › PSO Autopilot
▶️ Play In Editor is about to start ┘ a 🛠️ fix beside each problem that has one
│
only when the set of problems changes
▼
🔔 one notification: [Open Startup Setup] [Fix Safe Items] [Dismiss]
Dismiss silences that exact set of problems for the project; a new or changed problem notifies
again. Project Settings > PSO Autopilot > Safety > Validate Setup Automatically turns it off. It
does not run in commandlets and shows no notification in unattended runs.
The wizard and the automatic check now read the engine side too:
| Engine check | Shown as | Fix |
|---|---|---|
PSO precaching off (r.PSOPrecaching=0): nothing can be prepared |
where to look – a device profile or platform ini can set it, so no one-click fix | |
Component precaching off (r.PSOPrecache.Components=0) |
– | |
| Niagara’s load-time precache off (UE 5.5-5.7) | – | |
| Proxy creation strategy 0 / 1 / 2: what an unprepared object does on its first frame | – | |
| Precache Crash Guard off |
UFXSystemAsset when the runtime discovery was (1.5.0), so a folder of Niagara systems previewed asSetup/PSOAutopilotSetupMonitor (an editor subsystem) runs the checks and owns the Message Log pageSetup/PSOAutopilotEngineChecks appends the engine rows to the project rowsbDependsOnOpenLevel. The editor modulePSOAutopilot.Setup.EngineChecks, PSOAutopilot.Setup.MonitorPolicy (the notification fingerprintPSOAutopilot.Setup.PreviewCountsVFX (0 of 7 before the fix, 7 of 7 after),PSOAutopilot.Setup.MonitorSkipsOpenLevelRows.r.PSOPrecaching=0 set on purpose: “1 of 19 checks needThe setup wizard’s choices were lost on restart and never reached a packaged build. Every
wizard action, the notification’s Fix Safe Items and the wizard’s own settings panel saved with
SaveConfig() alone, which for a defaultconfig class updates only the running editor’s config:
nothing reached Config/DefaultGame.ini or DefaultEngine.ini, so the startup level, loading
level, content folders and packaging entries were gone after a restart and a cook never saw them.
Setting them in Project Settings was not affected. The wizard now writes each change to the default
config file, as Project Settings does. Found while recording the 1.6 tutorial.
| Where a wizard change went | Before 1.6 | 1.6 |
|---|---|---|
| The running editor | ||
Config/DefaultGame.ini / DefaultEngine.ini |
||
| After an editor restart | ||
| A cooked build |
PSOAutopilotConfig::PersistToDefaultConfig,TryUpdateDefaultConfigFile). It is injectable, so the tests use a recorder and never rewrite theUPSOAutopilotSettings’s own ApplySimpleSetupToAdvancedSettings and PostEditChangePropertyTryUpdateDefaultConfigFile).PSOAutopilot.Setup.PersistWritesDefaultConfig (a temporary ini under Saved/PSOAutopilot/Tests),PSOAutopilot.Setup.ActionsPersistSettings.DefaultGame.ini / DefaultEngine.ini.The Bloat Profiler scans instead of waiting for a run. It used to list the top ten assets of the
last Play In Editor warmup, where PSO requests are zero by design. Scan Content to Prepare now
reads the same content the warmup uses, follows each asset’s package dependencies to the materials
it reaches, and counts the shader maps and shaders behind them (a master’s, or a static
permutation’s own). The scan is time-sliced, so the editor stays responsive on large projects.
| View | One row per | Sorted by, at first |
|---|---|---|
| asset you prepare, with the materials it reaches | Shader Maps, heaviest first | |
| master or static permutation | Shaders, heaviest first | |
| folder, summed | Shader Maps, heaviest first |
A filter, double-click to open,
CSV export to Saved/PSOAutopilot/BloatProfiler_<time>_*.csv,
and the last run’s live counts merged in.
The Shader Diet (Tools > PSO Autopilot > Open Shader Diet, or Window > Tools) finds shader
permutations the project compiles, cooks and prepares but its content does not use:
| Finding | What it looks for | Apply |
|---|---|---|
| Allow Static Lighting, low-quality lightmaps, the stationary sky light – when no level under /Game has built lighting | turns it off in DefaultEngine.ini |
|
| master-material flags (skeletal mesh, morph targets, clothing, Cascade and Niagara particles, geometry collections and caches, hair) nothing in the asset registry needs, referencers walked through the instance chain | clears the flag in one undoable transaction; save the material to keep it | |
| masters whose eight or more instances each compile their own copy | none: a content decision |
Each finding shows its evidence and how sure it is:
High, or
Check first when a
Blueprint, a level or another opaque asset references the material and might still need it.
Nothing changes until you press Apply.
Found while testing the new tools against real content, before release:
72+), the summary says how many maps are partial, both CSVsDiet/PSOAutopilotBloatScan (the static scan), Diet/PSOAutopilotShaderDiet (the analysis and itsDiet/SPSOAutopilotShaderDiet (the tab); SPSOAutopilotProfiler is rewritten on top ofPSOAutopilot.BloatScan.Helpers, .Empty, .Graph, .SortClick;PSOAutopilot.ShaderDiet.ProjectSettings, .UsageFlags, .Permutations, .SavingText;PSOAutopilot.TabVisibility now covers the Shader Diet tab.Plugins/PSOAutopilot/Content/Scripts held stale copies of the helper scripts that nothing loadedPlugins/PSOAutopilot/Scripts is the only copy.Run-CI.ps1 on UE 5.7: build OK, 70/70 automation tests (52 before 1.6), coverage 1001 >= 900,Run-PluginBuildGate.ps1 8/8 on the final 1.6.0 tree: UE 5.5, 5.6, 5.7 and 5.8, each with full-StrictIncludes): build OK, ODR scan clean (76 .cpp),The Stutter Lab in a packaged UE 5.8.3 Development build (D3D12 SM6, RTX 5060 Ti), on a shader
platform this machine had never run, so every pipeline in it was new to the GPU driver. Recorded in
real time, one run:
| Items that popped in (invisible for at least one frame) | 10 of 22 | 6 of 22 |
| Longest wait for a pipeline | 310 ms (19 frames) | 63 ms (2 frames) |
| Effects | all 6 held back, 103-262 ms | all 6 within 2 frames (32-63 ms) |
| Average frames invisible | 5.55 | 0.36 |
| Frames over 33 ms / worst frame | 19 / 35.4 ms | 18 / 36.5 ms |
The smart-skip cache now knows the render configuration, the GPU driver and the project version, a
warmup that finds nothing is no longer remembered as done, a cancelled batch stays alive until its
pipelines finish, and content mounted at runtime can be prepared as it arrives. Effects get their
custom-depth pipelines, and warmups that run during gameplay leave on-screen textures alone.
The Shader Diet now knows which materials ship, counts the shader maps static permutations
really compile, finds landscape layer combinations and measures what a fix saves;
the Bloat Profiler reads exact counts from a cook; and both run headless for CI.
| Change | You notice it if… | |
|---|---|---|
| Smart skip knows the render configuration and the driver: a new RHI, anti-aliasing method, quality level or GPU driver prepares again | your players change graphics settings, or update their driver | |
| Smart skip knows the build: the project version is part of the key, and 16 configurations are remembered (was 8) | you ship patches that edit content, or your players switch graphics presets | |
A warmup that finds nothing is not remembered: it warns, reports Success=false and looks again next start |
your Content to Prepare resolves to nothing in a build | |
| Effects get their custom-depth pipelines | your effects turn on custom depth while the game runs (outlines, highlights, masks) | |
| Gameplay-time warmups leave textures alone | you use Warmup Content (Time-Sliced) or Prepare Content Mounted at Runtime | |
| Simple Setup says when it ignores the Advanced folders | you configured Content Folders in Advanced mode, or in an ini file | |
| The Crash Guard holds a cancelled batch until it finishes: no 30 s limit | you cancel warmups on slow hardware | |
| Content mounted at runtime (opt-in) and Warmup Content (Time-Sliced) | you mount mods, DLC or other pak files while the game runs | |
| A Shader Diet that knows what ships: a Ships column, soft references, distinct permutations | you act on the Diet’s findings | |
| Landscape layer combinations in the Shader Diet | your project has a painted landscape | |
| Measure Savings and Use Cooked Counts | you want numbers instead of dashes and lower bounds | |
Headless runs: -run=PSOAutopilotShaderDiet, with -FailOn=high for CI |
you want the reports in a build pipeline | |
| The scoreboard and the Bloat Profiler fit their text | you run the Stutter Lab on a machine with a long GPU name, or read long paths in the Bloat Profiler | |
Pak chunks, measured savings and a CI baseline: downloaded pak chunks are prepared, 64 assets at a time; Measure Savings compiles the masters in full; Ships knows the game mode and game instance; -Baseline= for -FailOn=high |
you download pak chunks during play, measure the Diet’s savings, or gate CI on the Diet | |
| A downloaded chunk and a dedicated server, run for real: a group stuck on slow compiles no longer stalls the rest of a pack; the last group is let go; the 10 s mount wait is 10 s | you prepare mounted content, or run dedicated servers |
Upgrading needs no action. The first start after upgrading prepares once: fingerprints stored by
1.6 never match the new key. Prepare Content Mounted at Runtime is off by default.
Smart skip knows the render configuration
The smart-skip key ignored the render configuration. It hashed the engine version, the
dependency settings and the prepared assets, so a start with another RHI, anti-aliasing method or
quality level reused the last run’s fingerprint and skipped a warmup whose pipelines were now the
wrong ones. The key now also hashes a schema version, the RHI, the shader platform, the feature
level, the 19 console variables that change which pipelines a material needs
(r.AntiAliasingMethod, r.MSAACount, r.MaterialQualityLevel, the velocity, scene colour, HDR,
global illumination, reflection, virtual shadow map and Nanite switches, and the sg.* quality
levels; a missing one is written as -), and the GPU vendor, adapter and driver version: a driver
update discards the driver’s pipeline cache.
| What changed since the last run | Before 1.7 | 1.7 |
|---|---|---|
| The prepared content | prepares | prepares |
| RHI (D3D11, D3D12, Vulkan, …) or shader platform | prepares | |
| Anti-aliasing, quality level, Lumen, Nanite, virtual shadow maps | prepares | |
| GPU driver | prepares | |
| Nothing |
A warmup that found nothing was remembered as a success. When Content to Prepare resolved to
no assets, the run finished with Success=true and stored its fingerprint, so the next start
reported a cached run and a broken setup looked like a fast one. It now logs a warning naming
Content to Prepare, shows “Nothing to prepare.”, logs Success=false (nothing to prepare) and is
not remembered. OnWarmupComplete still fires, so a startup flow never waits on it.
PSOAutopilot::CacheKey::OnAppendCacheKey()Smart skip key, render configuration: v=3;rhi=D3D12;sp=PCD3D_SM6;fl=SM6;project=1.0.0.0;...Warmup/PSOWarmupCacheKey.h/.cpp: GatherRenderConfig (game thread) and the pureBuildRenderConfigSignature; the hash itself stays on the worker thread.FPSOWarmupFingerprintStore::ShouldRememberRun decides what is remembered.PSOAutopilot.CacheKey.Signature (every part changes the signature; hook parts are order-free),PSOAutopilot.CacheKey.Gather (the live gather fills every part) andPSOAutopilot.SmartSkip.RemembersOnlyRealRuns (no assets, failures or the editor preview are neverPrecache Crash Guard: 12 objects from a cancelled run are still held after 60 s; their graphics tasks have not finished.TimeoutSeconds still releases early, for anyone who sets one.PSOAutopilot.CrashGuard.UntimedHold: an entry outlives any wait while a task runs, warns once,CrashGuard.Quarantine test.QueueJITWarmupForAssets, Blueprint): prepares any list ofFPSOWarmupDiscovery::MakeWarmableClassFilter and FindWarmableAssetsUnder: one class filterPSOAutopilot.RuntimeContent.Find (a mounted root’s warmable assets, maps excluded),.Gate (game worlds only, and only when the setting is on), .Hold (the JIT manager holds each.Release (it lets go once the queue hasAllMaps section, or, if both are/Game, so it never claims a material does not ship when it cannot know);Static permutations counted instances, not shader maps. Instances with the same static
switches, landscape layers and base property overrides compile one shader map between them. The
rule now reports on distinct parameter sets (threshold 8, unchanged), names instances, distinct
sets and how many ship, and estimates each distinct set once.
| A master with… | 1.6 reported | 1.7 reports |
|---|---|---|
| 40 instances, all with the same switches | 40 permutations | nothing (1 set) |
| 40 instances, 12 distinct switch sets | 40 permutations | 12 sets |
“High” confidence missed soft references and materials assigned from code. Referencers now
include soft references, and a master that nothing but materials references is reported at Low
confidence: “Nothing references this material or its instances, so it may be assigned from code
or loaded by path.”
Diet/PSOAutopilotCookReach.h/.cpp (the roots and a pure breadth-first walk),Diet/PSOAutopilotLandscapeDiet.h/.cpp, and Diet/PSOAutopilotDietRunner.h/.cpp: one runner thatPSOAutopilot.CookReach.Walk and .MapRoots (on a synthetic graph), PSOAutopilot.ShaderDiet.Ships,.NoReferencer, .Permutations (rewritten for distinct sets), .Landscape, .Sort and.LandscapeMaps.~). It needs a renderer: the editor has-AllowCommandletRendering and says so otherwise.72+). This reads your last cook’s shader libraries underSaved/Cooked/<Platform>/<Project>/Content (this editor’s shader format when the cook has it)+.0 when no shader map had been read. Without a renderer (anyDiet/PSOAutopilotCookedShaders.h/.cpp reads ShaderArchive-*-<format>-*.ushaderbytecode with theFSerializedShaderArchive (archive version 2 on UE 5.5–5.7, 3 on 5.8) and theShaderAssetInfo-*.assetinfo.json.PSOAutopilot.ShaderDiet.Measure (with a renderer it measures the engine’s default material),.UnknownCount, PSOAutopilot.CookedShaders.Counts and .Pick, andPSOAutopilot.BloatScan.CookedCounts and .NotInCook.+).PSOAutopilot.ShaderDiet.Measure run once with a D3D12 renderer (not -NullRHI) measured both
-run=PSOAutopilotShaderDiet runs the Shader Diet and the Bloat Profiler without the editor
UI and writes the same CSV files as the tabs:
UnrealEditor-Cmd.exe MyProject.uproject -run=PSOAutopilotShaderDiet -Report=diet,bloat,landscape -FailOn=high
| Option | Does |
|---|---|
-Report=diet,bloat,landscape |
the reports to write (default diet,bloat); landscape findings are rows of ShaderDiet.csv |
-Out=<folder> |
where (default Saved/PSOAutopilot/ShaderDiet_<date>_<time>) |
-Measure |
measure savings (add -AllowCommandletRendering) |
-CookedShaders=auto or =<cooked Content folder>, -ShaderFormat=<format> |
exact Bloat Profiler counts from a cook |
-FailOn=high |
exit 1 when a high-confidence finding is on content that ships |
Exit 0 is clean, 1 is -FailOn=high tripping, 2 is a report that could not be written or a named
cook that could not be read. Memory stays bounded on large projects: a commandlet has no editor to
keep assets for, so it collects garbage between masters.
PSOAutopilot.ShaderDiet.Commandlet (options, defaults and the exit code).-Report=diet,bloat,landscape -CookedShaders=auto exits 0 and writesShaderDiet.csv and the three BloatProfiler_*.csv; adding -FailOn=high exits 1 on its three-Measure -AllowCommandletRendering, D3D12) the same run exits 0 and reads thePSOAutopilot.ShaderDiet.Measure covers.A patch that edited content without adding or removing an asset was skipped. A cooked build
has no file dates, so the asset half of the key is the list of asset paths, and a source-built
engine often reports the same engine version for every build. The key now also hashes the
project’s version (Project Settings > Description > Project Version), and the schema moves to
v=3. Bump the version for each patch you ship, or add your own build number through
OnAppendCacheKey().
| What changed since the last run | Before | Now |
|---|---|---|
| An asset added or removed | prepares | prepares |
| Existing assets edited, Project Version bumped | prepares | |
| Existing assets edited, nothing bumped | OnAppendCacheKey()) |
The fingerprint store kept 8 entries. Every combination of RHI, anti-aliasing, quality levels
and game parts is its own fingerprint, so a player who tried a few presets pushed out the one they
went back to, and it prepared again. The store keeps 16.
Effects that turn on custom depth while the game runs compiled on first draw. Niagara prepares
an effect with default parameters, custom depth off, so an outline, highlight or mask switched on
for an effect at runtime found no custom-depth pipelines ready. For every material used with
Niagara sprites or ribbons, the warmup now also asks for the custom-depth pass (movable, both
culling directions) through the vertex factory the effect draws with, found by name: still no
Niagara dependency. The log counts them once per run:
Effects: 24 materials had their custom-depth pipelines prepared.
To cover custom depth for every primitive, not only effects, the engine has
r.PSOPrecache.CustomDepth=2 (read-only: set it under [ConsoleVariables] in
DefaultEngine.ini), at the cost of more pipelines.
Warmups that run during gameplay touched textures on screen. Warmup Content (Time-Sliced)
and Prepare Content Mounted at Runtime forced textures resident against the startup warmup’s
budget, which nothing gave back until the next warmup, and rebuilt the render resources of virtual
textures that could be drawing. They now prepare pipelines only and leave textures to the engine’s
streaming.
Simple Setup ignored the Advanced Content Folders without saying so. Simple Setup prepares only
its own choices, so a project configured in Advanced mode, or in its ini files, and left in Simple
Setup prepared nothing from those folders. The setup check and the warmup’s log now say so:
Simple Setup has no Content to Prepare, so the 3 Content Folder(s) set for Advanced mode are not prepared. Add them to Content to Prepare, or set Setup Mode to Advanced.
The precache throttle’s comment blamed a D3D12 uniform-buffer overload; it limits how many
pipeline compiles are in flight, and now says so.
PSOAutopilot.CacheKey.Signature and .Gather (the project version changes the key and isPSOAutopilot.SmartSkip.RemembersSixteenConfigurations,PSOAutopilot.EffectCustomDepth.Params and .VertexFactories,PSOAutopilot.RuntimeContent.LeavesTexturesAlone and PSOAutopilot.SettingsIgnoredAdvancedFolders.Run-CI.ps1TESTING.md for these changes.…m_SimplMaterialInstanceConsta). They end in an ellipsis, withF:/…/Saved/Cooked/Windows/<Project>/Content).Saved/Cooked/Windows/<Project>/Content.PSOAutopilot.StutterLabFitText (text that fits keeps its size, the fit, the step down when a fontPSOAutopilot.CookedShaders.DisplayPath (inside andRun-CI.ps1 on UE 5.7: build OK,Run-PluginBuildGate.ps1 8/8A code review of the 1.7.0 tree before packaging found fifteen problems, all in features that had not
shipped yet. Each was fixed test-first.
/Game was never prepared, although the setting said pak files. It listenedpakchunk12-Windows.pak) mounts none. It now watches pak mounts too: a chunk other than 0 is prepared/Game is still not seen. The 2026-10-09 entry’s “a bare pakFound no warmable assets in pak chunk N.Nothing to prepare. and Success=false, and itsReady. with Loaded N packages before play andSuccess=true (packages loaded, no shaders to prepare), or Ready, with warnings. with its preload-FailOn=high could pass on a finding that ships. The roots now follow the cooker’s ownUCookOnTheFlyServer::GetGameDefaultObjects).-Measure uses the same code.UMaterialInstance::HasOverridenBaseProperties compares.-Report=landscape analysed every master material first, for findings it then threw away. It-Baseline=<ShaderDiet.csv>: -FailOn=high ignores the findings an earlier run’s CSV lists, so aFPSOMountedContentWatcher (mounts waiting for their assets to register), FPSOJitLoadQueue (one groupFPSOWarmupDiscovery::FindWarmableAssetsInChunk and SelectMountedAssets,FPSOWarmupProgressReporter::DescribeCompletion, FPSOAutopilotCookReach::AddGameDefaultObjects,FPSODietRunOptions::Masters and FPSODietCommandletOptions::MakeRunOptions.PSOAutopilot.RuntimeContent.Settle, .SettingsOnce, .Chunks, .CookedFallback, .Groups.QueueScales; PSOAutopilot.ProgressCompletion; PSOAutopilot.CookReach.GameDefaults;PSOAutopilot.ShaderDiet.MeasureCompiles, .PermutationIdentity, .MeasureKeepsApplied,.RunnerMasters and .Baseline. RuntimeContent.Gate now covers servers and pak chunk numbers.ShaderDiet.MeasureCompiles needs a renderer and returns early under -NullRHI, which is howRun-CI.ps1 runs. Run PSOAutopilot.ShaderDiet without -NullRHI too (TESTING.md says so now).ShaderDiet.MeasureCompiles was watched failing and passing in the UE 5.7.4 editor on D3D12PSOAutopilot.ShaderDiet tests pass.Run-CI.ps1 on UE 5.7: build OK, 112/112 (69 runtime, 43 editor), coverage 1001 >= 900,Run-PluginBuildGate.ps1 8/8 on UE 5.5–5.8, unity and non-unity: 0 plugin warnings, 0The two runtime checks the previous entry left open were run: a packaged UE 5.7.4 build mounting a downloaded pak
chunk during play, and a dedicated server. They found three defects in the new mounted-content code, fixed below.
Time-sliced warmup: N assets still have M pipeline tasks compiling after 10 s; .... Later runs, including a firstPrepare Content Mounted at Runtime is on, but this process never renders (a dedicated server): content mounted during play is left alone.Precache Crash Guard: N objects are still held after 60 s; their graphics tasks have not finished.PSOAutopilot.RuntimeContent.HandOff (a drained group waits 10 s on its tasks, then is handed over; finished tasksRuntimeContent.Settle now measures the wait on the clock.PSO Autopilot Sandbox Work\PSOChunk57): 96 assetspakchunk1 was held back at startup and mounted 3 s into play on a worker thread throughFCoreDelegates::MountPak, as ChunkDownloader does. Final run: Preparing 96 assets from pak chunk 1. 0.5 s after the-server mode (IsRunningDedicatedServer, FApp::CanEverRender false), uncooked. It logged the-game logged the subscription and, for that empty mount, Found no warmable assets in content mounted at /ChunkProbe. 10.5 s after the mount.-game run first (stalled at 64 of 96; 32 still loaded after a garbageRuntimeContent.HandOff andRuntimeContent.Settle failed against stubs. The handle fix has no unit test (it needs a gameRun-CI.ps1 on UE 5.7: build OK, 113/113 (70 runtime, 43 editor), coverage 1001 >= 900.Run-PluginBuildGate.ps1 8/8 on UE 5.5–5.8, unity and non-unity: 0 plugin warnings, 0 plugin