Retropie Console – Teaching an Arcade Panel to Explain Itself with RGB Lighting

When I created the arcade cabinet, I wanted light up buttons because I knew I wanted the lighting configuration to change with each platform being played.

I found RGBCommander online, which was deprecated and unsupported, meanwhile it worked on my console when I figured it out. During this process, I found another tool, LED Spicer, that I’m not going to use because I’m too far invested with RGBCommander, but if I ever upgrade the OS behind my RetroPie setup, I may consider switching.

RGBCommander has been great, I love it, but it’s also not the easiest to setup. The main configuration file is an XML file that contains thousands of lines for multiple systems, game-specific lighting, folder-level for multi-disc/disk games, etc. It can get mind boggling, and if you edit the file once, add a system and come back again, it can be painful to remember how the file works to then execute.

The panel we started with

The cabinet uses an Ultimarc I-PAC Ultimate I/O as its combined input and lighting interface. The control panel includes:

  • two illuminated joysticks;
  • eight RGB action buttons for each player;
  • separate Coin and Start buttons for each player;
  • a centrally mounted RGB trackball;
  • centre utility buttons labelled Exit, Mode, Menu, and Play; and
  • an additional illuminated Coin Insert output.

Each RGB control consumes three LED channels—red, green, and blue. The Ultimate I/O provides enough channels to address the panel controls individually, while RGB Commander changes their colours in response to EmulationStation system and game events.

The authoritative configuration is rgbcmdd.xml, installed on the cabinet at:

/usr/sbin/rgbcommander/rgbcmdd.xml

I have so many backups of this file it isn’t funny. That XML file is much more than a list of colours. It records the relationship between logical controls, physical positions, Ultimate I/O output pins, emulator processes, system profiles, and—in some cases—individual games.

First, understand what the button names really mean

The first trap was the model assuming that the numbered button names followed the panel from left to right. They do not, I had wired them up like this, one set for Player 1, the other for Player 2:

               TOP ROW
┌────────┬────────┬────────┬────────┐
│BUTTON2 │BUTTON4 │BUTTON6 │BUTTON8 │
├────────┼────────┼────────┼────────┤
│BUTTON1 │BUTTON3 │BUTTON5 │BUTTON7 │
└────────┴────────┴────────┴────────┘
              BOTTOM ROW

Odd-numbered buttons are on the bottom row. Even-numbered buttons are on the top row.

That means this apparently straightforward profile:

<control name="P1_BUTTON1" colour="Red"/>
<control name="P1_BUTTON2" colour="Blue"/>
<control name="P1_BUTTON3" colour="Green"/>
<control name="P1_BUTTON4" colour="Yellow"/>


appears physically as:
Top:     Blue   Yellow
Bottom:  Red    Green

This distinction became the foundation for every console profile. A colour list that looks correct in logical button order can be visibly wrong on the real panel.

We found that photographs were indispensable. An early PlayStation layout looked plausible in the XML but revealed the wrong physical placement when illuminated. A later ColecoVision calibration used deliberately distinct colours in each position, making the odd/even arrangement impossible to misread. Once one profile proved the physical map, we could apply the same understanding to both players and every other system.

I used photos, passed into the agent, so it could map the buttons, to the XML definition, to create confidence and build an understanding of how the physical world worked. This blew me away.

Player 2 had a wiring surprise

Player 1 uses the expected RGB channel order. Its joystick occupies pins 1, 2, and 3; Coin uses 4, 5, and 6; Start uses 7, 8, and 9; and the action buttons continue through pins 10 to 33.

The right side of the board is different. Player 2 is wired in BGR order, so its pin triplets are deliberately listed in reverse:

<control name="P2_BUTTON1" pin="75,74,73"/>
<control name="P2_BUTTON2" pin="78,77,76"/>

The same compensation applies to the other Player 2 buttons, joystick, Coin, and Start controls.

At first glance, descending pin numbers look like a configuration error waiting to be tidied up. In reality, they are the correction. Changing them to ascending order would swap red and blue on the entire Player 2 side.

While I knew this as I spent the time wiring up each button, the model behind the agent didn’t, so it had to learn the setup. It discovered this on its own as I provided images and it analyzed the XML.

The trackball introduced a smaller naming complication. Its real hardware name is TRACKBALL, connected to pins 40, 41, and 42. Older profiles also contain P1_TRACKBALL and P2_TRACKBALL aliases. Those aliases are useful for compatibility, but neither one is a substitute for the physical TRACKBALL entry. Removing the middle name would leave the actual illuminated trackball out of the profile.

Recreating familiar controllers on an arcade layout

The most recognizable profiles borrow the visual language of the original controller.

The PlayStation layout uses violet, lime, cyan, and red in the physical positions corresponding to Triangle, Circle, Cross, and Square. The first calibration exposed a placement error when I was programming the XML file manually; the corrected version now appears consistently on both player sides. PSP uses the same face-button arrangement but illuminates Player 1 only, reflecting the handheld’s single-player nature.

The SNES profile uses the familiar green, blue, yellow, and red Super Famicom/PAL palette. Dreamcast uses blue, yellow, green, and red, with subdued additional controls. Saturn uses the Japanese Model 2 pad palette: green, yellow, and blue across the primary bottom row, grey across the upper row, and a muted fourth position.

These profiles are not trying to turn the arcade buttons into perfect replicas. The shapes and spacing are different. Colour provides just enough recognition to make the translation intuitive.

Other systems benefit more from simplicity than authenticity:

  • Atari 2600 illuminates one action button.
  • Game Gear lights two bottom-row actions for Player 1, along with its Start and joystick treatment.
  • SG-1000 exposes two white action buttons for both players.
  • WonderSwan and WonderSwan Color use distinct two-button handheld palettes.
  • Vectrex presents two actions per player with matching cyan joysticks.
  • SuperGrafx illuminates only the first two bottom-row controls in red and blue.

Dark buttons are just as important as lit ones. If a system only needs two controls, six unlit buttons communicate that fact much faster than a diagram hidden in a manual.

Some profiles explain function rather than history

Not every emulated system has one canonical gamepad. Classic computers may use a joystick in one game, the keyboard in another, and a mouse in a third. Ports and Steam titles can vary even more widely.

For those collections, the lighting is intentionally practical:

  • pointer-first systems such as Macintosh, ScummVM, ResidualVM, and Z-machine illuminate the physical trackball and likely click actions;
  • two-button computer profiles illuminate the first two bottom-row actions and the joysticks;
  • the broad PC profile exposes six coloured actions per player, both joysticks, and the trackball;
  • controller-driven engines use a conventional four-colour action layout; and
  • variable collections such as Ports and Steam use broad diagnostic profiles rather than pretending that every game shares one controller.

This is an important design boundary. The panel should be honest. When the exact layout varies by game, the lights can identify the available control families without claiming more certainty than we have.

Branding can be useful too

The ZX Spectrum profile is a deliberate exception to the usual mirrored Player 1 and Player 2 treatment.

Both gameplay sides use the same red, green, yellow, and blue action arrangement, but the Coin and Start controls form the classic Spectrum colour sequence:

ControlColour
Player 1 CoinRed
Player 1 StartYellow
Player 2 CoinGreen
Player 2 StartBlue

Both joysticks remain dim.

Functionally, the two sides still behave consistently. Visually, the upper controls become a small piece of system branding. Mirroring the Player 1 colours onto Player 2 would be more symmetrical but would erase the point of the design.

System-wide profiles are not always enough

RGB Commander can select a profile from the EmulationStation system name, but some games need more specific treatment.

Arcade collections are the obvious example. MAME games can have ROM-specific button mappings, so the panel can illuminate the controls used by an individual title and fall back to the shared arcade profile when no game-specific definition exists.

SuperGrafx revealed a different version of the same problem. It has its own two-button system profile, but five SuperGrafx titles also appear as per-game emulator routes inside the PC Engine collection: 1941, Aldynes, Battle Ace, Dai Makaimura, and Madou Ou Granzort.

We added matching ROM-specific entries to the PC Engine lighting profile. Those games now receive the red-and-blue SuperGrafx layout regardless of which collection launches them, while ordinary PC Engine titles retain their normal lighting.

The lesson was that the menu folder is not always the whole story. Lighting must sometimes follow the actual game or emulator route rather than the collection label alone.

Lighting must never prevent a game from starting

RGB Commander needs to identify the process that actually runs an emulator. Most Libretro systems eventually launch retroarch. Our Amiga configurations use standalone Amiberry.

The original Amiga lighting entries could leave RGB Commander waiting for the wrong process. Repeated launches filled its message queue, and RetroPie’s start hook could block before Amiberry appeared. A feature intended to clarify the controls was now capable of stopping the game entirely.

We corrected both sides of the failure:

  1. The Amiga and Amiga CD32 profiles now identify amiberry as their emulator process.
  2. The launch hook gives RGB Commander two seconds and then continues regardless:
timeout --signal=TERM --kill-after=1s 2s \
  rgbparse "$1" "$3" > /dev/null 2>&1 || true

The first correction makes the lighting work. The second establishes the right priority: lighting is helpful, but gameplay is essential. A lighting-service failure must never strand the user outside the emulator.

We validated the correction with A500, A500+, A1200, CD32, and CDTV launches, as well as ordinary Libretro games.

Preserving RGB Commander in Dual DInput mode

The I-PAC also provides the cabinet controls. We keep it in Dual DInput mode so it exposes two independent panel gamepads rather than pretending to be a keyboard or collapsing the cabinet into one Xbox-style player.

RGB Commander 0.4.0.5 recognizes the I-PAC’s older keyboard-mode USB product ID, but not the product ID used in Dual DInput mode. Returning to keyboard mode would have fixed the lights by breaking the input design.

Instead, we created a narrowly scoped compatibility library that is preloaded only into the RGB Commander service. For the Dual DInput device, it changes the product ID reported to that one process. It does not change the real USB descriptor, firmware, input events, or LED protocol, and no other application sees the substitution.

This lets the input and lighting systems remain independent. The virtual gamepad merger handles gameplay controls, while RGB Commander continues to address the physical Ultimate I/O directly.

Our editing and test routine

A lighting change is not complete when the XML looks right. Our normal process is:

  1. Confirm the physical row and logical button number.
  2. Define both players for a two-player system, or intentionally limit a handheld to Player 1.
  3. Preserve Player 2’s reversed BGR pin order.
  4. Decide explicitly whether unused controls should be dim or fully off.
  5. Preserve the real TRACKBALL output when compatibility aliases are present.
  6. Validate the XML before installation.
  7. Back up the live configuration.
  8. Restart RGB Commander and inspect its service log.
  9. Launch a Libretro game and a standalone Amiberry game.
  10. Compare the illuminated panel with the intended physical layout—preferably with a photograph.

The XML is validated before installation with:

xmllint --noout rgbcmdd.xml

After installation, RGB Commander should be active and the emulator launch should never pause waiting for it.

We keep the live XML, a copy beside the full-drive backup, and a working copy on another computer. The file is small, but it represents hours of physical tracing, calibration, system research, and design decisions. Losing it would be more like losing part of the wiring diagram than losing a cosmetic preference.

The result

In the front end, the cabinet can still be playful. The Player 1 joystick is associated with red, Player 2 with blue, and the joystick lighting animates while the buttons display different colours ready for action.

Launch a game and the purpose changes. A one-button system becomes visually simple. A PlayStation game presents a familiar four-colour arrangement. A pointer-oriented computer brings the trackball into focus. A two-player system mirrors the useful controls across both sides. Unused buttons disappear.

The panel no longer asks a new player to understand eight identical glowing buttons. It offers a suggestion and provides guidance before the game even starts.

That is the difference between lighting a control panel and teaching it to explain itself.

Retropie Console – Scraping in the new world, when scrapers fail you

I am an advocate for gaming history and archiving – both retro and modern. I love the fact we have places like archive.org for preserving BIOS and ROM sets for many systems and consoles.

Artwork is also incredibly important. There are a number of good sources: ScreenScraper is one I have supported and used for a long time, and there are also data and artwork collections at places such as Lemon64 and LaunchBox.

Sometimes collecting the metadata is easy. The scraper software in RetroPie is good: it connects us to these services and helps match the games in a library. But what happens when filenames contain all the extra characters and classification tags commonly found in archives such as TOSEC? What happens when several releases have almost the same name, or when a technically successful match is simply the wrong game?

We are living through exciting, interesting, and exceptional times in computing, especially with the advent of artificial intelligence, the rise of large language models, and agents such as Codex.

I can hear your eyeroll – please stick with me and hear me out on this one.

My thoughts on AI generally

I have been heavily involved in the AI space for the past two years. I have participated in the development of LLM-based solutions, I have a good grasp of how to use an LLM as a tool, and I recognize the importance of keeping a human in the loop.

I have enough experience with prompt engineering to direct an agent toward what I want, recognize when it has misunderstood me, and steer the work back on course. Understanding the limitations is just as important as understanding the capabilities. I firmly believe that people who develop real experience with AI will be better placed to succeed in the future.

The difference between the amount of work I can complete in a day with and without an LLM is astounding.

I will write more about my thoughts on AI, and I have several related ideas percolating for my YouTube channel. I understand the fears—very much so. I also shake my head when I see these tools being used carelessly, or judged only by the results of using them carelessly.

I use LLMs and agents as a way to improve my own knowledge and capabilities:

  • I have learned games development at a deeper level for the Playdate and Adafruit MacroPad platforms
  • I use LLMs to help refine emails and make them sound more professional. I never simply use the generated text verbatim; I merge useful ideas into my own version.
  • I have been learning to manage Linux more effectively through work on my homelab and this arcade cabinet.
  • I have used agents to help debug difficult issues, especially by analyzing logs that might otherwise have taken me hours to correlate—for example, determining the exact cause of a Windows 11 blue screen.

This RetroPie metadata project is another example, but it goes beyond asking a chatbot a question or requesting a short script. All of the supporting code was developed through agentic work under my direction.

What “written using AI” means in this project

I described the problem, the behavior I wanted, and the risks I was unwilling to accept. The agent helped inspect the existing cabinet, wrote the tools, ran read-only analyses, interpreted the resulting reports, and refined the implementation as we encountered real-world edge cases.

I actually thought this was risky – giving the agent access to my fully curated 3.6Tb image to clean up scraping. I had full backups – two digital copies and a copy of my SDD on a hard drive, so I was prepared to fail. After all, you learn by failing and I’ve learned to fully embrace that.

My direction established the important rules:

  • inspect the current library before changing it;
  • preserve good work that already exists locally;
  • never treat a fuzzy match as a fact;
  • keep scraper access and credentials separate from the maintenance toolkit;
  • make the default behavior read-only;
  • require an explicit command before modifying anything;
  • back up a gamelist before replacing it; and
  • produce reports that a person can inspect at every stage.

The agent turned those requirements into a collection of working Python tools behind a single retropie-metadata command. It was developed iteratively: run an audit, examine what the real collection revealed, adjust the logic, and test again. The code was generated agentically, but the objective, constraints, acceptance decisions, and definition of “correct” remained mine.

That distinction matters. I did not ask an AI to decide what every game was and then blindly rewrite the cabinet. I used an agent to help build a system that makes ambiguity visible and gives me control over the decision.

In other words, the coding agent is handled much like the scraper itself: extremely capable, very useful, and always subject to verification.

Back to scraping

A well-organized RetroPie library looks effortless. Every title has the right name, description, artwork, and launch path. Browse the menu and everything appears to belong exactly where it is. That has been my dream with this project.

Maintaining that illusion is harder than creating it. LOL

Our arcade cabinet contains dozens of console, computer, handheld, arcade, and specialist systems. Over time, games move, filenames change, emulator collections are reorganized, and metadata comes from several generations of tools. A missing screenshot is easy to notice. A plausible-looking but incorrect match is much more dangerous.

The obvious answer is to run a scraper again. I deliberately chose a different approach.

Rather than allowing a scraper to rewrite the library, we treat it as an untrusted source of suggestions. The cabinet is audited first, existing local work is recovered wherever possible, external results are staged and scored, and only reviewed matches are allowed into a gamelist.

The result is slower than clicking “scrape everything”, but vastly easier to understand, verify, and repair.

Why a successful scrape can still be wrong

A scraper is good at finding likely matches. It cannot always know what a file means in a particular collection.

Several situations can produce a confident but incorrect result:

  • regional releases with similar names;
  • translated, hacked, prototype, or alternate versions;
  • disc and cartridge editions of the same game;
  • titles with punctuation, subtitles, or filename tags;
  • computer software whose filename describes the media rather than the game;
  • MAME short names that require a separate name map; and
  • custom launch entries that look like games but actually invoke a script or mounted image.

A bad match is not necessarily obvious. The title may look reasonable and the artwork may even be from the same series. Once that result is written into gamelist.xml, it becomes difficult to remember which fields were carefully curated and which ones came from an automated guess.

There is also more to library health than missing artwork. A gamelist can contain a beautiful entry whose ROM path no longer exists. The filesystem can contain media that no game references. Two copies of a gamelist can disagree. A scraper cache can hold useful data that was never merged—or stale data that should never be merged.

We needed a process that could distinguish all of those conditions.

Our rule: audit before acquisition

The metadata workflow is coordinated by a cabinet tool called retropie-metadata. It does not contact ScreenScraper, store scraper credentials, or download media. Its job is to understand the local library and control what happens to external results.

The first step is verification:

cd /home/pi/mame-organizer/toolkit
./retropie-metadata verify
--output work/verification.json

This checks every known copy of each system’s gamelist.xml and verifies the files referenced by those lists. We want to know whether the current library is internally consistent before introducing any new data.

Next, we identify what is actually missing.

./retropie-metadata audit
--output-tsv work/missing.tsv
--output-summary work/missing-summary.json

The detailed TSV is intended for inspection and filtering. The JSON summary gives us a compact view of the scope. Separating those outputs is useful: the summary tells us whether we have a small repair or a collection-wide problem, while the tabular report tells us which individual entries require attention.

At this stage nothing has changed. That is an important feature, not an incomplete operation.

Recover local work before downloading replacements

Old gamelists, scraper caches, and media directories often contain perfectly usable information. A game that is missing a description in the active list may already have one in another trustworthy local source. Artwork may exist on disk under an older path. Reusing that work is safer than asking a remote service to identify the game again.

We search for recoverable local material.

This produces a recovery audit rather than silently inserting the results. We inspect the proposed sources, confirm that they refer to the same games, and only then repeat the operation with --apply.

That order gives local evidence priority:

  1. Preserve correct metadata that has already been curated.
  2. Reconnect media that already exists.
  3. Resolve stale paths where the intended target is clear.
  4. Send only the genuinely unresolved entries to the external scraping stage.

It also avoids unnecessary downloads and reduces the number of ambiguous matches that need human attention later.

Treat external scraping as an import operation

Once local recovery is complete, the unresolved report becomes the input to a preparation step:

./retropie-metadata prepare-scrape
--missing work/missing.tsv
--review work/local-recovery/local-recovery-audit.csv
--output-dir work/scraper-staging

Preparation creates normalized titles, mappings, and target lists. It still does not contact the network. The point is to make the request explicit before another tool sees it.

This is where known naming exceptions belong. The cabinet’s Skyscraper configuration preserves title aliases, platform corrections, MAME mappings, matching hints, targeted inclusion lists, and records of deliberately skipped items. Those files explain the collection’s intent in a repeatable form.

Aliases and hints are better than manually correcting the same title after every future scrape. They turn one-off knowledge into part of the maintenance system.

The external scraper operates outside our metadata tool. Its returned cache is imported or placed under the staging directory. We then analyze the results without merging them:

./retropie-metadata review-scrape
--staging work/scraper-staging
--output-dir work/scraper-review

The review stage scores candidate matches and produces material for human inspection. A score helps prioritize the work, but it is not permission to modify the library. We still review the normalized name, platform, proposed match, and any ambiguity before approval.

If the results look wrong, the response is not to accept them and clean up afterward. We stop, improve the aliases or hints, and repeat preparation and review.

Merge approved results, and nothing else.

Approved candidates are passed to the merge stage:

./retropie-metadata merge-scrape
--staging work/scraper-staging
--review-dir work/scraper-review
--output-dir work/scraper-merge

Even this is a dry run. The generated merge can be inspected before it reaches an active gamelist. Only after the approved output is correct do we add --apply.

All modifying operations require an explicit --apply or --execute. Gamelist updates are written atomically and receive timestamped backups. A partial write should never be able to leave the front end with half a valid XML file, and a newly discovered problem should have a clear rollback point.

We also exit EmulationStation before applying changes. The front end reads and may write its own metadata, so editing the same gamelists while it is running creates an avoidable ownership conflict.

The operating rule is simple:

Generation is automatic. Acceptance is deliberate.

Audit the media as well as the XML

A cleanly parsed gamelist.xml does not prove that the library is clean. The final audit includes the media directories:

./retropie-metadata audit-media
--output-dir work/media-audit

This lets us separate several different states that are often collapsed into “missing artwork”:

  • missing: the gamelist expects media that is not present;
  • reusable: suitable media already exists locally but is not connected to the entry;
  • orphaned: a media file exists but no gamelist entry references it; and
  • uncatalogued: content exists outside the expected catalogue relationship and needs review.

Those categories lead to different actions. A missing image may need to be sourced. A reusable image needs a corrected reference. An orphan may be left over from a renamed game, or it may be the only surviving copy of wanted artwork. Automatically deleting every orphan would throw away evidence before we understood it.

Stale game paths receive their own analysis:

./retropie-metadata analyze-stale
--verification work/verification.json
--output-tsv work/stale.tsv
--output-summary work/stale.json

This is especially valuable after reorganizing a ROM collection. Metadata should follow intentional moves, but an unresolved path should remain visible rather than being guessed into a similarly named file.

Verify again before declaring success

After approved changes are applied, we run the verification step again. The second pass should demonstrate that:

  • the gamelists still parse correctly;
  • referenced games and media exist;
  • the intended missing fields were filled;
  • no unrelated entries were replaced;
  • repaired paths point to the correct files; and
  • the remaining exceptions are understood and documented.

Only then do we restart EmulationStation and inspect the results in the interface.

This gives the process a useful shape:

verify
  ↓
audit what is missing
  ↓
recover trustworthy local data
  ↓
prepare unresolved targets
  ↓
scrape into staging
  ↓
score and review candidates
  ↓
preview the approved merge
  ↓
apply
  ↓
verify again

Every arrow leaves behind a report. If something unexpected appears at the end, we can trace where it entered the process instead of trying to reconstruct a single opaque scraper run.

Why we do not start over with a clean cache

When scraper results become confusing, deleting the cache and starting again is tempting. For a large library, that is an expensive and sometimes destructive reflex.

A cache may contain years of valid results, hand-resolved titles, and media that is no longer easy to retrieve. Rebuilding it also does not correct a bad alias, an incorrect platform, or an ambiguous filename. Without fixing the underlying interpretation, a clean scrape may simply reproduce the same mistake at a larger scale.

We preserve the existing cache and review reports when making targeted corrections. A clean-slate scrape is considered only when it is genuinely necessary, and its completion is verified before it is allowed to generate a replacement gamelist.

What changed for us

The biggest improvement was not better artwork. It was confidence.

We can now answer questions that a one-click workflow obscures:

  • Which entries were incomplete before the run?
  • Which fields came from existing local data?
  • Which games were sent to the external scraper?
  • What match did the scraper propose?
  • Why was that match approved?
  • Which files changed when the merge was applied?
  • Does the final gamelist still point to real games and media?

The scraper remains useful. It is simply no longer trusted with the final decision.

The same principle applies to the AI agent that helped me build the toolkit. Agentic development made it possible to inspect a large, messy collection and create a purpose-built set of tools in a fraction of the time it would otherwise have taken me. That speed did not remove the need for judgment. It made good guardrails even more important.

Human-in-the-loop cannot mean clicking “yes” at the end of a process nobody understands. In this project it means setting the rules, examining intermediate evidence, challenging suspicious output, refining the implementation, and deciding what is allowed to become part of the live collection.

That distinction scales well beyond RetroPie. Any automated system—whether it is matching a game or writing the code that performs the match—can produce a convincing mistake. The safest workflow separates discovery from authority: let automation find possibilities, preserve the evidence, test what it produces, and require an explicit review before those possibilities become reality.

For our cabinet, that combination of agentic development and human control turned metadata maintenance from a risky rewrite into a controlled, repeatable repair process. It is also a practical example of why I am optimistic about AI: not because it replaces expertise, curiosity, or responsibility, but because it can amplify all three when it is directed thoughtfully.

I was very surprised at how effective using an agent was for this process. The fact it was able to identify and pull box artwork rather than, say, a marquee – which was a problem I had during my initial scraping attempts.

Despite what I said earlier about “The result is slower than clicking scrape everything”, I think this process actually ended up being faster. Why? I’m not repeatedly running a scraper constantly against Screenscraper.fr and hoping for the best; when even the scraper fails, I could then ask the agent to search other databases to find the correct art and details and it did perfectly because it had clear data to act on.

All under my control and guidance, with a human in the loop.

I plan on making the tools available on Github as an example that someone could augment and update.

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.