---
name: linux-gaming
description: "Run Windows games on Linux (Arch/CachyOS) — Lutris, proton-cachyos, DXVK, VKD3D, Gamemode, MangoHud, NVIDIA/AMD config, game-specific references."
tags: [gaming, lutris, proton, wine, dxvk, vkd3d, cachyos, nvidia, gamemode]
version: 1.3.0
author: Hermes Agent
---

# Linux Gaming (Arch/CachyOS)

Class-level guide for running Windows games on Linux using Lutris + Proton/Wine runners.

## Architecture

```
~/Games/
  <GameName>/          ← organized per-game dir (game files here)
  ...                  ← Lutris manages prefixes separately
```

Lutris stores runners at `~/.local/share/lutris/runners/wine/` and game configs in `~/.config/lutris/` + SQLite DB at `~/.local/share/lutris/pga.db`.

## Prerequisites

```bash
# Essential packages
sudo pacman -S lutris gamemode lib32-gamemode mangohud lib32-mangohud

# Optional but recommended
sudo pacman -S gamescope goverlay protonup-qt
```

## Runner Selection

| Runner | Source | Best for | Notes |
|--------|--------|----------|-------|
| `proton-cachyos` | CachyOS GitHub/ Lutris list | CachyOS users, DX12 games | Latest: v11.0 (May 2026). Very frequent updates. |
| `GE-Proton` (proton-ge-custom) | GloriousEggroll GitHub | Steam + non-Steam, best compatibility patches | Latest: GE-Proton10-34 (Mar 2026). |
| `wine-ge-custom` (Lutris Wine-GE) | GloriousEggroll | Lutris non-Steam games | Older (GE-Proton8-26, Feb 2024). Prefer proton-cachyos. |
| `wine-cachyos-opt` | CachyOS repo (`/opt/wine-cachyos`) | Manual Wine prefix setups | Gets FS/ESYNC, staging patches. |

**Recommendation for CachyOS:** Use `proton-cachyos` via Lutris — it's the most up-to-date (days old) and optimized for the distro.

## gbe_fork (Modern Maintained Goldberg Fork)

When the original Goldberg emulator (v0.2.5, 2019) doesn't work with newer games (2024+), use **gbe_fork** by Detanup01 — an actively maintained fork updated as of April 2026.

**Source:** https://github.com/Detanup01/gbe_fork/releases

**Key differences from old Goldberg:**
- **Regular** `steam_api64.dll` (~11MB) — standalone, drop-in replacement. Just replace the DLL.
- **Experimental** `steam_api64.dll` (~22MB) — includes `steamclient64.dll` (111KB) for injector-based approach + steamclient_loader_x64.exe
- Both use the same `steam_settings/` directory format as Goldberg
- Built as four release packages: `emu-win-release.7z`, `emu-win-debug.7z`, `emu-linux-release.tar.bz2`, `emu-linux-debug.tar.bz2`

### Recommended Installation

```bash
# Download Windows release (DLLs for use with Proton/Wine)
curl -sL "https://github.com/Detanup01/gbe_fork/releases/download/release-2026_04_25/emu-win-release.7z" -o /tmp/gbe_fork.7z
7z x /tmp/gbe_fork.7z -o/tmp/gbe_fork

# Install standalone (regular) steam_api64.dll
cd /path/to/game/bin64
mv steam_api64.dll steam_api64.dll.orig
cp /tmp/gbe_fork/release/regular/x64/steam_api64.dll .
echo 3321460 > steam_settings/steam_appid.txt  # use correct AppID

# Or use experimental (with steamclient support)
cp /tmp/gbe_fork/release/experimental/x64/steam_api64.dll .
cp /tmp/gbe_fork/release/experimental/x64/steamclient64.dll .
```

### When to Use Each Variant

| Variant | Use Case |
|---------|----------|
| Regular `steam_api64.dll` (~11MB) | Most games. Standalone, no injector needed. |
| Experimental `steam_api64.dll` (~22MB) | Games that need `steamclient64.dll` injected alongside. Works with `steamclient_loader_x64.exe`. |
| ColdClientLoader + steamclient64 | Games where `ForceInjectSteamClient=1` is needed. Legacy cracks. |

**2025+ games:** Prefer gbe_fork regular `steam_api64.dll` first. If it fails with interface errors, try the experimental variant. If both fail, fall back to ColdClientLoader injection (see Anti-Cheat & Cracked Game Handling section).

## Steam Emulator Reference

See `references/goldberg-emulator.md` for detailed info on Goldberg (original) + gbe_fork (modern maintained fork) including download URLs, file listings, steam_settings config examples, and a scenario-based selection guide.

## Adding a Non-Steam Game to Lutris

### ⚠️ CRITICAL: Lutris Managed DXVK/VKD3D vs Built-in Runner DXVK/VKD3D

When using **proton-cachyos** (or any GE-Proton) as the Wine runner in Lutris, these runners **already include DXVK and VKD3D-Proton built in**. If you also enable Lutris's managed DXVK/VKD3D in the game config, Lutris will try to inject its own runtime DLLs — which conflicts with the runner's built-in ones and causes a `MissingRuntimeComponentError`.

**Rule of thumb:**
- **proton-cachyos** runner → set `dxvk: false`, `vkd3d: false`, `dxvk_nvapi: false` in Lutris config. The runner handles it internally.
- **Plain Wine** (system wine, wine-cachyos-opt) runner → set `dxvk: true`, `vkd3d: true` so Lutris injects its managed runtime.
- **Wine-GE** (old wine-ge-custom) → same as proton-cachyos, has them built in → disable Lutris managed.

The runner's built-in VKD3D is loaded via the `proton` script's environment, not through Lutris's DLL injection — so Lutris's dll_manager can't find the DLLs and raises the error.

### Option A: Lutris CLI (YAML file)

Create a game YAML config and install. See the config for `proton-cachyos` (managed DXVK/VKD3D disabled):

```yaml
name: GAME_NAME
runner: wine
game:
  exe: $GAMEDIR/game.exe
  prefix: $GAMEDIR
  working_dir: $GAMEDIR
  arch: win64
wine:
  version: proton-cachyos-<version>
  dxvk: false          # proton-cachyos has it built-in
  vkd3d: false         # proton-cachyos has it built-in
  dxvk_nvapi: false    # proton-cachyos has it built-in
  esync: true
  fsync: true
system:
  prefix_command: gamemoderun
  env:
    VKD3D_CONFIG: no_upload_hvv,single_queue
    PROTON_ENABLE_NVAPI: "1"
    MANGOHUD: "0"
```

Then install: `lutris -i /path/to/game.yml`

### Option B: GUI (Recommended for first setup)

1. Open Lutris → `+` → **Add locally installed game**
2. Set runner to **Wine**
3. Select **proton-cachyos-<version>** as Wine version
4. Point executable to `~/Games/<GameName>/game.exe`
5. Set working directory to `~/Games/<GameName>/`
6. **Do NOT enable** DXVK, VKD3D, DXVK_NVAPI (proton-cachyos handles these)
7. Enable: **FSYNC**, **ESYNC**
8. Set prefix command: `gamemoderun`
9. Set environment variables per game needs

### Option C: Direct SQLite + JSON Config (headless GUI-less setup)

When Lutris CLI hangs (no display), insert the game into the SQLite DB and create the config JSON directly. See the code in the game-specific references for exact syntax.

## Folder Organization

```
~/Games/
  GameName/
    game.exe          ← main executable
    Data0.bdt         ← game assets
    ...               ← all game files
```

Keep game files in `~/Games/<GameName>/` — one folder per game. Lutris creates the Wine prefix separately.

## Related Skills

- `linux-system-tuning` — OS-level tuning: CPU governor, kernel params, sysctl, zram, bootloader, NVIDIA power management. **Start here before game-specific tuning.** A tuned OS gives more FPS than any single game setting.
- `remote-access-ssh` — SSH session management, aliases, and sudo access patterns for remote boxes.

## Performance Setup

### Gamemode

```bash
sudo pacman -S gamemode lib32-gamemode
systemctl --user enable gamemoded --now
```

Use as prefix command in Lutris: `gamemoderun`

### MangoHud (FPS overlay)

```bash
sudo pacman -S mangohud lib32-mangohud goverlay
```

Toggle in-game with Shift+F12 by default, or Shift_L+F1 if configured via GOverlay.

### NVIDIA-Specific Optimizations

| Setting | Purpose |
|---------|---------|
| `PROTON_ENABLE_NVAPI=1` | Enables NVIDIA DLSS/Reflex via DXVK-NVAPI |
| `VKD3D_CONFIG=no_upload_hvv,single_queue` | Reduces DX12 stutter on NVIDIA |
| `WINE_FULLSCREEN_FSR=1` | FSR upscaling in fullscreen |

### Gamescope (micro-compositor)

```bash
sudo pacman -S gamescope
# Launch via: gamescope -W 2560 -H 1440 -- gamemoderun lutris ...
```

## Fish Shell on Remote Hosts

When SSHing into a machine that uses **fish shell** (like CachyOS with default fish), be aware of syntax differences:

### `$?` vs `$status`

fish uses `$status`, NOT `$?`:
```bash
# ❌ Bash pattern (breaks on fish)
echo "exit: $?"

# ✅ fish-compatible
echo "exit: $status"
```

### Heredocs (`<< EOF`)

fish does NOT support bash-style heredocs. Use echo with single-quoted strings or write to a file via Python. Best practice: write the script locally and scp it:

```bash
write_file content="..." path="/tmp/script.sh"
sshpass -p 'pw' scp /tmp/script.sh user@host:/tmp/
sshpass -p 'pw' ssh user@host 'bash /tmp/script.sh'
```

### Globbing (wildcards)

fish with `NO_MATCH` error crashes on non-matching globs. Check existence before globbing.

### Background (`&`)

fish handles `&` differently. Use `terminal()` with `background=true` for long-running processes.

## Anti-Cheat & Cracked Game Handling

### Anti-Cheat Types Under Proton

| Anti-Cheat | Linux/Proton Status | Notes |
|------------|---------------------|-------|
| **EasyAntiCheat (EAC)** | ✅ Works | EAC has native Linux support. Repacks disable it. |
| **BattlEye** | ✅ Works | BattlEye supports Proton. Repacks bypass it. |
| **XIGNCODE3** | ✅ Works | Runs inside Proton sandbox. **No hypervisor bypass needed on Linux.** |
| **nProtect GameGuard** | ⚠️ Variable | Some versions work, some don't. |
| **Denuvo Anti-Cheat** | ✅ Works | DRM handled by crack; AC works under Proton. |

### Why Hypervisor Bypasses Aren't Needed on Linux

On Windows, XIGNCODE3 and similar kernel anti-cheats check for virtualization (Hyper-V) and refuse to launch when detected.

On Linux with Proton:
- XIGNCODE3 **can't run as a kernel driver** — Linux won't load a Windows .sys driver
- Only user-mode components run inside the Proton/Wine sandbox
- **No Hyper-V exists** on Linux to be detected
- The same cracked copy that needed a bypass on Windows works **without any bypass** on Linux

### Cracked Game Handling (Launcher-Based Cracks)

Many repacks include a `_Crack/` folder with a launcher that injects Steam emulation + crack DLLs:

- **Launcher** → `steamclient_loader_x64.exe`
- **Config** → `ColdClientLoader.ini` (defines Exe, AppId, paths)
- **Steam emu** → `coldclient/steamclient.dll` + `steamclient64.dll`
- **Crack** → `DenuvOwO.dll` (Denuvo bypass)

**🚨 Set Lutris executable to the LAUNCHER, not game.exe.** Running the game .exe directly gives `"unable to initialize SteamAPI"`.

### ⚠ Critical: Don't Replace the Crack's steam_api64.dll

DenuvOwO cracks ship their own `steam_api64.dll` (~320KB) that is **not** a simple Steam API redirector — it's a full crack wrapper with 1089 Steam API exports (every function the game could call). Replacing it with gbe_fork's standalone DLL will:

1. **Lose crack functionality** — The DenuvOwO bypass hooks are in the crack wrapper, not in gbe_fork
2. **Produce interface errors** — gbe_fork will log `"unable to create interface isteamuser"` because the game's compiled exports expect the crack wrapper's interface layer, not gbe_fork's
3. **Game still fails** — The emulator runs but the DenuvO protection isn't bypassed

**Rule:** Always keep the crack's original `steam_api64.dll`. gbe_fork replaces legitimate Steam APIs on clean copies — for cracks, the crack's own DLL is the active component.

### 🔧 DenuvOwO.ini Settings for Linux

When a DenuvOwO crack ships with `DenuvOwO.ini`, apply these mandatory changes for Linux:

```bash
# Disable hypervisor bypass (won't work on Linux - no kernel driver loading)
sed -i 's/AutoLoadHV=true/AutoLoadHV=false/' DenuvOwO.ini

# Disable message box prompts (ugly in Wine)
sed -i 's/GoRevertMsg=true/GoRevertMsg=false/' DenuvOwO.ini
```

**Why `AutoLoadHV=false`?** On Windows, DenuvOwO cracks inject a kernel driver to hide from XIGNCODE3's VM detection. On Linux:
- Windows kernel `.sys` drivers **cannot load** — Linux won't run them
- XIGNCODE3's kernel component doesn't exist under Proton/Wine
- Only user-mode runs inside the sandbox
- **No bypass needed** — the crack's hypervisor code would just fail harmlessly, but better to disable it to avoid errors/log noise

### 🔬 Crack Setup Diagnosis

If the launcher doesn't work:

| Check | Fix |
|-------|-----|
| `steamclient_loader_x64.exe` is the Lutris executable? | Set exe to launcher, not game.exe |
| `ForceInjectSteamClient=1` in ColdClientLoader.ini? | `sed -i 's/ForceInjectSteamClient=0/ForceInjectSteamClient=1/'` |
| coldclient/ paths use forward slashes? | `sed -i 's|\\\\|/|g' ColdClientLoader.ini` |
| `steam_appid.txt` exists in game dir? | `echo <APPID> > steam_appid.txt` |
| Original `steam_api64.dll` intact (~320-400KB)? | Should NOT be gbe_fork (11MB) or coldclient (18MB) |
| `DenuvOwO.ini` has `AutoLoadHV=false`? | Hypervisor bypass not needed on Linux |
| `lib32-gamemode` installed? | `sudo pacman -S lib32-gamemode` else LD_PRELOAD conflicts with injector |

#### 🛠️ ColdClientLoader Configuration Fixes

**1. Force Inject Steam Client:** `ForceInjectSteamClient=1` (default 0 waits forever)

**2. Fix path separators:** Windows backslashes → forward slashes for Wine/Proton.

**3. Create `steam_appid.txt`:** `echo "<AppId>" > steam_appid.txt`

**4. Do NOT replace `steam_api64.dll` with `steamclient64.dll`:** They export different functions. Keep the original.

**5. Gamemode LD_PRELOAD can interfere:** Disable prefix_command to isolate. Install `lib32-gamemode`.

**6. `steam_settings/` location:** Must be accessible from the loader directory. Symlink if needed:
```bash
ln -sf coldclient/steam_settings steam_settings
```

**7. Alternative: Goldberg/gbe_fork standalone (clean copies only):** For DRM-free Steam copies or non-DenuvOwO cracks, replacing `steam_api64.dll` with gbe_fork's regular DLL and running game.exe directly works. **Do NOT do this for DenuvOwO cracks** — their 320KB DLL contains the bypass hooks.

### Steam Appmanifest Fallback

If injectors fail, create a fake Steam appmanifest to make Steam think the game is legit:

```bash
# Create app manifest
cat > ~/.local/share/Steam/steamapps/appmanifest_<APPID>.acf << 'HERMES_EOF'
"AppState"
{
    "appid"     "<APPID>"
    "Universe"  "1"
    "installdir" "Game Name"
    "StateFlags" "4"
    "name"      "GAME NAME"
}
HERMES_EOF

# Symlink game files
ln -sfT "/path/to/actual/game" "/path/to/.local/share/Steam/steamapps/common/Game Name"
```

Then restart Steam and launch from library.

### Common Anti-Cheat Pitfalls

- **Don't launch `start_protected_game.exe`** — launch the main `game.exe` directly. EAC/BattlEye stubs are dummies in repacks.
- **XIGNCODE3 games** — run fine under Proton. No bypass needed.

## Common Pitfalls

1. **proton-cachyos has DXVK/VKD3D built-in**: Set `dxvk: false`, `vkd3d: false`, `dxvk_nvapi: false` to avoid `MissingRuntimeComponentError`.
2. **Lutris CLI hangs without display**: Use SQLite + direct config for headless setup.
3. **Separate prefixes per game**: Avoid sharing Wine prefixes between different games.
4. **UMU overrides Lutris wine version with GE-Proton**: Set `PROTONPATH=proton-cachyos-11.0` env var to force a specific Proton.
5. **`STEAM_COMPAT_DATA_PATH` needs numeric AppId**: protonfixes extracts AppId from path via regex `\d+`. A path like `/home/.../crimson-desert` causes `IndexError`. Use `/home/.../3321460` instead.

## Game-Specific References

See `references/` directory for per-game launch configs, optimal settings, and known issues.

- [Elden Ring](references/elden-ring.md)
- [Crimson Desert](references/crimson-desert.md)
- [Goldberg / gbe_fork Emulator](references/goldberg-emulator.md)

# Linux System Tuning

Class-level guide for auditing and optimizing a Linux system — from "what have I got?" discovery through progressive performance tuning layers. Designed for gaming/coding/hacking workloads.

## Philosophy

Tune in layers. Never jump to the flashy setting without understanding what you're starting with:

1. **AUDIT** — collect a full system baseline
2. **FUNDAMENTALS** — kernel params, CPU governor, I/O scheduler, swappiness
3. **MEMORY** — zram / swap tuning
4. **GPU** — NVIDIA driver params, power limits, persistence
5. **BOOT** — bootloader params for security / performance
6. **WORKLOAD** — Gamemode, MangoHud, per-game / per-tool tuning
7. **VERIFY** — confirm every change landed, benchmark before/after

## Prerequisite: Sudo Access

**This must be checked first, before promising any deep changes.** Running `ssh host 'sudo -n true'` tells you if passwordless sudo works. If it doesn't:

```bash
# Check current state
ssh user@host 'sudo -n true 2>/dev/null && echo NOPASSWD_OK || echo NEEDS_PASSWORD'
```

If password-protected, ask the user to run:
```bash
echo 'user ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/99-user
```

Do NOT try to pipe passwords to `sudo -S` via SSH — this is blocked for security reasons. Options:
- Ask the user to set up NOPASSWD as above
- Use a wrapper script (scp + run via tool's session management, if available)
- Scope down to only user-level changes (config files, env vars, fish config)

## Layer 1: System Audit

Run these commands in dependency order — each layer builds on the previous one's output:

### Layer 1a: Identity & Uptime
```bash
uname -a
cat /etc/os-release
uptime
whoami; id
```

### Layer 1b: Hardware Inventory
```bash
lscpu | grep -E "(Model name|CPU\(s\)|Thread|Core|Arch|L1|L2|L3)"
lspci | grep -iE "(vga|3d|audio|nvme|sata)"
lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL
free -h
zramctl
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
```

### Key: amd-pstate-epp Driver

If `scaling_driver` is `amd-pstate-epp` (Ryzen Zen 2+), the governor and EPP interact:

```bash
# Available EPP values
cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences
# Current EPP (often 'power' or 'balance_power' by default)
cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
```

See Layer 2 for tuning.

### Layer 1c: GPU State
```bash
nvidia-smi --query-gpu=name,temperature.gpu,utilization.gpu,memory.total,memory.used,memory.free --format=csv,noheader
nvidia-settings -q all 2>/dev/null | head -20
```

### Layer 1d: Storage & Filesystem
```bash
df -h /
btrfs filesystem df / 2>/dev/null || df -T /
findmnt -T / 2>/dev/null
sudo btrfs subvolume list / 2>/dev/null
```

### Layer 1e: Packages & Services
```bash
pacman -Q | wc -l           # Total packages (Arch)
dpkg -l | wc -l             # Total packages (Debian)
pacman -Qm | wc -l          # AUR count (Arch)
systemctl list-units --type=service --state=running --no-pager
systemctl --user list-units --type=service --state=running --no-pager
```

### Layer 1f: Networking
```bash
ip addr show | grep -E "inet " | grep -v 127.0.0.1
ip route show default
ss -tulpn | head -20
```

### Layer 1g: Boot & Kernel
```bash
cat /proc/cmdline
ls /boot/ | head -30
cat /etc/default/grub 2>/dev/null || ls /boot/loader/entries/ 2>/dev/null
```

### Layer 1h: Recent Activity
```bash
tail -50 /var/log/pacman.log 2>/dev/null | grep -E "\[ALPM\] (installed|upgraded)" | tail -15
last -5 2>/dev/null
journalctl --list-boots 2>/dev/null | head -10
```

### Layer 1i: Shell & User Config
```bash
echo $SHELL
head -50 ~/.config/fish/config.fish 2>/dev/null || head -50 ~/.bashrc 2>/dev/null
ls ~/.*_aliases ~/.config/fish/ ~/.config/starship* 2>/dev/null
```

### Audit Report Format

Compile findings into a structured report:
- **Hardware** — table: CPU | GPU | RAM | Storage | Swap
- **System State** — uptime, load, GPU temp/util, CPU governor
- **Packages** — totals + relevant versions (NVIDIA, Wine, Proton, etc.)
- **Filesystem** — usage %, Btrfs layout, mount points
- **Issues Found** — bullet list with severity indicators
- **What's Good** — positive findings

## Layer 2: CPU Governor + EPP Tuning

The CPU governor controls frequency scaling. For gaming/coding, `performance` or `schedutil` beats `powersave`.

### Check current driver & governors
```bash
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
```

### amd-pstate-epp Driver (Ryzen Zen 2+)

On Ryzen systems, the `amd-pstate-epp` driver is the default. It has a secondary control called **EPP** (Energy Performance Preference) that overrides the governor on some hardware. Even with `performance` governor, if EPP is `power`, the CPU stays throttled.

```bash
# Check current EPP
cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
# Available values: default performance balance_performance balance_power power

# Set both governor AND EPP to performance
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference
```

**Older amd-pstate (non-EPP)** or **acpi-cpufreq** drivers only use `scaling_governor` — no EPP file exists.

### Intel (intel_pstate / intel_cpufreq)

On Intel, `intel_pstate` has similar `energy_performance_preference` but uses `hwp_dynamic_boost`. Check:
```bash
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver  # should be 'intel_pstate'
# Max/min perf
cat /sys/devices/system/cpu/intel_pstate/max_perf_pct
cat /sys/devices/system/cpu/intel_pstate/min_perf_pct
```

### Set via systemd service (persistent)

Use the **correct driver name in the description** — the service should set both governor AND EPP if amd-pstate-epp:

```bash
sudo tee /etc/systemd/system/cpufreq.service << 'HERMES'
[Unit]
Description=Set CPU governor + EPP to performance
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c '\
  for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "$i"; done; \
  for i in /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference; do echo performance > "$i"; done'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
HERMES
sudo systemctl daemon-reload
sudo systemctl enable --now cpufreq.service
```

### One-shot (immediate, non-persistent)
```bash
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
```

### Governor Selection

| Governor | When to use |
|----------|-------------|
| `performance` | Gaming rigs, coding workstations, low-latency audio. Keeps max clock always. Won't hurt desktop power bills meaningfully. |
| `schedutil` | Adaptive — best for laptops, mixed workloads, power-sensitive setups. Not all CPUs/drivers expose this governor. |

**For gaming desktops:** `performance` is simple and effective. `schedutil` is better-balanced but may not be available (check `scaling_available_governors`).

## Layer 3: Kernel Boot Parameters

Add gaming/performance flags to bootloader config.

### For Arch/CachyOS (Limine)

Edit `/boot/limine.conf`. **Limine uses `cmdline:` (lowercase, colon), not `CMDLINE=`.**

**⚠️ CachyOS + snapper generates many cmdline entries.** Every snapshot boot entry has its own `cmdline:`. A single `sed` won't cut it — use a Python regex to replace ALL occurrences:

```python
import re, subprocess
# Read current config
r = subprocess.run(["sudo", "cat", "/boot/limine.conf"], capture_output=True, text=True)
content = r.stdout

# Append gaming params to every cmdline line
gaming_params = "mitigations=off split_lock_detect=off preempt=full"

def add_params(match):
    line = match.group(0)
    if "mitigations=off" in line:
        return line  # already updated
    return line.rstrip() + " " + gaming_params

new_content = re.sub(r'cmdline:.*', add_params, content)

# Write back
with open("/tmp/limine_new.conf", "w") as f:
    f.write(new_content)
subprocess.run(["sudo", "cp", "/tmp/limine_new.conf", "/boot/limine.conf"])
subprocess.run(["sudo", "chmod", "644", "/boot/limine.conf"])
```

This updates the boot cmdlines for the current running kernel AND every snapper snapshot entry, so you can still boot snapshots with the same performance params.

### For GRUB
Edit `/etc/default/grub`:
```diff
- GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
+ GRUB_CMDLINE_LINUX_DEFAULT="quiet splash mitigations=off split_lock_detect=off"
```
Then: `sudo grub-mkconfig -o /boot/grub/grub.cfg`

### For systemd-boot
Edit `/boot/loader/entries/*.conf` and add params to the `options` line.

### Key Parameters

| Param | Effect | Who should use |
|-------|--------|----------------|
| `mitigations=off` | Disables CPU speculation mitigations (Spectre/Meltdown) | Gaming rigs, low-risk isolated machines |
| `split_lock_detect=off` | Disables split-lock detection overhead | Intel-only, some games trigger split locks |
| `processor.max_cstate=1` | Prevents deep C-states (wake latency) | Gaming/low-latency audio |
| `intel_pstate=passive` | Uses acpi-cpufreq governor instead of Intel's | Intel, for schedutil support |
| `nowatchdog` | Disables NMI watchdog (saves 1-2% CPU) | All |
| `nmi_watchdog=0` | Alternative watchdog disable | All |

### Verify after reboot
```bash
cat /proc/cmdline
```

## Layer 4: sysctl Tuning

### VM / Memory
```bash
sudo tee -a /etc/sysctl.d/99-performance.conf << 'HERMES'
# Reduce swap tendency — keep things in RAM
vm.swappiness=10
# Increase dirty page threshold (write coalescing)
vm.dirty_ratio=40
vm.dirty_background_ratio=10
# Reduce VMA cache pressure (good for gaming with large assets)
vm.vfs_cache_pressure=75
HERMES
```

### Network (gaming latency)
```bash
sudo tee -a /etc/sysctl.d/99-performance.conf << 'HERMES'
# TCP BBR congestion control
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
# Increase buffer sizes
net.core.rmem_max=134217728
net.core.wmem_max=134217728
# Enable TCP fastopen
net.ipv4.tcp_fastopen=3
HERMES
```

### Kernel / Scheduler
```bash
sudo tee -a /etc/sysctl.d/99-performance.conf << 'HERMES'
# Increase kernel task max (for mmap-heavy games)
vm.max_map_count=2147483642
# PCI express ASPM off (reduces latency)
pcie_aspm=off
# Kernel same-page merging (KSM) — disable for gaming
kernel.numa_balancing=0
HERMES
```

Apply: `sudo sysctl --system`

## Layer 5: zram / Swap Optimization

zram compresses pages in RAM before swapping — good for machines with NVMe swap (avoids flash wear). The default zram setup from CachyOS/Arch usually uses LZ4 or LZO-RLE.

### Tuning
```bash
# Check current
zramctl

# Change compression algorithm (zstd better ratio, slightly slower CPU)
echo zstd | sudo tee /sys/block/zram0/comp_algorithm

# Make persistent (write a udev rule or use systemd-zram-generator)
# For systemd-zram-generator, edit /etc/systemd/zram-generator.conf:
# [zram0]
# zram-size = ram / 2
# compression-algorithm = zstd
```

### Swappiness Tuning
On zram, `vm.swappiness=10` is usually fine (see Layer 4). If the system is swapping too aggressively on zram, raise to 20-30 — zram compression is cheaper than disk I/O.

## Layer 6: Gamemode Configuration

Gamemode (`gamemoderun`) is the easiest way to get one-shot performance boosts per application.

### Config (~/.config/gamemode.ini)
```ini
[general]
renice=10
desiredgov=schedutil
softrealtime=auto
reaper_freq=5

[gpu]
apply_gpu_optimisations=accept-responsibility
gpu_device=0
nvidia_powermizer_mode=1
nv_custom_memclock_offset=0

[cpu]
# Pin specific games to performance cores (use hwloc/lscpu to find core ranges)
# pin_cores=0-7

[supervisor]
# Allow Gamemode to control governor on demand
superisor_keys=yes
```

### Verification
```bash
gamemoded -s
gamemoderun glxspheres64  # test run
```

## Layer 7: NVIDIA GPU Tuning

### Power Management
```bash
# Force maximum performance (P0 state)
sudo nvidia-smi -pm 1

# Set power limit (RTX 3070 default 240W; can be lowered to ~180W for efficiency)
sudo nvidia-smi -pl 200

# Lock clock speed (optional, specific game tuning)
sudo nvidia-smi -ac 5001,1590  # mem_clock, gpu_clock
```

### Environment Variables (per-game in Lutris)
```bash
PROTON_ENABLE_NVAPI=1      # DLSS/Reflex
VKD3D_CONFIG=no_upload_hvv,single_queue  # DX12 stutter reduction
WINE_FULLSCREEN_FSR=1      # FSR upscaling
MANGOHUD=1                 # FPS overlay
```

### Driver Persistence
```bash
sudo systemctl enable nvidia-persistenced --now
```

## Layer 8: Verification Checklist

After any change, verify it applied:

```bash
# CPU governor
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u

# Kernel params
cat /proc/cmdline

# sysctl
sysctl vm.swappiness
sysctl net.core.default_qdisc

# GPU
nvidia-smi -pm 1 | head -5
nvidia-smi --query-gpu=power.draw,clocks.current.graphics,clocks.current.memory --format=csv,noheader

# zram
zramctl

# Gamemode
gamemoded -s

# Memory
free -h
```

## Fish Shell Considerations

When running these commands on remote hosts using fish:
- **No `$?`** — use `$status` instead
- **No heredocs** — bash heredocs (`cat > file << 'EOF'`) fail on remote fish. Use one of:
  - **Base64 + Python** (most reliable): encode content locally, write via remote Python
    ```bash
    CONTENT_B64=$(echo "your file content here" | base64)
    ssh user@host "python3 -c 'import base64;f=open(\"/path/to/file\",\"w\");f.write(base64.b64decode(\"$CONTENT_B64\").decode());f.close()'"
    ```
  - **SCP a script file** and execute it with `bash`
  - **Python one-liner with single quotes** — escape any single quotes in the content
- **Wildcard errors** — fish kills the pipeline on non-matching globs; check existence first: `test -d /sys/class/whatever; and echo ...`
- **Background processes** — use tool's `background=true` instead of `&`
- **Greeting output breaks SCP** — If fish config prints a welcome message, `scp` chokes. Wrap greetings in `if status is-interactive; ...; end`

## Common Pitfalls

1. **Sudo not NOPASSWD** — Check before promising deep changes. Ask user for NOPASSWD setup.
2. **Btrfs commands need root** — `btrfs subvolume list` and `btrfs filesystem usage` need sudo.
3. **Limine config format** — Limine uses `CMDLINE=...` with `=`, not `/etc/default/grub` format.
4. **Fish heredocs crash** — Use `python3 << 'EOF'` or scp a script, never bash heredocs over SSH to fish shell.
5. **Governor doesn't persist across reboot** — Must use udev rule or systemd service, not just `echo` to sysfs.
6. **Systemd-boot vs GRUB vs Limine** — Always check which bootloader is installed before editing.
7. **CPU governor module not loaded** — Some CPUs need `acpi-cpufreq` or `intel_pstate` kernel module. Check `lsmod | grep cpufreq`.
8. **PAM lockout on repeated wrong sudo passwords** — If you try sudo with a wrong password 3+ times, `pam_faillock` locks the account. Symptom: known-good password gets "Sorry, try again" every time. The lock clears on reboot, or you can `sudo faillock --user username --reset` from another privileged account. **Always check `sudo -n true` before promising deep changes.**
9. **amd-pstate-epp governors limited** — Some systems only expose `performance` and `powersave`. No `schedutil` — this is hardware/firmware driver behavior, not a config error. Use `performance` with EPP=performance instead.
10. **Fish greeting output breaks SCP** — If the remote shell is fish with a greeting/welcome echo on startup, `scp` will fail with "Received message too long". Either wrap the echo in `if status is-interactive; ...; end` or use `rsync`/base64+Python file writing instead.

## References

- `references/ryzen-3700x-rtx3070-optimization.md` — Specific tuning blueprint from this session

# Remote Access / SSH Management

Orchestrate SSH sessions to local lab machines using aliases, key-based auth, and connection patterns that survive across sessions.

## Prerequisites

- SSH client (`ssh`) installed
- SSH keys configured (typically `~/.ssh/id_ed25519` or `id_rsa`)
- Authorized keys registered on remote hosts

### Setup

```bash
# Check your SSH keys
ls -la ~/.ssh/

# Verify your primary key
cat ~/.ssh/id_ed25519.pub

# Check your config for aliases
cat ~/.ssh/config
```

---

## 1. Using Aliases (Recommended Pattern)

Your `~/.ssh/config` defines named hosts with shortcuts for hostname, user, and key.

### Common Aliases

```bash
# Your CachyOS machine
ssh cachyos          # → raymond@192.168.1.111

# Your Hackintosh homelab
ssh homelab          # → liveuser@192.168.1.13
```

### Config Best Practices

**User name must match remote config** — this is a common pitfall. Your `~/.ssh/config` shows:

```bash
Host cachyos
    HostName 192.168.1.111
    User raymond        # ← Not "ramon"!
    IdentityFile ~/.ssh/id_ed25519
    StrictHostKeyChecking accept-new
```

**Key pattern**: The alias name (`cachyos`) is your shorthand; the `User` field is what the remote expects.

---

## 2. Direct Connection (No Alias)

If you need to connect without an alias or test a new host:

```bash
# Basic connection
ssh -t username@hostname

# With Pseudo-TTY (for interactive shells)
ssh -t -p 22 username@hostname

# With password auth fallback (if keys fail)
ssh -o PreferredAuthentications=publickey,password username@hostname
```

---

## 3. Sudo Access on Remote Machines

**Check before promising deep system changes.** If you need root on the remote, verify NOPASSWD sudo first:

```bash
ssh alias 'sudo -n true 2>/dev/null && echo NOPASSWD_OK || echo NEEDS_PASSWORD'
```

If password-protected, you **cannot** pipe a password to `sudo -S` via SSH — this is a security block in tooling. Two options:

- **Ask the user** to set up NOPASSWD:
  ```bash
  echo 'username ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/99-username
  ```
- **Scope down** to user-level changes only (config files, env vars, shell config, local binaries)

**⚠️ Never try the same wrong password repeatedly.** After 3 failed sudo authentications, `pam_faillock` locks the account — the correct password will be rejected with "Sorry, try again" until a reboot or manual `faillock --reset`.

Do NOT promise deep optimization (CPU governor, kernel params, sysctl, bootloader) until NOPASSWD is confirmed.

### Fish Shell Note
When the remote runs fish, `sudo -n true` still works (it's a command, not a shell feature). But `&&` chaining works differently — use `; and` for the fish-idiomatic equivalent, or run separate SSH calls.

## 3. Troubleshooting Common Issues

### "Permission denied (publickey,password)"

**Step 1: Verify your SSH key is registered on the remote**

```bash
# On remote machine, check authorized_keys
ssh username@hostname "cat ~/.ssh/authorized_keys"

# Or on your local machine, add your key
ssh-copy-id username@hostname
```

### "Received message too long" (SCP fails)

**Step 2: Fish greeting output interferes with SCP**

If the remote shell is **fish** and prints a welcome/greeting message on login, SCP chokes because it parses the stdout for binary protocol:

```
scp: Received message too long 3801784608
scp: Ensure the remote shell produces no output for non-interactive sessions.
```

Fix: On the remote machine, wrap the greeting in `if status is-interactive; ...; end` so it only prints in interactive TTY sessions:

```fish
# Wrong — breaks SCP:
echo "Welcome to CachyOS"

# Right — interactive only:
if status is-interactive
    echo "Welcome to CachyOS"
end
```

Alternatively, use `rsync` instead of `scp`, or write files via base64+Python (see pattern below).

### "Sorry, try again" with known-good password (PAM lockout)

**Step 3: PAM faillock triggered**

After 3+ wrong password attempts (e.g., from automated scripts), `pam_faillock` locks the user account. Even the correct password will be rejected with "Sorry, try again."

Fix:
- **Reboot** the machine — clears the in-memory fail count
- Or from a privileged shell: `sudo faillock --user username --reset`

Prevention: Check NOPASSWD sudo before attempting privileged operations. Don't pipe passwords through automated SSH sessions.

### "Pseudo-terminal will not be allocated because stdin is not a terminal"

**Step 2: Force Pseudo-TTY allocation**

```bash
ssh -t -p 22 username@hostname
```

### "Host key verification failed"

**Step 3: Accept new host key**

```bash
ssh -o StrictHostKeyChecking=accept-new username@hostname
```

### "Connection refused"

**Step 4: Check network connectivity**

```bash
# Ping the host
ping hostname

# Check SSH port
ss -tulpn | grep 22

# If behind Tailscale/VPN, verify tunnel
ping -c 1 192.168.1.111
```

---

## 4. Advanced Patterns

### Remote File Writing to Fish Shell Hosts

Writing files to a remote host running fish is tricky — heredocs (`cat > file << 'EOF'`) don't work. Use one of:

### Option A: SCP (preferred when file fits in /tmp)
```bash
# Write script locally, SCP it to remote
cat > /tmp/fix.py << 'PYEOF'
#!/usr/bin/env python3
# ... fix logic ...
PYEOF
scp /tmp/fix.py user@host:/tmp/
ssh user@host "python3 /tmp/fix.py"
```

### Option B: Pipe via stdin (simplest for one-shot scripts)
```bash
# cat script to local python, pipe stdin through SSH
# This works because fish doesn't interpret the pipe content
cat /tmp/fix.py | ssh user@host "python3"
```

### Option C: base64 + Python (works everywhere, no temp files)

```python
import base64, subprocess

host = "user@hostname"
content = """your multi-line
file content here"""

encoded = base64.b64encode(content.encode()).decode()
cmd = f'python3 -c \'import base64;f=open("/path/to/remote/file","w");f.write(base64.b64decode("{encoded}").decode());f.close()\''

r = subprocess.run(["ssh", host, cmd], capture_output=True, text=True, timeout=15)
print("OK" if r.returncode == 0 else r.stderr)

# For scripts that need +x, chain a chmod:
subprocess.run(["ssh", host, f"chmod +x /path/to/remote/file"], timeout=10)
```

This works regardless of what shell the remote runs, because Python interprets the file content from the decoded string — no shell interpolation issues.

**Alternative: SCP with fish greeting fix**
If fish prints a greeting, either disable it (wrap in `if status is-interactive`) or use `rsync`:
```bash
rsync -avz local_file user@host:/path/
```

### Port Forwarding

```bash
# Local port forwarding (remote → local)
ssh -L 8080:localhost:8080 cachyos

# Remote port forwarding (local → remote)
ssh -R 8080:localhost:8080 cachyos

# Dynamic SOCKS proxy
ssh -D 1080 cachyos
```

### X11 Forwarding

```bash
ssh -X username@hostname    # X11 forwarding
ssh -Y username@hostname    # Trusted X11 forwarding
```

### Batch Operations

```bash
# Connect without password prompt (key-based)
ssh -o PreferredAuthentications=publickey username@hostname

# Connect with password fallback
ssh -o PreferredAuthentications=publickey,password username@hostname
```

---

## 5. Quick Reference

| Action | Command |
|--------|---------|
| Basic SSH (alias) | `ssh cachyos` |
| Basic SSH (direct) | `ssh -t -p 22 raymond@192.168.1.111` |
| Force Pseudo-TTY | `ssh -t -p 22 ...` |
| Accept new host key | `ssh -o StrictHostKeyChecking=accept-new ...` |
| Port forward (local→remote) | `ssh -L 8080:host:port alias` |
| Port forward (remote→local) | `ssh -R 8080:host:port alias` |
| X11 forwarding | `ssh -X alias` |
| Check remote authorized_keys | `ssh alias "cat ~/.ssh/authorized_keys"` |

---

## 6. Session Persistence

Your SSH sessions persist across Hermes turns. To verify you're connected:

```bash
whoami
pwd
uname -a
```

---

## 7. Remote Project Discovery & Transfer

When the user asks to look at or pull a project from a remote machine, use the discovery workflow: search common project dirs, list source files, inspect structure, then `tar czf -` pipe to local extraction. See `references/remote-project-discovery.md` for the full pattern.

## 8. Related Resources

- `~/.ssh/config` — Your SSH alias definitions
- `~/.ssh/authorized_keys` — Remote authorized keys
- `~/.ssh/id_ed25519` — Your primary SSH key
- `references/remote-project-discovery.md` — Remote project discovery and transfer workflow

---

## Quick Start

1. **Use your alias**: `ssh cachyos`
2. **Verify connection**: `whoami`, `pwd`
3. **Run your workload**

---

## Related Skills

- `github-auth` — SSH key setup for GitHub
- `debugging-hermes-tui-commands` — Debugging TUI commands (may involve remote access)