Skip to content

Repository files navigation

LaunchLayer

LaunchLayer

Layered launch orchestration for Steam games on Linux

One Steam launch string. Per-game configs. GameMode, Gamescope, MangoHUD, and more — assembled automatically.

License: CC BY-NC-SA 4.0CIShellPlatform

For Linux gamers who tune every launch — GameMode, CPU affinity, MangoHUD, Gamescope, VRAM hogs, network latency — but do not want a different Steam launch string per title.

LaunchLayer sits in Steam’s Launch Options ahead of %command%. It loads layered config, runs preflight checks, and builds the wrapper chain before your game starts.

Without LaunchLayerWith LaunchLayer
gamemoderun mangohud gamescope -W 3440 … %command% pasted per game"/path/to/launchlayer" %command% once per game
Settings scattered across Steam, shell aliases, and one-off scriptsPlain KEY=VALUE files: profiles → presets → per-game
No preflight for vm.max_map_count, shader bloat, or VRAM pressureDoctor, cache trim, compositor-aware display detection

Built for a tuned workstation (7900X3D, RTX 3080 Ti, Wayland / Plasma 6), with auto-detection and profiles for Steam Deck, Flatpak Steam, BSD, and WSL2.

Requirements: bash 4.2+, Steam (or any launcher that passes %command%). Optional tools (fzf, gamescope, …) enhance the stack but are not required for basic launches.

See it work

Preview the resolved layers and launch chain without starting a game:

./launchlayer --show-config 2357570
=== Config for AppID 2357570 (Overwatch®) ===
Layers:
→ profiles/arch-linux.env
→ profiles/nvidia-desktop.env
→ default.env
→ presets/competitive.env
→ games/2357570.env
Launch chain:
gamemoderun → taskset → game-performance → dlss-swapper → gamescope → %command%

LaunchLayer TUI game picker
Interactive TUI — browse games, preview configs, flip toggles

Quick paths

I want to…Go to
Get running in five minutesQuick start
Paste into Steam’s Launch OptionsSteam integration
Browse and edit games interactivelyInteractive TUI · screenshots
Share configs with similar machinesCommunity hub
Understand the launch pipelineHow a launch works
Full CLI command tablesdocs/cli.md
TUI menus and shortcutsdocs/tui.md
Module-level internalsdocs/architecture.md
Licenses / inject / nest Gamescopedocs/third-party.md
Docs map (topic → page)docs/README.md

Contents

Section
Quick start
Steam launch options
·What it does
How a launch works
Configuration
CLI reference
Interactive TUI · docs/tui.md
Community hub
System tuning
Third-party licenses
Docs index
/Project layout
+Optional dependencies
Testing · Release runbook · Changelog
?FAQ
Contributing
§License

Quick start

  1. Clone to a stable path (Steam needs a fixed absolute path in Launch Options):
git clone https://github.com/bolens/launch-layer.git ~/launchlayer
cd~/launchlayer
  1. Run onboarding — completions, symlink, launch string, and machine defaults:
./launchlayer --setup --completions --symlink --print-launch-option --write-local-config

This installs shell completions, adds ~/.local/bin/launchlayer, prints your Steam launch string, and writes launch.d/local.env. Add --systemd for the maintenance timer or --backup-timer for scheduled config backups.

  1. Paste into Steam — copy the printed string into each game’s Launch Options (same string for every title):
"$HOME/launchlayer/launchlayer" %command%

See Integrating with Steam launch options for UI paths, Flatpak notes, and verification.

  1. Scaffold a per-game config:
./launchlayer --init-appid 2357570 competitive # by AppID
./launchlayer --init-appid "Overwatch" competitive # by name
./launchlayer --tui # or browse interactively
  1. Sanity check:
./launchlayer --doctor

If Proton titles misbehave, fix vm.max_map_count once — see System tuning.


Integrating with Steam launch options

LaunchLayer hooks into Steam by prefixing the normal game command. Steam replaces %command% with Proton wrappers, the game binary, and any args Steam already knows about; LaunchLayer loads config, runs preflight, builds wrapper chains, then execs that command.

Use the same launch string on every game you want managed. Per-game tuning lives in GAMES_DIR/<AppID>.env—you do not need different launch options per title.

1. Get your launch string

Run onboarding (recommended) or print the string alone:

./launchlayer --setup --symlink --print-launch-option
# or
./launchlayer --setup --print-launch-option
# or (also printed at the end of doctor)
./launchlayer --doctor

Example output:

"$HOME/launchlayer/launchlayer" %command%
ApproachLaunch stringNotes
Absolute path (recommended)"$HOME/launchlayer/launchlayer" %command%Most reliable—Steam’s environment often has a minimal PATH; --print-launch-option prints your real path
Symlink (after --setup --symlink)"$HOME/.local/bin/launchlayer" %command%Use the full path to the symlink (realpath ~/.local/bin/launchlayer); bare launchlayer usually fails in Steam

Rules:

  • Keep %command% at the end. Without it Steam never runs the game binary.
  • Quote the script path when it contains spaces.
  • Do not substitute the game .exe or Proton command for %command%—LaunchLayer receives the full Steam-built argv automatically.
  • Replace other wrapper prefixes (gamemoderun %command%, mangohud %command%, sd0 %command%, etc.) with LaunchLayer; enable those features in config instead (GAMEMODE=1, MANGOHUD=1, DISABLE_STEAM_DECK=1, …).

2. Paste into Steam

Per game (typical workflow—repeat for each title, or copy/paste the same string):

  1. Open SteamLibrary
  2. Right-click the game → Properties
  3. In General, find Launch Options
  4. Paste your launch string, e.g. "$HOME/launchlayer/launchlayer" %command%
  5. Close Properties and launch the game normally
Steam Deck / Big Picture

On Deck: Library → select game → gear iconPropertiesGeneralLaunch Options. The same %command% string applies.

3. Flatpak Steam

If Steam is installed via Flatpak, the sandbox must read your LaunchLayer install:

  • Script under $HOME (e.g. ~/launchlayer/…) → usually works as-is
  • Script outside $HOME (e.g. /path/to/launchlayer) → grant filesystem access:
flatpak override --user com.valvesoftware.Steam --filesystem=/path/to/launchlayer

Check access and get a tailored hint:

./launchlayer --detect-environment
./launchlayer --doctor

The flatpak-steam profile layers automatically when Flatpak Steam is detected.

4. Per-game settings (no launch-option edits)

After the launch string is set once per game, adjust behavior with per-game configs—not by changing Steam’s field again:

./launchlayer --init-appid 2357570 competitive # scaffold GAMES_DIR/2357570.env
./launchlayer --edit-appid "Overwatch"# open in $EDITOR
./launchlayer --show-config 2357570 # resolved layers + launch chain

Steam sets SteamAppId / STEAM_APPID when launching; LaunchLayer uses that to pick GAMES_DIR/<AppID>.env (or auto preset when no file exists).

5. Verify before or after first launch

Preview the resolved chain (terminal—no game start):

SteamAppId=2357570 ./launchlayer --dry-run %command%
# or
./launchlayer --show-config 2357570

After a real launch, inspect history:

./launchlayer --launch-stats 2357570
tail ~/.local/state/launchlayer/launch.log

Health check:

./launchlayer --doctor

Troubleshooting

SymptomLikely causeFix
Game starts but LaunchLayer never runsLaunch string missing or wrong gameConfirm Launch Options on that title; path must point to the launchlayer script
Game never starts / instant exit%command% omittedUse "/path/to/launchlayer" %command%—not the script alone
Permission denied or No such fileBad path or Flatpak sandboxUse absolute path; for Flatpak Steam see Flatpak Steam
Wrong preset / no per-game configNo GAMES_DIR file yet./launchlayer --init-appid APPID preset or --tui
Double wrappers / odd behaviorOld launch option left in placeRemove gamemoderun, mangohud, dlss-swapper, sd0, etc. from Steam; configure via LaunchLayer (GAMEMODE, MANGOHUD, DLSS_SWAPPER, DISABLE_STEAM_DECK)
Proton crashes / map errorsLow vm.max_map_count./launchlayer --sysctl install — see System tuning

What it does

AreaBehavior
Layered configPlain KEY=VALUE files stack: profiles → default.envlocal.env → preset → per-game overrides
Auto-detectionDistro, GPU, compositor, display resolution/VRR, X3D V-Cache CPU mask, native vs Proton
PreflightChecks vm.max_map_count, shader/compat cache size (optional trim), VRAM, GPU power/processes, disk space, concurrent launches
Runtime tuningNetwork (ethtool), PipeWire latency, NVIDIA power mode, Proton/DXVK/VKD3D env, shader-cache boost, DLSS/FSR4/XeSS upgrade knobs
VRAM managementPause configured systemd units (Sunshine, etc.) during play; resume on exit
Launch chainLAUNCH_WRAPPERS_BEFORE → GameMode → CPU affinity → game-performance → DLSS swapper → LAUNCH_WRAPPERS → Gamescope (--mangoapp when both Gamescope and MangoHUD) → MangoHUD → game
CLI + TUIManage configs, backup/restore, doctor checks, optional Community hub

Use --dry-run %command% to print the resolved config and chain without starting the game.


How a launch works

When Steam invokes the script, run_game_launch in lib/launch.sh runs this pipeline:

flowchart LR
A["↻ Recover state"] --> B["· Resolve AppID"]
B --> C["≡ Load config"]
C --> D["◦ Detect flags"]
D --> E["▤ Hardware defaults"]
E --> F["⊛ Preflight"]
F --> G["◆ VRAM hogs"]
G --> H["⚡ Runtime tune"]
H --> I["→ Build chain"]
I --> J["▶ Exec game"]
style A fill:#1e293b,stroke:#475569,color:#e2e8f0
style B fill:#1e293b,stroke:#475569,color:#e2e8f0
style C fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
style D fill:#1e293b,stroke:#475569,color:#e2e8f0
style E fill:#1e293b,stroke:#475569,color:#e2e8f0
style F fill:#422006,stroke:#f59e0b,color:#fef3c7
style G fill:#1e293b,stroke:#475569,color:#e2e8f0
style H fill:#1e293b,stroke:#475569,color:#e2e8f0
style I fill:#14532d,stroke:#22c55e,color:#dcfce7
style J fill:#14532d,stroke:#22c55e,color:#dcfce7
Loading
Step-by-step (text)
  1. Recover stale state — Resume VRAM-heavy services left paused after a crash (lib/vram.sh)
  2. Resolve AppID — From SteamAppId, STEAM_APPID, or launch argv (lib/config.sh)
  3. Load layered config — Profiles → default.envlocal.env → preset or per-game file; then apply_defaults and apply_detected_defaults
  4. Detect game flags — Native vs Proton, EAC/BattlEye, engine hints (lib/steam/detect.sh)
  5. Auto hardware defaults — X3D CPU mask, display resolution/refresh for Gamescope (lib/hardware/)
  6. Parse extra args — Split GAME_EXTRA_ARGS into argv appended after %command%
  7. Preflight checks — Skipped when BENCHMARK=1 (lib/preflight.sh): sysctl, shader/compat caches, VRAM, GPU power/processes, disk, concurrent launch guard
  8. Tool warnings & anticheat guardrails — Missing optional tools; warn on risky settings for EAC/BattlEye titles
  9. VRAM hogs — Optionally pause configured systemd user units with refcount + exit trap (before runtime tuning)
  10. Runtime tuning — Network (ethtool), PipeWire latency, CPU perf profile, NVIDIA power mode, Proton/DXVK/VKD3D env
  11. Build launch chain — Assemble wrappers per build_launch_chain in lib/runtime/chain.sh
  12. ExecPRE_LAUNCH_CMD → run chain + %command% + extras → POST_LAUNCH_CMD; log to ~/.local/state/launchlayer/launch.log

For module-level detail, see docs/architecture.md. Config-key cheat sheets: docs/cli.md. Inject / license policy: docs/third-party.md. Docs map: docs/README.md.


Configuration

Settings are plain KEY=VALUE files. Later layers override earlier ones. Full key tables: docs/cli.md. TUI editors: docs/tui.md.

Layer order

flowchart BT
P["0 · profiles/*.env<br/><i>machine</i>"]
D["1 · default.env<br/><i>global</i>"]
L["2 · local.env<br/><i>this machine</i>"]
R["3 · presets/*.env<br/><i>gameplay</i>"]
G["4 · games/AppID.env<br/><i>per-game — wins</i>"]
P --> D --> L --> R --> G
style P fill:#312e81,stroke:#6366f1,color:#e0e7ff
style D fill:#1e3a5f,stroke:#3b82f6,color:#dbeafe
style L fill:#164e63,stroke:#06b6d4,color:#cffafe
style R fill:#365314,stroke:#84cc16,color:#ecfccb
style G fill:#14532d,stroke:#22c55e,color:#dcfce7
Loading
OrderFilePurpose
0launch.d/profiles/*.envMachine profiles (auto-detected or via LAUNCHLAYER_PROFILES)
1launch.d/default.envGlobal infrastructure defaults
2launch.d/local.envMachine-local overrides (gitignored; from --write-local-config; force-overwrites profile/default keys)
3launch.d/presets/*.envGameplay preset via per-game INCLUDE=or auto standard/native when no per-game file
4games/<AppID>.envPer-game overrides in GAMES_DIR (wins over everything above)

Preset loading: If GAMES_DIR/<AppID>.env exists, only that file is loaded (plus its INCLUDE= chain). Auto standard.env / native.env applies only when no per-game file exists. Per-game files usually start with INCLUDE=presets/competitive.env (or another preset) and then override individual keys.

After files load, runtime detection fills any still-unset keys: PipeWire latency, network tuning, NVIDIA checks, VRAM hog filtering, disk thresholds, and platform guardrails (Steam Deck, WSL2, containers).

Where data lives

LocationDefaultContents
LAUNCHLAYER_CONFIG_DIRrepo rootlaunch.d/ shipped layers + optional local.env
LAUNCHLAYER_GAMES_DIR~/.local/share/launchlayer/gamesPer-game <AppID>.env files
~/.config/launchlayer/user prefstui.conf, backup.conf, hub.conf
~/.local/state/launchlayer/runtimeLaunch logs, PID/stamp files (see Runtime state)

Per-game configs are not stored under launch.d/ in git—only in GAMES_DIR. Example: examples/games/2357570.env (Overwatch 2).

Auto preset selection

When no per-game .env exists:

Path
NNative Linux buildpresets/native.env
PEverything else (Proton)presets/standard.env

Presets

PresetUse case
standardDefault Proton titles — GameMode on
competitiveOnline / latency-sensitive — extends standard with MangoHUD, Gamescope, VRR, VRAM hogs, network tune
lightweight2D / indie — minimal overhead
nativeNative Linux — skips Proton env and cache checks

Init with: ./launchlayer --init-appid APPID competitive

Machine profiles

Profiles in launch.d/profiles/ layer automatically based on detection, or set explicitly:

LAUNCHLAYER_PROFILES=steam-deck,flatpak-steam # comma-separated# legacy: LAUNCHLAYER_PROFILE=steam-deck
CategoryProfiles
Distrosarch-linux, debian, fedora, suse, nixos, alpine, void, gentoo, solus, clearlinux, immutable-linux
Environmentsteam-deck, flatpak-steam, wsl2, bsd, macos, non-systemd
GPUamd-gpu, intel-gpu, nvidia-desktop (auto-layered)

Common config keys

Per-game files typically start with INCLUDE=presets/competitive.env, then override individual keys:

# Layering
INCLUDE=presets/competitive.env
# Wrappers and game args
DLSS_SWAPPER=1 # 1=dlss-swapper (NGX+presets), dll=dlss-swapper-dll (presets only)
PROTON_DLSS_UPGRADE=0 # 1=Proton-CachyOS/GE DLSS DLL upgrade (not Valve Proton)
PROTON_FSR4_UPGRADE=0 # 1=FSR4 upgrade (RDNA3 auto → PROTON_FSR4_RDNA3_UPGRADE)
PROTON_XESS_UPGRADE=0 # 1=XeSS upgrade (Intel / forks)
SHADER_CACHE_BOOST=1 # raise Mesa/NVIDIA shader cache size limits
LD_BIND_NOW=0 # 1=eager dynamic linking (Arch Gaming)
DISABLE_VBLANK=0 # 1=Mesa vblank off / immediate present
VKBASALT=0 # 1=ENABLE_VKBASALT (vkBasalt layer)
VKBASALT_CONFIG_FILE= # optional path to vkBasalt.conf
LSFG_VK=0 # lsfg-vk (owned Lossless Scaling required)
OBS_VKCAPTURE=0 # obs-gamecapture / obs-vkcapture after Gamescope
SPECIAL_K=0 # Special K under Proton (WINEDLLOVERRIDES + optional inject)
RESHADE=0 # Wine ReShade local inject
GAMESCOPE_NESTED_FIX=1 # strip LD_PRELOAD for nested desktop Gamescope
LATENCYFLEX=0 # 1=LFX=1 (LatencyFleX layer)
DISABLE_STEAM_DECK=0 # 1=SteamDeck=0 (Bazzite sd0)
FRAME_RATE= # e.g. 60 → DXVK_FRAME_RATE + VKD3D_FRAME_RATE
LAUNCH_WRAPPERS=""# custom PATH wrappers after DLSS; do not list dlss-swapper when DLSS_SWAPPER is set
LAUNCH_WRAPPERS_BEFORE=""
GAME_EXTRA_ARGS="-skipintro -nolog"
UNSET_VARS="DXVK_ASYNC VKD3D_CONFIG"# Hooks (local only — hub publish rejects non-empty values; hub apply strips them)
PRE_LAUNCH_CMD=""
POST_LAUNCH_CMD=""# Flags
FORCE_NATIVE=1 FORCE_PROTON=1
BENCHMARK=1 DEBUG=1
# Features (0/1 unless noted)
GAMEMODE MANGOHUD MANGOHUD_CONFIG MANGOHUD_LOG
GAMESCOPE GAMESCOPE_W GAMESCOPE_H GAMESCOPE_R
GAMESCOPE_ADAPTIVE_SYNC GAMESCOPE_FSR GAMESCOPE_FSR_SHARPNESS GAMESCOPE_HDR
VRAM_HOGS LAUNCH_WATCHDOG NETWORK_TUNE PIPEWIRE_LOW_LATENCY
GPU_POWER_CHECK NVIDIA_POWER_MODE GAME_PERFORMANCE DLSS_SWAPPER
DISABLE_CPU_AFFINITY CPU_AFFINITY_RANGE CONCURRENT_LAUNCH_GUARD
DISABLE_NIC_EEE DISABLE_WIFI_POWER_SAVE DISK_TUNE
MALLOC_ALLOCATOR ENABLE_HDR OVERRIDE_PROTON
PROTON_DLSS_UPGRADE PROTON_FSR4_UPGRADE PROTON_XESS_UPGRADE
PROTON_NVIDIA_LIBS PROTON_NVIDIA_LIBS_NO_32BIT SHADER_CACHE_BOOST
LD_BIND_NOW VKBASALT LATENCYFLEX DISABLE_VBLANK
DISABLE_STEAM_DECK FRAME_RATE
# Preflight thresholds
SHADER_CACHE_CHECK SHADER_CACHE_MAX_GB SHADER_CACHE_TRIM SHADER_CACHE_BOOST_GB
COMPATDATA_CHECK COMPATDATA_MAX_GB COMPATDATA_TRIM
VRAM_PREFLIGHT_MIN_MB DISK_PREFLIGHT_MIN_GB GPU_VRAM_PROCESS_MIN_MB
VM_MAX_MAP_COUNT_MIN VM_MAX_MAP_COUNT_FIX
# Proton / GPU (passed through when set)
PROTON_* DXVK_* VKD3D_* __GL_* __VK_* SDL_* MESA_* RADV_* AMD_* INTEL_*

Inject, capture, Conty, and Wine keys: docs/cli.md · licenses / purchase gates: docs/third-party.md · nest Gamescope: docs/third-party.md § Nested Gamescope.

Upscaling paths (DLSS / FSR / XeSS)

See also CachyOS: Forcing the Latest DLSS Preset.

ApproachWhen to use
DLSS_SWAPPER=1CachyOS dlss-swapper: NGX updater + latest SR/RR/FG presets at launch
DLSS_SWAPPER=dllManual DLL replace + dlss-swapper-dll (presets only, no NGX)
PROTON_DLSS_UPGRADE=1Proton-CachyOS / GE download latest DLSS into the prefix (needs those forks)
PROTON_FSR4_UPGRADE=1Same forks for FSR4; RDNA3 GPUs auto-use PROTON_FSR4_RDNA3_UPGRADE
PROTON_XESS_UPGRADE=1Same forks for XeSS
dlss-updaterGUI app to replace game-folder DLLs offline — no CLI; LaunchLayer detects it and tips only

Prefer one DLSS path per game (DLSS_SWAPPERorPROTON_DLSS_UPGRADE) to avoid double upgrades. Do not also list dlss-swapper in LAUNCH_WRAPPERS when DLSS_SWAPPER is set.

Latency knobs (Arch Gaming)

KeyEffect
LD_BIND_NOW=1Eager symbol bind (first-call latency)
DISABLE_VBLANK=1Mesa vblank_mode=0 / immediate present; NVIDIA __GL_SYNC_TO_VBLANK=0
VKBASALT=1ENABLE_VKBASALT=1 (vkBasalt Vulkan layer)
LATENCYFLEX=1LFX=1 (LatencyFleX); pair with DISABLE_VBLANK=1 when possible

Bazzite / Deck identity & FPS caps

KeyEffect
DISABLE_STEAM_DECK=1SteamDeck=0 (Bazzite sd0) — full graphics menus when Deck mode locks settings
FRAME_RATE=NDXVK_FRAME_RATE + VKD3D_FRAME_RATE (restart to change; best latency of the FPS-cap methods)

See Bazzite launch options. Prefer LaunchLayer keys over pasting sd0 / dlss-swapper into Steam when using "…/launchlayer" %command%.

Display detection

Cross-compositor probing covers KDE/Plasma, GNOME/COSMIC, Hyprland, Sway, wlroots compositors, and X11 stacks (via xrandr). Compositor IPC probes are gated so inactive tools (e.g. hyprctl on KDE) do not false-match. Wayland sessions auto-set GAMESCOPE_EXPOSE_WAYLAND=0.

Inspect detection: ./launchlayer --detect-environment (see docs/cli.md)


Interactive TUI

./launchlayer --tui # always opens the TUI (interactive terminal required)
launchlayer # same when symlinked; also opens TUI with no args when fzf + TTY

Requires fzf for fuzzy menus with live previews; without it, numbered prompts are used instead.

LaunchLayer main menu
Main menu with status banner

LaunchLayer game picker with live preview
Game picker — fuzzy search, live config preview, Ctrl-E/Ctrl-D shortcuts

LaunchLayer per-game quick toggles
Quick toggles — inherited vs per-game overrides (green/red)

Full menu tree, shortcuts, and preferences:docs/tui.md (includes screenshots)

Regenerate screenshots after UI changes: make tui-screenshots (requires VHS and fzf).


Community hub

Share per-game configs and discover settings from similar machines (GPU, OS, display tier, profiles, Deck/Flatpak/WSL flags). Optional — local launches do not need the hub. Client: lib/hub/; backend: Convex app in hub/.

Setup — copy the template and set your deployment URL and publish token:

mkdir -p ~/.config/launchlayer
cp share/launchlayer/templates/hub.conf.example ~/.config/launchlayer/hub.conf
# hub_url=https://your-deployment.convex.site# publish_token=<same value as Convex HUB_PUBLISH_TOKEN># Optional: machine_label, fingerprint_level (minimal | standard | detailed)

Hub publish/delete is fail-closed: set HUB_PUBLISH_TOKEN on the Convex deployment and matching publish_token in hub.conf. For local open hubs only, set HUB_ALLOW_OPEN_PUBLISH=1 on the deployment (never in production). Published configs cannot include remote-exec keys (PRE_LAUNCH_CMD, wrappers, OVERRIDE_PROTON, VRAM-hog controls); hub apply strips those if present. INCLUDE= paths must stay under launch.d/.

Hub rate limiting also fails closed. Route the HTTP actions endpoint through an ingress that overwrites a client-identity header, then set HUB_TRUSTED_CLIENT_IP_HEADER to that header name and HUB_IDENTIFIER_HASH_KEY to at least 32 random characters. Direct clients must not be able to supply the trusted header.

Hub CLI commands:docs/cli.md § Community hub

Also useful without the hub: --suggest-config APPID|NAME [--apply] ranks ProtonDB reports for this machine and can write allowlisted knobs into games/<AppID>.env (docs/cli.md § Games and config). TUI: Games → Game → [Edit] Suggest from ProtonDB.

The TUI exposes hub flows under Community hub (main menu) and [Hub] Community configs (per-game actions), including viewing history and applying a historical version (also on Apply config by ID).

Deploy or develop the backend from hub/ (Node 22+, pnpm pinned via hub/package.jsonpackageManager). The repo root package.json is a scripts-only shim (no lockfile) — always install inside hub/. Prefer Vite+ (vp) when available — it resolves the pinned pnpm. Otherwise enable Corepack and call pnpm directly. From the repo root you can also use bash scripts/hub-pm.sh … / make test-hub / make lint-hub.

cd hub
# With Vite+ (preferred):
vp install
vp run dev # development — runs convex dev
vp run lint # ESLint + tsc
vp run convex:deploy # production only# Without Vite+ (Corepack + pnpm):
corepack enable
pnpm install
pnpm dev
pnpm run lint
pnpm run convex:deploy

Point hub_url in hub.conf at your deployment’s HTTP actions URL (e.g. https://your-deployment.convex.site).

See docs/architecture.md for similarity weights, fingerprint levels, and HTTP routes. Do not commithub/.env.local, hub/.convex/, or hub/node_modules/ — they are gitignored; make check runs check-hub-git to catch accidental staging.


System tuning

vm.max_map_count (Proton)

Elasticsearch’s package sysctl can reset vm.max_map_count to 262144, which breaks some Proton games:

./launchlayer --sysctl install
# or manually:
sudo cp share/launchlayer/sysctl/elasticsearch.conf /etc/sysctl.d/
sudo sysctl --system
sysctl -n vm.max_map_count # expect 2147483642

ⓘ Remove /etc/sysctl.d/99-proton-vm.conf if present—it is superseded by elasticsearch.conf.

Set VM_MAX_MAP_COUNT_FIX=1 in config to raise the value at launch when passwordless sudo is available.

Passwordless sudo for runtime tuning (optional)

Certain settings (NETWORK_TUNE=1, VM_MAX_MAP_COUNT_FIX=1, DISK_TUNE=1, and Wi‑Fi power-save disable) require root to query or modify hardware/kernel state. To allow LaunchLayer to apply these at game startup without a password prompt:

  1. Create a sudoers override configuration file:

    sudo visudo -f /etc/sudoers.d/launchlayer
  2. Add a rule (replace username with your Linux user). Verify absolute paths with which ip ethtool sysctl iw iwconfig tee:

    username ALL=(ALL) NOPASSWD: /usr/sbin/ip, /usr/bin/ethtool, /usr/bin/sysctl, /usr/bin/iw, /usr/sbin/iwconfig, /usr/bin/tee
    
    • ip / ethtool / sysctl — bring NIC up, ring buffers, EEE, TCP low-latency, vm.max_map_count
    • iw / iwconfig — disable Wi‑Fi power save when DISABLE_WIFI_POWER_SAVE=1
    • tee — write I/O scheduler under /sys/block/<dev>/queue/scheduler when DISK_TUNE=1 (LaunchLayer validates the path before calling tee)

    Prefer the narrowest paths that exist on your distro (/usr/sbin/ip vs /bin/ip, etc.).

Workstation setup (optional)

sudo ./scripts/setup-workstation-tuning.sh

Installs irqbalance, enables btrfs autodefrag when applicable, and installs the X3D IRQ affinity helper + irq-affinity-x3d.service when the helper binary is found.

systemd timers

Maintenance — stale launch cleanup + cache report (launchlayer-maintenance.timer):

./launchlayer --install-systemd
# or: ./launchlayer --setup --systemd

Backup — scheduled config export + prune (launchlayer-backup.timer; configure backup.conf first):

./launchlayer --backup-timer install
# or: ./launchlayer --setup --backup-timer

Both write user units under ~/.config/systemd/user/ with the resolved script path.


Project layout

launchlayer # ▶ entry point (bash 4.2+)
launch.d/ # ≡ shipped layers: default.env, profiles/, presets/, *.txt lists
anticheat-appids.txt # known EAC/BattlEye AppIDs
native-appids.txt # known native Linux AppIDs
lib/ # ⚙ core modules (config, launch, hardware, tui, …)
hub/ # ◉ community hub client (fingerprint, HTTP)
hub/ # ◉ optional Convex backend (vp / pnpm)
share/launchlayer/ # ▣ templates, sysctl, systemd units, completions
examples/games/ # ◆ tracked example per-game configs
scripts/
tui-screenshots/ # VHS frame scripts + fixtures (make tui-screenshots)
check-staged-hub-secrets.sh
setup-workstation-tuning.sh
test/ # ✓ bats integration + unit tests
docs/ # [docs/README.md](docs/README.md) — topic → page map
README.md # docs index + drift checklist
architecture.md # module load order, paths, hub API
cli.md # full CLI command reference
tui.md # interactive TUI menus, shortcuts, screenshots
third-party.md # licenses, purchase gates, nest Gamescope
release_runbook.md # version bump + GitHub release
assets/
launchlayer.svg
tui-main-menu.png
tui-game-picker.png
tui-quick-toggles.png

Runtime state

Under $XDG_STATE_HOME/launchlayer (default ~/.local/state/launchlayer/):

FilePurpose
launch.logStructured launch history (rotated; default max 5000 lines)
paused-vram-unitssystemd units stopped for VRAM
paused-vram-pidsPIDs tracked for VRAM hog pause
vram-hog-refcountNested launch refcount
active-launch.pidCurrent game PID
launch-watchdog.pidCleanup subprocess when LAUNCH_WATCHDOG=1
x3d-cpus / x3d-cpus.metaCached V-Cache CPU mask
shader-cache-check-<AppID>.stampRate-limit shader cache preflight
compatdata-check-<AppID>.stampRate-limit compatdata preflight

Optional dependencies

The script degrades gracefully when tools are missing. Run --doctor or --detect-environment for distro-aware install hints.

ToolUsed for
fzfInteractive TUI
gamemoderunGameMode CPU governor
game-performanceCPU perf profile wrapper
gamescopeCompositor upscaling, VRR
mangohudOverlay
vkbasaltVulkan post-process layer via VKBASALT=1
lsfg-vkFrame gen layer via LSFG_VK=1 (needs owned Lossless Scaling — third-party)
obs-vkcapture / obs-gamecaptureCapture wrap via OBS_VKCAPTURE=1
latencyflexLatencyFleX layer via LATENCYFLEX=1
conty32-bit container wrap via CONTY=1
protontricks / winetricksPrefix verbs / winecfg / registry
replay-sorceryChain-wrapped replay via REPLAY_CAPTURE=1
gpu-screen-recorderPreferred external recorder (REPLAY_TOOL) — not chain-wrapped
wine-discord-ipc-bridgeDiscord IPC via DISCORD_IPC=1
dlss-swapperNGX + latest DLSS presets via DLSS_SWAPPER=1 (CachyOS wiki; package cachyos-settings)
dlss-updaterOptional GUI for offline DLL replace (detected/tipped only — no launch CLI)
tasksetPin to X3D V-Cache CCD
nvidia-smi, nvidia-settingsVRAM/power checks
ethtoolNETWORK_TUNE (ring buffers, EEE)
iw / iwconfigWi‑Fi power-save disable under NETWORK_TUNE
teeDISK_TUNE scheduler writes (with passwordless sudo)
pw-metadataPIPEWIRE_LOW_LATENCY
curlCommunity hub HTTP client
jq or python3Hub apply / ProtonDB suggest
systemd user sessionVRAM_HOGS unit pause/resume

Anticheat and native detection

  • launch.d/anticheat-appids.txt — Known EAC/BattlEye AppIDs; guardrails warn on risky settings (DEBUG=1, DXVK_ASYNC)
  • launch.d/native-appids.txt — Known native Linux builds; skips Proton env unless FORCE_PROTON=1
  • Heuristics in lib/steam/detect.sh also inspect install manifests; --scan-anticheat and --scan-detections help keep lists accurate

Testing

make test# bats integration + unit (parallel when GNU parallel is installed)
make test-unit # bats test/unit only
make test-integration # bats test/integration only
make check # shellcheck + check-hub-git + bats (shell gate)
make check-hub-git # fail if hub secrets are staged
make check-dependency-pins # enforce exact deps, lock integrity, Action SHAs
make test-hub # hub unit + convex tests (via scripts/hub-pm.sh)
make lint-hub # hub ESLint + tsc
make check-hub # lint-hub + test-hub
make test-all # shell bats + test-hub
make check-all # check + check-hub (full local gate matching CI)
make bump-version VERSION=X.Y.Z
make check-version # LAUNCHLAYER_VERSION consistency gate

Releases: follow docs/release_runbook.md. Notes live in CHANGELOG.md.

Or directly:

bats --jobs "$(nproc)" --no-parallelize-within-files test/unit test/integration
shellcheck -x -P lib -a --severity=warning launchlayer test/helpers.bash scripts/*.sh
bash scripts/hub-pm.sh install # once, from repo root (or cd hub && pnpm install)
bash scripts/hub-pm.sh lint
bash scripts/hub-pm.sh test

Hub dependency audits run weekly via .github/workflows/hub-audit.yml (or workflow_dispatch), not on every PR.


FAQ

Do I need a different launch string per game? No. Use the same "/path/to/launchlayer" %command% on every title. Per-game tuning lives in GAMES_DIR/<AppID>.env.

Can I keep gamemoderun or mangohud in Steam’s launch options? Remove external wrappers from Steam and enable GAMEMODE=1, MANGOHUD=1, GAMESCOPE=1, etc. in config instead — otherwise you get double-wrapped launches.

Steam Overlay / Steam Input broken under nested Gamescope? LaunchLayer clears LD_PRELOAD around nested desktop Gamescope by default (GAMESCOPE_NESTED_FIX). See docs/third-party.md § Nested Gamescope and docs/cli.md § Gamescope nest.

Special K, ReShade, lsfg-vk, Conty — where are licenses and keys?docs/third-party.md (licenses / purchase gates) · docs/cli.md (keys) · docs/tui.md § Advanced config (Inject & Wine).

Does this work with Flatpak Steam? Yes. Installs under $HOME usually work as-is; paths outside $HOME need a Flatpak filesystem override. Run ./launchlayer --detect-environment and see Flatpak Steam.

Do I need the community hub? No. Local launches, the TUI, backup/restore, and doctor all work without it. The hub is optional for sharing configs with similar machines. Hub strip rules: docs/architecture.md · docs/cli.md § Community hub.

Where do per-game configs live? In ~/.local/share/launchlayer/games/<AppID>.env by default — not in the git repo. See Configuration.

Commercial use? This project is CC BY-NC-SA 4.0. Commercial use requires separate permission from bolens. Third-party tool licenses: docs/third-party.md.


Contributing

Issues and pull requests are welcome at github.com/bolens/launch-layer.

make check # shellcheck + hub secret guard + bats
make check-all # check + hub lint/test (needs hub/node_modules)
make test# bats only

Star history

Star History Chart

License

CC BY-NC-SA 4.0 — non-commercial use with attribution; derivatives must use the same license.

Third-party tools keep their own licenses. See docs/third-party.md for upstream links, SPDX notes, purchase gates (e.g. Lossless Scaling for lsfg-vk), and redistrib rules. LaunchLayer never vendors proprietary/GPL binaries into this repository.

You may use, modify, and share this project for personal or non-commercial purposes if you credit bolens, link to github.com/bolens/launch-layer, and release any derivatives under the same terms. Commercial use requires separate permission.

About

Layered launch orchestration for Steam games on Linux with preflight checks, per-game environments, and composable wrapper chains.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages