Using the Steam Deck as a Controller for Desktop Gaming

TLDR

I made a script which utilizes USBIP to expose the Steam Deck’s controller to the network, then connect to it from the desktop.

Reasoning

I love the Steam Deck’s controls: dual-analog, trackpads, 4 rear buttons, and Valve’s incredible input configurator. While Valve has announced plans for a standalone controller, we’ve been in the dark ever since. Not a word.

I initially had Sunshine (game streaming server/remote desktop) set up to act as a controller but Sunshine shows the inputs from Moonlight (client program) as a plain xbox controller, meaning limited access to the controller configuration and any configurations are universal instead of per-game. This solution felt wasteful due to video/audio encoding and decoding, when all I really want is the controller. Same goes for the built-in Steam stream features.

I started looking into solutions and found USB/IP. The Steam Deck’s controller is simply a USB device, which USB/IP can bind to.

Prerequisisites

This works best on decks with aftermarket operating systems on them as my setup requires a writiable root partition. I use CachyOS handheld and as far as I know, there’s nothing that SteamOS has that this version of CachyOS doesn’t.. other than being a machine you can actually administrate.

sudoers (optional)

On both the server and the client, members of group wheel should be allowed to run usbip without entering a password so the connections are made seamlessly:

# sudo EDITOR=nano visudo

then add the following on a newline in the file:

%wheel = (root) NOPASSWD: /usr/bin/usbip

This is optional of course, because you could type (tap) your password in every time.

ssh

I also generated a ssh key for my deck and put it on my desktop for passwordless authentication so I can do everything in one script.

I do have concerns regarding security when doing so as the deck being a portable device leaves it more prone to theft. I plan on discussing these concerns more in a future post.

A “screensaver”/inhibitor program

The deck will go to sleep after some specified time. I need a way to prevent this. You could really use any foreground program here since all it needs to do is “pause” the execution of the script from until your selected program closes.

Something like mpv looping any video you’d like would work great, you’d just need a way to stop playback and close mpv (though this brings back the video decoding/battery problem I was trying to avoid in the first place)

I wrote myself a custom one. It’s a small C program using raylib that has a DVD-esque bouncing logo to indicate to the user that the deck is in “controller mode”. This program doesn’t guarantee a client is connected, rather it simply displays a “screensaver” while waiting for touchscreen input. On such input it exits.

Unfortunately my program doesn’t inhibit automatic system suspension as I had hoped, but it does black-out the display, which minimizes distraction and saves on battery (at least on the OLED model). I’m sure theres something I could do better to inhibit sleep, but in the meantime I launch the script in desktop mode and manually block suspension by toggling it in KDE’s power/battery tray-menu.

The Script

The final, albeit simple, script that is run on the Deck is as follows:

#!/bin/bash
sudo usbip bind -b 3-3
ssh navi doas usbip attach -r cirno -b 3-3
/usr/local/bin/steamdeck-inhibitor
ssh navi doas usbip deatach -p 00
sudo usbip unbind -b 3-3

Where navi is my desktop and cirno is my deck (Tailscale’s MagicDNS handles name resolution). If you’re doing this yourself you’ll have to change those hosts.

Ah, I also use a freakish mismatch between doas on my Gentoo boxes, and sudo on pretty much everything else. Swap/replace these commands as necessary.

With Sunshine/Moonlight

If you want game streaming with Sunshine and using USB/IP you can replace the steamdeck-inhibitor line with whatever command you use to launch Moonlight. I’ve done so myself, but I haven’t figured out an equally elegant way to start and stop Sunshine sessions. It’s scuffed, but this is what I do currently:

  1. Launch the script
  2. Use the touchscreen to select which Sunshine “Application” I want to stream.
  3. Game
  4. When I’m done, put the Deck to sleep to force unbind any USB/IP connections
  5. Turn the Deck back on
  6. Use the default Moonlight quit session bind (RB+LB+Start+Select) to return to the main menu
  7. Close Moonlight to let the script finish executing

Whereas with the steamdeck-inhibitor program:

  1. Launch script
  2. Game
  3. Tap screen

Closing Thoughts

This setup is great because you get your cloud-synced, per-game Steam input configurations and comparatively low battery consumption to using Sunshine as a controller. I haven’t measured, but on my home network there is little latency. None that I notice as a human.

Occasionally the controller disconnects. Not pleasant during an Elden Ring session but luckily it hasn’t done so during any tough fights, just during the intermediate areas and travel.