Retropie Console – Game Controller Setup

3 years ago I wrote a blog entry that covers how I setup a bunch of arcade buttons and two joysticks attached to an Ultimarc I-Pac board.

With doing a big final push to get every emulator and control working, I realized that keyboard mode would not work for the arcade panel. What do I do? When I tried Xbox mode, it saw the controller as a single set of controls rather than two separate set.

I missed using the Dual DInput mode! Setting that mode, and reprogramming the buttons, everything worked as I needed it to!

Also, I can use either a wireless controller as player 1 and player 2, and switch to the arcade panel too. It does mean that everything is setup for two player rather than up to four players (Gauntlet, anyone)? It’s something I’ll have to figure out in the future, but for now it works for a wide range of game seamlessly, which is what I always wanted.

The “virtual connector” setup is a small Linux evdev/uinput merger. It keeps two always-named virtual players while accepting either the matching panel controls or wireless controller. 

Lets get into the details:

One of the deceptively difficult parts of building an arcade cabinet is not connecting the controls. It is making those controls appear consistently to every emulator, every time the cabinet starts.

Our control panel uses an Ultimarc I-PAC Ultimate I/O, with two 8BitDo wireless controllers available for games that are better suited to a conventional gamepad. The goal sounded simple:

  • the left side of the panel and the first wireless controller should both operate Player 1;
  • the right side of the panel and the second wireless controller should both operate Player 2; and
  • games should see the same two controllers regardless of USB detection order or whether the wireless controllers are powered on.

Getting there required more than a set of RetroArch mappings. We put the I-PAC into Dual DInput mode, normalized the panel’s raw inputs, and built a small virtual-controller service that combines the physical devices into two stable players.

Why we chose Dual DInput mode

The I-PAC Ultimate I/O can present its controls to the computer in different ways. Keyboard mode is convenient for a simple MAME cabinet, but this machine also runs emulated home computers. Those systems need a real keyboard, and having the arcade panel generate keyboard presses creates conflicts that are difficult to reason about.

Xbox mode avoided the keyboard problem, but it exposed the panel as only one player. That did not satisfy the basic requirement of a two-player cabinet.

Dual DInput mode was the right fit. In this mode, the I-PAC identifies as USB device d209:0412 and exposes two independent Linux gamepads—one for each side of the control panel. It gives the cabinet a clean separation between the physical keyboard and the arcade controls, while preserving both panel players.

After switching modes, we tested the raw events instead of assuming that the labels on the panel matched the resulting button numbers. That uncovered an important asymmetry:

Physical controlRaw Player 1 inputRaw Player 2 input
Action buttons 1–6Buttons 0–5Buttons 0–5
StartButton 6Button 7
CoinButton 7Button 6
Centre utility buttonButton 10Button 10

Player 1 and Player 2 report Coin and Start in the opposite order. This is not a problem once it is known, but it is exactly the sort of detail that produces baffling emulator mappings if it is left uncorrected.

It was decided that the raw I-PAC layout should never leak into the emulator configuration. Both sides would be translated to one common logical layout first.

Why ordinary controller indices were not enough

Dual DInput gave us two panel gamepads. Turning on both 8BitDo controllers added two more. Linux and RetroArch could therefore see as many as four physical gamepads:

I-PAC Player 1
I-PAC Player 2
8BitDo controller 1
8BitDo controller 2

The numeric order of those devices was not dependable. It could change with USB discovery order, controller power state, or the mode selected on a wireless controller. A fixed configuration such as “Player 1 uses controller index 0” might work after one boot and select a different device after the next.

There was a second usability issue. We did not want players to choose between a panel profile and a wireless profile before starting a game. The panel and the assigned controller are alternate ways to control the same player. The emulator should not need to know which one is being used.

The solution was to move that decision below RetroArch.

The virtual-controller merger

We wrote a small Python service called arcade-gamepad-merger. It reads Linux input events using python3-evdev and creates two new gamepads through /dev/uinput:

I-PAC P1 ─────────┐
                  ├── Arcade Merged Player 1
first 8BitDo ─────┘

I-PAC P2 ─────────┐
                  ├── Arcade Merged Player 2
second 8BitDo ────┘

RetroArch sees only the stable logical destination it cares about. Pressing left on the Player 1 panel joystick or on the first 8BitDo produces left on Arcade Merged Player 1. The same arrangement applies to Player 2.

The service does not take exclusive ownership of the physical devices. That is deliberate. EmulationStation can still see them, diagnostic tools can still inspect them, and RGB Commander can continue talking directly to the I-PAC lighting hardware.

The merger performs several useful translations along the way:

  • it converts the panel’s physical button order to our preferred RetroPad B/A/Y/X layout;
  • it corrects the reversed Coin and Start inputs on the two I-PAC interfaces;
  • it maps Coin to virtual button 10 and Start to virtual button 11 for both players;
  • it maps the centre utility inputs to virtual button 12 for hotkey enable and Game Focus;
  • it combines simultaneous sources safely, so releasing a button on one device does not cancel the same button while it is still held on the other; and
  • it releases the state belonging to a disconnected source without recreating both virtual controllers.

That last point matters. If a wireless controller goes to sleep during a session, the virtual devices remain present. A running emulator does not suddenly lose and rediscover its Player 1 and Player 2 devices.

The service starts at boot, depends on /dev/uinput, and is supervised by systemd. If it fails unexpectedly, it is restarted automatically.

Handling two modes of the same wireless controller

Our two wireless controllers are physically the same 8BitDo model, but they did not end up using the same operating mode.

The normal arrangement is:

AssignmentModeUSB identityPurpose
First 8BitDoD mode2dc8:3013Wireless source for Player 1
Second 8BitDoX mode2dc8:3106Wireless source for Player 2

We originally expected both to remain in D mode. In practice, the second controller’s receiver repeatedly fell back to USB product 2dc8:3109 with the name 8BitDo IDLE. We ruled out USB autosuspend, hub faults, low-voltage warnings, and EmulationStation mappings. The receiver was genuinely losing its active D-mode link.

X mode remained stable, so we taught the merger to understand both formats. DInput reports stick positions in a 0…255 range, while XInput uses signed values from -32768…32767. The merger scales the XInput axes into the common virtual range. It also converts XInput’s analogue trigger axes into the digital L2 and R2 buttons expected by our shared RetroArch profile.

This lets two differently configured physical controllers produce exactly the same logical control layout.

Giving RetroArch stable names and explicit mappings

Creating the virtual devices solved the unstable hardware order, but there was one more RetroArch detail to handle.

RetroPie’s controller-selection file is configured to resolve the two virtual controllers by name whenever a Libretro game starts:

joystick_selection_by_name = "true"
input_player1_joypad_index = "Arcade Merged Player 1 #1"
input_player2_joypad_index = "Arcade Merged Player 2 #1"

Name-based selection is much safer than storing numeric indices. However, changing input_playerN_joypad_index does not automatically copy that device’s autoconfiguration into the selected player port. We therefore kept explicit Player 1 and Player 2 button and axis bindings in the global RetroArch configuration, and installed matching autoconfiguration profiles for both virtual devices.

The normalized layout is:

FunctionVirtual input
Face buttonsButtons 0, 1, 3 and 4
L/RButtons 6 and 7
L2/R2Buttons 8 and 9
Select/CoinButton 10
StartButton 11
Hotkey Enable / Game FocusButton 12
L3/R3Buttons 13 and 14
D-padHat 0
Left stickAxes 0 and 1
Right stickAxes 2 and 3

For MAME, the old arrangement referred to four joystick numbers and included extra stick-click fallbacks for Coin and Start. Once the merged devices were working, we simplified MAME to use Joy 1 and Joy 2 only. Its global Start inputs also needed to be the proper START1 and START2 types.

Keeping the button lighting working

Dual DInput created one unrelated compatibility wrinkle. RGB Commander 0.4.0.5 recognized the I-PAC’s keyboard-mode USB product, d209:0410, but not its Dual DInput identity, d209:0412.

Changing the I-PAC back to keyboard mode just to satisfy the lighting program would have undone the whole input design. Instead, we added a tightly scoped compatibility library that is preloaded only into the RGB Commander service:

[Service]
Environment=LD_PRELOAD=/usr/sbin/rgbcommander/compat/libipac-rgbcommander-compat.so

For the d209:0412 device, the shim changes only the product ID reported to RGB Commander. It does not modify the real USB descriptor, firmware, input events, or LED protocol, and it has no effect on any other process. The controls remain genuine Dual DInput devices while the existing lighting software continues to recognize the board.

The input merger and the RGB compatibility layer are intentionally separate. One normalizes gameplay controls; the other preserves lighting support.

Testing the complete path

We tested at each layer rather than jumping straight into a game:

  1. Confirm the I-PAC appears as two event devices in Dual DInput mode.
  2. Inspect raw events to record the actual button and hat codes for both sides.
  3. Confirm the merger service creates Arcade Merged Player 1 and Arcade Merged Player 2.
  4. Use evtest on each merged device and verify that both of its physical sources produce the expected events.
  5. Restart the cabinet and confirm that RetroPie resolves the virtual controllers by name.
  6. Test different Libretro cores rather than assuming one successful emulator proves the whole configuration.

The final post-reboot test covered lr-MAME, lr-FBNeo, lr-snes9x, and lr-VICE. We also used Bubble Bobble to verify that the X-mode controller was correctly translated and routed to Player 2.

The result

The finished system presents two dependable controllers to games:

  • Player 1 can use either the left side of the cabinet or the first 8BitDo.
  • Player 2 can use either the right side of the cabinet or the second 8BitDo.
  • Coin, Start, action buttons, sticks, triggers, and hotkeys have the same logical meaning regardless of their physical source.
  • Wireless sleep or reconnection does not renumber the virtual players.
  • Computer emulators retain a genuine keyboard.
  • The I-PAC’s RGB lighting continues to work in Dual DInput mode.

The broader lesson was that controller mapping is easiest to maintain when hardware quirks are normalized in one place. Instead of teaching every emulator about four physical devices, two I-PAC layouts, and two wireless protocols, we created one stable boundary: Arcade Merged Player 1 and Arcade Merged Player 2.

Everything above that boundary is now pleasantly boring—which is exactly what we wanted.