Platform optimizations
Some engine settings belong to one platform: a workaround for a driver, a default that suits one RHI. Rusteal declares them per platform in the project’s rusteal.toml and the CLI writes the ones of its own platform into the engine’s per-platform config files. This chapter is how that works, why it is built this way, and what to keep in mind when adding one.
The first one: Vulkan’s fence on NVIDIA
The reason the mechanism exists. On Linux, with an NVIDIA card (seen with drivers 595.84 and 595.104.02, engines 5.8.2 and 5.8.3, an RTX 4060, COSMIC on Wayland with the editor under XWayland), the editor segfaulted one to three times a day inside the driver:
vkWaitForFences
FVulkanFence / FVulkanSwapChain::AcquireImageIndex
FVulkanViewport::Present
Every one of those crashes came within two seconds of AcquireNextImage() failed due to the outdated swapchain in the log. The swapchain goes out of date when a window changes: a secondary window closes (Project Settings, Message Log, the PIE window), PIE stops, a window is resized. The engine then recreates the swapchain, passing the old one as oldSwapchain, and acquires the next image with a CPU fence, because r.Vulkan.CpuWaitForFence is 1 by default; vkWaitForFences crashes in the driver. It happened in the Project Browser too, with no project open and no DLSS, so neither a project nor Streamline (stubbed on Linux) was the cause.
r.Vulkan.CpuWaitForFence=0 makes the engine wait on a GPU semaphore and never call vkWaitForFences there. Epic already ships it as the default on Android (Engine/Config/Android/AndroidEngine.ini: “Use semaphores instead of fences for image wait”). The variable is ReadOnly: the console refuses it, but it is read at RHI start-up from the [ConsoleVariables] section of the Engine config, so it has to be in an .ini, not set from code at run time. With it in place the crashes stopped.
A packaged game runs the same VulkanRHI, and its swapchain goes out of date on resize, fullscreen toggles, alt-tab and resolution changes: players on Linux with NVIDIA are exposed too. On Windows the projects use DirectX 12 and the path never runs.
Where an .ini setting can go
| File | Read by | Notes |
|---|---|---|
Engine/Config/ConsoleVariables.ini, [Startup] | the editor and Development builds | Compiled out of Shipping and Test (#if !DISABLE_CHEAT_CVARS): never for something the game needs. |
Engine/Config/Linux/LinuxEngine.ini, [ConsoleVariables] | every project and every game packaged with that engine install, and the Project Browser | Lost when the engine is reinstalled or upgraded (a 5.8.4 install replaces Engine/Config). |
<Project>/Config/Linux/LinuxEngine.ini, [ConsoleVariables] | that project’s editor and game, on Linux only | Versioned with the project; survives engine upgrades; this is the one Rusteal writes. |
The engine one, as it was added on the development machine, in the section the engine’s file already has:
[ConsoleVariables]
; larger timeout since drivers may take longer time
g.TimeoutForBlockOnRenderFence=60000
memory.MemoryPressureCriticalThresholdPercent=10
; Use semaphores instead of fences for swapchain image wait (avoids NVIDIA driver crash in vkWaitForFences after an out-of-date swapchain)
r.Vulkan.CpuWaitForFence=0
and the project one, in a project that had no Config/Linux before:
[ConsoleVariables]
; Use semaphores instead of fences for swapchain image wait (avoids NVIDIA driver crash in vkWaitForFences after an out-of-date swapchain)
r.Vulkan.CpuWaitForFence=0
Both are needed on a machine that also shows the crash in the Project Browser: the engine’s file covers what runs before a project is loaded; the project’s file is what goes with the game when it is packaged elsewhere (another machine, CI) or with an engine that does not have the change. Startup confirms either in the log:
LogConfig: Set CVar [[r.Vulkan.CpuWaitForFence:0]]
An engine-level change is per machine and invisible to the project: keep it out of what Rusteal manages, and repeat it after every engine upgrade.
Declared in rusteal.toml
[optimizations.linux.Engine.ConsoleVariables]
"r.Vulkan.CpuWaitForFence" = 0
[optimizations.<platform>.<File>.<Section>], with the platforms linux, windows and mac and any config file name (Engine, Game, Input, …). The section name is the .ini section, quoted when it has dots or slashes ("/Script/Engine.RendererSettings"). Values are TOML strings, integers, floats or booleans, written as the engine reads them (True, False).
The default table is rusteal-cli/optimizations.toml, embedded in the CLI (optimizations::DEFAULTS). Every template’s rusteal.toml.tera ends with the same text, so a new project shows its optimizations and the game developer edits them in place. every_template_carries_the_default_optimizations fails if a template drifts from the defaults: change optimizations.toml and the templates together. A project whose rusteal.toml has no [optimizations] table, every project made before this existed, gets the defaults; an empty [optimizations] table means none. Unknown platforms and non-scalar values are refused when the file is read.
Written by the CLI, chosen at compile time
rusteal-cli/src/optimizations.rs. The platform is not detected at run time: optimizations::HOST is a #[cfg(target_os = ...)] constant, as build_cmd::HostPlatform::CURRENT. The Linux CLI only ever writes optimizations.linux into Config/Linux/, the Windows CLI only optimizations.windows into Config/Windows/. A Windows machine never touches the Linux settings, and the engine stages Config/Linux for Linux builds only, so a Windows package carries none of them.
The settings are the same on every Linux machine, whatever its GPU. Writing them only where an NVIDIA card is found was rejected: the file is versioned, and machines that disagree would rewrite it back and forth in every commit; the player’s GPU is unknown at package time anyway; and the setting costs nothing on other drivers.
apply(root, switch) goes over the .ini files of Config/<Platform>/ and those the table names:
- strip the block between
BEGINandEND(two comment lines:; Rusteal optimizations, ...: beginand; Rusteal optimizations: end), keeping the user’s text around it; - with
Switch::On, render the table as its own sections inside a new block, leaving out every key the user’s text already sets in that section (the user’s value wins, and the key is not written twice); - write the user’s text and the block, or delete the file when nothing is left of it, which is the case of a file Rusteal created itself.
A file with no Rusteal block and nothing to add is never rewritten, not even to normalize its line endings. Writing a separate section block at the end of a file that already has [ConsoleVariables] is valid because the engine merges repeated sections: FillFileFromBuffer (ConfigCacheIni.cpp) calls FindOrAddSectionInternal for every section header. The user’s own lines must stay outside the markers: whatever is between them is rewritten.
When it runs
| Command | Call |
|---|---|
rusteal new | optimizations::report("new", ...) once the project files exist, before the plugins are installed. |
rusteal watch | report("watch", ...) before the first snapshot: each developer, on each machine, gets the settings by starting the watch. |
rusteal package | Snapshot::take, report("package", ...), BuildCookRun (build_cmd::run_cmd_code, which returns the exit code instead of exiting), Snapshot::restore, then the exit code. |
--optimizations <on|off> (Switch::parse: on/off, 1/0, true/false; on by default) is on the three commands. watch --optimizations off removes the block until the next watch; package --optimizations off ships without it and restores the project as it was, so the developer’s editor keeps whatever state it had. A package interrupted with Ctrl+C skips the restore; the next rusteal watch rewrites the block.
rusteal build does not write them: the build must not change versioned files behind the user’s back, and the editor reads the .ini when it starts, which new or watch has already prepared.
Adding one
- Find the root cause and check the setting on the platform’s editor and on a packaged game: the engine log shows
LogConfig: Set CVar [[name:value]]when a console variable is applied at start-up. - Check whether it can be set at run time instead. A setting the game code can change after start-up belongs in Rust, behind
#[cfg(target_os = "...")], where the game crate is compiled per platform; the.iniroute is for what the engine reads before Rust is loaded (ReadOnlyconsole variables, RHI and platform settings). - Add it to
rusteal-cli/optimizations.tomlwith a comment saying what it fixes, and append the same lines to everyrusteal-cli/templates/*/*/rusteal.toml.tera;cargo test -p rustealchecks they agree. - Document it in the game developer’s handbook (
cli/optimizations.md) and in this chapter.
Existing projects pick a new default up only if their rusteal.toml has no [optimizations] table; the others list their own and add it by hand, which the release notes must say.