Eleven emulators on the head unit

The tile that froze the HMI, and nineteen runs to read a gamepad ended with one NES game and a controller that finally worked. That proved a native frontend could receive input. It did not make a game library. I wanted Rusty Nail, my small Rust launcher, to become the shell: systems on the card, an honest list of what each core could do, and one HOME path back through the factory HMI.

The target remained the MIB2 Standard in the Scirocco, an i.MX6 running QNX 6.5 behind an 800 by 480 screen. Rusty Nail runs as a bounded native process from the card. It reads a checked library snapshot, starts one emulator child at a time, and gives the controller back to the browser after the child exits. Audio and persistent saves remained off throughout this work.

That process boundary was the design that made expansion survivable: a core could own its ROM copy, graphics context and input reader for one run, then fail or return without leaving those objects for the next system to inherit, while the parent re-checked the card snapshot and remaining deadline before it admitted another child.

A browser before a launcher

The first QNX Rusty Nail build was deliberately browsing-only. Home showed systems and title counts from card metadata. A shelf opened a scrolling list; details showed the filename stem and recorded size. Empty shelves remained visible because the library should describe the card it has, rather than hide a system whenever content is absent.

The browser reads the card marker and metadata only. It rejects symlinked metadata and invalidates its snapshot if the card is removed, duplicated or its JSON changes. It does not open ROMs, BIOS files or saves while browsing. The controller rules were equally narrow: directions repeat, B opens, A backs out, and holding HOME for one second exits. A report expires after 200 ms, keeping stale controller state from crossing a pause or a core return and ensuring that the browser never carries an input history forward to a later child process. Analogue centring was left alone because I had no quiet-pad evidence for it.

That candidate passed 292 Rust tests, seven C adapter tests, staged-file hash checks and shell syntax checks. The owner then ran it on the MIB and described the browser as working smoothly. That is physical evidence for the visible browser and navigation. A native exit log and individual card-removal cases were still missing, so I did not turn that report into a full lifecycle claim.

One core at a time

Mega Drive came first. The browser required a second B press on a detail page before Play appeared, so the press that opened a title could not launch it. The core received a verified RAM copy of the selected cartridge after the browser checked its size and SHA-256 against the library. It received no writable ROM, BIOS or save path. HOME stopped the core and returned to details; another HOME hold in the browser exited the application.

The new core paths accumulated in separate processes: Mega Drive, SNES, GBA, FBNeo for Neo Geo and CPS-1, PlayStation, Game Boy, Game Boy Color, Master System, Game Gear, CHIP-8, PICO-8 and Amiga 500. They shared the launcher, verified-card discipline, Screen/EGL handoff and controller snapshot, while each kept its own content limits and lifecycle. A library entry did not make a system playable. It had to pass the selected core's admission checks, then a target run.

The host work was useful for finding interface faults before the car did. Three Mega Drive titles ran 600 frames each through the actual adapter. SNES owner games ran 1,200 frames across two lifecycles. Two GBA games ran 1,200 frames each, with hundreds of changed frames and tens of millions of non-black pixels. FBNeo ran Metal Slug and Final Fight in fresh processes, 1,200 frames each per process, because its upstream lifecycle did not support a reliable second initialisation in the same process.

The checks also had to reject bad media. A malformed archive, mismatched hash, missing BIOS member, symlinked path or occupied volatile child location stopped launch before the selected core received data; the browser became a gatekeeper for card state rather than a menu that hoped an emulator could recover later.

Those were smoke checks. They proved that each adapter could load a checked input, produce changing video and return through its expected cleanup path on the host. They did not measure the MIB's frame rate, prove every game or create an audio path.

Host core coverage and two separate native observations. Frame counts, loop rate and fresh callback rate measure different workloads and are not directly comparable.

The card receipt separates a build from a delivered launcher: the copy process checked the removable card marker, wrote only planned package files, read every one back by hash, retained the launch counter and verified the unrelated card population before and after the operation. That says the intended binaries reached the card without silently replacing content or factory-acquisition files. It does not say that each delivered core launched on the head unit, survived its whole lifecycle or achieved a given speed once it reached QNX.

The card grew with the ports. Twenty games covered fourteen requested systems plus Arcade/CPS-1. Two verified copy operations added 430,035,065 bytes while preserving 1,298 of 1,300 original non-Apple files byte-for-byte; only the library JSON and checksum list changed. A complete content inventory stayed separate from the browser's smaller active library until the reader could admit the larger disc and multi-file layouts safely.

The games that did run

The first physical runs mattered more than the host numbers. Metal Slug reached native gameplay, and a later captured run returned from it with HOME after 4,846 frames. Final Fight later recorded 6,281 frames in 106.016 seconds, about 59.2 loop frames per second, three overruns and a HOME return. That loop counter says the main loop kept moving for that workload. It does not establish audio, visual quality, full arcade compatibility or a steady display rate.

There were failures around those successes. Metal Slug first exposed a missing parent-name check in optional graphics-file lookups. The actual ARM binary reproduced the null pointer path, and a guarded check corrected it. Another run could return a child successfully while the browser behind it refused new controller presses. The root cause was a neutral-input gate that required non-button status bits to become zero. The corrected mask kept the real button bits, and the host fixture then required an actual fresh B press before it accepted browser recovery.

That correction passed 83 checks and was delivered with the other child binaries unchanged. The later native run provided the result I needed: the browser survived two N64 child aborts, launched six further children, accepted five HOME returns and eventually exited at its own time limit after 297 seconds. That run established working navigation after returns. It did not explain an earlier restart to the Volkswagen logo, and it did not establish a fresh HOME exit from the browser itself.

N64 was the useful failure

Mario 64 was slow enough that the answer could not be softened. The native run recorded 23 fresh video callbacks across 5.071 seconds: 4.54 fresh callbacks per second. The core accounted for 4,885 ms of that interval, 96.3 percent, while presentation accounted for 178 ms. It was a short observed workload, not a universal benchmark, yet it was enough to record N64 as a native performance failure.

I removed duplicate presents from the software path and retained individual timing records around each core call. That work avoided presenting an image twice when the core returned the same frame, but it did not claim a speedup. A GLES2 and HLE renderer then became the next candidate, with a QNX hardware-context contract, checked framebuffer ownership and a source-built runtime subset for the missing C++ and fortified-libc support symbols. None of that substituted a system library or treated a successful archive build as a game.

The diagnostic route passed on the unit. It created the selected GLES2 context, read back a GPU pattern, presented 58 frames, cleaned up with zero heap bytes and returned to the factory menu. That was a hardware lifecycle check. It did not exercise the game plugin.

The actual GPU game trial got farther than the software failure. Both Mario 64 and Mario Kart loaded their ROMs, reached graphics-ready and began the cached interpreter. Both then aborted after the plugin-start marker. The browser remained alive and accepted further launches. The marker is the last recorded phase, not an identified failing call, so no binary replacement followed it. Mario Kart's 172 pre-plugin callbacks in 5.009 seconds were kept out of the performance column because they do not show rendered gameplay or playable speed.

This narrowed the fault without solving it. The old ROM-load failure had been passed, the no-ROM diagnostic had exercised the chosen GPU lifecycle, and the game hosts reached their graphics handoff before aborting; the missing evidence was the call after that marker, so changing a renderer, runtime or C++ ABI component at random would have replaced one recorded failure with several unknown ones.

The UI kept improving while the emulator work was messy. Footer labels moved one logical pixel upward. The top-left square stopped being a static placeholder and ran Rusty Nail's existing demo jukebox. Startup scanning gained cooperative repaint checkpoints, so the opening dot moved while the card was enumerated instead of freezing until the scan finished. Host renders exercised those details; the next native run confirmed the layout and animation on the MIB.

The scan path deserved the same caution as the cores. It enumerated directories, hashed cartridges, checked disc companions and inspected ZIPs while an existing five-minute run deadline continued to count down. Repaint checkpoints were limited to one frame every 32 ms and stayed on the existing display owner, so the change did not introduce a render thread, a second input loop or a new concurrent card reader. A clock reversal, deadline expiry or display failure cancelled the scan; a latched redraw failure prevented a partial library from being published even when a media helper had already found a candidate. The host exercise showed a visible dot in frame zero, eight different images in the first second and 180 across the complete 16-second sequence, while the later native run recorded 410 presented frames during a 24-title scan.

Those counters need the same qualification as the game counters: they record calls accepted by the Rusty Nail browser after its synchronous scan checkpoints, not panel scanout, a guarantee that filesystem operations never block, proof of controller responsiveness during a busy device call, or an assertion that every library size will produce the same cadence. Their value was narrower and more useful: the original frozen first frame had become a changing native image while the browser continued its bounded discovery work, with cancellation preserving the prior index instead of exposing a half-built catalogue.

The evidence also preserved the limits of that recovery path. A forced termination did not establish clean graphics or USB teardown. A child could return zero while the browser still waited for a fresh neutral controller report. The card check noticed a removed card or altered JSON only on its two-second cadence, so a card removed and restored wholly between checks escaped that particular detector. ZIP and disc work remained bounded by the selected adapter rather than becoming a general filesystem service. The P4 panel was not part of this launcher path. Audio samples were generated by several cores and discarded. Save RAM started fresh or remained volatile. Each of those decisions reduced the chance that a game experiment would leave a durable surprise behind, but none substitutes for a physical run of the exact selected title, a captured native log and an observed return to the factory HMI.

By the end of 18th September, Rusty Nail was a real multi-system launcher with several delivered cores, observed navigation and selected native gameplay. N64 was explicitly failing on performance and then on a later graphics-plugin path. The next work was to preserve that distinction while expanding the library and isolating the abort rather than calling eleven emulators finished.