Questlight Studio
Docs / Platforms and performance

24. Platforms and performance

In short: RPG Saga Engine is built and tested on Windows (Win64) and plays on the Steam Deck. It has a low-end fallback path with no Lumen, Nanite or Virtual Shadow Maps, a Steam Deck device profile, a scripted benchmark with budgets, and a platform layer (saves, users, controllers, suspend) that prepares console work. Mobile code exists but is not built or verified yet, and no console has been tried.

24.1 Basics

24.1.1 What is supported, honestly

PlatformStatus
Windows PC (Win64)Editor and packaged game. Built and tested.
Steam DeckPlayed and checked: 1280 x 800, gamepad prompts for the device in use, readable text, a device profile. Not yet measured on real hardware for every scene.
Android and iOSNot verified. The plugin's descriptor lists Win64 only, in SupportedTargetPlatforms and in every runtime module's PlatformAllowList. The mobile code (touch controls, phone UI, rendering profiles) is in the source and passes its PC-side tests, but no Android or iOS build has been compiled or run. The first Android build is blocked on the engine's Android target component not being installed on the test machine.
ConsolesNot built or certified. We have no console SDKs or devkits. The framework only prepares the ground (see below).

If you need a platform that is not Win64, you change the descriptor yourself (steps below) and take on the verification.

24.1.2 Four things every project should do

  1. Pick the quality level on a real low-end machine, or in the editor with RSE.Visual.DeckPreview 1 (it applies the Steam Deck profile, the Low scalability level and a 1280 x 800 window).
  2. Run the benchmark (RSE.Perf.Benchmark) before you ship and keep the report.
  3. Read the content budget warnings in Validate All Data. They point at meshes without LODs and textures that are too big.
  4. Switch off the systems you do not use (Project Settings → RPG Saga Engine → Features). A disabled system's subsystems are not created at all, so it costs nothing at run time.

24.1.3 Scalability and the low-end path

The player picks a quality level in Options → Quality: Low, Medium, High or Epic. On top of the engine's levels the framework supports a fallback path for weak hardware:

  • no Lumen, no Nanite, no Virtual Shadow Maps (classic LODs, baked or simple lighting, cheap cascaded shadows);
  • a cheap anti-aliasing method with an internal resolution scale;
  • fewer particles, less foliage, a shorter view distance on Low;
  • the visual styles (realistic, stylized, cel-shaded, low-poly retro) all work without the heavy features.

The Low level is meant to look like the Steam Deck. In your own project that means Low sets the resolution scale, the shadow quality, the texture quality, the effects and foliage quality to the Deck's values, so what you see on a PC at Low is what the Deck draws.

One trap: a setting written in Project Settings → Rendering has a higher priority than the scalability files. If you set Shadow Map Method to Virtual Shadow Maps there, Low and Medium cannot turn them off any more (the log says "Setting the console variable ... with 'SetByScalability' was ignored"). Leave it to the scalability levels.

24.2 How it works

24.2.1 Device profiles

Unreal picks a device profile by platform name only: a Windows build running through a compatibility layer on a Steam Deck is simply Windows. The framework therefore switches profile itself at game start, in URSEDeckProfileSubsystem (a game instance subsystem that runs in the game, not in the editor):

  1. It detects a Steam Deck from the SteamDeck=1 environment variable (Steam sets it), the -SteamDeck command-line switch, or Input → Devices → Simulate Steam Deck.
  2. If Apply Deck Profile is on and the command line has not chosen another profile, it activates the profile named in Performance → Deck Device Profile (SteamDeck, or SteamDeck_Linux for a native Linux build).
  3. The player's saved quality levels are then applied over the profile's defaults, so Options → Quality still wins.

The profile holds two kinds of values. The sg.* quality groups are defaults (priority: scalability), so the player can change them. Everything else is a hard limit of the platform (priority: device profile): Nanite off, Lumen off, Virtual Shadow Maps off, motion blur off, and so on. They stay active whatever quality the player chooses.

The plugin does not ship the Deck profile. It lives in your project's Config/DefaultDeviceProfiles.ini. A working starting point is in Troubleshooting below. Without it the log says "Device profile 'SteamDeck' not found" and nothing changes.

On a PC you can force a profile for one run with -dp=SteamDeck.

24.2.2 The Steam Deck experience

  • Input device. URSEInputDeviceSubsystem follows the last device used. A Deck starts on the gamepad. Key prompts show only the device in use, with the right button names. Gamepad label sets (a few styles) are chosen by the player in the options, else by the project, else automatically.
  • Left stick in menus. While a menu has focus, the left stick moves the cursor like a D-pad (settings under Input → Menu Stick).
  • Text size. The project's DPI scaling rule works from the screen's short side, so text stays readable at 800p. The built-in screens never draw text smaller than UI Theme → Minimum Font Size.
  • Resolutions and aspect ratios. Panels grow with their text up to a maximum, long text is clipped or wrapped, and fixed-size windows shrink as a whole on small screens instead of overflowing.
  • First start. Once, if the player has no frame-rate limit, the framework sets Deck First Run Frame Rate Limit (40, the Deck's 40 Hz mode). The player changes it in the options.

24.2.3 Preview and benchmark

RSE.Visual.DeckPreview 1 shows the look and the relative cost (draw calls, triangles, memory) on your PC. It does not show the Deck's real frame rate, because a PC GPU is several times faster. RSE.Visual.DeckPreview 0 restores your settings.

RSE.Perf.Benchmark plays a scripted run and writes a report. It needs no input and takes about three minutes with the default steps. The steps are data (Performance → Benchmark Steps): open a map, run console commands, wait, capture (with an optional camera orbit), and capture a whole battle with a time limit. It measures every frame through the engine's performance system (frame, game thread, render thread, GPU, draw calls, triangles) and takes a memory snapshot at the end of each segment. The first second of each segment (Warmup Seconds) is not counted.

RSE.Perf.Benchmark            current settings
RSE.Perf.Benchmark Deck       preview the Steam Deck first
RSE.Perf.Benchmark Quit       quit the game when finished
RSE.Perf.Benchmark Stop       stop and write what was recorded

Definitions: average FPS is frames divided by time. 1% low is 1000 divided by the mean of the slowest 1% of frame times. A hitch is a frame longer than Hitch Frame Ms (a warning, not a failure). The report goes to Saved/Perf/<timestamp>/: report.csv (one row per segment with PASS or FAIL and the reasons), frames.csv and summary.html.

24.2.4 Performance budgets

Performance → Budget (FRSEPerfBudget) holds the limits a capture is checked against. A value of 0 means not checked. The defaults are the Steam Deck budget:

LimitValue
Average FPS30 or more
1% low25 FPS or more
GPU time (average)17 ms or less
Draw calls (worst frame)1500 or less
Triangles (worst frame)2 million or less
Physical memory6 GB or less
Texture memory2 GB or less

RSE.Perf.Budget <Tier> checks any capture of yours against another budget (Default goes back to the Deck budget).

Content budgets work at edit time. Project Settings → RPG Saga Engine → Content Budgets (URSEContentBudgetSettings, editor only) defines categories (for example Character, Boss, Prop) by folder or name token, each with a maximum triangle count, a minimum LOD count, material slots, texture size and, optionally, material instruction counts. Validate All Data warns about meshes, textures and materials over their category's budget.

24.2.5 Saves, users and the app lifecycle

These are the pieces that differ between platforms. They all go through one layer you can configure.

Save backend. Every save and load goes through URSESaveSubsystem, which turns the game into bytes and hands them to a backend. Pick it in Project Settings → RPG Saga Engine → Save → Backend:

BackendUse it for
File (default)PC and Steam Deck. Our own files under Saved/SaveGames, with a checksum, a temporary file renamed over the slot and the last good save kept as a backup. Checks free disk space first (Min Free Disk Space MB).
Save Game SystemConsoles, and any platform whose rules require the platform save API. Uses the engine's ISaveGameSystem. The bytes keep the checksum, so damage is detected everywhere.
CustomA C++ class implementing IRSESaveBackend for cloud storage or a platform API the engine does not cover.

Slots, autosaves, thumbnails, the load screen and the Blueprint nodes work the same on every backend. The save screens handle No Space, Corrupt (shown as damaged, never as empty, never half-loaded), User Signed Out, Busy and Failed. Every failed save fires On Save Failed with the reason. RSE.Save.SimulateFault makes the backend fail with NoSpace, Corrupt, SignedOut or Failed so you can see each message on PC.

Platform users. URSEPlatformUserSubsystem follows the engine's platform users. The title screen's "press start" makes the user of the pressing device the primary user (Platform → Select User On Press Start). Saves are per user: the default user keeps Saved/SaveGames, others get Saved/SaveGames/User<N>. When the primary user signs out, saving stops and the game returns to the title screen (Return To Title On Sign Out). Feed your own login flow with Report User Login Changed.

Controllers and suspend. URSEAppLifecycleSubsystem pauses the game and opens a reconnect screen when the primary user's controller disconnects (Pause On Controller Disconnect). On PC and handhelds, Allow Continue Without Controller lets the player dismiss it with Confirm from another device. The game also pauses while the app is in the background and finishes save writes in flight first (Save → Suspend Flush Timeout). Replace the reconnect screen with your own Widget Blueprint derived from RSE Screen Widget, reading RSE Reconnect View Model, in Platform → Reconnect Widget Class. Pauses go through the framework's time control, so in co-op one player's disconnect never pauses everyone.

Simulate all of it on PC: RSE.Platform.SimulateDisconnect, SimulateReconnect, SimulateSignOut, SimulateSignIn, SimulateSuspend, SimulateResume, and RSE.Platform.Dump.

24.2.6 Static checks that keep the plugin portable

The automation test RSE.Plugin.PlatformApis scans the plugin's runtime modules and fails on Windows-only APIs and headers, process or URL launches, direct file access outside the save backend, hardcoded absolute paths and keyboard-only key handling. Run it after you change the plugin's source.

24.3 Key classes and assets

Class / assetModuleRole
URSEPerformanceSettingsRSEPerformance: Deck profile names, first-run frame cap, preview resolution, benchmark steps, budget.
URSEDeckProfileSubsystemRSESwitches to the Deck device profile at game start.
URSEBenchmarkSubsystemRSEThe scripted benchmark behind RSE.Perf.Benchmark.
FRSEPerfBudget, RSEPerfStatsRSE / RSECoreA budget, and the math for averages, 1% low, percentiles and reports (headless).
URSEContentBudgetSettingsRSEEditorEdit-time content budgets.
URSEInputDeviceSubsystemRSELast used device, Deck detection, prompt style.
URSEPlatformUserSubsystem, URSEAppLifecycleSubsystemRSECore / RSEPlatform users, sign-in, controller and suspend handling.
IRSESaveBackendRSECorePluggable save storage.
URSEMobileSettings, URSEMobileProfileSubsystemRSEMobile tiers and tier selection (unverified, see below).
URSETouchLayout, URSETouchInputSubsystemRSETouch controls (unverified on devices).

24.4 Settings

WhereWhat
Project Settings → RPG Saga Engine → PerformanceDeck Device Profile, Deck Linux Device Profile, Apply Deck Profile, Deck First Run Frame Rate Limit, Deck Preview Resolution, Benchmark Steps, Budget, Warmup Seconds, Uncap Frame Rate, Map Load Timeout Seconds, Report Folder, Write Frame Csv, Quit When Unattended
Project Settings → RPG Saga Engine → PlatformPause On Controller Disconnect, Allow Continue Without Controller, Select User On Press Start, Return To Title On Sign Out, Pause On Suspend, the reconnect screen texts
Project Settings → RPG Saga Engine → SaveBackend, Suspend Flush Timeout, Keep Backup, Min Free Disk Space MB, slot and autosave settings
Project Settings → RPG Saga Engine → Content BudgetsCategories and thresholds (editor only)
Project Settings → RPG Saga Engine → FeaturesSwitch whole systems off
Project Settings → RPG Saga Engine → Input → DevicesPrompt device, Simulate Steam Deck, gamepad label set

See Appendix B for every property and its default.

24.5 Blueprint usage

Most of this chapter is settings and console commands. The nodes you may reach for:

  • Is Feature Enabled (RPG Saga|Features): hide a button in your own UI when a framework system is switched off.
  • Report User Login Changed on the platform user subsystem: tell the framework your own online login changed.
  • On Return To Title Requested on the app lifecycle subsystem: show a message before the game goes back to the title screen after a sign-out.
  • On App Resumed on the platform user subsystem: open your pause menu when the app comes back.
  • On Save Failed on the save subsystem: show the reason when a save fails.
  • Is Using Touch on the input device subsystem: switch your own prompts when the player touches the screen.

Example: show a message when saving fails.

  1. Event BeginPlay → Get Game Instance → Get Subsystem (RSE Save Subsystem).
  2. Drag from the subsystem and bind On Save Failed.
  3. In the bound event, use the Result to pick a text and call your own message box.

24.6 C++ usage

A custom save backend, for a platform's cloud storage, is a UObject class that implements IRSESaveBackend:

UCLASS()
class UMyCloudSaveBackend : public UObject, public IRSESaveBackend
{
    GENERATED_BODY()
public:
    virtual FName GetBackendName() const override { return TEXT("MyCloud"); }
    virtual ERSESaveBackendStatus WriteSlot(const FRSESaveUser& User, const FString& SlotName, const TArray<uint8>& Payload) override;
    virtual ERSESaveBackendStatus ReadSlot(const FRSESaveUser& User, const FString& SlotName, TArray<uint8>& OutPayload) override;
    virtual bool SlotExists(const FRSESaveUser& User, const FString& SlotName) override;
    virtual bool DeleteSlot(const FRSESaveUser& User, const FString& SlotName) override;
    virtual void GetSlotNames(const FRSESaveUser& User, TArray<FString>& OutSlots) override;
};

Set Save → Backend to Custom and pick the class in Custom Backend Class. The synchronous calls may run on worker threads, so the implementation must be thread-safe. Override WriteSlotAsync, ReadSlotAsync and Flush when the platform has its own asynchronous path. The save subsystem adds the checksum, the metadata and the screens' messages, so a backend only moves bytes.

Branch on the platform in your own code with the engine's usual macros and the framework's device queries (URSEInputDeviceSubsystem::IsSteamDeck()), not with Windows-only calls. Keep file access inside the save backend so the static check stays green.

24.7 Advanced

24.7.1 Mobile (not verified)

The mobile work is split in three parts. All of it is untested on a phone.

  • Rendering. On Android (Vulkan) and iOS (Metal) the mobile forward renderer is used, with no Lumen, Nanite, Virtual Shadow Maps, distance fields or screen-space GI. URSEMobileSettings (Mobile Rendering) defines three tiers (Low, Mid, High), each with a device profile per platform and a budget: for example High is 30 FPS, at most 600 draw calls, 400k visible triangles, one shadowed dynamic light and 1 GB of textures. The device profiles live in Plugins/RPGSagaEngine/Config/Mobile/ and derive from the engine's own tier profiles. RPG Saga Engine → Advanced tools → World → Mobile → Apply Rendering Profiles (or RSE.Mobile.ApplyProfiles [DryRun]) merges them into your project without touching your own sections. URSEMobileProfileSubsystem applies the tier at start on a device.
  • Materials. RSE.Mobile.CheckMaterials [Android | iOS] compiles every plugin material for the mobile feature level and writes a report. The shipped result for Android is 32 materials, 30 compile. The two post-process passes of the cel-shaded and retro styles fall back to off on mobile, and the other style settings still apply.
  • Preview. RSE.Mobile.Preview <Example | Arena | All> [Android | iOS] [Low | Mid | High] drives the editor's mobile preview and checks a capture against the tier's budget. First measurements show the starter example map over the High triangle budget (trim it before a phone build).
  • Touch controls and phone UI. Touch is a third input device with data-driven layouts (URSETouchLayout, five ship in /RPGSagaEngine/Input/Touch), gestures, millimetre-based sizes, a 9 mm minimum target, safe areas and phone variants of the main screens. Try it on PC with RSE.Input.Touch.Emulate 1 (mouse as a finger) and RSE.UI.Phone.Simulate 1. See User interface.

What is missing: a compiled Android or iOS build, real frame times, thermals, memory, tier detection on real GPUs, and the feel of every touch threshold. To try it in a working copy, add "Android" (and "IOS") to SupportedTargetPlatforms and to the PlatformAllowList of the runtime modules (RSECore, RSE, RSEOnline, RSENet, RSEGAS, RSEStateTree, RSECommonUI) in RPGSagaEngine.uplugin. Editor modules stay Win64. Install the engine's Android target component first (Epic Games Launcher, the engine's Options → Target Platforms), and keep SDK paths out of the project: they are machine settings. A phone build also needs Android or iOS project settings (arm64, Vulkan, ASTC textures, mobile forward shading). Treat the first result as a starting point and expect to fix things.

24.7.2 Consoles

The framework prepares what it can without an SDK: the save backends above, platform users, controller disconnect and suspend, gamepad-only play (verified screen by screen), an action bar through the optional Common UI plugin, and a static scan for non-portable code. What it did not do: build, run or certify on any console, safe-area checks on console hardware, platform glyph sets and strings, achievements and activities.

For a console release you need the platform SDK, the engine's platform extension and a devkit. Set Save → Backend to Save Game System (or your custom backend), feed sign-in changes from your platform login if the engine's event is not enough, add your platform's button glyphs and required strings, and run the platform holder's own certification checklist. The descriptor's PlatformAllowList must also list the platform.

24.7.3 Controller-only play

Every screen and flow was checked for use with a gamepad alone: focus on open, D-pad and stick navigation, confirm and back, tab switching with the shoulder buttons, scrolling, drag-and-drop alternatives and no dead ends. Three automated checks keep it true: RSE.UI.Navigation.* (drives each screen with synthetic gamepad key events), RSE.Input.GamepadCoverage (fails when an input action or a key list has no gamepad key) and RSE.CommonUI.ActionBarBindings (screens bind their actions for the Common UI action bar). The per-screen report is docs/product/controller-report.md.

24.7.4 Performance habits

  • No Tick by default. Framework actors and components start with Tick off. Events, timers or a batching subsystem drive the logic. Keep your own code the same.
  • Flat data for hot paths. The grid and pathfinding use contiguous arrays, not an actor per tile. Repeated visuals use instanced meshes.
  • Soft references for heavy assets (meshes, animations, sounds, UI), loaded when needed.
  • Debug code stays out of Shipping. The console commands in this chapter are not in Shipping builds.
  • Give heavy meshes LODs. With Nanite off on the Deck, meshes draw their fallback or classic LODs.

24.8 Troubleshooting

SymptomCause and fix
The game does not use the Steam Deck settingsThe profile is missing from your project, so the log says "Device profile 'SteamDeck' not found". Add it to Config/DefaultDeviceProfiles.ini (example below). The Deck is detected from SteamDeck=1, -SteamDeck or Simulate Steam Deck. -dp=SteamDeck forces it from the start on any PC.
Text is too small on the DeckBuilt-in screens never go below UI Theme → Minimum Font Size. In your own widgets use the theme fonts.
Meshes look blocky on the DeckNanite is off in the Deck profile, so meshes draw fallback or classic LODs. Give them LODs. The content budget warnings point at the worst offenders.
Low quality still shows Virtual Shadow MapsProject Settings → Rendering → Shadow Map Method is set to Virtual Shadow Maps. It overrides scalability. Put it back to the default.
The Tactics grid is missing on a forward or mobile rendererThe default Grid Visual Mode is a decal that needs the deferred renderer. Set it to Tiles in Tactics Zone settings.
Saving fails on a disk that is nearly fullMin Free Disk Space MB. Free space or lower it. The previous save stays.
A save slot shows as damagedThe checksum failed and there is no readable backup. It is shown as damaged on purpose, never as empty. Keep Keep Backup on.
The reconnect screen never goes away on a handheldAllow Continue Without Controller is off, so the prompt waits for a controller (console behaviour).
An Android or iOS build failsThe descriptor allows Win64 only. See the mobile steps above.

A starting Steam Deck profile for your Config/DefaultDeviceProfiles.ini:

[DeviceProfiles]
+DeviceProfileNameAndTypes=SteamDeck,Windows

[SteamDeck DeviceProfile]
DeviceType=Windows
BaseProfileName=Windows
; default quality (the player's Options still win)
+CVars=sg.ResolutionQuality=70
+CVars=sg.ShadowQuality=0
+CVars=sg.PostProcessQuality=0
+CVars=sg.EffectsQuality=0
+CVars=sg.FoliageQuality=0
; hard limits of the Deck
+CVars=r.Nanite=0
+CVars=r.Lumen.DiffuseIndirect.Allow=0
+CVars=r.Lumen.Reflections.Allow=0
+CVars=r.Shadow.Virtual.Enable=0
+CVars=r.AntiAliasingMethod=4
+CVars=r.MotionBlurQuality=0
+CVars=RSE.Dialog.LivePortraits=0

24.9 See also