Building a Pinball Emulator
I wanted to find out how far I could get building a pinball emulator with AI. The hardest part was knowing when to believe it.
In fall 2025, I was figuring out screen sizes for virtual pinball and how to build physical flipper buttons. By late that year, I was exploring Linux SD-card images and trying to understand what it would take to run the software for a pinball machine. In early 2026, I was experimenting with QEMU and an early emulator prototype.
That curiosity grew into Kiki. What excited me was the chance to explore what AI could help me do with an unfamiliar system. Much of the discussion I was encountering was skeptical of its usefulness for work like this. I wanted to try it and see where it held up.
The opening clip shows Kiki's current implementation on my cabinet. This article looks back at its early QEMU development, when I was the sole human developer of the original Kiki code, working with Claude, Codex, and Binary Ninja. QEMU, Visual Pinball X, game and table content, and other dependencies have their own authors.
A pinball machine asks its software to coordinate a lot of things at once: displays, audio, switches, motors, lighting, and persistent settings. Those parts have expectations about each other. A request needs the right response, a read needs the right blocking behavior, and a buffer needs to stay alive until its consumer is finished.
Kiki's early QEMU prototype became an extended investigation of those expectations. The starting point was ARM Linux software and a development workstation. QEMU provided CPU execution; a custom shim modeled parts of the machine around it. The journal followed the work from initial inspection through device emulation, graphics and media plumbing, and tests that made the assumptions explicit.
How the work developed
- Fall 2025: exploring virtual-pinball displays and physical controls.
- Late 2025: learning about SD-image partitions and how to approach emulating the machine.
- Early 2026: experimenting with QEMU and prototype code.
- Mid-March: I was still working on the encrypted storage and did not yet have the LUKS key.
- Late March: the hardware arrived, including the PCIe reader, carrier board, and CPU. By the end of the month, I had obtained the key.
- Late April: Pokémon reached its logo and “Startup in Progress” screen under QEMU. Sounds repeated and startup appeared to stall; I was still figuring out the nodebus communication.
- Early May: Pokémon was further along, and I began generalizing the emulator. The goal expanded to other Spike 3 games.
- Mid-May: I had Pokémon LE 0.81.0 working. The source archive ends shortly before this milestone.
The storage investigation and hardware work came before the April–May source history in this archive. The first retained commit marks one part of a project that was already underway.
An early detour through Godot
Before I had the original Pokémon game working, I experimented with its extracted .pck file to create a basic playable version. Pokémon brought something new to the Stern games I was exploring: a connection to Godot. That opened an interesting route to trying things out before the full emulation was ready.
The PCK experiment and the emulator were separate efforts. One gave me a basic playable version to explore; the other needed to run the original software and reproduce the machine behavior it expected.
By late April, the QEMU version could show the logo and begin startup, but it was still a rough device model. I was using an LD_PRELOAD shim to intercept the devices I had found, with repetitive placeholder responses to hardware requests. The repeated sounds and stalled startup made the remaining work tangible: I needed to understand what the game expected from the nodebus and how those exchanges affected its progress.

Knowing when to push back
The difficult part was judging the conclusions the AI was drawing. A detailed explanation could sound convincing while resting on an assumption nobody had checked. I did substantial manual reverse engineering, brought in my own theories, and checked those explanations against the binary and runtime behavior.
One persistent AI memory note claimed that nonvolatile storage—the state the machine keeps between runs—was fully stubbed out. I pushed back. Checking the files showed that one store was writing successive records with timestamps that matched the runs. Other stores contained data but had older timestamps; whether their save paths had been reached was still an open question. The blanket claim had turned a specific uncertainty into a much larger, misleading problem.
Checking the evidence also meant questioning my own assumptions. Earlier, the instructions I saw in the disassembly did not match addresses supplied by the AI analysis. The file hashes matched: we were looking at the same binary, but the tools used different address bases. In that case, the AI's analysis had been right.
I needed to keep observations, interpretations, and unanswered questions separate. That discipline determined what to test next and when an explanation had earned enough confidence to build on.
The files were loading. Our logs couldn't see it.
At another point, the shim logged no scene-file opens. The investigation had treated that silence as evidence that the scenes were not being read, and kept expanding the file hooks to find the missing calls.
A lower-level trace of operating-system file access showed 34 distinct scene files opening during a two-minute run. The calls were taking paths our library hooks did not intercept. The missing evidence was a limitation of the instrument.
That changed the next question. Runtime instrumentation showed the game repeatedly selecting the same two scenes, which matched the frozen startup screen and repeated menu sounds. File loading was happening; the investigation needed to follow the scene-selection loop and the state keeping it there. This did not solve startup by itself, but it stopped the investigation from treating successful file access as a failure.
Performance changed the architecture
The first graphics approach put substantial rendering work inside the emulated environment. Measurements and experiments narrowed the problem enough to justify moving that work across a process boundary.
The later design marshalled graphics operations to a native host process. Media decoding also used host-side services. The guest could continue making familiar calls while the host did work that was expensive to perform through emulation.
- ARM software under QEMU
- Device and API shim
Graphics operations
Media packets
- Rendering and media services
- Desktop presentation
Moving work introduced new responsibilities: message formats, state synchronization, and resource identity. A rendering call in one process had to mean the same thing when decoded in another. That boundary gave us concrete things to check.
Returning to a result I trusted
Later experiments aimed at improving node-bus accuracy started breaking gameplay inputs. A change could make one run look cleaner, then leave Start or Action appearing held down or produce a flood of button events.
I pointed back to the last trusted input behavior, before that investigation had spread into the switch layer. The recovery used the actual known-good Git revision. The switch path was restored from it, and protocol experiments stayed behind a separate option. That gave further work a stable baseline without pretending the remaining node-bus questions were solved.
Making a hypothesis testable
By mid-May, the project had tests for device messages, graphics message layouts, and switch-state interpretation. These could run without launching a game, which made it easier to isolate a mistake from the rest of the emulator.
The graphics protocol was represented in both C and Python. Tests checked that operation identifiers and packed message sizes agreed. A separate device-protocol model gave the C implementation something concrete to compare against. For switch inputs, a small truth-table test pinned down how a raw bit and an inversion flag became the state seen by the game.
Those checks did not prove the whole emulator was correct. They made specific claims testable, and helped keep a plausible change from silently contradicting behavior we had already established. Game-specific addresses were also being moved into a machine profile, making those assumptions easier to find and inspect.
The journal had to be checked too
The record was useful only if its claims could be checked. During a video investigation, AI memory asserted that decoding had already been ruled out, citing two journal subsections that did not exist.
I had made checking the actual journal a rule. Following it exposed the invented references and led back to a working host-decoding path that had been deleted during refactoring. The path was recovered from Git. I also insisted that a failure to reach the host decoder be visible, because a silent fallback to slow CPU decoding could masquerade as an unrelated game-performance problem.
The restored path delivered decoded frames again. Getting them rendered and advancing past the startup menu remained separate problems. Keeping that distinction in the journal mattered: a successful component test should not become a claim that the whole game works.
A useful entry captured the observation, the hypothesis, the experiment, the result, and what remained unknown. Separate AI-agent reviews could challenge an interpretation, but agreement between tools still needed evidence behind it.


Looking back
I find myself wondering how much of this work a newer model could now accomplish from a much shorter prompt. That makes the code alone a less satisfying account of the project for me. The excitement was in finding out what was possible as I went, and learning enough to recognize when a plausible answer was wrong.
kiki-early contains the code and journal from this spring 2026 work, with MIT terms for my original retained work. It is an edited record of that investigation, intended for reading rather than running. The original commit dates are preserved. Some material is omitted, and the historical run results describe the implementation at the time, not this reduced snapshot.
Kiki has continued well beyond this version. If you're interested in working on the project, get in touch with me about the current work.
This is independent research, not an affiliated or endorsed project. This article describes the early QEMU work; it does not claim to document Kiki's current architecture.