Repository navigation
Architecture 05 Steam Library Launch
This page documents the two SteamClient modules that drive library discovery and game launching:
| File | Role |
|---|---|
src/steam_client/library.rs |
Owned/Family-Shared game discovery, PICS app metadata, store info, update detection. |
src/steam_client/launch.rs |
The launch entry point (play_game) and the update/verify download driver. |
Both are impl SteamClient blocks split out of steam_client.rs for readability. The struct, shared use super::* imports, and free helpers live in the parent module (src/steam_client.rs).
Why this page matters for the umu-launcher work:
launch.rs::play_gameis where the native vs Proton decision begins, but it then hands off to a pipeline that ultimately builds theproton runcommand elsewhere. The full call path and every Proton/native decision point are traced in §3 and §6.
-
library.rsanswers "what can this account play, and is it up to date?" It talks to Steam's CM service methods (Player.GetOwnedGames,FamilyGroups.*,StoreBrowse.GetItems) and to the PICS product-info system, then caches results to disk. -
launch.rsanswers "run this game now."play_gameresolves the right launch entry, decides native-Windows vs native-Linux vs Proton, performs Cloud sync-down, spawns the process (directly or through the launch pipeline), records the running game, blocks on exit, and syncs the cloud back up. It also hosts the shared download engine used by bothupdate_gameandverify_game.
fetch_owned_games (library.rs:8) calls the Player.GetOwnedGames service method with include_appinfo and include_played_free_games, maps each result into an OwnedGame, and writes save_library_cache(&owned) (library.rs:44) so the library is browsable offline. refresh_owned_games (library.rs:109) is a thin re-fetch. load_cached_owned_games (library.rs:113) reads the cache.
fetch_family_shared_apps (library.rs:51) is a two-step CM flow:
-
FamilyGroups.GetFamilyGroupForUser→ resolves the caller'sfamily_groupid. If0, the account is in no family group and an empty list is returned (library.rs:69-73). -
FamilyGroups.GetSharedLibraryAppswithinclude_own(false)→ lists apps shared by other members (not the caller's own), capped atmax_apps(10_000), producingSharedApp { app_id, name, owner_steamid }.
The owner_steamid is the first entry of owner_steamids (library.rs:102). This is the account whose license must authorise the launch, and it is exactly why family-shared launches force Steam integration on (see §3.1).
All raw appinfo flows through fetch_pics_buffer (library.rs:228), which issues a CMsgClientPICSProductInfoRequest job and returns the app's product-info buffer (usually binary VDF, occasionally text). Consumers parse it with find_vdf_in_pics rather than the text-only parse_appinfo (a recurring comment warns that binary appinfo silently yields no DLC/depots otherwise):
| Function | Line | Returns |
|---|---|---|
get_extended_app_info |
library.rs:255 |
ExtendedAppInfo { name, DLC ids, depots, launch options, active branch } |
fetch_ufs_save_specs |
library.rs:303 |
UfsSaveSpec[]: Cloud auto-cloud save rules |
fetch_online_required |
library.rs:316 |
bool derived from common/category
|
fetch_store_apps |
library.rs:345 |
StoreAppInfo[] via StoreBrowse.GetItems (over CM, not HTTPS) |
get_product_info |
library.rs:393 |
Vec<LaunchInfo>: the launch entries that drive play_game
|
get_product_info is the direct dependency of the launch path: it fetches the PICS buffer and calls parse_launch_info_from_vdf(appid, &raw_vdf) (library.rs:403).
Free helpers in this file:
-
launch_options_from_section(library.rs:430): raw(executable, arguments)pairs from the PICSconfig/launchsection (the data behind thelaunch-optionscommand). -
depots_from_section(library.rs:412): numeric depot ids/names only.
Ownership is implied: OwnedGame comes from GetOwnedGames, while SharedApp is everything available not owned. The actual depot-key check that proves ownership at download time lives in the download driver (get_depot_key skips depots that aren't owned, see launch.rs:404-414).
Update detection compares local vs remote depot manifests:
-
check_for_updates(library.rs:119) iterates installed games, readslocal_manifest_info(library.rs:161) fromappmanifest_<id>.acf, short-circuits to update available if Steam'sStateUpdateRequiredflag is set (authoritative + offline-safe), otherwise fetchesremote_manifest_idsand compares. -
installed_depots_need_update(library.rs:454): a pure helper that flags an update only when an installed depot's manifest differs from remote (non-installed platform depots are ignored). Covered byupdate_detection_tests(library.rs:462).
The public entry point is play_game (launch.rs:8-184). It is the orchestrator. It does not itself build the Proton command. Signature:
pub async fn play_game(
&mut self,
app: &LibraryGame,
proton_path: Option<&str>, // explicit/driver-supplied Proton
user_config: Option<&UserAppConfig>, // per-game env / launch options
force_windows: bool, // --windows: run the .exe path
force_native_engine: bool, // --native-engine: Luxtorpeda etc.
steam_enabled: bool, // --steam: bridged launch
) -> Result<LaunchInfo>let steam_enabled = steam_enabled || !app.is_owned; // launch.rs:20
#[cfg(target_os = "linux")]
if steam_enabled { crate::utils::ensure_steam_running(); } // launch.rs:25-28A family-shared game can only be authorised by a running host Steam client, so any non-owned app forces steam_enabled on regardless of the --steam flag. When enabled, the host Steam client is started silently (Linux-only, best-effort) so Steamworks/Family-Sharing can initialise.
let launch_options = self.get_product_info(app.app_id).await?; // launch.rs:30This is the bridge into library.rs::get_product_info → parse_launch_info_from_vdf → Vec<LaunchInfo> (the launch-options data: per-entry executable, arguments, working dir, and a LaunchTarget platform tag).
The selection logic (launch.rs:38-76) is platform-installation-aware. A game commonly advertises Windows, macOS, and Linux entries, but only one platform's depot is on disk:
-
exe_exists(o)(launch.rs:38-47): does this entry's executable actually exist underinstall_path(backslashes normalised)? -
prefer_windows_target = force_windows || proton_path.is_some()(launch.rs:56): whenever we intend to run through Wine/Proton. -
If preferring Windows (launch.rs:57-68): first an installed
WindowsProtonentry, else any installed entry (a Linux-only game can't honour a--protonrequest against a non-existent.exe), else anyWindowsProtonentry, else the first. - Otherwise (launch.rs:70-75): first installed entry, else the first.
This guards against the game_executable_not_found failure mode.
let native_windows = force_windows
|| (cfg!(target_os = "windows") && launch_info.target == LaunchTarget::WindowsProton); // launch.rs:82-83
let chosen_proton_path = if native_windows {
None
} else {
match launch_info.target {
LaunchTarget::NativeLinux => None,
LaunchTarget::WindowsProton => proton_path.or(Some(launcher_config.proton_version.as_str())),
}
}; // launch.rs:85-94Two outcomes are decided here:
| Condition | native_windows |
chosen_proton_path |
Spawn path |
|---|---|---|---|
--windows, or Windows host running a Windows entry |
true |
None |
spawn_windows_native |
Linux entry (NativeLinux) |
false |
None |
spawn_game_process (NativeRunner) |
| Windows entry on Linux | false |
proton_path ?? launcher_config.proton_version
|
spawn_game_process (WineTkgRunner) |
launcher_config is loaded at launch.rs:78. proton_version is the global default fallback.
Proton/Wine only exists on Linux. On a Windows host, a Windows game runs natively, so the
.exeis executed directly rather than routed through the Proton pipeline.
If enable_cloud_sync and online (launch.rs:96-131): build a CloudClient, resolve the remote root, sync_down. Divergent saves are left untouched (never clobbered) and the game launches with whatever is on disk. UFS save specs are fetched for the later upload.
let mut child = if native_windows {
self.spawn_windows_native(app, &launch_info, user_config).await? // launch.rs:134
} else {
self.spawn_game_process(app, &launch_info, chosen_proton_path,
&launcher_config, user_config, force_native_engine, steam_enabled).await? // launch.rs:136
};-
spawn_windows_native(process.rs:291-343): direct, no compat layer. Normalises the exe path, builds args (VDF args + userlaunch_options), resolves the working dir (VDFworkingdir→ exe parent → install dir), writessteam_appid.txt, setsSteamAppId+ userenv_variables, thenCommand::spawn(). This path bypasses the launch pipeline entirely. -
spawn_game_process(process.rs:345-380): the Linux compat path. It is a thin shim: it builds aPipelineContext(copying inapp,launch_info,launcher_config,user_config,proton_path,force_native_engine,steam_enabled), attaches a logging session, then runsLaunchPipeline::with_default_stages(). All command/env construction and the actual Proton invocation happen inside the pipeline, not here. It returnsctx.child.
A RunningGame record is written (launch.rs:150-158) so aurelia stop <app_id> can find and kill the process. For the Proton path it also records a per-game compatdata wineprefix (and only a compatdata prefix: sweeping the shared master prefix on stop would also kill the bridged Steam client, launch.rs:141-149). Then it blocks on child.wait(), clears the running record (launch.rs:160-162), and finally sync_ups the cloud (failures logged, never surfaced as a launch error: the game already ran, launch.rs:164-181).
launch_game (launch.rs:186-196) is a fire-and-forget variant: it spawns through spawn_game_process and returns without waiting (no cloud, no run tracking).
The --steam bridged launch is threaded as the steam_enabled boolean from play_game (§3.1) into PipelineContext.steam_enabled. The behavioural difference is realised downstream in WineTkgRunner::build_env (src/infra/runners/wine_tkg.rs):
-
Bridged (
steam_enabled):STEAM_COMPAT_CLIENT_INSTALL_PATHpoints at the real host Steam (utils::host_steam_client_path()), and the steam-client DLL overrides are left empty so Proton's builtinlsteamclientbridges to the running client. -
Standalone:
STEAM_COMPAT_CLIENT_INSTALL_PATHpoints at a fake steam trap (utils::setup_fake_steam_trap: dummysteam/steam.shscripts thatexit 0), and the steam DLLs are neutralised (steamclient=n;…).
Distinct from the host bridge is the master Windows Steam runtime: when enabled, WineTkgRunner::prepare_prefix spawns a background Windows steam.exe inside the wineprefix (<proton> run …\steam.exe -silent …) and waits for readiness. It is provisioned by install_master_steam() in src/launch/mod.rs.
update_game (launch.rs:198) and verify_game (launch.rs:207) both call start_manifest_download(appid, verify_mode, shared_state) (launch.rs:216-574). It resolves the install root + appmanifest, builds ManifestSelections (remote manifests for update, local InstalledDepots for verify), fetches content servers, and downloads each depot from CDN hosts (per-depot key via get_depot_key, manifest request code, CDN auth token) on a spawned Tokio task while a 250 ms ticker forwards DownloadProgress over an mpsc channel. On success it writes a fresh appmanifest. download_aborted (launch.rs:581) checks the abort signal (poison-safe). This path is independent of game launching.
| Type | Location | Notes |
|---|---|---|
LaunchTarget (enum NativeLinux / WindowsProton) |
steam_client.rs:143-147 |
The platform tag that drives the native-vs-Proton branch. |
LaunchInfo { app_id, id, description, executable, arguments, workingdir, target } |
steam_client.rs:149-158 |
One resolved launch entry. |
RawLaunchOption { executable, arguments } |
steam_client.rs:160-164 |
Raw PICS config/launch pair (the launch-options command output). |
OwnedGame, SharedApp, LibraryGame, ExtendedAppInfo, UfsSaveSpec, StoreAppInfo
|
steam_client.rs |
Library/metadata DTOs. |
parse_launch_info_from_vdf (steam_client.rs:929-1012) is the platform classifier. Decision order ([steam_client.rs:952-979]):
- VDF
config/oslist:"linux"→NativeLinux,"windows"/other non-empty →WindowsProton,"macos"→ skipped. - Else executable extension:
.exe/.bat→WindowsProton,.sh/contains"linux"→NativeLinux. - Else host OS default.
Entries sorted to prefer key
"0".
| Function | File:line | Role |
|---|---|---|
play_game |
launch.rs:8 |
Launch orchestrator, native/Proton decision, cloud, wait. |
launch_game |
launch.rs:186 |
Fire-and-forget pipeline launch. |
update_game / verify_game / start_manifest_download
|
launch.rs:198 / 207 / 216 |
Depot download/verify engine. |
fetch_owned_games / fetch_family_shared_apps
|
library.rs:8 / 51 |
Library discovery. |
get_product_info |
library.rs:393 |
PICS → Vec<LaunchInfo> for the launcher. |
get_extended_app_info / fetch_store_apps / fetch_ufs_save_specs / fetch_online_required
|
library.rs:255 / 345 / 303 / 316 |
App/store metadata. |
check_for_updates / installed_depots_need_update
|
library.rs:119 / 454 |
Update detection. |
play_game (steam_client/launch.rs:8)
├─ get_product_info (steam_client/library.rs:393)
│ └─ parse_launch_info_from_vdf (steam_client.rs:929) → Vec<LaunchInfo>
├─ [native_windows] spawn_windows_native (steam_client/process.rs:291) → Command::spawn (NO pipeline, NO Proton)
└─ [else] spawn_game_process (steam_client/process.rs:345)
└─ LaunchPipeline::with_default_stages().run() (src/launch/pipeline.rs)
1. ResolveGameStage (src/launch/stages/resolve_game.rs)
2. ResolveProfileStage (src/launch/stages/resolve_profile.rs)
3. ResolveComponentsStage (src/launch/stages/resolve_components.rs) ← picks Runner
4. ResolveDllProvidersStage(src/launch/stages/resolve_dll_providers.rs)
5. PreparePrefixStage (src/launch/stages/prepare_prefix.rs) → runner.prepare_prefix
6. BuildEnvironmentStage (src/launch/stages/build_environment.rs) [no-op]
7. BuildCommandStage (src/launch/stages/build_command.rs) → runner.build_command → CommandSpec
8. PreflightStage (src/launch/stages/preflight.rs)
9. SpawnProcessStage (src/launch/stages/spawn_process.rs) → runner.launch → Child
10. FinalizeStage (src/launch/stages/finalize.rs)
| Module | Path | Relationship to launch.rs |
|---|---|---|
| Launch pipeline | src/launch/pipeline.rs |
spawn_game_process constructs a PipelineContext and runs the default stages. Carries proton_path, force_native_engine, steam_enabled. Proton is invoked here only indirectly, via runner.build_command() (BuildCommandStage) and runner.launch() (SpawnProcessStage). |
| Runner selection | src/launch/stages/resolve_components.rs:63-85 |
Second native/Proton decision. Linux + wants_luxtorpeda (force --native-engine / pinned) → LuxtorpedaRunner. Else by launch_info.target: NativeLinux → NativeRunner, WindowsProton → WineTkgRunner. |
| Wine/Proton runner | src/infra/runners/wine_tkg.rs |
build_env (env: STEAM_COMPAT_*, WINEPREFIX, WINEDLLOVERRIDES, GPU PRIME, host-vs-fake-steam bridge), build_command (assembles the full argv), prepare_prefix (compatdata + master Steam), launch (spawn). |
| Runner command builder |
src/utils.rs:6-38 (build_runner_command) |
The single chokepoint that emits proton run vs bare wine. WineTkgRunner::build_command calls it, then appends the game exe + args. |
| Proton manager | src/proton.rs |
Download/list/remove runtimes only: it does not build or run any launch command. compat_tools_dir, list_installed, install_github_package, VALVE_PROTONS. launch.rs references it only indirectly (the chosen proton_version name is later resolved by utils::resolve_runner). |
| Steam host helpers | src/utils.rs |
ensure_steam_running, is_steam_running, host_steam_client_path, setup_fake_steam_trap, steam_wineprefix_for_game. |
| Running registry | src/running.rs |
record_launch / clear for aurelia stop. |
| Cloud sync | src/cloud_sync.rs |
CloudClient, CloudPathResolver, UfsSaveSpec. |
umu-launcher replaces direct proton run invocation with a unified runtime launcher (umu-run, configured via GAMEID/PROTONPATH/STORE/WINEPREFIX env). Below is every decision point where Proton/Wine vs native is chosen and every place the command + environment is assembled, in call order.
-
play_gamenative gate:native_windowsat launch.rs:82-83 andchosen_proton_pathat launch.rs:85-94. Ifnative_windowsis true the entire pipeline (and thus umu) is bypassed viaspawn_windows_native. umu only matters for theelsebranch.chosen_proton_path(proton_path?? globalproton_version) is the Proton runtime that umu would receive asPROTONPATH. -
play_gamespawn branch (launch.rs:133-137):native_windows ? spawn_windows_native : spawn_game_process. The wineprefix-recording branch at launch.rs:141-149 also assumes Proton semantics (per-gamecompatdata). umu uses the same compatdata layout, so this should remain valid. -
ResolveComponentsStage(src/launch/stages/resolve_components.rs:63-85) is the canonical runner selection:NativeRunner/WineTkgRunner/LuxtorpedaRunner. umu replaces whatWineTkgRunnerdoes, either as a newUmuRunnerselected here, or by swapping the command/env builders insideWineTkgRunner. -
VDF platform classifier (
steam_client.rs:952-979): produces theLaunchTarget(NativeLinux/WindowsProton) that decisions 1–3 read. Not a Proton-invocation site, but the upstream source of truth.
| Concern | Location | What umu changes |
|---|---|---|
| Runner binary → command |
src/utils.rs:6-38 build_runner_command
|
The one place emitting <proton> run (line 22-25) vs bare wine (27-29). Replace with umu-run construction. |
| Full argv assembly |
WineTkgRunner::build_command (src/infra/runners/wine_tkg.rs) |
Calls build_runner_command, appends game exe + VDF args + user launch options → CommandSpec. umu wants umu-run <exe> <args>. |
| Environment |
WineTkgRunner::build_env (src/infra/runners/wine_tkg.rs) |
Sets STEAM_COMPAT_APP_ID, STEAM_COMPAT_DATA_PATH, WINEPREFIX, STEAM_COMPAT_CLIENT_INSTALL_PATH (host bridge vs fake trap), WINEDLLOVERRIDES, WINEDLLPATH/WINEPATH, GPU PRIME, MangoHud, WINEDEBUG. umu expects GAMEID, PROTONPATH, STORE, WINEPREFIX. Map them here. |
| Prefix / compatdata creation |
WineTkgRunner::prepare_prefix (src/infra/runners/wine_tkg.rs) |
Creates steamapps/compatdata/<appid> (umu/Proton both require it) and optionally the background Windows Steam runtime. |
| Final spawn |
WineTkgRunner::launch (src/infra/runners/wine_tkg.rs) + SpawnProcessStage
|
Spawns the CommandSpec. Unchanged conceptually for umu. |
| Runtime resolution |
src/utils.rs resolve_runner / src/proton.rs compat_tools_dir/list_installed
|
How the Proton runtime named by chosen_proton_path is located on disk (Steam common, compatibilitytools.d, Lutris, fuzzy). umu would consume the resolved path as PROTONPATH. |
The --steam/Family-Sharing bridge (§3.7) is enforced via steam_enabled and realised in build_env (host STEAM_COMPAT_CLIENT_INSTALL_PATH + empty steam-DLL overrides vs fake trap + neutralised overrides). Since umu reuses STEAM_COMPAT_CLIENT_INSTALL_PATH, the bridged-vs-standalone branch should carry over unchanged, but the steam-DLL-override neutralisation and the in-prefix master Steam runtime (prepare_prefix) must be re-applied in whatever umu-based runner replaces WineTkgRunner.
Key takeaway:
launch.rsdecides whether Proton is used (native_windows/chosen_proton_path) but never how. The actual Proton command isbuild_runner_command(utils.rs:6) wrapped byWineTkgRunner::build_command/build_env/launch.src/proton.rsis download-only. umu integration touchesutils::build_runner_commandand theWineTkgRunnerbuilders, plus the runner selection inresolve_components.rs, notplay_game's decision logic.
Users
-
Usage
- Global behavior
- Authentication
- Library
- Store & discovery
- Collections
- Install & maintenance
- Launching
- Depots & branches
- Downgrade & pinning
- Steam Cloud
- Steam Workshop
- Friends & chat
- Inventory & market
- Configuration
- Proton & Wine
- Windows Steam runtime
- Luxtorpeda plugin
- umu-launcher plugin
- Launch scripts
- Session daemon
- Files & locations
- Exit codes & logging
- Windows Steam Runtime
Maintainers
Architecture