Getting the New Steam Controller Gyro Working in Ship of Harkinian on Linux

I bought a new Steam controller and wanted to try it. I used it with Ship of Harkinian on Linux, launched it via Steam, and noticed gyro wasn’t detected. Below is how I got native gyro working for SoH.

Like any program, launching through Steam grants you the full capabilities of the controller except for native access to gyro. The solution is to emulate the mouse or joystick which is fine for programs that don’t support gyro natively but terrible for applications that do.

Ship of Harkinian is an SDL2 application. The new Steam Controller support is in SDL3’s HIDAPI Steam controller driver, where the controller is handled under the Triton driver. That means SoH can have a working motion-aim path while the controller support needed to feed it is sitting one SDL generation away.

The useful bridge here is sdl2-compat. It provides a libSDL2-2.0.so.0 that looks like SDL2 to the application, but forwards the calls into SDL3. SoH still thinks it is running against SDL2. Underneath that, SDL3 handles the controller and exposes the gyro and accelerometer.

No SoH patches were needed for this setup.

Example of the working gyro:

The problem

SoH already has motion aiming support. The issue is getting the controller’s gyro exposed through the API SoH is using.

The chain looks like this:

1
2
3
4
5
6
Ship of Harkinian
  -> SDL2 GameController API
  -> sdl2-compat libSDL2-2.0.so.0
  -> SDL3
  -> SDL3 HIDAPI Steam/Triton driver
  -> new Steam Controller gyro/accelerometer

Why not just use Steam Input?

Steam Input is a reasonable workaround for some games but you lose native gyro.

Steam Input can present a virtual controller or map gyro to mouse/stick input. That is useful, but it also adds another layer that can create input translation issues(think control stick deadzone, acceleration, etc). For SoH, I wanted the native controller path: buttons as controller buttons, gyro as gyro. Gyro emulating the control stick gives terrible results akin to playing on an N64 controller.

This is overkill if you only want basic gamepad input. It is mainly worth doing if you want the controller’s motion sensors visible to SDL.

Requirements

This was tested on Linux.

You need:

  • A Linux system with normal build tools installed
  • CMake
  • Ninja or Make
  • Git
  • A recent SDL3 checkout with the Steam Controller Triton HIDAPI driver(I used the master branch from 2026-05-16)
  • A current sdl2-compat checkout
  • A Ship of Harkinian build that dynamically links libSDL2-2.0.so.0(I used 9.2.3)
  • The new Steam Controller puck plugged in
  • Steam must not be running

Check the puck with lsusb:

1
2
3
lsusb | grep -i valve

Bus 001 Device 022: ID 28de:1304 Valve Software Steam Controller Puck

For the puck I was targeting, I expected one of these IDs:

1
2
28de:1304
28de:1305

Build SDL3 and sdl2-compat locally

I installed this into a private prefix under ~/.local so I did not have to replace the system SDL libraries.

1
2
3
4
5
6
7
mkdir -p ~/src
cd ~/src

git clone https://github.com/libsdl-org/SDL.git sdl3
git clone https://github.com/libsdl-org/sdl2-compat.git

PREFIX="$HOME/.local/sdl-stack"

Build and install SDL3:

1
2
3
4
5
6
7
cd ~/src/sdl3
cmake -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX="$PREFIX"

cmake --build build -j"$(nproc)"
cmake --install build

Then build sdl2-compat against that SDL3 install:

1
2
3
4
5
6
7
8
cd ~/src/sdl2-compat
cmake -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX="$PREFIX" \
  -DCMAKE_PREFIX_PATH="$PREFIX"

cmake --build build -j"$(nproc)"
cmake --install build

After that, $PREFIX/lib should contain the SDL2 compatibility library and the real SDL3 backend:

1
2
libSDL2-2.0.so.0
libSDL3.so.0

Run Ship of Harkinian with the SDL2 shim

The runtime part is simple: put the local SDL stack ahead of the system libraries.

If you want, you can use a wrapper script to launch it:

1
2
3
4
5
6
7
8
9

cat > ~/.local/bin/soh-sdl3 <<'EOF'
#!/usr/bin/env bash
SOH_DIR="${SOH_DIR:-$HOME/Apps/soh}"  #Put your path to SoH here
export LD_LIBRARY_PATH="$HOME/.local/sdl-stack/lib:$LD_LIBRARY_PATH"
exec "$SOH_DIR/soh.appimage" "$@"
EOF

chmod +x ~/.local/bin/soh-sdl3

Adjust SOH_DIR and the executable name for your install. If you are running an extracted build instead of an AppImage, point it at that binary instead.

For AppImages, LD_LIBRARY_PATH still works because the dynamic loader sees it before resolving the executable’s shared libraries. If you extracted the AppImage manually, the same idea applies.

With the controller connected, the window should show the controller as a Steam Controller the same as when launching from Steam.

Steam Controller List in SoH

Enable gyro in SoH

Launch SoH.

  1. Open the controller settings.
  2. Add a gyro device if it wasn’t automatically added.

The controller should be instantly added.

Gyro menu without the controller

Gyro menu with the controller

How this works

SoH asks SDL2 whether the controller has a gyro sensor. In SDL2 terms, that is SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO). If the sensor exists, SoH can enable it with SDL_GameControllerSetSensorEnabled.

With sdl2-compat in the middle, those calls are translated into the SDL3 gamepad sensor path. SDL3 then talks to the controller through its HIDAPI Steam Controller driver.

The important part is that SoH does not need to know about the new controller. It only needs SDL to expose a controller with a gyro.

Troubleshooting

The controller shows up as a generic gamepad

Your SDL3 build is probably too old, or it did not build the Steam/Triton HIDAPI driver.

A quick check:

1
strings "$HOME/.local/sdl-stack/lib/libSDL3.so.0" | grep -i triton

If that shows nothing, rebuild SDL3 from a newer checkout.

SoH still loads the system SDL2

Check what is actually being loaded:

1
LD_DEBUG=libs ./soh 2>&1 | grep -i SDL

If your local libSDL2-2.0.so.0 is not listed, the dynamic loader is not using your shim.

Common causes:

  • SoH has a bundled SDL2 next to the binary(which I haven’t seen in the appimage)
  • The binary has an RPATH or RUNPATH pointing at bundled libraries
  • A launcher prepends its own runtime libraries
  • LD_LIBRARY_PATH is being overwritten in a wrapper script

If you lauch with Steam closed and without any launcher, none of these should apply.

Touchpad clicks aren’t recognized

The new Steam Controller’s touch pads aren’t currently supported in SDL3, as far as I’m aware. You lose the touch pads in exchange for gyro. It looks like they’re exposed as touch pads, because they are, and SoH doesn’t know how to translate that to button presses.

Limitations

This is Linux-only.

This also depends on SoH dynamically loading SDL2 in a way that can be overridden. If your build is statically linked or aggressively bundles its own SDL2, this approach needs adjustment.

The gyro path works because it fits the existing SDL sensor API. More controller-specific inputs are less clean, especially touchpad clicks and touch sensors that do not work.

Quick version

Build SDL3 and sdl2-compat into a local prefix:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
PREFIX="$HOME/.local/sdl-stack"

git clone https://github.com/libsdl-org/SDL.git sdl3
git clone https://github.com/libsdl-org/sdl2-compat.git

cd sdl3
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PREFIX"
cmake --build build -j"$(nproc)"
cmake --install build

cd ../sdl2-compat
cmake -B build -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX="$PREFIX" \
  -DCMAKE_PREFIX_PATH="$PREFIX"
cmake --build build -j"$(nproc)"
cmake --install build

Run SoH with the shim first in the library path:

1
2
3
4
5
6
LD_LIBRARY_PATH="$HOME/.local/sdl-stack/lib:$LD_LIBRARY_PATH" ./soh

or 


LD_LIBRARY_PATH="$HOME/.local/sdl-stack/lib:$LD_LIBRARY_PATH" ./soh.appimage
updatedupdated2026-05-182026-05-18