GregOrigin - TargetFrame: Automatic runtime scalability for mainstream PC's

Watch it in action

Read the manual

A functional open source version is available at https://github.com/gregorik/TargetFrame-Core, please file Issues there if appropriate. Fab hosts the fully featured Pro version. The manual reflects the Pro feature set.

TargetFrame is an automatic, zero-configuration runtime scalability management system for Unreal Engine 5.6+. Stop guessing what settings your players need: TargetFrame automatically benchmarks hardware, sets a stable baseline, and dynamically steps down resolution scale and overall quality during gameplay to defend your target frame rate.

Designed to eliminate "stuttering" reviews on launch day, this plugin handles the dirty work of performance scaling so you can focus on building your game.

Key Features

🛡️ Runtime Governor

Never drop below your target frame rate during a boss fight again. The Runtime Governor constantly monitors the smoothed FPS. If the frame rate drops below your configured threshold for a sustained period, the Governor steps in to automatically lower the ResolutionScale and OverallQuality step-by-step until the target FPS is restored. When the action cools down, it smoothly scales back up to maximize visual fidelity.

⚡ Auto-Benchmarking & Tiering

On boot, TargetFrame Core runs a fast, non-intrusive hardware scan. It assigns the machine to a hardware tier (Entry, Mainstream, Performance) based on CPU cores, RAM, and GPU capabilities. It automatically applies the recommended settings for that tier without the player ever having to open an Options menu.

📦 Fire-and-Forget Shipping Capsule

Hate writing custom save-game logic for graphics settings? The Fire-and-Forget Shipping feature completely abstracts scalability. The plugin automatically locks in the optimal settings after an initial "stabilization period," giving players a consistently smooth experience out-of-the-box.

🔲 Upscaler-Safe UI

Upscaling a 1080p render to 4K is great for performance, but it can make your UI text look like a blurry mess. TargetFrame Core automatically separates your UI rendering from your 3D scene rendering. The 3D scene scales dynamically, while your Slate/UMG UI remains locked at crisp, native resolution.

  • Dynamic Nanite Budgeting: Instead of brutally dropping the global scalability level, the Pro version surgically relaxes Nanite's MaxPixelsPerEdge and time budgets during heavy scenes, keeping your shadows and post-processing intact while slightly reducing distant geometric density.

  • Hardware Ray Tracing Guards: Lumen Hardware Ray Tracing is gorgeous but demands massive VRAM. The Pro version automatically intercepts the boot sequence, checks the physical VRAM and GPU capabilities, and gracefully falls back to Software Ray Tracing if the player's hardware would crash or stutter.

  • VRAM Exhaustion Protection: Automatically clamps Texture Pool sizes and disables memory-hungry features like Nanite Tessellation on hardware with 8GB of VRAM or less to prevent catastrophic Out of Video Memory crashes.

  • Vendor-Specific Profiles: Detects Intel ARC, AMD, and NVIDIA GPUs, applying specific capability maximums and dynamic resolution preferences to avoid known driver performance cliffs.

  • CSV Telemetry Export: Automatically writes detailed frame-time data, intervention logs, and hardware specs to CSV files during playtests, making it incredibly easy for your QA team to identify performance bottlenecks.

Update 0.2.0 — 2026-03-21


Fixes

  • FSR version ordering — DeterminePreferredUpscaler now evaluates FSR3 > FSR2 > FSR in both vendor-specific and generic fallback paths. Previously FSR (1.0) could be selected over FSR3 when all three were available. (TargetFrameSubsystem.cpp)

  • DLSS excluded from runtime quality adjustment — TryAdjustUpscalerQuality guarded against all modes except FSR variants, silently skipping DLSS and XeSS. Changed to a blacklist of Native/TSR so every vendor upscaler participates in the runtime ladder. (TargetFrameSubsystem.cpp)

  • Widget "Skip Onboarding" opened control panel — HandleSkipOnboarding called SetControlPanelVisible(true) and RefreshPresentation() after dismissing the wizard, unexpectedly showing the control panel. Removed both calls so "Skip" truly dismisses. (TargetFrameUserExperienceWidget.cpp)

  • GPU vendor detection false positives — DetectGPUVendor matched the substring "arc" (hitting "search", "march", etc.) and "uhd" (hitting "Thunderbolt"). Tightened to "intel arc" and "uhd graphics". (TargetFrameSubsystem.cpp)

  • Asymmetric quality step ladder — StepQualityUp applied knobs in the same order as StepQualityDown. Reversed to overall quality > Nanite > resolution > triangle cull > upscaler, so the cheapest-to-restore knob is raised first. (TargetFrameSubsystem.cpp)

  • Redundant hardware snapshot refreshes — ApplyCurrentPolicy and EvaluateRuntimeBudget each called RefreshHardwareSnapshot() despite Initialize already doing so. Removed the duplicate calls. (TargetFrameSubsystem.cpp)

  • OverallQualityLevel exposed raw -1 to Blueprint — GetOverallScalabilityLevel() returns -1 for "Custom". LoadStateFromGameUserSettings now maps -1 to FallbackOverallQualityLevelWhenCustom before writing Status.OverallQualityLevel, so Blueprint consumers never see a sentinel value. (TargetFrameSubsystem.cpp)

  • CanReduceCostFurther / CanIncreaseQualityFurther ignored activatable upscalers — Both functions checked IsVendorUpscalerActive(), missing upscalers that were available but not yet activated. Changed to DeterminePreferredUpscaler() != Native/TSR so an available vendor upscaler is correctly treated as a quality knob. (TargetFrameSubsystem.cpp)

Performance

  • Debounced SaveCurrentSettings disk writes — Runtime governor adjustments (TryAdjustResolutionScale, TryAdjustOverallQuality) now call MarkSettingsDirty() which updates local state immediately but defers ApplySettings + SaveSettings to disk. FlushDirtySettings() fires every 5 seconds when dirty, and is guaranteed to flush in Deinitialize(). One-shot operations (ApplyCurrentPolicy) still save immediately. (TargetFrameSubsystem.h, TargetFrameSubsystem.cpp)

  • Reduced hardware benchmark work scale — RunHardwareBenchmark work scale lowered from 10 to 5, cutting the synchronous hitch roughly in half. Added a UE_LOG Warning advising callers to run the benchmark during a loading screen. (TargetFrameSubsystem.cpp)

  • Hitch rejection for EMA — Tick frames longer than HitchThresholdSeconds (default 0.1 s) are excluded from the SmoothedFPS exponential moving average, preventing load spikes or hitches from triggering false quality step-downs. (TargetFrameSubsystem.cpp)

  • Telemetry snapshot throttle — RecordTelemetrySnapshot was called every tick. Now gated behind TelemetrySnapshotIntervalSeconds (default 1.0 s) to reduce CSV profiler overhead. (TargetFrameSubsystem.cpp)

  • Capability probe cached — ProbeProjectCapabilities now sets bCapabilitiesProbed on first run and early-exits on subsequent calls, avoiding repeated CVar lookups. (TargetFrameSubsystem.cpp)

New Features

  • XeSS upscaler support — Added ETargetFrameUpscalerMode::XeSS and full integration: capability probing (r.XeSS.Enabled / r.XeSS.Quality), preferred-on-Intel selection in DeterminePreferredUpscaler, ApplyXeSSState / GetXeSSQualityModeValue implementations, runtime status refresh, and vendor-active checks. Controlled by bPreferXeSSOnIntel in DeveloperSettings. (TargetFrameTypes.h, TargetFrameDeveloperSettings.h, TargetFrameSubsystem.h, TargetFrameSubsystem.cpp, DefaultTargetFrame.ini)

  • Runtime governor pause/resume — SetRuntimeGovernorPaused(bool) BlueprintCallable on the subsystem, plus PauseTargetFrameGovernor / ResumeTargetFrameGovernor static wrappers in the Blueprint library. Consecutive-sample counters reset on resume to prevent stale readings from triggering immediate adjustments. (TargetFrameSubsystem.h, TargetFrameSubsystem.cpp, TargetFrameBlueprintLibrary.h, TargetFrameBlueprintLibrary.cpp)

  • ResetToDefaults — BlueprintCallable that restores managed triangle-cull states, clears all runtime overrides, resets Status, and re-probes capabilities. Wrapped as ResetTargetFrameToDefaults in the Blueprint library. (TargetFrameSubsystem.h, TargetFrameSubsystem.cpp, TargetFrameBlueprintLibrary.h, TargetFrameBlueprintLibrary.cpp)

  • Policy-changed delegate — OnPolicyChanged (FTargetFramePolicyChangedDelegate) BlueprintAssignable multicast delegate fires on every RecordPolicyAction call with the action string and reason, enabling Blueprint-side reactions to policy changes. (TargetFrameSubsystem.h, TargetFrameSubsystem.cpp)

  • Frame-rate clamping — When bClampFrameRateToTarget is enabled, SetTargetFPS and ApplyCurrentPolicy set t.MaxFPS to match the target, preventing the engine from rendering frames above the budget target. (TargetFrameDeveloperSettings.h, TargetFrameSubsystem.cpp, DefaultTargetFrame.ini)

Build & Packaging

  • UMG dependency scope — Moved "UMG" from PublicDependencyModuleNames to PrivateDependencyModuleNames since only the widget implementation uses it. (TargetFrame.Build.cs)

Example project generators seen in videos:

CreateTargetFrameExperienceWidget.py (18.1 KB)
CreateTargetFrameShowcaseMap.py (2.2 KB)
CreateTargetFrameTestMap.py (1.0 KB)
TargetFrameMap.zip (76.7 KB)

0.3.0 Changelog, 23 April 2026

This release is a major step towards a more user-friendly, definitive scalability solution. Enjoy!

Settings

  • Simple Mode settings surface — Added a new Simple Mode layer in Project Settings > Plugins > TargetFrame for target FPS, quality bias, first-launch benchmark behavior, runtime adaptation, low-VRAM and integrated-GPU protection, crisp UI handling, stabilization lock, telemetry, and opt-in vendor upscaler or triangle culling support. The existing detailed policy controls now live under Advanced Mode categories and remain available for full tuning.
  • CPU Tick & Animation Governor — TargetFrame now detects when the game is CPU-bound (by comparing total frame time to GPU frame time). When CPU-bound, the governor steps down CPU-intensive scalability groups (sg.FoliageQuality and sg.ViewDistanceQuality) first, avoiding unnecessary degradation of GPU features like resolution scale and upscaler quality.
  • Virtual Shadow Map Guardrails — Added bManageVirtualShadowMaps to the Vendor and Memory Policy. When in low-VRAM mode, the policy dynamically reduces r.Shadow.Virtual.MaxPhysicalPages (defaulting to 1024) to prevent VSMs from blowing out the VRAM budget.

Sample Project Improvements

  • CPU & GPU Stress Showcase — The TF_TargetFrameShowcase map now natively stresses both the GameThread and VRAM. Upgraded ATargetFrameShowcaseActor to spawn an intense 420-element ticking geometry swarm (driving CPU load) and enabled shadows on all dynamic orbit point lights (driving Virtual Shadow Map pressure). This creates a realistic, demanding environment that clearly demonstrates the new CPU Governor and VSM guardrails in action.

Fixes

  • UE 5.7 descriptor alignment — Updated TargetFrame.uplugin to declare EngineVersion: 5.7.0 so the plugin matches the sample project and no longer risks a first-run compatibility rejection when the project is opened in UE 5.7.
  • Fire-and-forget startup target preserved — ScalabilityGameInstance no longer forces StartupTargetFPS back over a fire-and-forget hardware-tier recommendation during startup. Sample projects now keep the wizard-selected 45/60/90 FPS target unless the manual startup path is being used intentionally.
  • Onboarding action uses the dedicated beginner alias — The runtime onboarding action now calls RunTargetFrameSetupWizard instead of the lower-level StartTargetFrame, keeping the widget behavior aligned with the documented beginner Blueprint API and the intended fire-and-forget setup path.

UX and Workflow

  • Editor-side Beginner Setup Wizard — Added a true Window > TargetFrame Beginner Setup Wizard editor entry that opens a branded dark blue and cyan setup tab with embedded TargetFrame logo art from the plugin resources. The window is now project-aware: it shows sample availability, can stay on the current map for real project work, can switch from the showcase to the test map when that is the better sample path, can reset first-run onboarding, start PIE, reopen the runtime wizard and panel in active PIE, re-run the recommended setup, refresh readiness, and jump straight to Project Settings > Plugins > TargetFrame.
  • Project-aware one-click beginner flow — Added Run Beginner Flow to the editor wizard. It resets onboarding, chooses the right map for the current context, starts PIE, waits for the runtime widget when present, applies the recommended setup automatically, and falls back to the Blueprint library path on maps that do not create the sample overlay.
  • Friendly Blueprint setup alias — Added RunTargetFrameSetupWizard, displayed in Blueprint as Run TargetFrame Setup Wizard, as the beginner-facing one-node alias for StartTargetFrameFireAndForget.
  • Automatic setup readiness card — Added a Blueprint-readable project check report and a Check This Project node for explicit readiness validation. In the included runtime widget this report now appears as an automatic Setup readiness card that refreshes while the panel is open, leads with a plain-language readiness state, and moves lower-level issue detail behind Show advanced details.
  • Beginner-focused onboarding and confirmation flow — Reworded the first-run TargetFrame widget around PC class, memory guide, recommended target, and simple next steps. After Use recommended setup, the widget now pauses on a confirmation state with Keep this setup, Show settings, and Start over instead of immediately dumping users into the full control panel.
  • Less engine-centric user-facing labels — Renamed the runtime widget presentation layer around player-readable concepts such as Recommended setup, Current status, Quick choices, Why this setup, Advanced details, and Setup readiness. Technical C++ names, config keys, status fields, and Advanced Mode settings remain unchanged.
  • Intent-first preset labels — Quick choices now lead with Lowest load, Most stable, Balanced, and High refresh, with the FPS target shown secondarily.
  • Setup Wizard main surface — Reordered the runtime panel around the beginner path: recommendation first, current status second, quick FPS choices third, setup reasoning fourth, and optional advanced details collapsed by default.
  • Showcase launcher wording — The showcase still avoids a blocking first-run modal, but the launcher now offers Get Recommended Setup until onboarding is completed, then switches to Open TargetFrame.
  • Regenerated sample widget asset — Rebuilt WBP_TargetFrameExperience from Scripts/CreateTargetFrameExperienceWidget.py so the saved UMG asset matches the current runtime code, labels, onboarding flow, and readiness-card presentation.

Documentation

  • Manual updated for the current beginner flow — Docs/TargetFrameManual.html now documents the project-aware editor wizard, one-click Run Beginner Flow, the live Use recommended setup path, the automatic Setup readiness card, intent-first preset labels, Get Recommended Setup, and the post-setup confirmation state.
  • FAQ aligned to the new flow — Docs/TargetFrameFAQ.html now explains the project-aware Beginner Wizard, automatic readiness behavior, confirmation-state buttons, intent-first preset labels, and the current UE 5.7 sample project state.
  • Expanded beginner quick start guidance — The current documentation now walks users through launching the sample or their own map, running the beginner flow, reading the recommendation, confirming the setup, reopening the panel with F10, and understanding when to use the editor wizard versus the live widget.

0.3.1 Changelog, 2026-04-23

This includes both requested features, and quick fixes needed after 0.3.0.

Added

  • Added a proper bDisableInEditor setting to TargetFrame so editor sessions can cleanly skip subsystem startup, automatic setup, policy application, and runtime governor work without hardcoded editor guards.
  • Added recommendation-preview support, baseline snapshot capture and restore, built-in TargetFrame presets (Laptop Safe, Showcase, QA 60 FPS, and Shipping Conservative), project-fix actions for Check This Project, structured policy history entries, map and world filters, PIE benchmark deferral, and lightweight policy-change notifications across the TargetFrame runtime.
  • Added Blueprint accessors for recommendation preview, baseline restore, built-in preset apply, project-fix application, and temporary editor-session overrides so the runtime UI and editor wizard can share the same workflows.
  • Added editor-wizard controls to temporarily enable TargetFrame for the current editor session, preview the recommended setup in PIE, load named presets, restore the captured baseline snapshot, and re-run readiness checks without changing project defaults.

Changed

  • Updated the sample project’s DefaultTargetFrame.ini override to set bDisableInEditor=True, so TargetFrame stays off by default in editor and PIE unless explicitly re-enabled for testing.
  • Updated the subsystem status summary so editor-disabled, filtered, benchmark-deferred, and observation-only sessions report explicit inactive reasons with clearer next-step guidance.
  • Updated the runtime widget to surface clearer inactive-session reasons, recommendation previews before apply, a live Setup readiness card, structured policy history in the advanced details area, and one-click fix actions from the readiness card.
  • Updated the runtime onboarding and quick-choice UX to use beginner-facing labels such as Get Recommended Setup, Use recommended setup, Current status, Quick choices, Why this setup, and the intent-first preset labels Lowest load, Most stable, Balanced, and High refresh.
  • Updated the post-setup flow so Use recommended setup now pauses on a confirmation state with Keep this setup, Show settings, and Start over instead of immediately dropping users into the full control panel.
  • Refreshed the editor-side Beginner Setup Wizard into a more project-aware surface with Run Beginner Flow, live PIE control buttons, clearer editor-session rule messaging, sample-map awareness, and richer feedback inside the window.
  • Regenerated WBP_TargetFrameExperience from Scripts/CreateTargetFrameExperienceWidget.py so the saved widget asset matches the current runtime UI, onboarding flow, readiness card, and preset labels.
  • Updated the plugin descriptor and packaging filter for the UE 5.7 sample/Fab path by refreshing TargetFrame.uplugin metadata and tightening FilterPlugin.ini to the shipped descriptor, source, config, and official resource files only.

Fixed

  • Fixed ScalabilityGameInstance so a fire-and-forget recommendation no longer gets overwritten by StartupTargetFPS during startup; the sample project now keeps the wizard-selected target unless the manual startup path is being used intentionally.

Documentation

  • Updated Docs/TargetFrameManual.html and Docs/TargetFrameFAQ.html to document the project-aware editor wizard, Run Beginner Flow, the Use recommended setup confirmation step, the automatic Setup readiness card, intent-first preset labels, and the current sample-project flow.

Video update on 0.3: https://www.youtube.com/watch?v=BeoDgp0lFSA

0.4.0 Changelog: 2026-05-18

Additions

  • Handheld / Steam Deck Auto-Detect — TargetFrame now parses the hardware RHIName and GPUBrand to automatically detect Handheld APUs (e.g. AMD Custom GPU 0405, Ryzen Z1). When detected, the governor automatically engages a Handheld Profile targeting a specific, battery-friendly FPS (defaults to 40 FPS) to guarantee stable performance without manual intervention.
  • Cinematic Override State — Added BeginCinematicOverride(int32 TargetCinematicFPS = 30) and EndCinematicOverride() to Blueprint. This enables developers to temporarily suspend the TargetFrame governor during cutscenes, forcing Native AA and Epic quality for pristine presentation, and seamlessly blending back to the dynamic gameplay budget when the scene ends.
  • Dynamic VRAM Pool Governor — TargetFrame now reads the physical graphics memory of the detected GPU and dynamically sets r.Streaming.PoolSize to a configurable safe fraction (default 72%) of available VRAM. This effectively prevents catastrophic texture streaming thrashing and VRAM over-allocation hitches on diverse PC hardware.
  • Battery & Thermal Awareness — The subsystem now hooks into FPlatformMisc::IsRunningOnBattery() to detect laptop battery power. When untethered from wall power, it actively clamps the target framerate (defaults to 30 FPS) to drastically reduce thermal output and prolong play sessions.

Showcase Map Improvements

  • Interactive USP Stations — Upgraded ATargetFrameShowcaseActor with four new interactive trigger zones along the camera spline:
    • Cinematic Override Zone: Forces the camera into the BeginCinematicOverride state to demonstrate seamless quality blending.
    • VRAM Stress Zone: Artificially clamps the texture pool limit to trigger and demonstrate the VRAM Pool Governor’s interception and correction logic.
    • Mock Handheld APU: Forces a “Laptop Safe” 40 FPS state, mocking the behavior of Steam Deck auto-detection.
    • Mock Battery Power: Forces a “Laptop Safe” 30 FPS state, mocking the behavior of battery power auto-detection.

Workflow and UX

  • Session-only editor override in the Beginner Wizard — Added editor-wizard controls to temporarily enable TargetFrame for the current editor session or restore the project-default editor rule. This keeps bDisableInEditor=True practical for sample projects while still making PIE validation fast.
  • Recommendation preview mode — Added PreviewRecommendedSetup / PreviewTargetFrameSetup so the runtime widget and editor wizard can show what TargetFrame would change before applying anything.
  • Built-in named presets and restore path — Added built-in Laptop Safe, Showcase, QA 60 FPS, and Shipping Conservative presets, plus baseline snapshot capture and restore so QA can always return to the pre-TargetFrame runtime state.
  • Actionable readiness fixes — Check This Project issues now carry fix actions and labels. The sample widget promotes the highest-priority action directly from the readiness card, and the editor wizard can re-check the live session after session-rule changes.
  • Clearer inactive-state reporting — Runtime summaries, setup notes, readiness output, and the onboarding presentation now report explicit inactive reasons when TargetFrame is disabled in editor, filtered by map/world, deferred on benchmark work, or forced into observation-only mode.
  • Structured policy history and notifications — Policy changes now record before/after summaries, trigger metrics, CPU/GPU context, and a short notification string. The runtime widget surfaces that history, and optional on-screen notifications can announce live policy changes.
  • Portable showcase sample — Moved the showcase actor and small sample support classes into the plugin module, shipped the sample maps under /TargetFrame/Maps, and added redirects from the old sample-project module so TF_TargetFrameShowcase opens correctly in clean projects.

Runtime and Guardrails

  • Map and world filters — Added optional allowed/blocked map-name filters and preview-world opt-out controls so projects can keep TargetFrame off on menus, preview scenes, utility worlds, or benchmark maps.
  • Benchmark guardrails in PIE — Added bDeferBenchmarkInPIE so synchronous hardware benchmark work can be deferred in PIE instead of hitching the editor preview, while still surfacing an explicit warning in status output.
  • Preset and baseline Blueprint API — Added Blueprint wrappers for recommendation preview, baseline capture/restore, built-in preset apply, custom preset save/load, saved preset enumeration, project-fix application, and editor-session override control.

Defaults

  • bDeferBenchmarkInPIE defaults to True.
  • bEnableMapAndWorldFilters defaults to False.
  • bDisableInPreviewWorlds defaults to True.
  • MaxPolicyHistoryEntries defaults to 12.
  • bShowPolicyNotifications defaults to True.

0.4.0 update, 2026-09-05 :bullseye:

At a glance:

  • Supports Unreal Engine 5.5, 5.6, 5.7 and 5.8 from one source tree, with 5.5 as the floor. :reverse_button:
  • Hardware tiers: performance index ceiling :reverse_button:
  • Hardware sweep & CPU-bound coverage :reverse_button:
  • Source portability :reverse_button:
  • Soak tests adapt to the host :reverse_button:
  • Standalone Sample Plugin added :reverse_button:
  • Extensive API update :reverse_button:
  • Extensive fixes :reverse_button:
  • Extensive testing update :reverse_button:

Detailed changelog follows:

Hardware tiers: the perf index now has a ceiling

  • Added TargetFramePolicyMath::IsUsablePerfIndex, which rejects both ends. A score more than
    MaxPlausiblePerfIndexEpicMultiple (10x) above the engine’s Epic boundary is treated as a failed
    measurement, and the other index decides - exactly as the negative case already did. On the shipped
    18/42/115 thresholds the ceiling is 1150, comfortably above real hardware (a measured RTX 5060 Ti
    is 231) and far below the observed failure.
  • The calibration report shares that predicate. Previously it re-derived usability inline, so a
    divergence would have had DecisionPath claim an index decided the tier when it had been rejected.
  • Added TargetFrame.PolicyMath.ImplausiblePerfIndexIsRejected. It pins the measured 4007.35
    case, both sides of the ceiling boundary, that a real high-end index is still accepted, and that
    two implausible indices yield Unknown rather than a guess. Confirmed to fail against the
    unfixed policy before the fix was applied.

A benchmark result an order of magnitude above the engine’s own Epic boundary means the measurement
is wrong, not that the hardware is extraordinary.

Suite is now 28 tests: 13 PolicyMath, 11 Runtime, 4 Soak.

Engine support: UE 5.5 through 5.8

TargetFrame now builds and runs on UE 5.5, 5.6, 5.7 and 5.8 from a single source tree, with 5.5 as
the floor.

CPU attribution re-confirmed against a real workload

  • Added TargetFrame.Soak.CPUBoundRealWorkload. CPUBoundLadder proves the branch works when the
    classifier is fed CPU-bound inputs; this proves those inputs arise from real work. It burns 30
    ms/frame of genuine game-thread cycles and asserts each link separately: the engine attributes real
    work to GGameThreadTime, the attribution clears the GPU by the classifier margin, the verdict is
    CPU-bound, and the ladder takes the CPU branch. Measured in one run — injected: game 40.0 ms / GPU 5.9 ms; real: game 32.2 ms / GPU 8.3 ms; both CPU-bound, both view distance 3 → 2, foliage 3 → 2. The injected test is not measuring an attribution that never occurs.
  • Worth recording for anyone extending the soak: a Sleep is not a substitute for CPU load. It makes
    the frame long without making the thread busy, leaving the reported game thread near 16 ms, which
    is exactly why this branch went uncovered.

Suite is now 27 tests: 12 PolicyMath, 11 Runtime, 4 Soak.

Hardware sweep and CPU-bound coverage

  • Added Scripts/RunHardwareSweep.ps1. The sweep drives
    TargetFrame.Runtime.HardwareCalibrationReport across every installed engine and records what the
    policy actually resolved to, per machine. Sample captures so far: an RTX 5060 Ti, which produced
    a byte-identical result on all four engines — measured confirmation that reading
    PerfIndexThresholds from GScalabilityIni makes tier selection engine-independent — and a
    Mali-G57 phone.

  • Added TargetFrame.PolicyMath.TierDecisionSurface to cover the tier mapping for hardware not
    physically available, including both measured profiles. It asserts that a better perf index never
    yields a lower pre-memory tier, and pins the low-memory boundary in both directions.

  • Added TargetFrame.Soak.CPUBoundLadder, closing the last known coverage gap. The CPU-bound
    branch of the reduction ladder had no automated test, for a reason recorded in the soak itself: its
    deficit is a Sleep, which makes a frame slow without making the game thread busy, so
    GGameThreadTime never showed CPU pressure — a 30 ms stall yields ~31 FPS while the reported game
    thread stays near 16 ms.

Suite is now 26 tests: 12 PolicyMath, 11 Runtime, 3 Soak.

Soak tests now adapt to the host, and Android runs on device

Validated on a Motorola moto g04 (Android 14, arm64, Unisoc T606, Mali-G57, 3.8 GB RAM, Vulkan): the
ASTC client cooks with zero cook warnings, packages, installs and runs the full suite on device.

The first device run was 22/24, and both failures were the tests assuming desktop-class hardware:

  • Soak.GovernorLadder asserted the deficit phase starts at MaxOverallQualityLevel. Android’s
    device profile caps overall quality at 2, so the assertion failed on a correct engine. It now
    measures the ladder from whatever ceiling the platform actually grants, requires only that headroom
    exists to lose, and logs when the platform capped it.
  • Soak.ShippingCadenceWithTravel asserted post-travel recovery. At ~25 FPS against a 30 FPS
    target the device never produced a surplus, so there was nothing to recover into and the assertion
    was testing the hardware. It now asserts recovery only where a surplus was actually observed;
    otherwise it checks the obligation that still applies — with no headroom the governor must hold
    cost steady rather than keep cutting — and reports not exercised in both the log and a test
    warning. It never quietly passes a phase it did not run.

The governor itself was correct throughout: under deficit it cut quality 2 → 0, and on recovery it
restored 0 → 2.

  • Added TargetFrame.Runtime.HardwareCalibrationReport (suite now 24 tests). GetHardwareCalibrationReport()
    was Blueprint-only with no console path, so there was no way to extract calibration data on a device.
    It logs every input and the decision under a TFCALIB prefix and asserts only self-consistency,
    since the correct tier is a property of the machine rather than of the code.
  • DecisionPath now names the index that actually decided. The phone’s synth benchmark returns a
    negative GPU index (-25.1). The tier rules correctly reject it and fall back to the CPU index
    (82.8 → Mainstream, then stepped down to Entry by the low-memory rule, target 45 FPS) — but the
    report displayed the rejected score beside the verdict, reading as though it had driven the result.
    In the one diagnostic meant for hardware sweeps, that was the wrong thing to leave ambiguous.

Source portability

  • One compatibility shim for the whole 15k-line module, in the new
    Private/TargetFrameEngineCompat.h: FTextureMemoryStats::GetTotalDeviceWorkingMemory() arrived in
    5.6, so on 5.5 the shim reproduces 5.6’s implementation verbatim against the same fields, which are
    identical in all four engines. Everything else compiled unchanged.
  • Tier calibration is unaffected. All four engines ship the identical
    PerfIndexThresholds_ResolutionQuality="GPU 18 42 115", and TargetFrame reads it from
    GScalabilityIni at runtime rather than hardcoding it.

Verified on all four engines

Gate 5.5.4 5.6.1 5.7.4 5.8.2
Editor Dev / Game Dev / Game Shipping 0 warnings 0 warnings 0 warnings 0 warnings
Automation, -game 27/27 27/27 27/27 27/27
Automation, packaged Win64 Development 27/27 27/27 27/27 27/27
Silently-skipped tests (asserted) 0 0 0 0
Cook + stage + pak + archive 0 cook warnings 0 0 0
Widget + both levels staged 3/3 3/3 3/3 3/3

| CPU branch, injected timings | game 40.0 ms vs GPU 5.5-6.2 ms; view distance and foliage 3 → 2 | same | same | same |
| CPU branch, real 30 ms/frame workload | game 31.3 ms | 31.6 ms | 31.4 ms | 31.4 ms — all CPU-bound, all 3 → 2 |
| Calibration captured per engine | Byte-identical across all four |||

On the floor engine additionally: all three assets at package version 1013, both maps open in the 5.5
editor with the expected actors, the widget loads, and zero content load errors.

Use GetHardwareCalibrationReport() and Scripts/RunHardwareSweep.ps1 to close these for your own
target hardware.

Hardware tiers

  • Fixed hardware tiers being roughly one step too low across the board. Tier selection compared
    GetLastGPUBenchmarkResult() against invented absolute thresholds of 130 (Mainstream) and 220
    (Performance). That score is UE’s normalized synth-benchmark perf index, the same scale the engine
    uses in [ScalabilitySettings] PerfIndexThresholds_*, where UE 5.7 ships GPU 18 42 115 — so 115
    is where the engine itself picks Epic quality. Requiring 220 meant a GPU the engine rated
    Epic-quality was classified Entry and handed a 45 FPS target.

    Tiers are now derived from the engine’s own thresholds, read from GScalabilityIni at runtime so
    they track the engine version and any project override. Entry is below the engine’s level-2
    boundary, Mainstream from there to Epic, Performance at or above Epic. The absolute scores remain
    as an explicit override behind bUseEngineCalibratedHardwareTiers=False, with their defaults
    corrected to 42/115.

  • Fixed constrained graphics memory forcing the Entry tier outright. A fast card with little VRAM
    is a quality-ceiling problem, not a frame-rate-target problem, and
    MaxQualityLevelInLowVRAMMode already handles it. Memory now steps the tier down by at most one
    place.

  • Added GetHardwareCalibrationReport() (FTargetFrameHardwareCalibrationReport), reporting the
    perf indices, the thresholds they were compared against, whether those came from engine config,
    which rule fired, whether memory stepped the tier down, and the resulting target. Log one per
    machine during a QA sweep to make tier tuning a data exercise.

Sample plugin

  • Added the TargetFrameSample plugin. The showcase actor, debug HUD, sample game mode, player
    controller and game instance now live there under sample-prefixed names
    (ATargetFrameSampleShowcaseActor, ATargetFrameSampleDebugHUD, ATargetFrameSampleGameMode,
    ATargetFrameSamplePlayerController, ATargetFrameSampleGameInstance), together with both demo
    levels under /TargetFrameSample/Maps/. It declares a dependency on TargetFrame and ships
    Win64/Linux.

    This restores the showcase-in-a-clean-project flow that the correctness pass gave up, without putting demo code
    back into the runtime module. TargetFrame remains the runtime policy plus one drop-in widget.

Distribution

  • Fixed plugin content being silently dropped from a packaged build. Three assets were
    affected and all three were invisible in the editor, which is why this went unnoticed:
    WBP_TargetFrameExperience (resolved by runtime path, so no hard reference for the cooker to
    follow - packaged games fell back to the bare native widget with none of the designer layout) and
    TF_TargetFrameTestMap (referenced by nothing, so the demo shipped with its second level missing
    and OpenLevel silently no-opped on it). Confirmed on both UE 5.7 and 5.8, and confirmed present
    even in a full unrestricted cook.

    The fix has to live in the consuming project’s DefaultGame.ini, because
    UProjectPackagingSettings is not merged from plugin config - a plugin cannot declare this for
    you. Assets need DirectoriesToAlwaysCook; levels need MapsToCook, which
    DirectoriesToAlwaysCook does not cover. See the packaging section of the integration guide.

  • Added Status.EvaluationWindowResets, a monotonic count of runtime evaluation-window restarts.
    RecentEvents is a small ring and evicts the corresponding entry once the experience widget is
    actually loaded, so it was not a sound signal to check a reset against.

  • Fixed the plugin shipping a stale settings widget. Content/UI/WBP_TargetFrameExperience.uasset
    existed in both the plugin and the sample project, and the two had diverged: the plugin’s copy was
    byte-identical to an older artifact snapshot while the project’s was a later regeneration. The
    generator script wrote to /Game/UI and the copy into the plugin was manual, so the shipped asset
    fell six days behind. The newer asset is promoted into the plugin, the project copy is gone, and
    Scripts/CreateTargetFrameExperienceWidget.py now generates directly into /TargetFrame/UI so the
    two cannot diverge again.

  • Removed the /Game/UI/ widget fallback from the sample player controller. It was unreachable in a
    consuming project and was the last /Game/ reference in either shipping plugin.

  • Added Scripts/BuildDemoDistribution.py, which stages the shippable demo project. See the
    distribution note in the project README.

Tests

  • Expanded automation coverage from 20 tests to 23, adding perf-index threshold parsing (including
    malformed and non-monotonic input), tier resolution across every engine boundary with an explicit
    regression case for the old 220 threshold, and the one-step memory constraint.

Governor correctness

  • Fixed the governor going blind below the hitch threshold. Frames longer than
    HitchThresholdSeconds (default 100 ms) were excluded from the smoothed frame rate
    unconditionally, and that was the only writer of SmoothedFPS. A machine that settled into a
    sustained sub-10 FPS state froze the filter at its last good value, so no deficit was ever seen and
    no step-down ever fired. Isolated long frames are still rejected as hitches; a run of them that
    outlasts the new SustainedSlowFrameWindowSeconds is now accepted as the real frame rate.
  • Fixed frame-rate-dependent smoothing. The filter used a fixed per-frame lerp, so its time
    constant varied by roughly 8x between a 30 FPS and a 240 FPS machine. Smoothing is now exponential
    in wall-clock time via FPSSmoothingTimeConstantSeconds.
  • Fixed frame timing being scaled by time dilation. The governor sampled the world tick delta, so
    slomo 0.5 reported double the real frame rate. All governor timing now uses the undilated
    application delta.
  • Fixed CPU/GPU misclassification. IsLikelyCPUBound compared max(Game, Render, RHI) against
    GPU time, but the render and RHI timers absorb GPU fence and present waits, so a GPU-bound frame
    inflated them to roughly GPU time and read as CPU-bound. The game thread is now the primary signal;
    render and RHI only corroborate across a wider margin.

Policy levers

  • Fixed the CPU governor permanently retiring the overall-quality lever. One CPU-bound step moved
    view distance and foliage, which makes GetOverallScalabilityLevel() return -1, which the
    overall-quality lever read as a protected custom mix and refused to touch for the rest of the
    session. TargetFrame now tracks the level it drives (Status.ManagedOverallQualityLevel) and
    applies presets while preserving the groups the CPU governor owns, so both levers compose. A
    genuinely player-authored custom mix is still protected.
  • Fixed DLSS runtime quality steps silently failing. The status never read r.NGX.DLSS.Quality
    back, so every DLSS step compared Auto against Auto, reported no change, and the governor fell
    through to the next lever. Upscaler steps are now judged by the requested state.
  • Fixed XeSS Native AA reading back as Ultra Performance. The write mapped it to 5 while the read
    clamped through a 0-4 rank table. Each vendor now has an explicit read/write mapping pair.
  • Fixed the recovery ladder not mirroring the reduction ladder. The CPU governor was the first
    lever down and also the first back up, so view distance and foliage oscillated ahead of everything
    else. It is now the last lever restored.

Ownership and lifecycle

  • Fixed BeginCinematicOverride bypassing every guardrail. It ignored the session-disabled gate,
    hardcoded Epic quality and 100% resolution past the low-VRAM, vendor and player ceilings, flattened
    protected custom mixes via SetOverallScalabilityLevel, and could persist that to disk. It now
    honours the same gates as every other apply path and is always session-only.
  • Added a cinematic-override failsafe. A missed EndCinematicOverride left the governor
    suppressed for the whole session with no timeout. It now ends on world change
    (bEndCinematicOverrideOnWorldChange), on a timeout (MaxCinematicOverrideSeconds), and on
    subsystem shutdown.
  • Fixed the hardware benchmark flattening protected custom quality mixes. RunHardwareBenchmark
    overwrites ScalabilityQuality in place; the derived preset is now rolled back when the player’s
    mix is protected, keeping the benchmark scores.
  • Fixed the secondary-screen-percentage CVar leaking. Turning Upscaler-Safe UI off mid-session
    left TargetFrame’s last value applied. The original value is now captured and restored.
  • Fixed dangling console-variable pointers. The CVar cache held raw IConsoleVariable* for the
    subsystem’s lifetime; a module unload (vendor upscaler plugin, editor hot reload) left it holding
    freed memory. The cache is now dropped on any module change.

API and diagnostics

  • SetTargetFrameTargetFPS no longer silently activates policy control. It re-applies only when
    TargetFrame is already active, or when the new bApplyPolicyNow pin is set.
  • Made capability probing real. bSupportsOverallQuality and bSupportsResolutionScale were
    just “is UGameUserSettings non-null”; they now probe the scalability group CVars and the actual
    resolution range. bSupportsTriangleCulling was always true from a game-instance subsystem and now
    requires a world, which is what the policy actually walks.
  • Fixed Check This Project flooding the event ring it displays. The setup panel re-ran it on a
    5 s timer and each pass appended to the 8-entry recent-events list, evicting real policy events.
    Results are now deduplicated and the panel polls every 30 s.
  • Fixed event timestamps running backwards. They used the evaluation clock, which resets on every
    world change and activation; a monotonic session clock is used instead.
  • Fixed presets discarding most of the setup. “Save current as preset” stored only target FPS,
    the capsule flag and the governor-pause flag. The payload is now versioned and also carries
    quality, resolution, dynamic resolution, triangle-cull scale, Nanite detail and upscaler state,
    restored through the guarded setters. Preset names containing = or ; now round-trip.
  • Scoped the editor-session override per game instance so concurrent PIE clients no longer share
    it, and cleared the process-wide override on EndPIE so it cannot leak into the next session.

Performance

  • Fixed the triangle-culling policy walking every actor in the world on every step. The candidate
    scan is now separated from the per-step rescale: heavy meshes and their base cull distances are
    cached, runtime steps only rescale that set, and the full scan is bounded by
    TriangleCullRescanIntervalSeconds plus world changes.
  • Fixed the sample debug HUD deep-copying the whole status block every frame, including the
    policy-history and event arrays, before its own visibility check. Added scalar GetSmoothedFPS,
    GetTargetFPS and GetEffectiveTargetFPS accessors for per-frame consumers.

Packaging

  • Removed the sample game classes from the shipped runtime module. AScalabilityGameMode,
    UScalabilityGameInstance, AScalabilityDebugHUD, AScalabilityPlayerController and
    ATargetFrameShowcaseActor were compiled into every consuming project, taking generic names in the
    global UObject namespace and carrying a tf.DebugHUD CVar, F8/F9/F10 key bindings, a
    bugscreenshot exec path, a hard /Engine/BasicShapes reference and a /Game/UI/ lookup with
    them. They now live in the sample project’s own module.
  • Removed the plugin’s Config/DefaultEngine.ini, which injected /Script/Scalability CoreRedirects
    into every consuming project.
  • Narrowed the packaged content filter to the drop-in experience widget and removed the duplicated
    showcase maps.

Found by the new runtime and soak suites

  • Fixed ResetToDefaults reporting the subsystem as uninitialized. It default-constructed the
    status block, clearing bInitialized - the public “is TargetFrame running” flag that the debug
    HUD, the settings widget and Check This Project all branch on - while the subsystem was still
    live. Any Blueprint calling Reset To Defaults left status permanently claiming it was not running.
  • Made the per-session disable override work outside the editor. SetSessionDisableOverride was
    gated behind WITH_EDITOR and GIsEditor, so in a packaged game it silently did nothing and
    BeginCinematicOverride would still mutate settings on a session the project had disabled. It is
    now a real runtime control in every build, reported as the new
    ETargetFrameInactiveReason::DisabledBySession.
  • Surfaced console-variable writes the engine drops. TargetFrame writes at
    ECVF_SetByGameSetting; when the console, a device profile, or project code using
    ECVF_SetByCode already owns a variable, the engine discards the write silently and the policy
    reported success anyway. Dropped writes are now listed in Status.RejectedConsoleVariables,
    called out in the setup notes, and logged once each.

Tests

  • Expanded automation coverage from 4 tests to 20 across three suites, all 20 verified green in a
    packaged Win64 Development build:
    • TargetFrame.PolicyMath (8): hitch rejection, sustained-slowdown observation,
      frame-rate-independent smoothing, managed-quality resolution, frame budgets, and a GPU-bound
      regression case for the CPU/GPU classifier.

    • TargetFrame.Runtime (10): observation-first behaviour, the CPU/overall-quality lever
      interaction, baseline round-trip, cinematic ceilings and session gating, live frame-rate caps,
      secondary-screen-percentage ownership and release, preset round-trip, scalar accessors, and
      monotonic event timestamps. These need a live world - run them from PIE or a packaged build.

    • TargetFrame.Soak (2, multi-phase). GovernorLadder accelerates the governor and sweeps the
      whole ladder: it asserts that a sustained deficit reduces cost within the configured floors,
      that a surplus actually restores cost rather than merely producing actions, that recovery does
      not oscillate, that the stabilization lock engages, that the classifier agrees with the timings
      each decision recorded, and that the baseline restores with no leaked cinematic override or
      governor suppression. ShippingCadenceWithTravel leaves the packaged cadence alone, checks that
      the first action respects warmup plus hysteresis, performs a level transition mid-soak, and
      verifies the subsystem survives it, resets its evaluation window, and still recovers afterwards.

      Known gap: the soak creates load with a per-frame stall, which is not visible to
      GGameThreadTime from either injection point available to an automation test, so it cannot
      exercise live CPU-bound branch selection. The classifier’s logic and its agreement with its own
      recorded inputs are covered; the live branch needs a real game-thread workload on target
      hardware. See the CPU/GPU classification notes in Docs/ProductionIntegration.md.

Production safety

Production safety

  • Added explicit-activation mode. TargetFrame initializes as an observer by default and cannot run the governor until StartAutomaticSetup or ApplyCurrentPolicy is called.
  • Automatic changes are session-only by default. Persist Automatic Changes is a separate, visible opt-in.
  • Added dedicated Server exclusion in the descriptor plus a runtime ShouldCreateSubsystem guard for dedicated and non-rendering processes.
  • Changed Simple Mode so texture-pool, VSM, HWRT, and Nanite ownership requires the separate Allow Project-Specific Rendering Overrides opt-in.
  • Changed production defaults to disable startup benchmarking, Editor execution, direct upscalers, texture-pool management, VSM changes, HWRT changes, Nanite policy, UI secondary percentage, triangle culling, periodic probe logs, and on-screen notifications.
  • Disabled paths no longer write reset/default values into project-owned dynamic-resolution, texture-pool, VSM, or secondary-screen-percentage controls.

Frame-budget correctness

  • Added requested and effective targets. The effective target respects GameUserSettings, t.MaxFPS, fixed frame rate, and engine smoothing.
  • Recovery headroom is clamped below known external limiters, eliminating unreachable 60/68 and 90/98 recovery windows in capped games.
  • Dynamic-resolution frame budgets now use the effective target.
  • Battery/AC transitions are polled during automatic targeting. Optional power caps are applied through GameUserSettings and restore the prior player cap.
  • Handheld detection now considers both GPUBrand and RHIName and recognizes additional Steam Deck/Van Gogh/ROG/Z1/Z2 identities.

Settings integrity

  • Baseline/cinematic snapshots now copy the complete base ScalabilityQuality structure and the GameUserSettings frame limit.
  • Custom per-group quality mixes remain -1 and are protected from overall-preset flattening by default.
  • Recovery respects captured overall-quality, resolution, view-distance, and foliage ceilings.
  • Restore Baseline now stops automatic policy control so the next governor tick cannot immediately overwrite the restored state.
  • Hardware benchmark results can be applied for the session without calling Unreal’s always-save ApplyHardwareBenchmarkResults path.

Runtime policy and observability

  • Replaced application-delta CPU classification with Game/Render/RHI/GPU thread timing that excludes ordinary frame-wait time.
  • CPU fallback exhaustion now falls through to compatible GPU fallbacks instead of terminating the step.
  • Added CPU-group checks to cost-floor/quality-ceiling detection and made CPU recovery independent of the current bottleneck label.
  • Runtime evaluation resets on world changes and suppresses invalid samples while unfocused, paused, or in a cinematic override.
  • Corrected the fallback ladder to reduce overall quality and Nanite detail before scene resolution, with inverse recovery.
  • Added cached CVar discovery, including cached misses, to prevent repeated failed FindConsoleObject polling.
  • Capability probing now requires project-enabled Nanite/ray-tracing state instead of CVar presence alone.
  • Status, setup summaries, CSV, policy history, and runtime probes now include effective target, external limiter, reachable thresholds, and thread timings.
  • Corrected device working-memory selection to use Unreal’s discrete/integrated GetTotalDeviceWorkingMemory behavior.

Developer workflow

  • Added four automation tests for unlimited budgets, user caps, engine smoothing, and CPU/GPU classification.
  • Added a standalone plugin README and Docs/ProductionIntegration.md, and included the documentation plus changelog in the packaged plugin filter.
  • Removed a transitive-include dependency from the showcase actor that was exposed by clean-host plugin packaging.
  • Aligned descriptor metadata and updated the sample documentation.

Verification

  • ScalabilityEditor Win64 Development builds successfully with UE 5.7.4 and UHT warnings-as-errors.
  • Scalability Win64 Development builds successfully.
  • RunUAT BuildPlugin succeeds for a clean Win64 host across UnrealEditor Development plus UnrealGame Development and Shipping.
  • TargetFrame.PolicyMath automation suite passes 4/4 under UnrealEditor-Cmd with NullRHI.

0.5.0 update, 2026-09-28 :bullseye:

Supports Unreal Engine 5.5, 5.6, 5.7 and 5.8 from one source tree, with 5.5 as the floor.

Lighting policy: Lumen, hardware ray tracing, MegaLights and the GI method :light_bulb:

TargetFrame can now manage how a scene is lit, as one more rung on the adaptation ladder. Off by
default in Advanced Mode (bEnableLightingPolicy); Simple Mode enables it together with its other
project-specific rendering overrides.

  • Six modes, cheapest to richest: Static, ScreenSpace, LumenSoftware, LumenHardware,
    LumenHardwareMegaLights, plus ProjectDefault (leave the project’s setup alone). Each maps to
    r.DynamicGlobalIlluminationMethod, r.ReflectionMethod, r.Lumen.HardwareRayTracing and
    r.MegaLights.Allowed.
  • Never above the project. The project’s own setup (read at startup from its renderer settings)
    is the ceiling for every tier and for the player’s choice. A project without MegaLights never gets
    it; a project on screen-space GI is never raised to Lumen.
  • Only rungs that exist. Hardware ray tracing and MegaLights are offered only where the
    hardware-RT guard allows them; Static only when the world has built lighting (configurable);
    MegaLights only when the project enables it (r.MegaLights.EnableForProject).
  • Per tier: Performance LumenHardwareMegaLights, Mainstream LumenSoftware, Entry
    ScreenSpace by default. At runtime the governor steps lighting down after culling and before
    overall quality, never below MinimumRuntimeLighting, and restores it in reverse order.
  • Screen-space fallback. When scalability scales Lumen off (GI quality 1 on UE 5.5 to 5.7), the
    scene would otherwise lose global illumination entirely; TargetFrame switches to screen-space GI
    instead (bFallBackToScreenSpaceWhenLumenIsScaledOff).
  • Reported in FTargetFrameStatus::Lighting, the setup notes, the preview, the capabilities scan,
    the CSV (LightingMode) and DescribeRuntimeState; captured and restored with every snapshot.

Player graphics settings: API and a ready-made screen :bar_chart:

  • GetPlayerSettings / ApplyPlayerSettings: quality, 3D resolution, frame-rate goal
    (0 = automatic), frame-rate cap, V-Sync, display mode, resolution, lighting, adaptive on/off.
    GetPlayerSettings returns the player’s choice, never the governor’s current reductions.
    ApplyPlayerSettings saves the choice, makes it the new ceiling, and resumes the governor from it.
  • GetRecommendedPlayerSettings returns a setup for this machine with its reasons, worded for
    players (“This graphics card scores 231 on the engine’s performance index; Performance starts at
    115.”).
  • GetAutomaticTargetFPS is what “Automatic” resolves to on this machine (tier, handheld and
    battery rules), for labelling the option.
  • UTargetFrameSettingsWidget: a complete, gamepad- and keyboard-navigable graphics screen in
    C++ Slate, with a “Recommended for this PC” button, revert, apply and a live frame-rate readout.
    UTargetFrameSettingsWidget::ShowGraphicsSettings(PlayerController) opens it; the experience
    widget gains a “Graphics settings…” button, on its own line under the quick choices, that
    does the same.
  • SetUserSettingsOverride points TargetFrame at a settings object other than
    GEngine->GetGameUserSettings().

Fixes :construction_worker_man:

Each was reproduced by a test that failed on the unfixed code before the fix was written.

  • Renderer console variables were written below the priority the project sets them at.
    URendererSettings applies the GI method, reflection method, Lumen hardware RT, anti-aliasing
    method and TSR upsampling at project-setting priority, which outranks the game-setting priority
    TargetFrame wrote at, so those writes were silently dropped. In 0.4.0 this meant the hardware
    ray-tracing guard could never actually clamp (invisible on machines that allow HWRT) and upscaler
    anti-aliasing writes did nothing. TargetFrame now writes at project-setting priority when, and only
    when, the variable is already there. Tests: HardwareRayTracingGuardCanClamp,
    LightingPolicyAppliesAndRestores.
  • Session-only changes were saved to disk. UGameUserSettings::ApplySettings always calls
    SaveSettings (verified in the 5.5 and 5.8 sources). TargetFrame called it at five sites that are
    meant to be session-only (cinematic mode, snapshot restore, benchmark apply, SaveCurrentSettings
    and dirty-settings flush), so with bPersistAutomaticChanges off, governor reductions, clamps and
    cinematic raises were still written to the player’s config. They now apply with
    ApplyNonResolutionSettings inside a render-state recreate context: nothing saved, no window
    re-apply. Test: SessionOnlyChangesAreNotSaved.
  • A preset at a non-default resolution was treated as a custom mix. The engine reports an overall
    level only when resolution quality equals the preset’s default, so “High at 100%” read as custom:
    the overall-quality lever was disabled and the player’s quality ceiling skipped, which let
    cinematic mode raise High to Epic. Presets are now recognised regardless of resolution, and the
    ceiling is the baseline’s minimum per-group level. Tests: PresetWithOwnResolutionIsNotACustomMix,
    PlayerApplyBecomesTheNewCeiling.
  • A stale baseline survived deactivation. ResetToDefaults and baseline restore released control
    but kept the old baseline, and the next activation reused it. The baseline is now marked stale and
    recaptured on the next activation.
  • The test harness itself resized the window and saved. Tests called ApplySettings(false),
    which re-applied the player’s saved 2560x1440 to the 1280x720 test window and saved to disk, so the
    soak ran at 1440p after the Runtime suite. Found through a new SOAK env log line; all tests now use
    ApplyNonResolutionSettings.
  • The resolution lever never moved for a player who had not saved a scale. Every supported engine
    gives a fresh profile ResolutionQuality 0, meaning “the project’s default screen percentage”
    (FQualityLevels::SetDefaults). TargetFrame read it as 0%: the status and the experience panel told
    players “3D scene at 0.0% scale” and “quality level -1”, and the player’s resolution ceiling became
    0, so every resolution step was clamped to nothing and the governor skipped that lever. The project
    default now resolves to what the engine renders at (its own screen-percentage heuristic for the game
    viewport); the lever steps down from it and back up to it, writing the default itself back rather
    than a fixed number, and applying the settings screen without touching the scale keeps it.
    Status.bResolutionScaleIsProjectDefault says which it is. Found while filming the tutorial: the
    soak tests pin the scale to 100% before they start, so none had ever run from a fresh profile.
    Test: ProjectDefaultResolutionIsAScale (red: dev-tests-58-defres-red.log).

Behaviour changes to check when upgrading :check_box_with_check:

  • A UGameUserSettings subclass that hooks ApplySettings no longer sees TargetFrame’s session
    applies. Hook ApplyNonResolutionSettings, which is still called. Its own fields are untouched
    (test: DerivedUserSettingsFieldsAreUntouched; a custom class is reported at Info level).
  • Renderer variables that the project sets are now actually changed by TargetFrame where a feature
    is enabled to change them (see the first fix). Anything that looked like it worked in 0.4.0 because
    the write was dropped will now take effect.
  • For players on the engine’s project-default resolution (every fresh profile), the governor’s
    resolution lever now moves, and Status.ResolutionScale reports the resolved percentage instead
    of 0. Code that treated ResolutionScale == 0 as “default” should read
    bResolutionScaleIsProjectDefault.

Sample: Overclock :joystick:

The TargetFrame Sample plugin’s demo is now Overclock, a short arcade game whose arena gets
heavier every level (more shadow-casting lights, effects and geometry) while TargetFrame holds the
frame rate, with a live policy HUD, the new graphics screen, CC0 music and sound, and a capture
timeline for Movie Render Queue. See the sample’s README.

  • The runtime monitor and showcase banner are Slate widgets now, not canvas drawing, so Movie
    Render Queue’s UI pass captures them. They sit in the player’s viewport layer, under the
    TargetFrame panel rather than over it. tf.DebugHUD still toggles the monitor.
  • The showcase lit correctly again under Lumen. After the project moved to Lumen, hardware ray
    tracing and MegaLights, its sunlight-scale key light and uncapped exposure washed the scene out to
    white; it now uses a dusk-level key light, a captured sky light and bounded histogram exposure.
  • Capture variants for tutorial footage: -OverclockGovernorDemo (the governor acts on the
    render’s fixed 60 FPS against a 90 goal, then the goal drops to 45) and -OverclockLightingTour
    (TargetFrame’s lighting modes stepped through ApplyPlayerSettings). One MRQ preset per video, each
    overriding MRQ’s game mode with the map’s own.
  • Unattended review for any sample map: -ReviewShots, -ReviewQuitAfter and -ReviewExec,
    whose entries can be timed (12@TargetFrameShowPanel). Screenshots are the engine’s own frame, and
    the subsystem writes them itself from the viewport’s capture delegate. UE 5.5 writes a requested
    screenshot only when screenshot tracing is compiled in, and it is not in Shipping, so 5.5 Shipping
    builds silently saved none.
    In Shipping the command line cannot pick the map (the engine ignores it), so reviews travel with
    -ReviewExec="open <map>|...".
  • Overclock’s TargetFrame strip is right-aligned: centred, it ran into the dash readout once the
    sparkline was visible.

Tests

41 tests: 16 PolicyMath (3 new, lighting), 21 Runtime (10 new: lighting, player settings, user
settings round trip, derived settings, custom-class reporting, HWRT clamp, preset recognition,
session-only saving, project-default resolution), 4 Soak.

Verification

Four-engine gate r4, 2026-09-27: re-run after the sample’s review screenshots moved to the capture
delegate. Clean tree per engine (RTX 5060 Ti, Windows 11). All 41 tests passed everywhere. On 5.6, the
-game log ended right after the session stopped, without its “EXIT CODE: 0” line, and there was no
crash report; a 5.6 re-run was clean on every row. The same table held for r3.

Check 5.5.4 5.6.1 5.7.4 5.8.3
ScalabilityEditor Win64 Development 0 warn* 0 warn* 0 warn 0 warn
Scalability Win64 Development 0 warn* 0 warn* 0 warn 0 warn
Scalability Win64 Shipping 0 warn* 0 warn* 0 warn 0 warn
Automation RunTests TargetFrame, -game 41/41 41/41 41/41 41/41
BuildCookRun cook + stage + pak + archive 0 cook warnings 0 cook warnings 0 cook warnings 0 cook warnings
Automation RunTests TargetFrame, packaged 41/41 41/41 41/41 41/41

* The only notice is the toolchain’s “compiler is not a preferred version” on 5.5 and 5.6.

Fab packages (Scripts/PackageTargetFrameFab.ps1), compiled from the staged files twice:

  • Non-unity: -StrictIncludes, i.e. no PCH and no unity.
  • Unity: Fab’s settings, bUseUnityBuild true and adaptive unity off. A unity leg passes only if its
    module really compiled as a unity blob.
Leg 5.5.4 5.6.1 5.7.4 5.8.3
TargetFrame, BuildPlugin Win64 pass / pass pass / pass pass / pass pass / pass
TargetFrame, BuildPlugin Linux pass / pass pass / pass pass / pass pass / pass
TargetFrame, BuildPlugin Android pass / pass (NDK r25b) pass / pass pass / pass pass / pass
TargetFrameSample, editor Win64 pass / pass pass / pass pass / pass pass / pass
TargetFrameSample, game Linux pass / pass pass / pass pass / pass pass / pass
TargetFrameSample, game Android pass / pass (NDK r25b) pass / pass pass / pass pass / pass

Each cell reads non-unity / unity.
Every pass is real compile actions for that platform with 0 warnings.

Shipping, packaged, run (Scripts/RunShippingValidation.ps1, 2026-09-27):

  • A Shipping build has no logging and no automation tests. Each package was therefore judged on its
    warnings, a run that stays up, exits 0 and leaves no crash report, and screenshots.
  • The screenshots are of the runtime monitor and the experience panel on the showcase map. They show
    TargetFrame’s live profile, tier, goal, frame rate, quality, render scale, lighting and events.
Shipping 5.5.4 5.6.1 5.7.4 5.8.3
Win64 package 0 warn* 0 warn* 0 warn 0 warn
Win64 run 7 shots, exit 0 7 shots, exit 0 7 shots, exit 0 7 shots, exit 0
Linux package (cross-compiled) 0 warn* 0 warn* 0 warn 0 warn
Linux run (WSL, lavapipe) 7 shots, exit 0 7 shots, exit 0 † 6 shots, exit 0 7 shots, exit 0
Android package (ASTC, arm64) 0 warn* 0 warn* 0 warn 0 warn

On Linux, TargetFrame correctly reports hardware ray tracing as Clamped on lavapipe, and the governor
tightens culling at about 1 FPS.

A gamified demo comes with v0.5.0:

0.5.0 Tutorial video, quite longish: