DEV Community


Gamepad Input with Rust (cont.)

jeikabu profile image jeikabu Originally published at on ・3 min read

Continuation of my previous work handling gamepad input with Rust.

hidapi DS4

Using hidapi to obtain DualShock 4 (PS4) input is also straightforward.

Using the same code as last time but vendor/product id of 0x054c/0x09cc and the following definitions:

#[derive(Copy, Clone)]
enum PS4ButtonFlags {
    DPadN = 0x0000,
    DPadNE = 0x0001,
    DPadE = 0x0002,
    DPadSE = 0x0003,
    DPadS = 0x0004,
    DPadSW = 0x0005,
    DPadW = 0x0006,
    DPadNW = 0x0007,
    DPadNone = 0x0008,
    DPadMask = 0x000F,
    Share = 0x0010,
    Options = 0x0020,
    Home = 0x0040,
    L1 = 0x0100,
    R1 = 0x0200,
    L2 = 0x0400,
    R2 = 0x0800,
    Square = 0x1000,
    Cross = 0x2000,
    Circle = 0x4000,
    Triangle = 0x8000,

struct PS4Input {
    header: u8,
    left_stick_x: u8,
    left_stick_y: u8,
    right_stick_x: u8,
    right_stick_y: u8,
    buttons: PS4ButtonFlags,
    something: u8,
    l2: u8,
    r2: u8,
Enter fullscreen mode Exit fullscreen mode

The d-pad behavior is unexpected; when there’s no input the lowest nibble has a value of 0x8 and a value of 0x0 when you’re pressing up.

There’s actually many more bytes of input data. I assume the remainder is related to the gyroscope and touchpad, but didn’t investigate.


The cross-platform SDL library also has Rust bindings: rust-sdl2.

First, install the SDL 2.0 binaries/framework.

To Cargo.toml add:

sdl2 = "0.31.0"
sdl2-sys = "0.31.0"
Enter fullscreen mode Exit fullscreen mode

On OSX cargo build fails with:

= note: ld: library not found for -lSDL2
        clang: error: linker command failed with exit code 1 (use -v to see invocation)
Enter fullscreen mode Exit fullscreen mode

From their documentation and this issue need to use:

sdl2-sys = "0.31.0"

features = ["use_mac_framework"]
version = "0.31.0"
Enter fullscreen mode Exit fullscreen mode

Minimal program based off the example in the docs:

extern crate sdl2;
extern crate sdl2_sys;

use sdl2::{
use std::collections::HashMap;

fn main() {
    // Initialize SDL
    let sdl_ctx = sdl2::init().unwrap();
    // Initialize game controller subsystem
    let controller_subsystem = sdl_ctx.game_controller().unwrap();
    let mut gamepads: HashMap<u32, GameController> = HashMap::new();
    // Obtain SDL event pump
    let mut event_pump = sdl_ctx.event_pump().unwrap();

    'running: loop {
        // Obtain polling iterator for events
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit {..} |
                Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                    break 'running
                Event::KeyDown { keycode: Some(keycode), .. } => {
                    println!("{}", keycode)
                Event::ControllerDeviceAdded { which, ..} => {
                    println!("Device added index={}", which);
                    // When device connected open it so we receive button events
                    let gamepad =;
                    gamepads.insert(which, gamepad);
                Event::ControllerDeviceRemoved{ which, ..} => {
                    println!("Device removed index={}", which);
                    gamepads.remove(&(which as u32));
                Event::ControllerButtonDown {which, button, ..} => {
                    // Gamepad button pressed
                    println!("Controller index={} button={:?}", which, button);
                _ => {}
Enter fullscreen mode Exit fullscreen mode

The above works on OSX, but it sounds like other operating systems may require a window in order to receive input events. In case that’s necessary, also investigated creating a window:

let video_subsystem =;

let window = video_subsystem.window("rust-sdl2 demo", 800, 600)
    // Window is "hidden" (but may appear in the task-bar)
    .set_window_flags(sdl2_sys::SDL_WindowFlags::SDL_WINDOW_HIDDEN as u32)
let controller_subsystem = sdl_ctx.game_controller().unwrap();
//controller_subsystem.set_event_state(true); // true by default

// Enable gamepad events when running in background
Enter fullscreen mode Exit fullscreen mode

The last line is a hint to SDL and seems to be required to obtain input events. There’s a define in sdl2_sys:

Enter fullscreen mode Exit fullscreen mode

First attempt, using std::str::from_utf8() with a slice of all but the trailing \0 (which makes from_utf8() panic):

    let hint0 = std::str::from_utf8(&bytes[..bytes.len()-1]).unwrap();
    println!("{}", sdl2::hint::set(hint0, "1"));
Enter fullscreen mode Exit fullscreen mode

Goofy, but works. Alternatively, std::ffi::CStr seems to provide the most straight-forward way to work with null-terminated strings:

let hint1 = std::ffi::CStr::from_bytes_with_nul(bytes).unwrap().to_str().unwrap();
println!("{}", sdl2::hint::set(hint1, "1"));
Enter fullscreen mode Exit fullscreen mode

I suspect there’s a way to coerce to CStr to &str, but this works.


So there’s hidapi which works with our controller and now PS4, but not the Xbox One gamepad. And then there’s SDL which doesn’t work with the XBone controller or ours, but works with PS4 (and many other controllers). SDL also leverages testing and gamepad compatibility work of that project, but at the cost of a pretty big external dependency and maybe headaches related to windows.

Will have to ponder this.

Discussion (2)

Editor guide
17cupsofcoffee profile image
Joe Clay

Another potential option - there's a library called gilrs which provides an abstraction over the platform-specific APIs for gamepad input. The ggez framework (which is in the process of moving away from SDL with the aim of not having any C dependencies) is planning on using it going forward, and I believe the Amethyst engine already does. The main issue with it at the moment is the lack of OSX support, but it sounds like they're planning on getting that sorted sooner rather than later.

Just to add even more confusion to the mix :)

jeikabu profile image
jeikabu Author

Nice! Thanks for the tips, that definitely gives me more to look into.