LLM assisted source lifting and game porting (1): From compiled binary to re-created source code, a foundation for game modernization and preservation
This post is me sharing my knowledge and learnings so far on leveraging LLMs in reverse engineering (arcade) video games. The goal is re-creating their source code from their original binaries and making them work on the target x86 Linux/Windows platform. This combination of “source lifting” and game porting work establishes the foundation for improving upon the original games with bugfixes, modding support or remasters, making these games playable on modern and different hardware.
A more technical deep dive on how specifically LLMs were utilized in this project is provided by a separate technical writeup. This post focuses primarily on the higher level aspects of the project itself and the LLM usage in it.
A word on “LLM-assisted writing”
This post was written with the support of an LLM. However, this here, is actually me, the human writing this section to be transparent about it. I did review this post prior publishing and also tweaked several things by hand.
My goal is to share the ideas, principles and concepts that I applied in these projects. I believe this knowledge can benefit similar use-cases, and in general help others in figuring out how to make hopefully better use of LLMs in doing great and meaningful things.
LLMs didn’t make writing this post possible, but rather lowered the bar for me to spend that extra time and effort in getting this out. I hope the result, no matter how and who/what wrote it, is what counts and achieves on the knowledge sharing aspect. Because in the end, that’s what I believe does actually matter.
Motivation
I’ve been around the arcade games scene for ~20 years. Over time, reverse engineering, and an interest in the hardware that keeps old games running came along, and kept driving my personal and professional development until this day. I’ve wanted the source to some of those games for a long time. A small binary patch is one thing; changing a game in a substantial way, or bringing it to another platform, is quite another. Reconstructing an entire game by hand means repeating a huge amount of tedious work before you can even get to the change you wanted to make.
For me, the interesting possibilities include proper ports to other hardware platforms, widescreen/HD support, modding, fixes, and upgrades to games. Personally, I’d always wanted to play DDR 4thMIX PLUS at 60 frames per second instead of 30.
I’ve been experimenting with LLM-assisted source lifting on the following two games:
- Dance Dance Revolution EXTREME that runs on the System 573 Digital arcade platform
- beatmania IIDX 9th style that runs on PC-based arcade hardware
By source lifting I mean working from the original executable toward compilable source for the original program, rather than writing a new game that merely looks similar. The larger job that involves a significant portion of human driven design and cognition is game porting; reconstructing the missing source by utilizing LLMs closes the gap that would otherwise stop me from doing it.
What am I actually trying to preserve?
When I show people progress, the questions are usually not about the agent workflow. They’re about what actually runs. Is this original machine code in an emulator? A new recreation? Does the reconstructed source compile back to the original binary? How do I know the LLM hasn’t replaced something subtle with a plausible shortcut?
Those are fair questions that establish important distinctions. My target is compiled reconstructed game logic, with the platform dependencies adapted to the new runtime. An emulator can serve as the reference without being the thing that executes the port’s game logic. I call this source lifting and game porting, rather than using “decompilation” as shorthand for every possible approach.
I’m not requiring a byte-matching original binary before porting. Matching would offer stronger evidence about the generated machine code, and my checks are not a substitute for that guarantee. What I want is faithful game behavior: graphics, audio, input response, and gameplay that behave as expected. That still includes subtle timing and old quirks that players rely on. “It looks right in a screenshot” is nowhere near enough, and not the reason to stop and call this done.
The first major milestone I’m working toward is functional convergence: getting the game working end-to-end with as much agent autonomy as practical, while correcting the fidelity differences discovered. This happens either by the agent itself through the use of purpose built tooling or through some additional support by human driven QA feedback when the agentic tooling hits limitations. I think of this as “90/10 convergence”—a working state, not a measured percentage of reconstructed code, verified behavior, or bugs eliminated. It is a milestone, not the end of source lifting.
Eventually I’d like a DDR remaster that brings the System 573 games to Linux/Windows. For IIDX, I want a consistent CLI across versions, with cabinet options and native BIO2 support to make running the old versions on newer cabinets easier. Those are ambitions, not features I’m claiming to have finished. Deliberate upgrades need to remain separate from the baseline reconstruction.
Give the agent something that can prove it wrong
The most useful tool I built is something I would call a game-loop debugger. It isn’t a CPU-level debugger (like GDB): it lets the agent step frames, advance the game, dump video and audio buffers, and inject gameplay inputs. Those operations can also be scripted to create something reflecting repeatable intergration tests.
For the original games/references of DDR and IIDX, the setup looks like follows:
- DDR: Use MAME as the runtime reference, add the game-loop debugger controls using Lua scripting interface
- IIDX: Use the original software and run it with a modified version of bemanitools that implements the game-loop debugger controls
The same game-loop debugger controls were built into a runtime that the source lifted DDR code runs on. For IIDX, since the source lifted version is rebuilt on the same runtime basis (just a Win32 application) as the original, we can use the same modified bemanitools tooling.
That gives the agent a way to reproduce a sequence and compare results, rather than just tell me its code seems reasonable. Randomness can still make runs diverge, and an emulator is not an infallible substitute for the cabinet. Matching it tells me about the behavior tested under that setup, not every behavior on the original hardware.
I split the evidence into fidelity tiers:
- A0: covers static analysis and tests grounded in the original decompiled and disassembled code
- A1: compares controlled runs with a purpose-built game-loop debugger in the development environment
- A2 is the planned comparison against original hardware
Some targetted ordinary code review happens outside those tiers. The ledger records which evidence exists—it doesn’t make an unchecked function correct.
Consider an agent hardcoding a timing window that should be read from data. That could survive both a screenshot and ordinary gameplay on one chart. Following the original data access, then testing inputs that distinguish the two implementations, is much more useful than asking the agent whether it thinks the game is accurate. This is an example of the kind of mistake the checks need to expose, not a claim about a specific chart format.
The runtime feedback has caught most of the defects I’ve encountered so far, but I don’t have a measured detection percentage. I still intend to playtest on the cabinet and explore a blind A/B test against my original System 573/Bemani PC hardware. That work isn’t done yet. An experienced player can notice things the automated comparisons don’t observe, and manual reverse engineering can miss those same quirks too.
The question I keep coming back to is: is more complexity necessary, or can I get the required result another way? That’s an argument for choosing checks carefully, not for dismissing differences. It also changed how I approach setting up the development and testing environment and the agents themselves.
Start with a place for the code to live
Early on I was too willing to start lifting and solve the development and testing environment later. That makes every call into hardware or a library an invitation for the agent to “improvise”. On a PC target, it’s easy to get sidetracked by a USB I/O board or firmware download that the development setup is meant to substitute. On System 573, PlayStation facilities are only part of the story: there’s additional arcade hardware and the ways the game accesses it matter just as much.
Next time, I want to define the target compiler, libraries, runtime, reference material, and application-facing hardware boundaries before lifting game code. I want a tiny program to compile and run through the intended entry point first. That is the source-lifting sandbox: the known environment within which the lifted code should work. It isn’t a demand to implement all the hardware up front. A rough marker for a platform boundary is useful; building an FPGA emulator for DDR before the game has even reached its first screen is not.
I would also write two short documents at the start. One describes the game: its screens, controls, rules, and things that have to stay in sync, like DDR’s music, arrows, and player input. The other describes the target platform: its software facilities and the specific hardware interfaces the application can see. Emulator and substitute-runtime source can help establish those interfaces, but the game doesn’t need a copy of the emulator’s internals.
Follow the path the player can see
The first useful slice to lift is usually from the original entry point to the first visible warning or startup screen. It proves that there is a connected running program, not just a pile of compiling functions. After that, an operator/test menu is a good candidate: it exercises text, graphics, input, and many device interfaces without requiring a complete gameplay system which helps a lot in establishing all the hardware platform interfaces and abstraction before piling a lot of game flow/engine stuff on top of it.
The trick is to specify what that slice does not require. “No sound” is ambiguous: it might mean no playback yet, but the sound menu and its calls still have to be dealt with. “Pass the hardware checks for now” should mean connecting the game’s real call path to an explicitly temporary platform stub. It does not mean pretending that the device has been faithfully recreated. From there I’d take the normal game path in smaller steps: attract loop, start and mode selection, song selection, gameplay, then results and game over.
This changed the way I brief agents. “Implement the operator menu” leaves too much room for them to solve the wrong problem. “Lift the path along the tracer-bullet (see Hunt & Thomas’ “The Pragmatic Programmer”) that renders and navigates the operator menu; connect its device calls through platform seams; return provisional success for these checks; don’t implement playback or emulate the devices” gives both a finish line and a boundary
A working game is a milestone, not the finish line
The agent should do as much as it can with the game-loop debug tooling: connect the functional paths, exercise them, and fix the functional defects and differences from the original that it detects. I playtest more broadly and feed concrete problems back for investigation and correction to speed things up, or “help” the agent what they can’t detect because of tooling limitations.
That is the “90/10” division of effort I have in mind, not a requirement to find every last bug before a functional milestone can be useful. Nor is it permission to call a known fidelity gap solved.
Reaching that state would still leave a larger reconstruction job. Ultimately, I want all of the code lifted, including code ordinary gameplay never reaches. A successful session cannot show that every error path, alternate mode, or cold function has been reconstructed. Finishing a selected path is evidence toward convergence, not proof that the whole game has reached it.
Closing that remaining gap should involve more than Human QA. Possibilities include checking actual code completeness, revisiting TODOs and unverified accuracy claims, and assembling a repeatable set of scripted runtime scenarios. I have not established which combination is effective enough, or what evidence should justify calling source lifting “100% done” before moving into the next major phase. That is the next major milestone to define, not a solved final step in this workflow. The technical write-up discusses those open directions in more detail.
Watch the “work around the work”
Another lesson took longer to notice. I let agents build elaborate ways to record progress: status files, confidence levels, notes, and more rules for updating all of them. Some of that was useful, but a lot of a long session could turn into maintaining the machinery instead of lifting code or checking a running game. It looked busy until I asked what actually changed between the start and end of a bounded stretch of work.
Auditing those stretches helped. I looked for time spent on lifting, runtime checks, documentation, and agentic machinery, then inspected what the machinery was buying me. Often the question I really needed answered was much smaller: has this function been lifted, and has it been exercised in the connected runtime? I don’t want to mistake a tidy progress report for a working game again. The detailed write-up explains how I would inspect a session without claiming that one impression or one measurement applies to every run.
My setup made it possible to leave an agent working while I was away: OpenCode through OpenChamber on a remote machine, with phone access when something actually needed a decision. Lean-CTX helped with caching and context, and REkit with binary analysis.
I also ran into cross-project context drift when I used shared Lean-CTX knowledge across many projects; I disabled that part of my setup. That’s an account of my configuration, not a blanket verdict on the tool. Likewise, a goal that keeps a session running helps only when the work has a clear stopping condition and the agent knows which decisions belong to me (i.e. the OpenChamber Goal feature).
The reproducible part
I put the engineering approach in a separate technical write-up: how I distinguish source lifting from other approaches, what the fidelity tiers establish, how the game-loop debugger supplies feedback, and how to set up the sandbox and scope connected slices. It should be useful even if you’re lifting a different game and application type with different tools. I want to share enough for someone else to take the ideas, give them to an agent, and adapt them to their own platform—not pretend that my exact workflow files would work unchanged everywhere.
There’s still a lot to try: smaller handoffs between contexts, different models, perhaps local models, and possibly parallel slices once the first running path is solid. I haven’t established that those changes improve this workflow yet. For now I’d rather let one connected game path take another day than make concurrency another problem to debug.
If you’ve attempted something similar, I’d be interested in what worked, what failed, and especially how you kept hardware boundaries and fidelity claims honest. I believe that we all benefit from sharing each others methods and results to improve on reconstructing source code for more old games in order to preserve them.
Acknowledgements
Thanks to the following folks who have been around and clanking along with me so far:
- Batteryshark for getting me into game source lifting with LLMs and showing me the potential this got
- GravitySandwich for bouncing ideas around, troubleshooting, and helping improve the source lifting approach
- 水 -sui- for knowledge exchange and sharing their progress and learnings on their own LLM-assisted game re-creation project
Next in the series: Part 2: Reconstruction, fidelity, and an LLM-assisted workflow.