Game streaming from CachyOS to anywhere with an AMD GPU
I wanted to stream games from my CachyOS desktop to other rooms in the house. NVIDIA-heavy guides everywhere, AMD users left to figure things out. So here’s the setup that actually works for me, and the gotchas I hit along the way.
The short version: Sunshine as host, Moonlight Qt as client, VAAPI as encoder, KMS as the capture method. AMF doesn’t exist on Linux, so don’t go looking for it.
Why not Apollo (yet)
If you’ve gone looking for a Sunshine alternative you’ll have run into Apollo — a Sunshine fork by ClassicOldSong with auto resolution matching, per-client display configs, and a built-in virtual display so you don’t need a dummy HDMI plug. It looks great. On Linux, almost none of it actually works yet.
The README is direct about it:
Currently Virtual Display support is Windows only, Linux support is planned and will be implemented in the future.
Without the virtual display, Apollo on Linux falls back to the same KMS capture path Sunshine uses, encodes against your physical monitor’s framebuffer, and behaves identically to Sunshine — except the install path is harder (AUR-only, with a CachyOS-specific build snag around the gcc15 dependency name). So on Linux today: stay on Sunshine. Revisit Apollo when its Linux virtual display lands. The rest of this post is Sunshine-only.
The AMD encoder situation
This part trips up a lot of people. On Windows, AMD has AMF — a clean hardware encoder API. On Linux, AMF doesn’t exist. Sunshine isn’t built with it. There’s no abstraction layer. The only hardware encoding path on AMD + Linux is VAAPI through Mesa.
Install the Mesa VA-API driver (CachyOS pulls it in by default with the rest of the Mesa stack, but just in case):
sudo pacman -S libva-mesa-driver libva-utils
vainfo
vainfo should list H264, HEVCMain, HEVCMain10, and on RDNA3+ also AV1. If you want HDR streaming later, you specifically need HEVC Main 10 or AV1 10-bit — that’s where the 10-bit profiles matter.
In Sunshine’s config (or via the web UI on https://localhost:47990):
encoder = vaapi
adapter_name = /dev/dri/renderD128
If you have more than one GPU, set adapter_name explicitly. There’s a known issue where Sunshine ignores adapter_name in some cases and grabs the wrong card — worth checking with radeontop while a stream is running.
Capturing the screen — go under the compositor
CachyOS defaults to Wayland. Sunshine has three capture paths on Linux: wlr-export-dmabuf (wlroots compositors only — Sway, Hyprland), the xdg-desktop-portal screencast (PipeWire-based; works on KDE, GNOME, anything else with a portal), and KMS. The portal works fine for desk sharing or recording, but it routes frames through compositor → portal → consumer, which you feel as added latency in fast-paced games.
KMS skips all of that. Sunshine reads frames straight from the kernel mode-setting interface, before the compositor ever sees them. That gives you:
- The lowest capture latency available on Linux — you’re reading the same scanout buffer the GPU is about to push to the monitor.
- Compositor-agnostic. Same path works on KDE, Hyprland, GNOME, sway, even a TTY.
- Full HDR. Today this is the only Sunshine path on Linux with end-to-end HDR support.
- No portal prompts, no per-session approval. The capability lives on the binary.
KMS capture needs a Linux capability on the binary:
sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))
readlink -f matters: Sunshine ships a versioned binary (/usr/bin/sunshine-VERSION) with a stable symlink (/usr/bin/sunshine). setcap does not follow symlinks, so it has to be applied to the real file.
You have to re-run this after every package update because pacman replaces the binary and the capability is dropped. The fix is a one-file pacman hook:
[Trigger]
Type = Path
Operation = Install
Operation = Upgrade
Target = usr/bin/sunshine
[Action]
Description = Restoring cap_sys_admin+p on sunshine binary for KMS capture
When = PostTransaction
Exec = /usr/bin/sh -c "setcap cap_sys_admin+p $(readlink -f /usr/bin/sunshine)"
The HiDPI scaling trap
This one cost me an evening. KMS capture grabs the physical scanout buffer of the primary CRTC. If your primary monitor is at 4K with KDE’s scale=2 set (so the desktop is logical 1920x1080 rendered at physical 3840x2160), Sunshine hands the encoder a 4K frame. The encoder then takes the top-left 1920x1080 of it and ships that to the client — without downscaling. From the client you see only the top-left quadrant of your desktop.
Same thing happens with any combination where the physical scanout is bigger than the encode size. Mirroring two displays at different scales reproduces it perfectly.
The fix is to make the captured display’s physical resolution match the encode size while a stream is in flight. I do this with a pair of small scripts wired into Sunshine’s global_prep_cmd.
The first one switches the mode and blocks until KWin actually applies it. The natural way to write this is kscreen-doctor … && exec and call it a day — but kscreen-doctor returns the moment its DBUS call is accepted, while the real mode-switch takes a few hundred ms. Sunshine starts its encoder the instant prep_cmd exits. If the script returns too early, the encoder still grabs the old buffer and you’re back to the top-left quadrant. So the script polls until the new mode is reported as live:
#!/usr/bin/env bash
# Switch HDMI-A-1 to 1080p native (scale=1) so KMS capture matches the
# stream resolution. Pairs with sunshine-stream-undo.
set -euo pipefail
OUTPUT="${SUNSHINE_OUTPUT:-HDMI-A-1}"
MODE="${SUNSHINE_STREAM_MODE:-1920x1080@120}"
SCALE="${SUNSHINE_STREAM_SCALE:-1}"
kscreen-doctor "output.${OUTPUT}.mode.${MODE}" "output.${OUTPUT}.scale.${SCALE}"
# Block until the new mode is the live one, so Sunshine's encoder
# doesn't init against the stale 4K buffer.
want_w="${MODE%x*}"
want_h="${MODE#*x}"; want_h="${want_h%@*}"
for _ in $(seq 1 40); do
read -r have_w have_h < <(
kscreen-doctor -j 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
out = next((o for o in data['outputs'] if o['name'] == '${OUTPUT}'), None)
if not out: sys.exit(1)
mode = next((m for m in out['modes'] if m['id'] == out['currentModeId']), None)
if not mode: sys.exit(1)
print(mode['size']['width'], mode['size']['height'])
" 2>/dev/null
) || true
[[ "${have_w:-}" == "$want_w" && "${have_h:-}" == "$want_h" ]] && exit 0
sleep 0.1
done
exit 0 # don't hang Sunshine if KWin never confirms
#!/usr/bin/env bash
# Restore HDMI-A-1 to its native 4K@120 + scale=2 after a stream ends.
set -euo pipefail
OUTPUT="${SUNSHINE_OUTPUT:-HDMI-A-1}"
MODE="${SUNSHINE_DESKTOP_MODE:-3840x2160@120}"
SCALE="${SUNSHINE_DESKTOP_SCALE:-2}"
exec kscreen-doctor "output.${OUTPUT}.mode.${MODE}" "output.${OUTPUT}.scale.${SCALE}"
chmod +x both, then point Sunshine at them:
global_prep_cmd = [{"do":"/home/<user>/.local/bin/sunshine-stream-prep","undo":"/home/<user>/.local/bin/sunshine-stream-undo"}]
do runs every time a Moonlight client connects; undo runs when the session ends (or the host shuts down mid-stream — Sunshine fires undo on cleanup). The TV briefly mode-switches to 1080p, the stream encodes 1080p over a matching 1080p capture buffer, and on disconnect the TV goes back to 4K@120.
The defaults in the scripts target my setup (HDMI-A-1, 1080p@120 ↔ 4K@120 scale=2) but every value is overridable via env var if you set them in the global app env block in apps.json.
If you launch games through gamescope
Steam launch options like gamescope -W 3840 -H 2160 -r 120 -f -- %command% are a common workaround for KDE Wayland hiding the native 4K resolution from games (I wrote a separate post on why that’s needed). The hardcoded -W 3840 -H 2160 fights the prep script above — the host drops to 1080p physical for streaming, but gamescope keeps rendering the game at 4K and forces a downscale. Wasted GPU, drifting cursor.
The fix is the same shape as the prep/undo scripts: a wrapper that reads the live mode of the primary output and hands those numbers to gamescope. At the desk → 4K. Mid-stream → 1080p. Same Steam launch line for both. The script and Steam launch options are in the 4K resolution post.
Client side
For desktops, laptops, and most handhelds, Moonlight Qt from the official repos is what you want:
sudo pacman -S moonlight-qt
HDR, HEVC, AV1, gamepad passthrough — all there.
Pairing
First time only:
- Open the web UI:
https://localhost:47990. Set an admin user. - On the client, point Moonlight at the host’s IP. It’ll show a 4-digit PIN.
- In the web UI, paste the PIN under PIN. Done.
The host needs ports 47984-47990 open (TCP and UDP). On CachyOS the default firewall is permissive on the LAN, but if you’ve locked things down, that’s the range.
Gotchas I hit, written down so you don’t have to
- The HiDPI scaling trap. Already covered above; if your client only sees the top-left of your desktop, this is why. Mode-switch the captured output to match the encode size for the duration of the stream.
- Bitrate spiking on static screens. Sunshine issue #3817 — VAAPI on AMD blows the bitrate up when the image barely changes. Cap your max bitrate in the Moonlight client; the host respects it.
- HEVC Main 10 artifacts on certain Mesa versions. Sunshine issue #4314. If you see weird color blocks, drop to plain HEVC (8-bit) or update Mesa.
- The capability disappearing after updates. Already mentioned above. It’s the single most common “it stopped working” cause.
- Audio echo when streaming to a device in the same room as the host. Mute the host’s output, or use
pavucontrolto route Sunshine’s sink elsewhere.
Does it actually feel good?
Yes, over Wi-Fi 6 on the same LAN. I get sub-10ms added latency on Moonlight’s own stats overlay, streaming from an RX 9070 XT host to an AYN Thor (Android handheld). Over Tailscale to a friend’s house — playable for slow games, painful for shooters. Same as any streaming solution; physics doesn’t care which protocol you picked.
TL;DR
CachyOS + AMD: Sunshine on the host, vaapi encoder, KMS capture with setcap cap_sys_admin+p, Moonlight Qt on the client. Mode-switch the captured output to match the stream resolution if your monitor uses fractional/2× scaling. No AMF. Don’t fight Wayland — go under it with KMS.
References
- Sunshine — Getting Started (LizardByte docs)
- Sunshine on GitHub (LizardByte/Sunshine)
- Apollo — Sunshine fork (ClassicOldSong/Apollo)
- The 2026 Guide to Linux Cloud Gaming with CachyOS & Sunshine — Karl.Fail
- AMF support on Linux — LizardByte discussion #1331
- AMD VAAPI bitrate excursion — Sunshine issue #3817
- HEVC Main 10 rendering artifacts — Sunshine issue #4314
- ArchWiki — Hardware video acceleration
- Moonlight Qt on GitHub (moonlight-stream/moonlight-qt)