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.