Streaming games from a headless Windows machine

About a year ago, Riot Games added their kernel-level anti-cheat system “Vanguard” to League of Legends. It marked the end of League on Linux (RIP /r/leagueoflinux) and the beginning of a year-long hiatus from the game. While not any great loss for me, League is a game I always come back to eventually (for better or worse), and a while ago temptation struck. I wanted to play some ARAMs to blow off some steam, but needed to find a way to play from my Linux desktop.
Option 1: Dual booting
Dual booting Windows is a valid option. Personally, I find it too irritating to shutdown and reboot into a Windows installation just to play a game. I like my Linux desktop and all the benefits that come with it. There’s also something about even having a Windows drive connected to my main PC which gives me the ick - Windows has a tendency to be disrespectful to other OSes and I didn’t want to risk it nuking my Linux install.
Option 2: Gaming in a virtual machine
Passing a GPU through to a VM is a well-documented thing which people do (see /r/VFIO). I’ve done this before on an unRAID server and it just about worked, but there was a lot of hacking and fiddling with settings to get it to work reliably. Nowadays, the deal-breaker with gaming VMs is, once again, anti-cheat software. Many anti-cheats can now detect if you’re gaming in a VM and ban you, even if you employ various hardware ID spoofing tricks in an attempt to convince them you’re using real hardware. It’s a no-go.
(I did consider spinning up a macOS VM and playing League on that, as Vanguard anti-cheat is apparently not required on macOS, but chose not to because there may be other anti-cheat games I’ll want to play which don’t work on macOS at all) (UPDATE: Riot are bringing Vanguard to macOS after all)
Option 3: Separate machine and a KVM switch
This is a legitimate option and a route I nearly went down, but ultimately decided against because I don’t have enough room at my desk to accommodate another PC or the bunch of extra cables required to hook up a KVM switch setup. Also, good KVM switches are expensive, and I wanted to keep things cheap.
Option 4: Streaming with Sunshine & Moonlight
Streaming games from a host PC to a client device is well-supported these days thanks to the Sunshine (server) and Moonlight (client) open source projects. These allow you to do low-latency, high-fidelity game streaming over the network. Since I’d recently upgraded to a 2.5GbE UniFi network with convenient cat6a ports to wire things up, I figured I had as good a network as any to stream games. It also meant I could put the PC in a different room entirely and keep it out of the way, solving my desk/office space problem.
The build

The host PC I’m using is the one I built for my wife using old parts after I upgraded mine. She stopped using it and switched to a laptop, so it was just gathering dust. After upgrading the CPU and motherboard, here are the specs:
- AMD Ryzen 5 5600XT CPU (6 cores, 12 threads)
- 32GB DDR4 Corsair Vengeance RAM
- Gigabyte B550M DS3H R2 microATX motherboard
- NVIDIA GTX 1070 GPU (8GB VRAM)
- 240GB MSI SATA SSD
- 1TB Crucial P3 Plus NVMe SSD
- 2.5G Intel I226-V PCIe NIC
Armed with my Ventoy USB drive and the latest Windows 11 x64 ISO from Microsoft’s website, I set about installing Windows 11 Pro.
Installing Windows
The enshittification of the Windows installation process is well-documented, so I won’t go into too much detail, but it was tiresome (you can’t create a local user without using command prompt hackery). I chose to install Windows onto the slower SATA SSD because I wasn’t sure if I’d keep the 1TB NVMe drive in this machine, and didn’t expect the speed difference to make any meaningful difference to performance (I was right). The 1TB drive would be for installing games.
Several reboots later and Windows 11 Pro was ready to go! Except that it wasn’t, because the default Windows 11 experience is a steaming pile of dog shit.
De-enshittifying Windows 11
Chris Titus’s tool helped a lot to disable telemetry and 101 other “features” which make Windows actively worse. I still had to manually uninstall a bunch of bloatware and tweak settings to my preference.
To Windows’ credit, installing applications can now be done in a way resembling a Linux package manager using any of winget, scoop, or choco from a command prompt/PowerShell/Windows Terminal. I chose winget, though my chipset, GPU, and NIC drivers would have to be installed the old fashioned way.
Doing all of this and installing Windows updates took 2+ hours. Oof.
Configuring Virtual Display Driver
Since this machine will be headless with only power and ethernet connected at the back, I needed to trick Windows into thinking it’s connected to a real display output which I can stream to my Linux desktop. To do this, headless ghost HDMI dongles were once popular, but it can be done entirely in software now thanks to the Virtual Display Driver (VDD) project. I installed the latest release (beta 25.7.23) and quickly ran into problems.
My monitor is 3440x1440@165Hz, which wasn’t an option in the list of available resolutions on the virtual display. I had to add this myself as a custom resolution, but any changes I made in the GUI tool were not being applied. It turns out the GUI tool is just an interface for an XML file which is entirely replaced with a default one whenever you install/reinstall the driver, unless you go into the file’s properties and set it to read-only (thank you, random GitHub user). With that out of the way and my virtual display activated, it was time to set up the actual streaming part.
Auto log-in and wake-on-LAN
I wanted this PC to be easy to turn on only when I wanted to actually use it and it had to be ready to use within a minute or two, which meant setting up wake-on-LAN for remotely powering it on and having it automatically log in without me having to type in a password. Not the best for security, admittedly - but this machine wouldn’t have access to any important data or secrets, so a sacrifice I was willing to make.
Wake-on-LAN required turning off fast boot, but otherwise everything was already set up by default for it to work. I was able to test this later via Moonlight’s “Wake PC” option in the right-click menu for the machine. Auto-login was more complicated and required adding and modifying Windows registry keys with regedit, but worked reliably once done.
Configuring Sunshine
Sunshine was simple to install with winget - no problems there. It simply runs as a service on start-up. After setting a username & password and doing some basic setup, I found the key part of Sunshine’s configuration was on the Audio/Video tab under config.output_name_windows. Here I had to grab the device ID of my virtual display and set it so that Moonlight connects to it correctly. I also set Device configuration to Deactivate other displays and activate only the specified display, then saved/applied the settings and restarted Sunshine. That’s it!
Configuring Moonlight
After successfully connecting to Sunshine, I changed the device settings from Moonlight to get the best latency I could, set the resolution to 3440x1440 and 165 FPS, unlocked the bitrate limit and cranked it to 500 Mbps, then enabled V-Sync and HDR.
Observations

The latency is astoundingly low given everything that’s happening to make streaming possible - about 10 ms on average when HDR is enabled, lower with it off. I don’t notice any input delay at all. However, certain colours, particularly reds/oranges, don’t render particularly well and are noticeably blurry/smudgy, but this is only really a problem when looking at the desktop, in-game menus, or red-coloured text. Text in general isn’t pixel-perfect, but it’s good enough for gaming.
I could probably sacrifice some latency and performance by increasing the quality settings in Sunshine (I’ve left it at P1 for now) but will save this for when I upgrade the GPU to something more powerful like a 9060 XT.
Playing League of Legends
This is where I hit a critical flaw with streaming League. The Vanguard anti-cheat detects that I’m using a virtual cursor and prevents it from moving - it gets locked to the centre of the screen. This obviously makes the game unplayable, but there is a solution: VirtualHere.
VirtualHere is USB-over-IP software which allows you to stream USB devices over the network and expose them as generic devices on the client machine. Luckily, I have a trackball mouse for general computer use and a Logitech Superlight 2 mouse just for gaming, so I was able to run the VirtualHere server on my Linux desktop with sudo vhusbd -c config.ini, run the VirtualHere client on the Windows machine, and activate my mouse from that. This disables the mouse in Linux, so if I want to alt-tab back to Linux while streaming, I have to switch to the trackball mouse - a sacrifice I am willing to make. Here’s my config.ini:
It=1770933628
EasyFindId=redacted
EasyFindPin=redacted
IgnoredDevices=187c/100e,46d/c52b,c76/161e,514c/4155,e8d/616,26ce/1a2,36b0/300e,bda/8771,291a/3361
DeviceNicknames=Logitech Superlight 2385F387B3234,0000,0000,12
I made sure to ignore any USB devices besides my mouse, as exposing them caused issues on my Linux desktop. This was easy to do from the client software which syncs any setting changes back to the server - I didn’t have to manually configure these device IDs myself.
Surprisingly, the latency still feels great and I don’t notice any difference between using the mouse directly over Moonlight/Sunshine and streaming it with VirtualHere. It just works, and fixes the League cursor blocking problem. I did look at open source alternatives to VirtualHere but couldn’t find any that seemed as stable or reliable, so I’ll be sticking with it for now.
Conclusion

I now have a fast, low-latency game streaming server which I can power on and connect to within seconds. I’ll definitely be using this for future Windows-only games, but will need to upgrade the GPU for anything more graphically-intensive than League of Legends.
Would I recommend it? Yes, if you have the time and inclination, or if you don’t like any of the alternatives. Ultimately the morally optimal solution is to avoid games with overreaching anti-cheat systems entirely, but I really wanted to play some League of Legends, so that went out of the window. Hooray!