GregOrigin - PSO Autopilot: Shader Compilation Warm-up System to Prevent Lag & Crashes

New editable sample widgets + BP for those interested:

CreateEditableLoadingWidget.py (5.6 KB)
LoadingWidgetTemplates_README.md (2.4 KB)

1 Like

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.

1 Like

Video update on v1.3:

1.3.2 update, 2026-06-13 :dizzy:

:sparkles: Features

  • :control_knobs: Per-platform performance overrides. A new Per-Platform Performance Overrides list (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.
  • :abacus: CI coverage commandlet. New headless 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.
  • :robot: One-command CI gate. New 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.

:wrench: Internal Hardening (settings → runtime snapshot)

  • :level_slider: Single source of truth for Simple-preset tuning. The Simple preset → performance-tuning mapping is now one shared helper (ApplySimplePresetTuning) applied identically to the persistent settings object and the runtime snapshot, so the two can never drift.
  • :puzzle_piece: Snapshot round-trip safety. 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.
  • :test_tube: Effective-capture overlay. CaptureEffectiveFrom layers the Simple-mode seed and the per-platform override over the raw capture; Advanced mode passes raw content through unchanged.
  • :sponge: Runtime snapshot sanitize. FPSOAutopilotSettingsSnapshot::Sanitize() trims, de-duplicates, and clamps the snapshot before warmup consumes it (wired into the live warmup start path alongside CaptureEffectiveFrom).
  • :stopwatch: Pipeline-flush tracker extracted. The engine pipeline-cache flush accounting (cumulative compiled vs. newly-discovered PSOs on a non-monotonic queue, drain fraction, and a sliding-window completion rate for the ETA) is now an isolated, unit-tested FPSOPipelineFlushTracker.

:bug: Bug Fixes

  • :floppy_disk: “Apply Simple Setup” no longer wipes manually-configured Advanced content. A previous code change introduced a nasty conflict: Applying Simple Setup reset the Advanced Content Folders, Specific Content to Prepare, and Content to Load Before Play lists, discarding any maps, assets, or packages entered directly in Advanced mode. Simple Setup choices are now merged into those lists, so existing Advanced entries are preserved.
  • :broom: No duplicate entries when merging. Folders are de-duplicated by path (and the gameplay seed level by asset path), so re-applying Simple Setup does not create repeated entries.

:books: Notes

  • The merge is additive: only the persistent Advanced fields are affected. Simple-mode warmup still runs from a clean Simple snapshot, so this change does not alter what Simple mode actually warms.
  • Because the merge is additive, changing Level To Open After Warmup and re-applying Simple Setup can leave the previous seed level listed in the Advanced lists. This only adds one extra, harmless warmup entry; remove it manually if undesired.
  • Thank you to Arash_R for pointing this out soon after the last release.

:white_check_mark: Validation

  • :test_tube: 10 automation tests pass (Automation RunTests PSOAutopilot, NullRHI), exit code 0: SettingsPresetTuningParity, SettingsSnapshotRoundTrip, SettingsEffectiveCapture, SettingsSanitize, SettingsPlatformOverride, PipelineFlushTracker, SettingsSimpleSetupMerge, LiveSmokeTest, CoverageReport, CoverageFolders.
  • :abacus: Coverage commandlet verified on both paths against the project (1001 discovered assets): -MinAssets=0 → “Coverage gate passed (1001 >= required 0)”, exit 0; -MinAssets=5000 → “Coverage gate FAILED … below … 5000”, exit 1.
  • :robot: 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.

1 Like

1.3.5 update, 2026-07-08 :man_pilot:

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.

:building_construction: Architecture

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)
  • :building_construction: Eight collaborators under 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.
  • :wrench: Encapsulation pass. Raw internal-session access was replaced with named lifecycle methods (ResetWorkAndCompileEvents, AbandonInFlightWork, ReleaseEngineRequests, ClearStagedNames), each a 1:1 wrap of the previous inline call sites. Dead code removed (MergeRuntimeAndFilter, QueueByPath).
  • :wrench: 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.
  • :test_tube: Automation test suite grew 10 → 18 tests. Every collaborator has a dedicated unit test; summary-text arms (preload / flush / demo-hold / cached-run / ready / fallbacks) and the JIT queue lifecycle (dedup, requeue-vs-abandon semantics) are now pinned.
  • :test_tube: The fingerprint unit test runs against an isolated .ini (new test-only config-path seam), so running the suite no longer clears the developer’s real smart-cache history.
  • :shield: New packaged runtime smoke gate (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.
  • :shield: 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.

:white_check_mark: Validation

  • :test_tube: Full CI green on the final commit: build + 18/18 automation tests (NullRHI) + coverage gate 1001 ≥ 900.
  • :package: Packaged Win64 smoke run: fresh cook, smart cache cleared → full warmup executed (1002 assets discovered, 1,749 PSOs drained in the pipeline flush), Warmup Finished. Success=true, no livelock.
  • :factory: Clean full-unity (adaptive-unity off, Fab-parity) 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. :slight_smile:

1 Like

v1.3.6 update, 2026-08-03 :pilot:

  • The plugin no longer ships /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 the
    whole class of potential issues rather than re-saving assets per engine.
  • Upgrading from 1.3.5 or earlier: installing over an existing copy does not delete files that were removed from the plugin, so the old Plugins/PSOAutopilot/Content/ folder — and its warnings, if present — can survive the upgrade. Delete that folder once; nothing references it.

:wrench: Internal hardening

  • Eight extracted Public/Warmup/*.h headers used FSoftObjectPath, TSubclassOf,
    TObjectPtr, and TWeakObjectPtr while including only CoreMinimal.h, which no longer
    provides them. They now include what they use, so they are self-contained for any consumer
    including them from their own module.

:bug: Warmup runtime fixes

  • Cancelling a warmup stranded forced texture residency. Every texture the run had touched was
    forced resident with a 300-second window, and a completed run winds that down to 60 seconds on
    hand-off. StopWarmup — which is BlueprintCallable and the Cancel path on the loading-flow
    handle — only dropped its bookkeeping, so a cancel left mips pinned for up to five more minutes.
    Cancel now winds down exactly as completion does.
  • Material discovery was quadratic. Each material gathered from a primitive component was
    de-duplicated by linearly scanning the whole pending work list, so a level whose components share
    materials cost O(n²) weak-pointer dereferences during discovery. Membership now goes through a
    set, making appends O(1). Worlds are warmable assets, so large levels hit this hardest.
  • Virtual textures were re-initialised once per referencing material. UpdateResource() tears
    down and recreates the render resource; it ran on every encounter, so a virtual texture shared by
    N materials paid N teardowns. It now runs once per texture per batch.
  • Forced-texture bookkeeping grew without bound. The forced-residency list was an array appended
    to once per (material, texture) pair, so the end-of-warmup restore pass issued one redundant
    SetForceMipLevelsToBeResident per duplicate; in a single frame, at the exact moment the loading
    screen hands off to gameplay. It is now a set. The per-encounter refresh is deliberately kept so a
    long warmup cannot let an early texture expire mid-run.
  • Covered by a new PSOAutopilot.WarmupMaterialDedup test that pins the de-duplication
    contract and the array/set sync across Reset.

v1.4.0 update, 2026-08-31 :chart_increasing:

:sparkles: New features at a glance :eyes:

  • UE 5.8 is now supported. PSO Autopilot ships for Unreal Engine 5.5, 5.6, 5.7 and 5.8.
  • The setup wizard generates the demo for your specific setup, no more manual stuff.
  • The setup wizard’s validation logic no longer lives inside the widget.
  • A single Blueprint node now runs the whole loading flow. (Existing Blueprints are retained.)
  • The editor module has automation tests for the first time.
  • The loading-flow handle is now usable from C++.

:sparkles: Features

  • 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.

:building_construction: Architecture

  • 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.

:wrench: Internal hardening

  • Helpers shared between the validation rules and the wizard’s fix actions were promoted to a header
    as inline/constexpr before the split. Two .cpp files in one module defining the same
    anonymous-namespace symbol collide under Unreal’s unity builds.

:test_tube: Tests & test tooling

  • The editor module has automation tests for the first time — six, covering the wizard’s
    validation rules: batch size, scan folders, excluded classes, the boot-loader checks, the demo row,
    and the mapping from rules to the rows you see.
  • A fake probe stands in for the editor, so states that are slow or awkward to reach for real — a
    configured-but-missing scan folder, a boot loader pointing at the wrong level, auto-start disabled
    — are now covered.

:books: Documentation & notes

  • The README, manual and FAQ now say the demo is generated by a wizard button, state the Python
    Editor Script Plugin prerequisite
    , and give a copy-pasteable command line for anyone who prefers
    it. The updated Manual.html is shipped with the product for convenience, though the online manual is still preferred.

One node for the loading flow, one call for status

:sparkles: Features

  • A single Blueprint node now runs the whole loading flow. Start PSO Autopilot Loading Flow
    (Async)
    reports progress, completion, cancellation and failure on execution pins, replacing the
    older pattern of calling a function, storing a handle, and binding a delegate by hand. The
    previous node still works and is marked deprecated, pointing at its replacement — existing
    Blueprints compile with a warning, never an error.
  • One call for all status values. Get PSO Autopilot Status returns a struct carrying
    everything the individual Status nodes expose, so a progress widget reads state once per frame
    instead of calling up to 24 separate nodes. The individual nodes are unchanged and not
    deprecated
    — binding one text block to one value is still the right thing to do.

:wrench: Internal hardening

  • The loading-flow handle is now usable from C++. It was exported with PSOAUTOPILOT_API and
    returned by a public function, but its header sat in Private/, so a C++ caller received a
    pointer to a type they could not include and could call nothing on. Blueprint users were
    unaffected, since reflection needs no header. The header moved to Public/; no API changed.

:test_tube: Tests & test tooling

  • PSOAutopilot.LoadingFlowActionLifecycle pins the async node’s contract: the factory returns a
    valid action even when the flow cannot start, no handle exists before Activate(), and cancelling
    before activation is safe and idempotent.
  • PSOAutopilot.StatusSnapshotParity asserts every status field equals the getter it mirrors, so
    the struct and the getters cannot drift apart.

Unreal Engine 5.8 support

:sparkles: Features

  • UE 5.8 is now supported. PSO Autopilot ships for Unreal Engine 5.5, 5.6, 5.7 and 5.8.
    No settings changed and no behaviour changed on 5.5–5.7.

:bug: Bug fixes

  • Removed 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.

:shield: Reliability / CI gates

  • A dedicated game-target build gate now runs BuildPlugin against every supported engine as a
    routine step, rather than only during packaging. Editor-target builds structurally cannot catch
    defects that appear only in a packaged game — which is how the 1.3.6 packaged-build defect
    reached the final packaging step before anything caught it.
  • The packaged-warmup smoke gate now covers Shipping, not just Development, and no longer leaks
    a running game process per run.
  • The release packer now derives the version from the plugin descriptor and cross-checks that
    VersionName and Version encode each other (1.4.0 → 140).

:test_tube: Tests & test tooling

  • New automation test PSOAutopilot.EditorPreviewWidgetFlags pins the nested-object traversal in
    the editor-preview widget path, so the argument removed above cannot be silently reintroduced as
    a narrower walk.

:books: Documentation & notes

  • The manual’s package-version diagram gains a fourth engine. Measured values, read from generated
    package headers: 5.5 → 1013, 5.6 → 1017, 5.7 → 1018, 5.8 → 1018. UE 5.8 did not bump the
    package version, so 5.7 and 5.8 content is mutually loadable.

:white_check_mark: Validation

  • Clean full-unity BuildPlugin — editor and packaged-game targets — against UE 5.5, 5.6, 5.7
    and 5.8. All green, with zero deprecation warnings originating in plugin source on any of them.
  • CI gate: build, 28/28 automation tests, coverage gate 1001 assets. 22 of those are runtime
    tests and 6 are the editor module’s first.
  • Demo generation verified on UE 5.8. Scripts/CreatePSOAutopilotDemo.py produced the full set
    (1001 material assets, both demo levels, boot loader, loading screen) with zero errors, saved at
    5.8’s own package version. This is the same check that backs the generate-don’t-bundle design on
    every other supported engine.
  • Cooked warmup smoke on UE 5.8: pass. A packaged Development build reached
    PSO Autopilot Warmup Finished. Success=true on content that engine generated itself —
    1002 assets discovered, the per-batch load/GC cadence observed across 23 collections, and the
    pipeline-cache flush drained a real backlog of 1961 PSOs in 23.3s. Confirmed non-vacuous by
    FingerprintMatched=false, so the smart-cache skip did not short-circuit the asset loop.
  • Shipping configuration validated, not just Development. The plugin compiles and links in the
    UnrealGame Shipping target on all four engines, and a packaged Shipping build completes
    warmup on both UE 5.7 and UE 5.8. Shipping is what customers actually ship — WITH_EDITOR is
    stripped, UE_BUILD_SHIPPING is set, and optimisation differs — so a Development-only smoke does
    not cover it.
  • The setup wizard was exercised by hand on UE 5.7 after the extraction above: the editor loaded
    with no plugin warnings, the wizard opened, and Regenerate Demo Content rewrote all 1000 material
    instances and both levels. No automated test drives Slate, so this check is the only one that
    covers the widget itself.

v1.4.1 update, 2026-09-06 :man_pilot:

A fix release over 1.4.0., aimed at projects that build file-by-file; upgrading from 1.4.0 needs no project changes.

:bug: Fixes

  • 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.

:test_tube: Tests

  • The async-node test now asserts which output pin fired. All three terminal paths leave the
    node inactive, so the previous assertions could not tell success from cancellation: which is why
    the pin defect went unnoticed. It also no longer cancels before its own failure case, which had
    been quietly preventing that case from running.

:white_check_mark: Validation

  • Full-unity and non-unity BuildPlugin gates, plus the collision scan, on Unreal Engine
    5.5 / 5.6 / 5.7 / 5.8. Automation suite: 28 tests, 0 failures. Coverage gate: 1001 assets.
    The pin fix was verified by reverting it and confirming the test fails.

An attempt at a visual explainer of the Setup Wizard:

1.4.2 update, 2026-09-09 :laptop:

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.


:bug: Fixes

  • 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.

:wrench: Hardening

  • The editor no longer has its own copy of the path conversion. It had one, it drifted from the runtime’s, and the gap between them is the defect above. The wizard now calls the runtime.

:test_tube: Tests

  • 28 → 36 automation tests. Seven pin the conversion itself – the absolute content root, subfolders, backslashes, plugin content, package paths left alone, unusable paths rejected rather than silently prefixed, and the settings repair. The eighth asserts that the editor and the runtime return identical results for the same input, which is the specific failure that shipped: not a wrong answer, but two different answers.

:books: Docs

  • New :new_button: : 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 :new_button: : 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.

:white_check_mark: Validation

  • CI on UE 5.7: build, 36/36 automation tests, coverage floor met.
  • Build gate on UE 5.5 / 5.6 / 5.7 / 5.8 in both unity modes – 8 runs, all passing,
    static ODR scan clean and plugin-source C4996 count 0 on every run.

v1.4.3 update, 2026-09-12 :newspaper:

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.


:shield: Reliability

  • 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.

:white_check_mark: Validation

  • Verified in both directions. Mis-pinned on a 5.5 project the smoke now fails in 29 seconds,
    before the game is launched at all, instead of running to a 300-second timeout.

:bug: Fixes

  • 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.

:sparkles: New

  • 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.

:test_tube: Tests

  • PSOAutopilot.AudioMuter pins the mix contents – including the flag that applies it to child
    sound classes, which defaults to false and would otherwise mute the master class only – and every
    asymmetry: double mute, double restore, restore without mute, and an unbound muter.

:white_check_mark: Validation

  • CI 43/43; build gate 8/8 across UE 5.5/5.6/5.7/5.8 in both unity modes.
  • Deliberately not claimed: the subsystem wiring has no test. That the mute runs on start and the
    unmute on the Idle transition was verified by reading all four exits, not by a test.

:sparkles: New

  • A 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.

:test_tube: Tests

  • PSOAutopilot.VersionBanner asserts the descriptor lookup resolves, so a mistyped plugin name
    cannot degrade silently to “unknown” in every log ever sent. The diagnostic’s decision table is
    pinned separately.

v1.5.0 update, 2026-09-26 :tada:

Niagara 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.

:bug: Niagara fixes

  • 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:

    • 5.8 turned Niagara’s load-time precache off by default (bCacheOnLoad = false) and, with the
      default r.PSOPrecache.ProxyCreationStrategy=1, hides an effect until its pipelines are ready. An
      unprepared short effect can therefore simply not appear the first time. The plugin calls
      UFXSystemAsset::PrecacheAssetPSOs(), new in 5.8.
    • 5.5-5.7 precache Niagara systems when they load (r.PSOPrecache.NiagaraPrecachePSOAtAssetLoadingTime=1,
      the default) and expose no way to start it from outside Niagara. The plugin loads the effects during
      the warmup and waits on their completion event. If a project turned that setting off, the log says
      so once instead of failing silently.
  • 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.

:white_check_mark: Validation

  • 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.

:test_tube: Tests

  • PSOAutopilot.DiscoveryFXSystems – a Content to Prepare folder of Niagara systems discovers every
    one (it found 0 of 7 before the fix).
  • PSOAutopilot.WarmupEngineFXSystemAsset – an effect is queued, processed and counted, both as an
    asset and through a component’s template, and the FX cursor resets with the rest of the work state.
    Cascade’s UParticleSystem stands in for Niagara, so the tests need no Niagara dependency either.

:books: Docs

  • The FAQ answers “Does PSO Autopilot prepare Niagara effects?”, with the per-engine behaviour above.
  • Two test names briefly hid two existing tests. Naming a new test PSOAutopilot.Discovery.FXSystems
    turned PSOAutopilot.Discovery into a group node in the automation tree, and the existing leaf test
    of that name silently stopped running (the same happened to PSOAutopilot.WarmupEngine). Caught by
    diffing the test list against the previous run; the new tests use sibling names instead.

:sparkles: New: The Stutter Lab, a demo that measures itself

  • The 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:

    • The pairs used are recorded per machine (Saved/PSOAutopilot/StutterLab.ini), so a second launch
      measures the next never-seen pair instead of re-measuring a warm one. -StutterLabReset forgets.
    • The Lab never draws a measurement set unless it is measuring. In the editor, in Presentation and in
      Replay it draws separate preview sets that look the same, because a set drawn once in PIE would
      no longer be cold when the project is packaged.
    • A run whose cold wave cost nothing (the driver already knew its pipelines) does not overwrite a
      recording that showed a cost; the scoreboard shows that recording and explains why.
  • 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.

:white_check_mark: Validation

  • 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.

:test_tube: Tests

  • PSOAutopilot.StutterLabMeterFrames, StutterLabMeterPopIn, StutterLabTimeline,
    StutterLabResultsJson pin the scoreboard’s arithmetic: the hitch threshold is strict, items still
    invisible when a show ends are charged rather than dropped, the Autopilot chapter cannot end (or be
    skipped) before the warmup finishes, and results survive a save/load.

Smaller fixes

:bug: Fixes

  • The cinematic boot loader added its widget at z-order 0, under any HUD the game added at the
    default, while every other loader used 999. All loaders now share one constant.

:sparkles: New

  • Is Audio Muted For Preparation (IsWarmupAudioMuted), a Blueprint query for a loading screen
    that wants to show a muted icon.

:test_tube: Tests

  • PSOAutopilot.AudioMuteWiringCancel and AudioMuteWiringFinish drive a real subsystem through a
    run with Mute Audio During Warmup on: muted once it starts, unmuted after a cancel and after a run
    that finishes on its own. Until now only the muter’s own rules were tested, not that the subsystem
    called it.

:white_check_mark: Validation

  • CI (Run-CI.ps1, PowerShell 7): build OK, 52/52 tests, coverage 1001 >= 900, stale-header guard OK.
  • Build gates: 8/8 – 5.5, 5.6, 5.7, 5.8 x unity and non-unity: build OK, ODR scan clean, zero C4996.

The first video for 1.5.0 is out (still displays 1.4.3 on the widget).

A fairly comprehensive video tutorial is out now:

v1.6.0 update, 2026-10-03 :clinking_glasses: :pilot:

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.

:world_map: 1.6.0 at a glance

Change You notice it if…
:bug: :sparkles: 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
:bug: Texture preheat respects the streaming pool your project is large, or its streaming pool small
:sparkles: Setup checks itself: Message Log page and one notification, at editor start, on a setting change, before Play always, in the editor
:bug: The setup wizard’s choices reach Config/Default*.ini you set the plugin up with the wizard before 1.6
:sparkles: Bloat Profiler rebuilt: a static scan of the content you prepare, with its shader maps you want to know what your startup warmup costs
:sparkles: 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.

:repeat_button: If you set the plugin up with the Setup Wizard before 1.6: its changes may never have been
written to Config/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 Precache Crash Guard

:bug: Fixes

  • 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
    :hourglass_not_done: Still waiting for PostLoad CacheShaders() :repeat_button: finish PostLoad, which sets it up
    :green_circle: Set up for rendering (a resource with a shader map) CacheShaders() :collision: uniform buffer rebuilt under live draws :next_track_button: left alone
    :white_circle: Never set up for rendering CacheShaders() :hammer_and_wrench: 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.

:sparkles: New

  • :gear: Project Settings > PSO Autopilot > Safety > Precache Crash Guard (on by default), plus
    r.PSOAutopilot.CrashGuard (1 by default) to turn it off without a rebuild. Either one off
    restores the pre-1.6 behaviour.
  • :bar_chart: Get Precache Crash Guard Stats (GetCrashGuardStats, Blueprint): what the guard did in the
    last run. The log carries a one-line summary at the end of every warmup:
    Precache Crash Guard: 16 materials already set up for rendering were left alone, 0 finished loading, 0 were set up.

:building_construction: Architecture

  • Warmup/PSOPrecacheCrashGuard.h/.cpp: the decision table as a pure function
    (DecideCacheShaders), PrepareMaterialForPrecache, which replaces the direct CacheShaders()
    call in FPSOAssetWarmupEngine::WarmMaterial, and FPSOPrecacheQuarantine. The subsystem owns the
    quarantine, reports it to garbage collection (AddReferencedObjects) and ticks it while it holds
    anything; FPSOBatchLoader::GetInFlightWork hands a cancelled batch over before Reset() drops it.

:test_tube: Tests

  • PSOAutopilot.CrashGuard.Decision pins the decision table, PSOAutopilot.CrashGuard.Quarantine
    the hold-until-complete and 30 s timeout, PSOAutopilot.CrashGuard.Summary the log line.

:white_check_mark: Validation

  • Two packaged UE 5.8.3 first launches (D3D12 SM5 and SM6) logged the line above: 16 materials left
    alone, none rebuilt. The 16 also prove the 5.7+ resource lookup: the old overload would have found
    no resource and rebuilt all of them.

Texture preheat respects the streaming pool

:bug: Fixes

  • 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
    

:building_construction: Architecture

  • FPSOTextureStreamingStep::TryForceResident reserves each texture’s size (all mips) against the
    pool (FitsResidencyBudget, pure); the warmup engine reaches the step through the new
    FPSOWarmupRuntimeContext::TextureStreaming. An unknown pool, or streaming off, never caps.

:test_tube: Tests

  • PSOAutopilot.TextureResidencyBudget: the budget stops at half the pool, remembers what it
    reserved, counts the textures it turned away, and never caps an unknown pool.

:books: Docs

  • The log’s “N further textures were left to normal streaming” counts each time the budget said no,
    so a texture shared by several materials can be counted more than once. Residency itself is exact.

Setup checks itself

:sparkles: New

  • The 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 :no_entry: error where to look – a device profile or platform ini can set it, so no one-click fix
    Component precaching off (r.PSOPrecache.Components=0) :warning: warning –
    Niagara’s load-time precache off (UE 5.5-5.7) :warning: warning –
    Proxy creation strategy 0 / 1 / 2: what an unprepared object does on its first frame :information_source: info –
    Precache Crash Guard off :warning: warning :hammer_and_wrench: Turn On

:bug: Fixes

  • The wizard’s discovery preview never counted effects. Its class filter was not given
    UFXSystemAsset when the runtime discovery was (1.5.0), so a folder of Niagara systems previewed as
    “0 VFX” – and a folder of effects and their materials could be called material-heavy – while the
    warmup prepared every effect in it. The preview and the warmup now count the same assets.
  • The automatic check complained in every gameplay map. “The loading level is not open” and “the
    current editor level is the Level To Open” describe the level open in the editor, not the project,
    so they fired at every editor start and Play. The wizard still shows them; the Message Log, the
    notification and its Fix Safe Items (which would have switched your level) skip them.
  • The notification’s third button was clipped to “..”: three buttons do not fit a notification’s
    default width. The notification is wider now.

:building_construction: Architecture

  • Setup/PSOAutopilotSetupMonitor (an editor subsystem) runs the checks and owns the Message Log page
    and the notification; Setup/PSOAutopilotEngineChecks appends the engine rows to the project rows
    for both the monitor and the wizard; validation rows carry bDependsOnOpenLevel. The editor module
    now depends on DeveloperSettings, EditorSubsystem, MainFrame and MessageLog.

:test_tube: Tests

  • PSOAutopilot.Setup.EngineChecks, PSOAutopilot.Setup.MonitorPolicy (the notification fingerprint
    and dismissal), PSOAutopilot.Setup.PreviewCountsVFX (0 of 7 before the fix, 7 of 7 after),
    PSOAutopilot.Setup.MonitorSkipsOpenLevelRows.

:white_check_mark: Validation

  • The monitor ran at editor start in the CI editor and wrote its rows to the Message Log.
  • Live, in a UE 5.8.3 editor with r.PSOPrecaching=0 set on purpose: “1 of 19 checks need
    attention”, the notification with all three buttons, the Message Log page, and no open-level row
    in a gameplay map.

The setup wizard’s choices reach Config/Default*.ini

:bug: Fixes

  • The 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 :white_check_mark: :white_check_mark:
    Config/DefaultGame.ini / DefaultEngine.ini :cross_mark: :white_check_mark:
    After an editor restart :cross_mark: lost :white_check_mark: kept
    A cooked build :cross_mark: never saw it :white_check_mark:

:wrench: Hardening

  • A config file that cannot be written (checked in, not checked out) no longer fails silently: the
    log says which file, and the change applies to the running editor only.
  • Every wizard action goes through one persister (PSOAutopilotConfig::PersistToDefaultConfig,
    TryUpdateDefaultConfigFile). It is injectable, so the tests use a recorder and never rewrite the
    host project’s config.

:books: Docs

  • UPSOAutopilotSettings’s own ApplySimpleSetupToAdvancedSettings and PostEditChangeProperty
    still save to the running editor only, as before. Code that calls them outside the wizard should
    persist the settings itself (TryUpdateDefaultConfigFile).

:test_tube: Tests

  • PSOAutopilot.Setup.PersistWritesDefaultConfig (a temporary ini under Saved/PSOAutopilot/Tests),
    PSOAutopilot.Setup.ActionsPersistSettings.

:white_check_mark: Validation

  • Live in a UE 5.8.3 editor: Fix Safe Items and the settings panel, then an editor restart; the
    settings and the startup map read back from DefaultGame.ini / DefaultEngine.ini.

Bloat Profiler rebuilt, and the Shader Diet

:sparkles: New

  • 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
    :package: Assets asset you prepare, with the materials it reaches Shader Maps, heaviest first
    :artist_palette: Materials master or static permutation Shaders, heaviest first
    :file_folder: Folders folder, summed Shader Maps, heaviest first

    A filter, double-click to open, :outbox_tray: 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
    :gear: Project settings Allow Static Lighting, low-quality lightmaps, the stationary sky light – when no level under /Game has built lighting turns it off in DefaultEngine.ini
    :label: Usage flags 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
    :herb: Static permutation sprawl 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: :green_circle: High, or :orange_circle: 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.

:bug: Fixes

Found while testing the new tools against real content, before release:

  • Shader counts were lower than a cook’s, without saying so. The editor compiles material shaders
    on demand, as it draws them, so its shader maps are usually partial. Counts from a partly compiled
    map are now shown as lower bounds (72+), the summary says how many maps are partial, both CSVs
    carry the flag, and the views sort by Shader Maps, which does not depend on what the editor has
    drawn.
  • A first click on a number column sorted it lightest first. It now puts the heaviest at the top; a
    second click reverses it.
  • The Shader Diet showed “~0 shaders saved” for every finding, and blamed masters that were “still
    compiling”.
    A finding describes a permutation nothing draws, and the editor compiles shaders only
    for what it draws, so it never has those shaders to count. Savings it cannot count now show a dash
    (–), the summary and the column tooltip say why, and a master without a shader map is described as
    such.

:building_construction: Architecture

  • Diet/PSOAutopilotBloatScan (the static scan), Diet/PSOAutopilotShaderDiet (the analysis and its
    fixes) and Diet/SPSOAutopilotShaderDiet (the tab); SPSOAutopilotProfiler is rewritten on top of
    the scan. The Shader Diet tab registers with the other two; the editor module now depends on
    RenderCore and RHI.

:books: Docs

  • Known limit: neither tool gives cook-accurate shader counts. That needs the engine’s shader-map
    layout, which is private to the engine; counts are labelled as lower bounds instead of guessed.

:test_tube: Tests

  • PSOAutopilot.BloatScan.Helpers, .Empty, .Graph, .SortClick;
    PSOAutopilot.ShaderDiet.ProjectSettings, .UsageFlags, .Permutations, .SavingText;
    PSOAutopilot.TabVisibility now covers the Shader Diet tab.

:white_check_mark: Validation

  • Live, in a UE 5.8.3 editor: the Bloat Profiler scan, its sorts, views, double-click and CSV export;
    the Shader Diet analysis and Apply.

Housekeeping

:wrench: Hardening

  • Plugins/PSOAutopilot/Content/Scripts held stale copies of the helper scripts that nothing loaded
    (the packer already excluded them). Removed; Plugins/PSOAutopilot/Scripts is the only copy.

:books: Docs

  • README, Manual and FAQ describe the Precache Crash Guard, the automatic setup check, the rebuilt
    Bloat Profiler and the Shader Diet; TESTING lists the 70 tests and the new manual checks.
  • A narrated tutorial film (about ten minutes) walks through 1.6, and opens on the first launch
    measured below.

Release verification

:shield: Reliability

  • Run-CI.ps1 on UE 5.7: build OK, 70/70 automation tests (52 before 1.6), coverage 1001 >= 900,
    stale-header guard fired as designed. Every new test was watched failing against a stub (or the
    unfixed code) before its implementation.
  • 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
    unity (Fab parity) and strict non-unity (-StrictIncludes): build OK, ODR scan clean (76 .cpp),
    0 C4996, 0 compiler warnings.
  • Each change above also builds on its own: the first four one by one (strict non-unity, UE 5.7);
    the last one is the final tree.

:white_check_mark: Validation – a first launch, measured

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:

:red_circle: Cold waves :blue_circle: Prepared by PSO Autopilot
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
  • Why nothing “stutters”: with UE 5.8’s default proxy creation strategy, a late pipeline makes an
    object invisible instead of stalling the frame. The frames over 33 ms are the same in both shows:
    they are this scene’s frame time, not pipeline compiles.
  • Caveats: a first launch can be measured once per shader platform (the driver keeps what it
    builds), so this is one run. The prepared column’s six are effects whose first one or two frames of
    activation the Lab counts as a pop-in.

1.7.0 update, 2026-10-26 :man_pilot:

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.

:world_map: 1.7.0 at a glance

Change You notice it if…
:bug: 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
:bug: 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
:bug: 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
:bug: Effects get their custom-depth pipelines your effects turn on custom depth while the game runs (outlines, highlights, masks)
:bug: Gameplay-time warmups leave textures alone you use Warmup Content (Time-Sliced) or Prepare Content Mounted at Runtime
:bug: Simple Setup says when it ignores the Advanced folders you configured Content Folders in Advanced mode, or in an ini file
:bug: The Crash Guard holds a cancelled batch until it finishes: no 30 s limit you cancel warmups on slow hardware
:sparkles: Content mounted at runtime (opt-in) and Warmup Content (Time-Sliced) you mount mods, DLC or other pak files while the game runs
:sparkles: :bug: A Shader Diet that knows what ships: a Ships column, soft references, distinct permutations you act on the Diet’s findings
:sparkles: Landscape layer combinations in the Shader Diet your project has a painted landscape
:sparkles: Measure Savings and Use Cooked Counts you want numbers instead of dashes and lower bounds
:sparkles: Headless runs: -run=PSOAutopilotShaderDiet, with -FailOn=high for CI you want the reports in a build pipeline
:bug: 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
:bug: :sparkles: 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
:bug: 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

:bug: Fixes

  • 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 :next_track_button: skipped prepares
    Anti-aliasing, quality level, Lumen, Nanite, virtual shadow maps :next_track_button: skipped prepares
    GPU driver :next_track_button: skipped prepares
    Nothing :next_track_button: skipped :next_track_button: skipped
  • 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.

:sparkles: New

  • :hook: A game can add its own parts to the key: bind PSOAutopilot::CacheKey::OnAppendCacheKey()
    (a native multicast delegate) and append strings such as an upscaler mode or a graphics preset.
    Parts are sorted, so binding order does not matter.
  • :receipt: The log names the render configuration once per run:
    Smart skip key, render configuration: v=3;rhi=D3D12;sp=PCD3D_SM6;fl=SM6;project=1.0.0.0;...

:building_construction: Architecture

  • Warmup/PSOWarmupCacheKey.h/.cpp: GatherRenderConfig (game thread) and the pure
    BuildRenderConfigSignature; the hash itself stays on the worker thread.
    FPSOWarmupFingerprintStore::ShouldRememberRun decides what is remembered.

:test_tube: Tests

  • PSOAutopilot.CacheKey.Signature (every part changes the signature; hook parts are order-free),
    PSOAutopilot.CacheKey.Gather (the live gather fills every part) and
    PSOAutopilot.SmartSkip.RemembersOnlyRealRuns (no assets, failures or the editor preview are never
    remembered).

:white_check_mark: Validation

  • The three tests above, watched failing against stubs first, then green on UE 5.7 with the full suite.

The Crash Guard holds a cancelled batch until it finishes

:bug: Fixes

  • A cancelled batch was released after 30 s even if its pipelines were still compiling. On slow
    hardware a precache task can outlive that, and garbage collection could then destroy a material
    with tasks in flight: the hazard the guard exists to prevent. The hold now has no time limit by
    default; an entry leaves only when every task has finished. A hold longer than 60 s logs one
    warning, so a stuck task is visible:
    Precache Crash Guard: 12 objects from a cancelled run are still held after 60 s; their graphics tasks have not finished.
    A positive TimeoutSeconds still releases early, for anyone who sets one.

:test_tube: Tests

  • PSOAutopilot.CrashGuard.UntimedHold: an entry outlives any wait while a task runs, warns once,
    and leaves when the task completes.

:white_check_mark: Validation

  • Watched failing against the 30 s default, then green on UE 5.7 with the existing
    CrashGuard.Quarantine test.

Content mounted at runtime

:sparkles: New

  • :package: Project Settings > PSO Autopilot > Advanced > Runtime Content > Prepare Content Mounted at
    Runtime
    (off by default). When it is on and a pak file or plugin mounts content during play,
    PSO Autopilot finds the warmable assets under the new root (the same class filter and exclusions
    as Content to Prepare, without maps) and prepares them in the background through the time-sliced
    JIT warmup. Nothing happens in the editor outside Play In Editor. The assets are found through
    the asset registry, so content whose registry is loaded on mount (a plugin or game feature) is
    found; a bare pak whose registry is never loaded logs that nothing appeared within 10 s.
  • :stopwatch: Warmup Content (Time-Sliced) (QueueJITWarmupForAssets, Blueprint): prepares any list of
    assets during play. It loads them asynchronously in groups of 64 and holds them until they have
    been prepared, so garbage collection cannot take them mid-warmup, then lets them go.

:building_construction: Architecture

  • FPSOWarmupDiscovery::MakeWarmableClassFilter and FindWarmableAssetsUnder: one class filter
    for Content to Prepare and mounted content. The JIT manager reports the assets it holds to garbage
    collection.

:test_tube: Tests

  • PSOAutopilot.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
    queued asset once, for garbage collection to see) and .Release (it lets go once the queue has
    drained; the subsystem keeps ticking it until then).

:white_check_mark: Validation

  • Watched failing first, then green on UE 5.7 with the full suite.

A Shader Diet that knows what ships

:sparkles: New

  • :ship: Ships. The Diet now works out what a cook of the project includes, the way the cooker does:
    Always Cook maps; Maps to Cook (or, if that list is empty, the AllMaps section, or, if both are
    empty, every map under /Game, so it never claims a material does not ship when it cannot know);
    Directories to Always Cook; the default maps; every primary asset that is not Never Cook; and
    everything those reach through game dependencies. With Cook Everything, everything ships.
    Every finding says whether its material ships; one that does not “costs editor compile time
    only” and sorts after the ones that do. The tab and the CSV gain a Ships column.
  • :snow_capped_mountain: Landscape layer combinations. Each painted landscape component compiles the shaders of its
    own combination of layers, so an open world can compile hundreds of copies of one landscape
    material. The Diet counts, per landscape material, the components, the distinct combinations, the
    distinct layer sets, the most and average layers per component and Max Painted Layers Per
    Component, and reports 16 combinations or more, naming how many repeat a layer set and differ only
    in weightmap packing. There is no automatic fix: it suggests Max Painted Layers Per Component,
    fewer layers, and a consistent layer order. The tab reads the open level (and says when only part
    of a World Partition landscape is loaded); the commandlet reads every map that ships.

:bug: Fixes

  • 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.”

:building_construction: Architecture

  • Diet/PSOAutopilotCookReach.h/.cpp (the roots and a pure breadth-first walk),
    Diet/PSOAutopilotLandscapeDiet.h/.cpp, and Diet/PSOAutopilotDietRunner.h/.cpp: one runner that
    the tab steps from Tick and the commandlet steps in a loop, so the rules cannot drift between them.
    Loading a map’s landscape actors refuses to run outside a commandlet, so the tab never loads
    actors into an open level.

:test_tube: Tests

  • PSOAutopilot.CookReach.Walk and .MapRoots (on a synthetic graph), PSOAutopilot.ShaderDiet.Ships,
    .NoReferencer, .Permutations (rewritten for distinct sets), .Landscape, .Sort and
    .LandscapeMaps.

:white_check_mark: Validation

  • Watched failing first, then green on UE 5.7 with the full suite. On the demo project the
    commandlet’s reach covers 1,259 shipping packages from 164 master materials.

Measured savings and cooked counts

:sparkles: New

  • :straight_ruler: Measure Savings (Shader Diet tab): loads the masters behind usage-flag and permutation
    findings, waits for their shaders to compile and fills in what each fix saves; a permutation
    finding estimates distinct sets × the master’s shaders (~). It needs a renderer: the editor has
    one; the commandlet needs -AllowCommandletRendering and says so otherwise.
  • :abacus: Use Cooked Counts (Bloat Profiler): the editor compiles shaders on demand, so its counts are
    lower bounds (72+). This reads your last cook’s shader libraries under
    Saved/Cooked/<Platform>/<Project>/Content (this editor’s shader format when the cook has it)
    and replaces every count with the exact one, then re-totals the assets and folders. With cooks
    for several platforms it reads the one that has this editor’s shader format; a material the cook
    does not contain (added since, say) keeps the editor’s count and its +.

:bug: Fixes

  • A project setting’s saving read 0 when no shader map had been read. Without a renderer (any
    commandlet) no master has an editor shader map, and the static-lighting rows reported that turning
    them off saves nothing. They now show a dash: unknown.

:building_construction: Architecture

  • Diet/PSOAutopilotCookedShaders.h/.cpp reads ShaderArchive-*-<format>-*.ushaderbytecode with the
    engine’s own FSerializedShaderArchive (archive version 2 on UE 5.5–5.7, 3 on 5.8) and the
    matching ShaderAssetInfo-*.assetinfo.json.

:test_tube: Tests

  • PSOAutopilot.ShaderDiet.Measure (with a renderer it measures the engine’s default material),
    .UnknownCount, PSOAutopilot.CookedShaders.Counts and .Pick, and
    PSOAutopilot.BloatScan.CookedCounts and .NotInCook.

:white_check_mark: Validation

  • On the demo project’s Windows cook, the commandlet read the PCD3D_SM6 libraries (16,894 unique
    shaders) and all 1,001 Bloat Profiler asset rows came out exact (no +).
  • PSOAutopilot.ShaderDiet.Measure run once with a D3D12 renderer (not -NullRHI) measured both
    kinds of estimate on the engine’s default material.

Headless runs

:sparkles: New

  • :robot: -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.

:test_tube: Tests

  • PSOAutopilot.ShaderDiet.Commandlet (options, defaults and the exit code).

:white_check_mark: Validation

  • On the demo project: -Report=diet,bloat,landscape -CookedShaders=auto exits 0 and writes
    ShaderDiet.csv and the three BloatProfiler_*.csv; adding -FailOn=high exits 1 on its three
    high-confidence project settings.
  • With a renderer (-Measure -AllowCommandletRendering, D3D12) the same run exits 0 and reads the
    Windows cook; the demo project has no usage-flag or permutation finding to measure, which the
    renderer run of PSOAutopilot.ShaderDiet.Measure covers.

Patches, effects and gameplay-time warmups

:bug: Fixes

  • 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 :next_track_button: skipped prepares
    Existing assets edited, nothing bumped :next_track_button: skipped :next_track_button: skipped (bind 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.

:test_tube: Tests

  • PSOAutopilot.CacheKey.Signature and .Gather (the project version changes the key and is
    gathered), PSOAutopilot.SmartSkip.RemembersSixteenConfigurations,
    PSOAutopilot.EffectCustomDepth.Params and .VertexFactories,
    PSOAutopilot.RuntimeContent.LeavesTexturesAlone and PSOAutopilot.SettingsIgnoredAdvancedFolders.

:white_check_mark: Validation

  • Each new test watched failing against a stub first (7 of 7), then green on UE 5.7: Run-CI.ps1
    97/97, and the build gates 8/8 on UE 5.5–5.8.
  • Not yet run in a packaged build: the four 1.7.0 rows added to TESTING.md for these changes.

The scoreboard and the Bloat Profiler fit their text

:bug: Fixes

  • The Stutter Lab’s scoreboard ran text past its card. The provenance line (which names the GPU),
    the headline and the footnote were drawn on one line at a fixed size; at 1080p and 1440p the
    provenance and the footnote ran past the card’s right edge, and a longer GPU name ran further. Each
    now shrinks to fit the card, down to a floor, measured again at the whole-pixel size it is drawn at.
  • The Bloat Profiler cut long asset paths and type names off at the column edge, so a path ran
    straight into the type beside it (…m_SimplMaterialInstanceConsta). They end in an ellipsis, with
    a gap before the next column, and the tooltip shows them in full.
  • Use Cooked Counts printed the cook’s full path (F:/…/Saved/Cooked/Windows/<Project>/Content).
    A cook inside the project is shown relative to it: Saved/Cooked/Windows/<Project>/Content.

:test_tube: Tests

  • PSOAutopilot.StutterLabFitText (text that fits keeps its size, the fit, the step down when a font
    rounds up to the next pixel, the floor) and PSOAutopilot.CookedShaders.DisplayPath (inside and
    outside the project, backslashes, a sibling folder that shares the project folder’s name). The
    ellipsis is declarative Slate with nothing to assert on; it was checked on screen.

:white_check_mark: Validation

  • Both new tests watched failing against stubs, then green. Run-CI.ps1 on UE 5.7: build OK,
    99/99, coverage 1001 >= 900, stale-header guard as designed. Run-PluginBuildGate.ps1 8/8
    on UE 5.5–5.8, unity and non-unity: 0 plugin warnings (the C4996 lines on 5.7 are all in
    engine headers).
  • On screen in the UE 5.7.4 editor: a Movie Render Queue frame of the scoreboard at 2560x1440 (every
    line inside the card) and the Bloat Profiler with cooked counts (relative path, ellipses).

Pak chunks, measured savings and a CI baseline

A 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.

:bug: Fixes: content mounted at runtime

  • A pak mounted under /Game was never prepared, although the setting said pak files. It listened
    only for plugin content roots, and a downloaded chunk of the game’s own cook (ChunkDownloader’s
    pakchunk12-Windows.pak) mounts none. It now watches pak mounts too: a chunk other than 0 is prepared
    from the assets the cooked asset registry tags with that chunk, which are there from the start, so one
    look settles it. Paks can mount on a worker thread; the work runs on the game thread. A pak cooked
    separately and mounted straight under /Game is still not seen. The 2026-10-09 entry’s “a bare pak
    logs that nothing appeared within 10 s” never happened for such a pak; a chunk with nothing warmable
    now logs Found no warmable assets in pak chunk N.
  • A mounted pack was in memory all at once. Warmup Content (Time-Sliced) requested every group of 64
    at the same moment, and the JIT manager holds what it loads until its queue drains. The groups now
    load one at a time: the next waits until the last has been prepared and let go.
  • The editor could hand over part of a plugin. It registers a mounted plugin’s assets over several
    frames, and the first poll that found any was taken as the whole plugin. A plugin’s mount is ready
    once its count holds for two polls while the registry is not gathering (or after 10 s). An asset that
    two mounts report, a pak that carries a plugin, is prepared once.
  • Dedicated servers loaded mounted packs for pipelines they never create. Mounted content is now
    watched only in a process that can render.
  • In a cooked game, mounted content now falls back to everything under the root when the class filter
    finds nothing, as the startup warmup’s folder fallback does. Queueing no longer deduplicates with a
    linear search per path (a 20,000-asset pack spent about half a second in one frame). A Content to
    Prepare folder that is not a content path logged its error on every half-second poll, about twenty
    times per mount; each mount now reads the settings once.

:bug: Fixes: the end of a run

  • A run that only loads packages before play said Nothing to prepare. and Success=false, and its
    package preload failures were missing from the summary. An Advanced setup that preloads packages and
    prepares nothing now ends Ready. with Loaded N packages before play and
    Success=true (packages loaded, no shaders to prepare), or Ready, with warnings. with its preload
    failures counted. The no-assets warning is a plain log line for that setup. A run with nothing at all
    is unchanged.

:bug: Fixes: Shader Diet

  • Ships missed what the cooker always cooks: the default game mode, the server game mode and the game
    instance class under Maps & Modes. Content reached only through a Blueprint game instance read as not
    shipping, so -FailOn=high could pass on a finding that ships. The roots now follow the cooker’s own
    list (UCookOnTheFlyServer::GetGameDefaultObjects).
  • Measure Savings in the editor measured nothing new. The editor compiles material shaders as
    something draws them, so the tab re-counted the same partial maps and showed the result as measured,
    and it waited on every compile in the editor. For a permutation finding whose master had no map it
    turned a count into unknown. It now submits every shader the masters’ maps list for this editor’s
    platform, waits for those masters alone, and leaves an estimate alone when it cannot count. The
    commandlet’s -Measure uses the same code.
  • Distinct static permutations were miscounted both ways. The key ignored static component masks and
    material layers, and compared the base property overrides as stored, so a box ticked with the parent’s
    own value counted as a new set. It now compares what the instance compiles with: the properties
    UMaterialInstance::HasOverridenBaseProperties compares.
  • Measure Savings reset applied rows to Apply, so a fix could be applied twice. Rows are measured in
    place; applied rows keep their state and their number, and are not measured.
  • -Report=landscape analysed every master material first, for findings it then threw away. It
    reads only landscapes now.
  • A master that failed to load counted as one with a shader map, so the project settings’ savings could
    read 0 instead of unknown.

:sparkles: New

  • -Baseline=<ShaderDiet.csv>: -FailOn=high ignores the findings an earlier run’s CSV lists, so a
    project can accept the findings no setting fixes (a landscape’s layer combinations, static
    permutations) and fail only on new ones. An unreadable baseline exits 2.

:building_construction: Architecture

  • FPSOMountedContentWatcher (mounts waiting for their assets to register), FPSOJitLoadQueue (one group
    at a time), FPSOWarmupDiscovery::FindWarmableAssetsInChunk and SelectMountedAssets,
    FPSOWarmupProgressReporter::DescribeCompletion, FPSOAutopilotCookReach::AddGameDefaultObjects,
    FPSODietRunOptions::Masters and FPSODietCommandletOptions::MakeRunOptions.

:test_tube: Tests

  • 13 new: PSOAutopilot.RuntimeContent.Settle, .SettingsOnce, .Chunks, .CookedFallback, .Groups
    and .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 how
    Run-CI.ps1 runs. Run PSOAutopilot.ShaderDiet without -NullRHI too (TESTING.md says so now).

:white_check_mark: Validation

  • Every new test watched failing against a stub first (13 of 13, and the one-look rule for chunks after
    them). ShaderDiet.MeasureCompiles was watched failing and passing in the UE 5.7.4 editor on D3D12
    (RTX 5060 Ti), where all 17 PSOAutopilot.ShaderDiet tests pass.
  • Run-CI.ps1 on UE 5.7: build OK, 112/112 (69 runtime, 43 editor), coverage 1001 >= 900,
    stale-header guard as designed.
  • Run-PluginBuildGate.ps1 8/8 on UE 5.5–5.8, unity and non-unity: 0 plugin warnings, 0
    plugin C4996 (the 100 and 312 C4996 lines on 5.7 are all in engine headers).
  • A packaged build that mounts a downloaded pak chunk, and a dedicated server, were run the same day and found three
    more defects: see the next entry.

A downloaded chunk and a dedicated server, run for real

The 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.

:bug: Fixes

  • One group whose pipelines compile slowly stalled the rest of a mounted pack. The time-sliced warmup holds a group
    until the engine has compiled its pipelines, and the next group waited for that. Those requests compile at the
    engine’s lowest priority, and in the first two packaged runs they did not finish for as long as the runs lasted
    (15 s), so the run stopped at 64 of 96 assets. A group still compiling 10 s after its last asset was prepared now goes
    to the Precache Crash Guard, which holds each object until its tasks finish, and the next group loads:
    Time-sliced warmup: N assets still have M pipeline tasks compiling after 10 s; .... Later runs, including a first
    launch from a new folder, compiled within a second and never needed it; the hand-off is covered by a test, not yet
    seen in a packaged run.
  • The last group of every Warmup Content (Time-Sliced) call stayed in memory for the session. The loader kept the
    streamable handle of the group it had requested, and a handle keeps its assets loaded. After a full garbage
    collection, 32 of the 96 assets were still in memory; they are let go once the JIT warmup has them, and none remain.
    What the warmup lets go of is reclaimed by the next garbage collection: during gameplay it forces none.
  • The 10 s wait for a mounted plugin’s assets lasted about five minutes. The poll runs on a ticker every half
    second, but a ticker hands its delegate the frame’s delta, and the wait added those up. It is measured on the clock
    now. Pak chunks (looked at once) and plugins whose assets arrive and settle were not affected.

:sparkles: New

  • On a dedicated server with the setting on, the log says why nothing happens:
    Prepare Content Mounted at Runtime is on, but this process never renders (a dedicated server): content mounted during play is left alone.

:books: Docs

  • The Crash Guard’s long-hold warning no longer says the objects are “from a cancelled run”:
    Precache Crash Guard: N objects are still held after 60 s; their graphics tasks have not finished.

:test_tube: Tests

  • PSOAutopilot.RuntimeContent.HandOff (a drained group waits 10 s on its tasks, then is handed over; finished tasks
    restart the wait). RuntimeContent.Settle now measures the wait on the clock.

:white_check_mark: Validation

  • Packaged pak chunk, in a scratch project outside the repo (PSO Autopilot Sandbox Work\PSOChunk57): 96 assets
    (8 materials, 80 instances, 8 meshes) labelled into chunk 1, packaged Development with chunks and IoStore on UE 5.7.4,
    D3D12 SM6, RTX 5060 Ti. pakchunk1 was held back at startup and mounted 3 s into play on a worker thread through
    FCoreDelegates::MountPak, as ChunkDownloader does. Final run: Preparing 96 assets from pak chunk 1. 0.5 s after the
    mount, all 96 prepared within about a second (24 PSO requests: the instances share their parents’ pipelines), and 0
    of 96 still loaded after a full garbage collection. With the setting overridden off on the command line, the same
    mount prepared and loaded nothing.
  • Dedicated server: the Launcher builds of UE 5.5–5.8 cannot build a Server target, so the server was the UE 5.7.4
    editor binary in -server mode (IsRunningDedicatedServer, FApp::CanEverRender false), uncooked. It logged the
    line above, and a content path registered 3 s into play (the event a plugin mount raises) got no reaction. The same
    binary in -game logged the subscription and, for that empty mount, Found no warmable assets in content mounted at /ChunkProbe. 10.5 s after the mount.
  • Each defect was shown in the packaged or -game run first (stalled at 64 of 96; 32 still loaded after a garbage
    collection; no give-up within 16 s), then fixed test-first where a unit test can see it: RuntimeContent.HandOff and
    the clock check in RuntimeContent.Settle failed against stubs. The handle fix has no unit test (it needs a game
    instance and an async load); the packaged run is its check, 32 before and 0 after.
  • Run-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
    C4996.