docs: user-facing documentation and signing key

This commit is contained in:
2026-08-27 12:32:26 +01:00
commit 017c04fc3c
16 changed files with 3422 additions and 0 deletions
+499
View File
@@ -0,0 +1,499 @@
<!-- Generated from the Runi source repository at v0.4.0. -->
<!-- Edit the original there, not this copy. -->
# 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.
```toml
# ~/.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`:
```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:
```bash
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:
```bash
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 `exec`s `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](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](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](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:
```json
{ "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:
```json
{ "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:
```json
{
"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](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](user-flows.md) for these in context, and
[customize-profiles.md](customize-profiles.md) for the profile TOML format.