SkillAgentSearch skills...

MPG

C++ library for handling USB gamepad inputs on multiple platforms.

Install / Use

/learn @FeralAI/MPG
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

MPG - Multi-Platform Gamepad Library

What is MPG?

MPG is a C++ library for processing and converting gamepad inputs, with support for XInput, DirectInput and Nintendo Switch. MPG has a fast and flexible API, but also makes no assumptions about your implementation details. This makes it a great option to use across different architectures, and facilitates easy integration into existing projects. Just implement a few methods and BYO USB implementation and you're all set!

Features

  • An abstract API for managing gamepad input state for:
    • XInput (PC, Android, Raspberry Pi, MiSTer, etc.)
    • DirectInput (PC, Mac, PS3)
    • Nintendo Switch
  • A standard set of USB descriptors, report data structures and conversion methods for supported input types
  • Per-button debouncing with a configurable interval
  • Use D-pad to emulate Left or Right analog stick movement
  • Supports common SOCD cleaning methods to prevent invalid directional inputs (👉😎👉 /r/fightsticks)
  • Overridable hotkeys for on-the-fly configuration

Installation

MPG is available in the Arduino and PlatformIO library feeds. Just open the library manager for your platform and search for MPG.

A zip package is available in the Releases section for manual installation.

Arduino IDE

If you're manually installing from a release zip, place the extracted folder (e.g. MPG-0.1.1) in your Arduino Libraries folder. On Windows this will usually be in C:\Users\%USERNAME%\Documents\Arduino\libraries.

MPG comes with a few example sketches: a full ATmega32U4 gamepad and a naive benchmarking application. The examples can be loaded from File > Examples > MPG.

PlatformIO

An alterative installation option for PlatformIO is to edit platformio.ini and add MPG to the lib_deps property:

[env]
platform = wizio-pico
board = raspberry-pi-pico
framework = baremetal
build_type = release
build_flags =
  -D PICO_USB
lib_deps =
  feralai/MPG@^0.1.1

PlatformIO will download the dependency once the platformio.ini file is saved.

There are no example projects included, but the following projects are example implementations:

  • GP2040 - Multi-platform Gamepad Firmware for RP2040 microcontrollers
  • vsFIGHTER-Firmware - Firmware for vsFIGHTER controllers by Leaf Cutter Labs.

Usage

There are two gamepad classes available: MPG and MPGS. The MPG class is the base class with all of the input handling and report conversion methods, while MPGS extends the base class with some additional methods for persisting gamepad options. Just create a derived class from one of these base classes, and use like this:

/*
 * MyAwesomeGamepad.ino
 */

#define GAMEPAD_DEBOUNCE_MILLIS 5

#include "Gamepad.h" // This will pull in our MPG implementation

Gamepad mpg(GAMEPAD_DEBOUNCE_MILLIS);

void setup() {
  mpg.setup(); // Runs your custom setup logic
  mpg.load();  // Load saved input mode, D-pad and SOCD options (MPGS class only)
  mpg.read();  // Perform an initial button read so we can set input mode

  // Use the inlined `pressed` convenience methods
  InputMode inputMode = mpg.options.inputMode;
  if (mpg.pressedR3())
    inputMode = INPUT_MODE_HID;
  else if (mpg.pressedS1())
    inputMode = INPUT_MODE_SWITCH;
  else if (mpg.pressedS2())
    inputMode = INPUT_MODE_XINPUT;

  if (inputMode != mpg.options.inputMode)
  {
    mpg.options.inputMode = inputMode;
    mpg.save(); // Input mode changed...better save it! (MPGS class only)
  }

  // TODO: Add your USB initialization logic here, something like:
  // setupHardware(mpg.options.inputMode);
}

void loop() {
  // Cache report pointer and size value
  static uint8_t *report;
  static const uint8_t reportSize = mpg.getReportSize();

  mpg.read();               // Read inputs
  mpg.debounce();           // Run debouncing if required
  mpg.hotkey();             // Check for hotkey changes, can react to returned hotkey action
  mpg.process();            // Process the raw inputs into a usable state
  report = mpg.getReport(); // Convert state to USB report for the selected input mode
  
  // TODO: Add your USB report sending logic here, something like:
  // sendReport(report, reportSize);
}

MPG Class

MPG provides some declarations and virtual methods that require implementation in order for the library to function correctly. A basic MPG class implementation requires just three methods to be defined:

  • MPG::setup() - Use to configure pins, calibrate analog, etc.
  • MPG::read() - Use to fill the MPG.state class member, which is then used in other class methods
  • getMillis() - Global timing function for checking debounce state (can be no-op if debounceMS set to 0)

An optimized Arduino MPG class implementation for a Leonardo might look like this:

#include <MPG.h>

class Gamepad : public MPG
{
  public:
    Gamepad(int debounceMS = 5) : MPG(debounceMS) { }
    void setup() override;
    void read() override;
}
/*
 * Gamepad.cpp
 *
 * Example uses direct register reads for faster performance.
 * digitalRead() can still work, but not recommended because SLOW.
 */

#include "Gamepad.h"

/* Define port/pins for easy readability */

#define PORT_PIN_UP     PF7 // A0
#define PORT_PIN_DOWN   PF6 // A1
#define PORT_PIN_LEFT   PF5 // A2
#define PORT_PIN_RIGHT  PF4 // A3
#define PORT_PIN_P1     PD2 // 1
#define PORT_PIN_P2     PD3 // 0
#define PORT_PIN_P3     PB1 // 15
#define PORT_PIN_P4     PD4 // 4
#define PORT_PIN_K1     PD0 // 3/SCL
#define PORT_PIN_K2     PD1 // 2/SDA
#define PORT_PIN_K3     PB6 // 10
#define PORT_PIN_K4     PD7 // 6
#define PORT_PIN_SELECT PB3 // 14
#define PORT_PIN_START  PB2 // 16
#define PORT_PIN_LS     PB4 // 8
#define PORT_PIN_RS     PB5 // 9

/* Input masks and indexes for setup and read logic */

#define PORTB_INPUT_MASK 0b01111110
#define PORTD_INPUT_MASK 0b10011111
#define PORTF_INPUT_MASK 0b11110000

#define PORTB_INDEX 0
#define PORTD_INDEX 1
#define PORTF_INDEX 2


/* Real implementation starts here... */

// Define time function for gamepad debouncer
uint32_t getMillis() { return millis(); }

void Gamepad::setup() {
  // Set to input (invert mask to set to 0)
  DDRB = DDRB & ~PORTB_INPUT_MASK;
  DDRD = DDRD & ~PORTD_INPUT_MASK;
  DDRF = DDRF & ~PORTF_INPUT_MASK;

  // Set to high/pullup
  PORTB = PORTB | PORTB_INPUT_MASK;
  PORTD = PORTD | PORTD_INPUT_MASK;
  PORTF = PORTF | PORTF_INPUT_MASK;
}


void Gamepad::read() {
  // Get port states, invert since INPUT_PULLUP
  uint8_t ports[] = { ~PINB, ~PIND, ~PINF };

  // Read dpad inptus
  state.dpad = 0
    | ((ports[PORTF_INDEX] >> PORT_PIN_UP    & 1)  ? GAMEPAD_MASK_UP    : 0)
    | ((ports[PORTF_INDEX] >> PORT_PIN_DOWN  & 1)  ? GAMEPAD_MASK_DOWN  : 0)
    | ((ports[PORTF_INDEX] >> PORT_PIN_LEFT  & 1)  ? GAMEPAD_MASK_LEFT  : 0)
    | ((ports[PORTF_INDEX] >> PORT_PIN_RIGHT & 1)  ? GAMEPAD_MASK_RIGHT : 0)
  ;

  // Read button inputs
  state.buttons = 0
    | ((ports[PORTD_INDEX] >> PORT_PIN_K1 & 1)     ? GAMEPAD_MASK_B1    : 0)
    | ((ports[PORTD_INDEX] >> PORT_PIN_K2 & 1)     ? GAMEPAD_MASK_B2    : 0)
    | ((ports[PORTD_INDEX] >> PORT_PIN_P1 & 1)     ? GAMEPAD_MASK_B3    : 0)
    | ((ports[PORTD_INDEX] >> PORT_PIN_P2 & 1)     ? GAMEPAD_MASK_B4    : 0)
    | ((ports[PORTD_INDEX] >> PORT_PIN_P4 & 1)     ? GAMEPAD_MASK_L1    : 0)
    | ((ports[PORTB_INDEX] >> PORT_PIN_P3 & 1)     ? GAMEPAD_MASK_R1    : 0)
    | ((ports[PORTD_INDEX] >> PORT_PIN_K4 & 1)     ? GAMEPAD_MASK_L2    : 0)
    | ((ports[PORTB_INDEX] >> PORT_PIN_K3 & 1)     ? GAMEPAD_MASK_R2    : 0)
    | ((ports[PORTB_INDEX] >> PORT_PIN_SELECT & 1) ? GAMEPAD_MASK_S1    : 0)
    | ((ports[PORTB_INDEX] >> PORT_PIN_START & 1)  ? GAMEPAD_MASK_S2    : 0)
    | ((ports[PORTB_INDEX] >> PORT_PIN_LS & 1)     ? GAMEPAD_MASK_L3    : 0)
    | ((ports[PORTB_INDEX] >> PORT_PIN_RS & 1)     ? GAMEPAD_MASK_R3    : 0)
  ;

  // No analog, but could read them here with analogRead() or fill outside of this method
  state.lt = 0;
  state.rt = 0;
  state.lx = GAMEPAD_JOYSTICK_MID;
  state.ly = GAMEPAD_JOYSTICK_MID;
  state.rx = GAMEPAD_JOYSTICK_MID;
  state.ry = GAMEPAD_JOYSTICK_MID;
}

Most of the code are the pin definitions and fancy formatting of bitwise button reads. You can also extend and override methods in the MPG class if you want to do something like change hotkeys, customize input processing steps, etc.

MPGS Class

If your platform supports some form of persistent storage like EEPROM, you can use the MPGS abstract class instead. The differences between the MPG and MPGS classes are:

  • MPGS class has two additional methods available for use:
    • save()
    • load()
  • The hotkey() method is overridden to automatically save all options on change.
  • MPGS requires two methods from GamepadStorage.h to be defined:
    • GamepadOptions GamepadStorage::getGamepadOptions();
    • void GamepadStorage::setGamepadOptions(GamepadOptions options);

Implement the MPG class as previously described, then implement the GamepadStorage class methods:

/*
 * storage.cpp
 *
 * Example storage for ATmega32U4.
 */

#include <GamepadStorage.h>
#include <EEPROM.h>

GamepadOptions GamepadStorage::getGamepadOptions() {
  GamepadOptions options =
  {
    .inputMode = InputMode::INPUT_MODE_XINPUT,
    .dpadMode = DpadMode::DPAD_MODE_DIGITAL,
    .socdMode = SOCDMode::SO
View on GitHub
GitHub Stars56
CategoryDevelopment
Updated1mo ago
Forks16

Languages

C

Security Score

100/100

Audited on Feb 8, 2026

No findings