Files
runi-dist/docs/config-schema.md

24 KiB

Configuration schema

All paths below follow the XDG Base Directory spec (config vs. data vs. cache) — see architecture.md (source repository) for the full table, including external paths Runi reads/writes (Steam's compatibilitytools.d, Steam library manifests, game install directories for OptiScaler). Runi itself is installed via pipx/uv tool install into an isolated environment, not system Python — see architecture.md (source repository).

Per-game config

~/.config/runi/games/<id>.toml, where <id> is Runi's own unique registry key (game.id below) — not the same thing as umu's GAMEID, which is deliberately shared across unrelated games. See architecture.md (source repository) for why these are kept separate.

# ~/.config/runi/games/steam-292030.toml
format_version = 9

[game]
id = "steam-292030"          # our own unique registry key; always distinct
installation_id = "b1e6c9a0-..." # UUID for this concrete installation
display_name = "The Witcher 3"
exe = "/home/user/Games/witcher3/witcher3.exe"
args = []
store = "steam"             # local|steam|gog|egs|itch|amazon|ubisoft
working_dir = ""            # optional; defaults to exe's parent dir
platform = "windows"        # original/content platform
port_target_os = ""         # empty|windows|linux; non-empty means community port
platform_label = ""         # custom origin label only when platform="other"
classification_source = "auto" # auto|manual; manual survives catalog reconciliation
install_root = ""           # stable root for relative launch/action paths

[launch]
provider = "executable"     # executable|xenia|rpcs3|ubisoft
engine = "proton"           # proton|host|steam (see below)
content = ""                # emulator content path when applicable

[store_binding]
installation_id = ""        # shared store installation; empty for normal games
process_names = []           # executable names used for real game lifecycle detection

[[actions]]
id = "settings"
label = "Settings"
role = "configuration"
provider = "executable"
engine = "inherit"
exe = "Settings.exe"
args = []
working_dir = "."
content = ""
wrappers = false

[identity]
runi_game_id = "pgid_..."    # canonical game across stores/installations
steam_appid = "292030"       # dedicated Steam identity; empty when unknown
umu_gameid = "umu-292030"    # exact GAMEID passed to UMU
steamgriddb_game_id = ""     # confirmed SteamGridDB selection; not a Steam AppID
ubisoft_id = ""              # Ubisoft Connect deeplink identity
ubisoft_space_id = ""        # local Ubisoft catalogue correlation UUID
ubisoft_install_id = ""      # local Ubisoft installation/catalogue identity
ubisoft_store_product_id = "" # Ubisoft Store master product/edition identity
canonical_title = "The Witcher 3: Wild Hunt"
source = "local Steam manifest" # evidence used for this resolution
confidence = "verified"         # unknown|review|high|verified|manual
validated_at = "2026-07-07 15:00:00"

[runtime]
backend = "umu"              # umu|native
proton = "GE-Proton9-27"     # matches a discovered ProtonVersion name, or "auto"
prefix = ""                  # optional override; else derived from game.id/backend
cpu_affinity = ""            # optional taskset CPU list, e.g. "0,1,2,3" or "0-3"

[env]
# free-form passthrough, e.g. DXVK_HUD = "fps"; dedicated OptiScaler provider keys must not be duplicated here

[gamescope]
enabled = false
output_width = 1920
output_height = 1080
render_width = 1280        # unset/equal to output = no upscaling
render_height = 720
filter = "linear"            # fsr|nis|linear|nearest (-F)
scaler = "auto"               # auto|integer|fit|fill|stretch (-S)
fullscreen = true
borderless = false
refresh_rate = 120           # nested display refresh; 0 = Gamescope default (-r)
fps_limit = 0                # optional cap; 0 = disabled (--framerate-limit)

[mangohud]
enabled = false
fps_limit = 0
config_file = ""             # optional path; else MangoHud's own default resolution order

[linuwux]
enabled = false               # wrap the launch with linuwux (LD_PRELOAD shim for
                               # Reflex/DenuvOwO-style DRM loaders); PATH-only dependency,
                               # never installed/managed by runi - see
                               # github.com/brcly/linuwux-runtime. Only GE-Proton/
                               # Proton-CachyOS are upstream-supported; `runi doctor`
                               # flags any other selected build as an advisory finding.
debug = false                  # LINUWUX_DEBUG=1; verbose, only for troubleshooting

[proton_tweaks]
fsr_fullscreen_hack = false
fsr_strength = 2

[optiscaler]
enabled = false
provider = "auto"            # auto|manual|ge-integrated
applied_provider = ""        # resolved/provisioned provider; managed by Runi
target_dll = ""              # e.g. dxgi.dll
version = "default"          # provider default or an exact OptiScaler version
fsr4_enabled = false          # runner-managed FSR 4; integrated provider only
fsr4_version = "default"

[optiscaler.integrated_config]
# Structured Section.Option keys passed through PROTON_OPTISCALER_CONFIG.
# Example: "Upscalers.Dx12Upscaler" = "fsr31"

[lossless_scaling]
enabled = false              # configure LSFG for the main Play action
multiplier = 2               # 2|3|4 displayed-frame multiplier
flow_scale = 1.0             # motion-estimation scale, 0.25..1.0
performance_mode = false     # faster model with a small quality tradeoff
hdr_mode = false             # compatibility mirror; managed by [hdr]
present_mode = "default"     # default|fifo|mailbox|immediate; lsfg-vk 1.x

[hdr]
enabled = false              # native game HDR; requires preconfigured Gamescope

[logging]
output = "console"           # console|file - never both at once; see below

[customize]
applied_profile = ""          # name of the currently-applied customize profile, if any

[notes]
key = ""                      # relates notes across multiple registrations of the same
                               # real-world game - defaults to game.id if unset; the notes
                               # themselves live as dated entries in
                               # ~/.config/runi/notes/<key>/<uuid>.md

[saves]
path = ""                     # save location for backup/restore - relative to the prefix's
                               # drive_c/, or an absolute path; the GUI can suggest paths
                               # from PCGamingWiki, but the user confirms what is stored here

The GUI presents each Gamescope width/height pair as one editable resolution selector with common presets. New games added through the GUI use the primary monitor's resolution for both output and render when it can be detected, while refresh_rate and fps_limit remain 0. Existing configurations and the on-disk schema keep the separate integer fields shown above. The GUI's Display Mode selector maps Fullscreen to fullscreen = true, Borderless Window to borderless = true, and Windowed to both fields being false.

The GUI places MangoHud before Gamescope in Wrappers. Its header toggle maps to enabled, its editable preset slider maps to fps_limit, and config_file remains hidden under Advanced until explicitly expanded.

Global config

~/.config/runi/config.toml:

[defaults]
backend = "umu"
proton = "auto"

[paths]
extra_proton_search_paths = []
prefixes_root = ""          # empty = ~/.local/share/runi/prefixes; see below

[covers]
auto_download = true
steamgriddb_api_key = ""  # optional; STEAMGRIDDB_API_KEY overrides this value

[library]
view = "grid"              # grid|list
card_size = 80             # 50..100; fourth mark by default, maps 120px to 240px

[ai_research]
enabled = false             # opt-in community HDR research; requests are still explicit
model = "gpt-5.6-terra"     # managed Runi default
reasoning = "medium"        # managed Runi default

[paths].prefixes_root

Where per-game Wine prefixes live. Empty - the default - keeps them under the data root at ~/.local/share/runi/prefixes. Set it to move a prefix collection that has outgrown that disk; ~ is expanded.

Three things worth knowing before setting it:

  • It must be absolute. A relative value is ignored, and runi doctor says so rather than letting it pass. Relative would resolve against the working directory, and the launcher deliberately sets that to the game's own folder - Wine prefixes would land inside the games.
  • Changing it moves nothing. A game with no explicit [runtime].prefix derives its prefix from this root, so the games whose prefixes stayed behind would get a fresh empty one on next launch. Move the directories across yourself; runi doctor reports any left behind (prefixes_root_unmoved) until you do.
  • A per-game [runtime].prefix still wins. This is only the default root for games that have not been pointed somewhere specific.

Store installations keep their own prefixes under ~/.local/share/runi/store-prefixes/, and each one persists its path absolutely, so this setting does not affect them.

[launch].engine = "steam"

For a game Steam launches, not Runi. Steam builds the command, and Runi contributes only what survives someone else owning the launch: the wrapper prefix (gamescope, MangoHud, linuwux, taskset) and the environment. Put this in the game's Steam launch options:

runi wrap <game-id> %command%

Steam substitutes the command it built for %command%, and runi wrap prepends Runi's stages to it. Verify before launching anything for real:

runi wrap <game-id> --dry-run -- /usr/bin/proton run /path/game.exe

What still works: OptiScaler (both providers), frame generation, Proton FSR tweaks, WINEDLLOVERRIDES, MangoHud, gamescope, linuwux and CPU affinity.

What has no effect, because Steam already decided it: [runtime].backend, [runtime].proton and [runtime].prefix — Steam picks the compat tool and uses steamapps/compatdata/<appid>. runi config says so when you set one of them, and the GUI greys them out. [runtime].cpu_affinity is not in that list: taskset is an argv prefix, so it survives.

Also given up: Runi's own log capture and latest.log, auxiliary launch actions, and the exit code — all of which belong to whoever owns the process.

If the first launch seems to hang

Adding launch options often coincides with the game's first launch under a different Proton build, and that first launch migrates the Wine prefix and compiles shaders. On slow storage this takes minutes, with Steam showing nothing but "Launching" throughout. It looks exactly like a broken wrapper.

Do not A/B it against a plain mangohud %command% and conclude from the result: by the time you try the second option, the first attempt has already paid the migration, so the comparison is a cold start against a warm one. That mistake cost an hour during this feature's own validation.

Steam's own log is where the truth is:

tail -f ~/.steam/steam/logs/console-linux.txt

Proton: Upgrading prefix from ... to ... means it is working, not stuck. And because Steam hides a wrapper's stderr, a shell script that logs "$@" and then execs runi wrap ... "$@" is the fastest way to see what Steam actually passed and what Runi said about it.

config.toml is written with user-only (0600) permissions because it may contain the SteamGridDB API key. Automatically downloaded covers live under ~/.cache/runi/covers/; imported custom covers are normalized and copied to ~/.local/share/runi/covers/custom/<game.uid>/cover.jpg. Custom covers therefore do not depend on the original selected image remaining in place. Validated identity lookups are cached for seven days under ~/.cache/runi/identity/; Runtime can force a fresh revalidation. Local INI discovery is bounded to the installation, ignores symlinks and large files, requires nearby Steam API evidence, and never persists an AppID until Steam confirms that it belongs to a game.

The OpenAI API key is deliberately absent from this schema. Preferences stores it in the system keyring, can keep it only for the current process, or reads OPENAI_API_KEY. Community HDR findings contain no secret and live in ~/.local/share/runi/knowledge.sqlite3.

Field notes

  • game.id must be unique — it's the config filename and the default prefix folder name. For Steam titles it's steam-<appid>; otherwise a slugified name/filename. Never the literal umu-default. Uniqueness is enforced, not assumed: register_game() refuses an id that already exists, so registering a second copy of a game can never replace the first. A second copy is offered <slug>-<build version> read from the executable (mortal-shell-ii-cl92120), never a numeric counter; when no build version tells the copies apart, Runi asks for a discriminator rather than inventing one. The build version is not stored as a config field and is not stable across an in-place game patch — the id is a name, not a fingerprint.
  • Parallel installations of the same game share identity.* (and therefore the PGID-keyed notes, artwork and compatibility knowledge) but keep separate Wine prefixes, settings, environment overrides, logs, save backups and mod receipts.
  • game.installation_id is a separate UUID4 generated once for a concrete installation. Unlike game.id (a human-typed/derived slug), it is what Runi uses internally to detect "has this game's install directory been replaced externally" (stamped as a small marker file, <game_dir>/.runi-installation-id, checked by core/config.py's install_marker_matches()/runi doctor). Not meant to be hand-edited.
  • identity.runi_game_id is Runi's opaque canonical identity, shared by separate installations and resolved from catalog aliases.
  • identity.ubisoft_store_product_id identifies the commercial Ubisoft Store edition used to resolve official boxshots. It is an edition alias and never replaces runi_game_id; local catalog entries persist it under aliases.ubisoft_store.
  • identity.umu_gameid is the value forwarded as umu's GAMEID env var. umu-<appid> for Steam titles (so community protonfixes key correctly), umu-default otherwise. Independently editable if you later find the specific umu-ID for a GOG/Epic title in the community umu-database.
  • identity.steam_appid is independent of installation source. It is used for Steam CDN artwork, Steam-aware save discovery and recipe matching.
  • identity.steamgriddb_game_id is written only after an exact automatic match or an explicit artwork selection. It never implies a Steam AppID or UMU identity.
  • game.display_name is the user-facing name and the artwork search term. It is editable and is not treated as a stable game identity.
  • game.platform records the original/content platform. A port from GameCube to Linux therefore keeps platform = "gamecube".
  • game.port_target_os records the OS of a community port's executable. The Library derives Windows Ports/Linux Ports from it; it is not a launch backend. classification_source = "manual" protects a player correction from later catalog reconciliation.
  • runtime.proton = "auto" selects the newest installed GE-Proton itself (umu backend only); for the native backend, proton must name an already-discovered/installed ProtonVersion.
  • The GUI presents runtime.backend as Launch engine and separates it from Runner family and Version. The family is derived from the concrete runtime.proton name, so existing TOML and CLI usage remain valid. Curated families are GE-Proton, UMU-Proton, Proton-CachyOS, Proton-EM and Steam-managed Proton Experimental; other discovered tools remain available under Advanced.
  • Gamescope refresh and FPS cap are independent. refresh_rate controls the nested display exposed to the game; fps_limit applies an optional cap.
  • runtime.cpu_affinity wraps the complete launch chain with taskset --cpu-list without invoking a shell. This supports older games that fail when they detect or execute across too many CPU cores.
  • [gamescope].filter vs [gamescope].scaler are independent axes (gamescope's -F and -S flags) — filter is the upscale algorithm, scaler is how the image is fit into the output window.
  • [env] is the escape hatch: anything not modeled as a named field (custom DXVK_HUD, PROTON_LOG, etc.) goes here and is passed through as-is.
  • [lossless_scaling] configures frame generation through a separately installed lsfg-vk layer and a user-owned Steam copy of Lossless Scaling. Runi detects both dependencies and never writes lsfg-vk's global config or distributes Lossless.dll. Disabled means no Runi override, so an external lsfg-vk profile can still apply. See lossless-scaling.md.
  • [hdr] is the single per-game source of truth for native HDR. Enabling it requires Gamescope to be configured already, adds --hdr-enabled, enables the Gamescope WSI layer and selects the runner-supported Proton/DXVK HDR variable. Runi only inspects the desktop HDR state; it never changes a monitor or compositor setting. Raw DXVK_HDR, ENABLE_GAMESCOPE_WSI, PROTON_ENABLE_HDR and PROTON_USE_HDR entries conflict with managed HDR. See hdr.md.
  • [logging].output picks where a game's process output goes: console (default — inherits the terminal, same as always) or file (redirected to ~/.local/share/runi/logs/<installation-id>/<timestamp>.log, with a latest.log symlink kept pointing at the most recent run). Never both at once. runi run <id> --log/--console transiently override this for a single run without touching the saved config.
  • [customize].applied_profile tracks which named "customize" profile (community upscaler/mod workaround) is currently applied, if any — see customize-profiles.md. Set/cleared by runi customize apply|remove, not meant to be hand-edited.
  • [notes].key is the only thing actually stored in the game's own config — the entries live under ~/.config/runi/notes/<key>/<uuid>.md (key defaults to the PGID if unset). Each entry has a date, title, and Markdown body. The GUI can relink a re-added game to an existing collection. Notes are deliberately kept outside the game's own config so notes survive runi remove — set the same key on two different registrations (e.g. after a reinstall creates a new game.id) to relate their notes. A deliberately lightweight starting point rather than a designed findings/compatibility schema — see roadmap.md (source repository).
  • [saves].path is where a game's save files live, for runi saves backup|restore|list. The GUI can query PCGamingWiki, resolve common Windows paths against the Wine prefix, and present candidates; the user must confirm the stored path. Relative paths resolve against the Wine prefix's drive_c/ (most saves live under a Windows user-profile path there); absolute paths are used as-is.

Every field above (plus its type, default, and description) is also queryable straight from the CLI — see runi schema below.

Compatibility boundary

Schema v8 is written for new/edited registrations. Schema-v2 through v7 registrations remain readable as Windows executable/Proton launches and are not rewritten by read-only operations. Their first later save creates a version-labelled backup, for example .toml.v2.bak, before writing v8. Schema v1 and unknown future schemas remain rejected.

The --json output contract

--json is an interface, not a convenience: the Decky plugin reads Runi by running these commands and parsing stdout, so the shape is the coupling between the two. Three properties hold everywhere.

Every document is an object. Listings carry their payload under a named key rather than being a bare array, which leaves room beside it:

{ "games": [ ... ] }        // list, steam-scan
{ "sections": [ ... ] }     // schema
{ "versions": [ ... ] }     // versions
{ "findings": [ ... ] }     // doctor

A failure carries the exception type, because core raises typed exceptions precisely so a caller can tell one from another, and a sentence gives a program nothing to branch on:

{ "error": { "type": "GameNotFoundError", "message": "no such game: minos" } }

A setting that will not take effect says so in the document. runi config --json reports both what changed and what that change cannot do, instead of sending the warning to stderr where a JSON consumer never reads it:

{
  "id": "minos", "updated": ["runtime.proton"], "ineffective": ["runtime.proton"],
  "notes": [{ "section": "runtime", "fields": ["backend", "proton", "prefix"],
              "engine": "steam",
              "reason": "Steam picks the compat tool and owns the prefix" }],
  "config": { ... }
}

--edit is refused under --json: it opens an interactive editor, which a program has no use for.

CLI reference

  • runi add [<exe>] [--id ID] [--name NAME] [--store local|steam|gog|egs] [--steam-appid N] [--backend umu|native] [--proton NAME|auto] [--args ...]
  • runi list [--json]
  • runi versions [--refresh] [--backend umu|native|all] [--json]
  • runi versions list-remote --source ge-proton|umu-proton|proton-cachyos|proton-em
  • runi versions install <source> [<version>|latest] [--json]
  • runi versions remove <name>
  • runi run <id> [--dry-run] [--verbose] [--debug] [--log|--console]
  • runi config <id> [--set section.key=value] [--edit] [--json]
  • runi steam-scan [--json]
  • runi optiscaler install <id> [--dll dxgi.dll] [--version latest]
  • runi optiscaler configure <id> [--provider auto|manual|ge-integrated] [--dll dxgi.dll] [--version default] [--fsr4]
  • runi optiscaler status <id>
  • runi optiscaler migrate <id> --to manual|ge-integrated [--dll dxgi.dll] [--version default] [--fsr4]
  • runi optiscaler remove <id>
  • runi remove <id> [--keep-prefix]
  • runi schema [--section runtime|gamescope|mangohud|linuwux|proton_tweaks|optiscaler|lossless_scaling|logging|customize|notes|saves] [--json]
  • runi customize list [--json]
  • runi customize apply <id> <profile-name-or-path> [--json]
  • runi customize remove <id> [--json]
  • runi mangohud-preset list [--json]
  • runi mangohud-preset save <name> (--from-file <path> | --from-game <id>) [--description TEXT] [--json]
  • runi mangohud-preset apply <name> <id> [--json]
  • runi mangohud-preset remove <id> [--json]
  • runi prefix open <id>
  • runi lossless-scaling [--json] — whether frame generation is usable
  • runi lossless-scaling install [--version TAG] [--json] — install the lsfg-vk Vulkan layer (never Lossless.dll); refuses if a layer of that name is already registered. See lossless-scaling.md
  • runi lossless-scaling remove [--json] — remove the layer runi installed
  • runi bundle export <id> --output <file.pfy>
  • runi bundle inspect <file.pfy>
  • runi bundle verify <file.pfy>
  • runi bundle import <file.pfy> [--exe <path>] [--as <new-id>]
  • runi saves backup <id> [--json]
  • runi saves list <id> [--json]
  • runi saves restore <id> <backup-file> [--json]
  • runi saves import-ludusavi <id> <backup-folder> [--preview] [--no-set-save-path] [--json]
  • runi gui — launches the PySide6 GUI (optional [gui] extra)
  • runi doctor [--fix] [--json] — audits the registry against the real filesystem: missing exes, stale OptiScaler/customize state, an install-directory marker mismatch, orphaned prefix/save-backup/log directories. --fix auto-clears the safe findings (stale state, marker mismatch) - never deletes an orphaned directory
  • runi doctor remove-orphan <path> [--yes] [--json] — permanently deletes one orphaned directory (the exact path as printed by runi doctor); confirms first (showing total size) unless --yes
  • runi notes <id> [--edit] [--json] — view/edit a game's findings (stored outside its own config, see [notes].key above)

See user-flows.md for these in context, and customize-profiles.md for the profile TOML format.