MPG
C++ library for handling USB gamepad inputs on multiple platforms.
Install / Use
/learn @FeralAI/MPGREADME
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 theMPG.stateclass member, which is then used in other class methodsgetMillis()- Global timing function for checking debounce state (can be no-op ifdebounceMSset to0)
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:
MPGSclass has two additional methods available for use:save()load()
- The
hotkey()method is overridden to automatically save all options on change. MPGSrequires two methods fromGamepadStorage.hto 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
