23 KiB
Docker/QEMU Emulation Scaffold
Scope
Workspace-local implementation:
emulation/
Purpose:
- Build a Docker-packaged QEMU environment for booting a generated copy of the extracted AArch64 rootfs.
- Expose the guest display through QEMU VNC and noVNC in a browser.
- Provide a browser dashboard for logs and early virtual switch events.
- Keep target rootfs evidence files unmodified.
Static-analysis boundary: this page documents the emulator scaffold and evidence-based design. Creating and booting emulation/work/rootfs.ext4 executes a generated guest copy, not the extracted evidence files directly.
Current Status
This page primarily documents the older Docker/VNC analysis scaffold. That
path still provides a useful browser framebuffer and diagnostics, but it is not
the gameplay path. The current playable implementation runs QEMU natively with
HVF, Cocoa, and CoreAudio and joins it to a native VPX table; see
native-playable-table.md.
The native path now produces a genuine 1920x1080 Pokémon game frame with the llvmpipe compatibility stack. The generated guest and netbridge are connected end to end: VPX contacts enter the same node model consumed by the real game, and real lamp/driver writes return to VPX through the supervised playfield protocol. This does not make the emulator an exact electrical model of every SPIKE board.
| Layer | Status | Evidence / remaining issue |
|---|---|---|
| Generated guest boot | Working | QEMU reaches SPIKE3 emulator init complete; see emulation/work/logs/qemu-serial.log. |
| Browser access | Working | noVNC and the dashboard respond, and capture-vnc.py records nonblack 1360x768 frames. |
| DRM device discovery | Working | virtio_gpu creates /dev/dri/card0, /dev/dri/renderD128, and fb0; the guest provides the target-compatible card1 alias. |
| Mesa EGL/GLES initialization | Working with compatibility overlay | The native probe reaches EGL and OpenGL ES 3.1 with Mesa softpipe; fbdev mode reaches a real surfaceless display and pbuffer. |
| Native target display | Working software-rendered baseline | HVF/Cocoa plus Debian llvmpipe produced a complete 1920x1080 game frame. This is CPU rendering, not Apple-GPU acceleration. |
| Native audio | Working baseline | QEMU HDA/CoreAudio carries the real game audio; VPX supplies only mechanical table sounds. |
| Netbridge/nodebus | Working playable subset | The real game consumes VPX cabinet/playfield contacts and publishes mapped lamp/driver output. The model is not a complete electrical reproduction of every node. |
| Switch dashboard | Diagnostic only | The browser remains optional; native VPX is the authoritative gameplay source when physics_authority=vpx. |
| Video decode | Working compatibility path | The host VideoToolbox bridge is preferred on macOS, with the existing guest software decoder retained as fallback. |
Remaining fidelity boundary
The extracted software assumes Raspberry Pi graphics and I/O while QEMU exposes generic VirtIO devices. The working native baseline therefore retains a narrow compatibility overlay and CPU renderer. Docker/VNC can still hit the historical EGLImage/FBO fallback path even though Cocoa displays the real game.
Several machine subsystems remain behavioral models rather than electrical replicas: the RP2040 netbridge, SPIKE nodes, TAS/MAX amplifiers, cabinet buses, and video decode devices. VPX supplies the missing physical playfield, but its ramp elevations, forces, and materials remain evidence-tagged estimates where the manual has no direct measurement.
Evidence Basis
The main game and SPIKE menu use a direct DRM/KMS display path rather than a desktop windowing path.
Confirmed string/import evidence:
/games/pokemon_pro/gameimportslibEGL.so.1,libGLESv2.so.2,libdrm.so.2, andlibgbm.so.1; seeanalysis/static-triage/game.objdump-p.txt./games/pokemon_pro/gamestrings include/sys/class/drm,/dev/dri/card1,/dev/dri/card0,drmModeSetCrtc fail,drmModePageFlip,gbm_bo_create,eglGetPlatformDisplayEXT, andegl resolution: %d x %d; seeanalysis/static-triage/game.strings./games/pokemon_pro/spike3/spike_menu/gamehas matching GBM/EGL/DRM strings at offsets including0x382cd0(/sys/class/drm),0x383030(/dev/dri/card1),0x383058(/dev/dri/card0), and0x383730(drmModeSetCrtc fail); seeanalysis/static-triage/spike_menu_game.strings./etc/init.d/S12lvds_panellaunches/games/spike3/bin/boot_displayearly, and/etc/init.d/game_monitorlater stopsboot_displaybefore launching/games/game.
Implementation consequence: both paths use QEMU virtio-gpu, not X11
forwarding. Docker exposes VNC/noVNC for analysis; playable macOS uses native
Cocoa directly.
Implemented Files
Primary files:
| Path | Purpose |
|---|---|
emulation/compose.yaml |
Docker Compose entry point and port mapping. |
emulation/docker/Dockerfile |
Debian-based image with QEMU, noVNC, ffmpeg, qemu-user-static, and AArch64 cross tools. |
emulation/docker/fetch-debian-arm64-kernel.sh |
Installs a Debian ARM64 kernel and initramfs into /opt/spike3-kernel/; the initramfs seeds DRM/virtio_gpu modules. |
emulation/scripts/prepare-rootfs-image.sh |
Copies target-root entries into emulation/work/rootfs-stage, injects helpers, and builds emulation/work/rootfs.ext4. |
emulation/scripts/run-qemu.sh |
Boots QEMU virt with virtio-blk, virtio-gpu, VNC, serial logging, user networking, and a virtio-serial control socket. |
emulation/guest/emu-init |
Guest init used by QEMU with init=/usr/local/spike-emu/bin/emu-init. |
emulation/dashboard/server.py |
Browser dashboard on port 8090 for links, logs, and virtual switch events. |
emulation/stubs/spike3emu_stub.c |
Optional AArch64 LD_PRELOAD stub for inert fake device opens/ioctls on GPIO, I2C, serial, input, and /dev/mem paths. |
analysis/netbridge-emulator/netbridge_emu.py |
Optional host-side netbridge emulator used when SPIKE3_NETBRIDGE_EMU=1. |
Generated runtime files:
| Path | Purpose |
|---|---|
emulation/work/rootfs.ext4 |
Generated guest root disk; not checked in as evidence. |
emulation/work/logs/ |
Host/container logs for QEMU, dashboard, noVNC, and conagent backend. |
emulation/work/control.sock |
QEMU virtio-serial socket used by the dashboard event bridge. |
emulation/work/netbridge.sock |
Optional QEMU socket backend for the Python netbridge emulator. |
Usage
Start the default boot-display target:
docker compose -f emulation/compose.yaml up --build
Open:
- Dashboard:
http://localhost:8090/ - Display:
http://localhost:6080/vnc.html?host=localhost&port=6080&autoconnect=true - Conagent backend emulator:
http://localhost:8088/
Try later targets after the virtual display is visible:
SPIKE3_AUTOSTART=spike-menu docker compose -f emulation/compose.yaml up
SPIKE3_AUTOSTART=game docker compose -f emulation/compose.yaml up
SPIKE3_AUTOSTART=monitors docker compose -f emulation/compose.yaml up
Run the main game with the Python netbridge emulator attached:
SPIKE3_NETBRIDGE_EMU=1 SPIKE3_AUTOSTART=game docker compose -f emulation/compose.yaml up --build
This starts analysis/netbridge-emulator/netbridge_emu.py on emulation/work/netbridge.sock, attaches it as a second QEMU PL011 serial port, maps guest /dev/ttyAMA5 to that serial path, and tells the preload shim not to fake /dev/ttyAMA5.
The guest init accepts these autostart targets:
boot-displayspike-menugamemonitorsshell
Current Emulator Behavior
The generated guest image:
- Mounts
/proc,/sys,/dev,/dev/pts,/run, and/tmp. - Creates writable
/connectivity,/connectivity/dump,/data, and/tmp/cache/mesa. - Preserves
/games/game -> /games/pokemon_pro/game. - Injects
/usr/local/spike-emu/bin/launch-target.shto startboot_display, SPIKE menu, game, or monitors. - Injects an optional
LD_PRELOADhardware stub at/usr/local/spike-emu/lib/libspike3emu_stub.sowhen the Docker image hasaarch64-linux-gnu-gcc. - Injects overlay Mesa DRI fallback drivers under
/usr/local/spike-emu/mesa-driwhen the Docker image has ARM64virtio_gpu,kms_swrast,swrast, andzinkdrivers. - Injects matching ARM64 Debian Mesa/GLVND EGL libraries under
/usr/local/spike-emu/mesa-libplus/usr/share/glvnd/egl_vendor.d/50_mesa.json, so the fallback DRI drivers are loaded by the same Mesa userspace stack instead of the target rootfslibEGL. The overlay intentionally does not shadow the target rootfslibstdc++.so.6; Debian bookworm's copy lacksGLIBCXX_3.4.31/GLIBCXX_3.4.32, which/games/gamerequires. - Injects ARM64 Mesa Vulkan/lavapipe support when present:
mesa-vulkan-drivers:arm64,libvulkan.so*,libvulkan_lvp.so, and/usr/share/vulkan/icd.d/lvp_icd.aarch64.json. - Injects
/usr/local/spike-emu/bin/display-probe, an AArch64 helper compiled fromemulation/guest-tools/display-probe.c, to test the guest GBM/EGL path before target launch. - Supports
SPIKE3_DISPLAY_BACKEND=auto|native|fbdev.autorecords the probe result under/run/spike-emu/display-backend.envand currently selectsfbdevpresentation by default;nativeis still available for forced comparison. - Supports optional
SPIKE3_NETBRIDGE_EMU=1. In that mode the container starts the Python netbridge emulator, QEMU adds a second PL011 serial backed byemulation/work/netbridge.sock, guest setup symlinks/dev/ttyAMA5to/dev/ttyAMA1, and the preload shim passes/dev/ttyAMA5through instead of opening/dev/null. - In
fbdevfallback mode,emulation/stubs/spike3emu_stub.cuses real Mesa surfaceless/pbuffer rendering when possible, supplies fake-but-consistent EGL/GBM/EGLImage objects where needed, treats final DRM framebuffer/page-flip calls as successful, and writes visible frames to/dev/fb0. - In fallback mode, the shim now also interposes enough GLES object setup for boot-display and SPIKE menu to get past their framebuffer-completeness path: synthetic GLES identity/limits, shader/program calls, framebuffer/renderbuffer/texture calls, and
/sys/class/gpio/*fake opens/writes. - Injects a generated
/etc/asound.confthat maps ALSAdefaultto the null PCM, clearing the prior missing-default-PCM abort inside the emulator copy. - Injects
/usr/local/spike-emu/bin/fb-message, compiled fromemulation/guest-tools/fb-message.c, so target exits can paint a useful diagnostic screen to/dev/fb0. - Starts a guest control agent that logs virtio-serial dashboard events to
/run/spike-emu/events.logand/connectivity/dump/log/spike-emu/control-agent.log. - Creates a cache manifest at
emulation/work/rootfs.manifestand cache status atemulation/work/rootfs-cache.status.SPIKE3_PREPARE_MODE=autoreusesemulation/work/rootfs.ext4when inputs match.
The dashboard:
- Links to noVNC and the existing conagent backend emulator.
- Reads host logs from
emulation/work/logs/. - Reports rootfs cache status and requested display backend from
/api/status. - Sends virtual switch/button events as JSON to
emulation/work/control.sockwhen QEMU is running. Verified APIs arePOST /api/eventandPOST /api/switch/<name>.
Verification Status
Commands run from the workspace root:
docker compose -f emulation/compose.yaml build spike3-emu
docker compose -f emulation/compose.yaml run --rm --entrypoint /workspace/emulation/scripts/prepare-rootfs-image.sh spike3-emu
SPIKE3_PREPARE_MODE=never SPIKE3_AUTOSTART=boot-display docker compose -f emulation/compose.yaml up --force-recreate
Confirmed results:
- Docker image contains
/opt/spike3-kernel/vmlinuzand/opt/spike3-kernel/initrd.img; the initramfs includesdrm.ko,drm_kms_helper.ko,virtio_dma_buf.ko, andvirtio-gpu.ko. - QEMU boots the generated
emulation/work/rootfs.ext4and reachesSPIKE3 emulator init complete; autostart=boot-displayinemulation/work/logs/qemu-serial.log. - QEMU serial log confirms
virtio-gpu-pci detected,Initialized virtio_gpu, andfb0: virtio_gpudrmfb frame buffer device. - Guest hardware log confirms
/dev/dri/card0,/dev/dri/renderD128,/sys/class/drm/card0,/sys/class/drm/card0-Virtual-1, and loadedvirtio_gpu/drmmodules. - noVNC responds at
http://localhost:6080/vnc.html; the dashboard responds athttp://localhost:8090/api/status. POST /api/switch/startreturnssent_to_guest: trueand appends a switch event toemulation/work/dashboard-events.jsonl.- The generated
/dev/dri/card1 -> card0symlink fixes the first/dev/dri/card1open failure forboot_displayand SPIKE menu. - QEMU
virtio-gpu-pci,max_outputs=2exposes two DRM connectors. The second connector is disconnected by QEMU VNC, so the LD_PRELOAD shim now snapshots the first connected connector and presents connector id39as connected with mode1360x768. This removes SPIKE menu's repeatedfailed to get DRM connector for display_number 1failure. - Adding
libegl1:arm64,libegl-mesa0:arm64,libgles2:arm64, and related ARM64 Mesa/GLVND packages to the Docker image removes the prior Mesa loader fataldid not find extension DRI_Mesa version 1. - Adding
mesa-vulkan-drivers:arm64to the Docker image makes the ARM64 lavapipe ICD andlibvulkan_lvp.soavailable to the generated rootfs overlay. emulation/scripts/capture-vnc.pycaptures QEMU VNC frames to PPM; earlier captures before thefbdevfallback were all black (1360x768, RGB extrema all zero).- Cache verification on 2026-07-07: after one rebuild, a second
docker compose -f emulation/compose.yaml run --rm --entrypoint /workspace/emulation/scripts/prepare-rootfs-image.sh spike3-emuloggedrootfs cache hit; reusing /workspace/emulation/work/rootfs.ext4. Hoststatshowedemulation/work/rootfs.ext4mtime unchanged at1783433532. boot-displayverification on 2026-07-07:docker compose -f emulation/compose.yaml up --force-recreatereachedSPIKE3 emulator init complete; autostart=boot-display; container-sidecapture-vnc.pywroteemulation/work/boot-display-fbdev.ppmwith RGB extrema[(0, 255), (0, 255), (0, 227)].spike-menuverification on 2026-07-07:docker compose -f emulation/compose.yaml run --name spike3-menu-test --rm --service-ports -e SPIKE3_AUTOSTART=spike-menu spike3-emuwroteemulation/work/spike-menu-fbdev.ppmwith RGB extrema[(0, 255), (0, 255), (0, 203)].gameverification on 2026-07-07:docker compose -f emulation/compose.yaml run --name spike3-game-test --rm --service-ports -e SPIKE3_AUTOSTART=game spike3-emuwroteemulation/work/game-fbdev.ppmwith RGB extrema[(0, 255), (0, 255), (0, 203)].- Diagnostic-screen verification on 2026-07-07: after adding
fb-messageand the generated ALSA null default,docker compose -f emulation/compose.yaml run --name spike3-game-diag --rm --service-ports -e SPIKE3_AUTOSTART=game spike3-emumoved past the priorUnknown PCM default/snd_pcm_prepareabort. It then hit shader/video failures and a segfault;capture-vnc.pywroteemulation/work/game-diagnostic.ppm, converted toemulation/work/game-diagnostic.png, showingSHADER LIBRARY INIT FAILUREandLOG=GAME.STDOUT.LOG. - Fallback GLES/framebuffer verification on 2026-07-07: after adding synthetic GLES framebuffer/renderbuffer/texture wrappers,
boot-displayno longer logsfailed framebuffer check for created target buffer,failed to create framebuffer, orcreate_window RETRY.capture-vnc.pywroteemulation/work/boot-display-fbo-wrap.ppmwith RGB extrema[(0, 255), (0, 255), (0, 42)]. - SPIKE menu verification on 2026-07-07: after adding
/sys/class/gpio/*to the fake path set, the prior repeatedinvalid GPIO 25/failed to set GPIO 25spam disappeared.capture-vnc.pywroteemulation/work/spike-menu-gpio-wrap.ppmwith RGB extrema[(0, 255), (0, 255), (0, 134)]. The deterministic blockers observed afterward are hardware-facing:OpenOCD init failed,failed to initialize bridge, TAS/MAX amp register verify failures, and missing real node hardware. - Main game verification on 2026-07-07:
capture-vnc.pywroteemulation/work/game-gl-limits.ppmwith RGB extrema[(0, 255), (0, 255), (0, 255)]. The old framebuffer retry is not the blocker, but Godot/Radium still reportsToo many varyings used in shader (1 used, maximum supported is 0), followed by HEVC/V4L2 setup errors andSegmentation fault. - Rootfs cache recheck on 2026-07-07 after the final shim rebuild: a second one-shot prepare logged
rootfs cache hit; reusing /workspace/emulation/work/rootfs.ext4. - Mesa dependency verification on 2026-07-07: adding missing ARM64 loader dependencies (
libX11-xcb,libwayland-client,libxcb-dri2,libxcb-present,libxcb-randr,libxcb-sync,libxcb-xfixes, andlibxshmfence) changeddisplay-probefromEGL_BAD_PARAMETERto a successful native GBM/EGL initialization. The probe log reportsEGL_MESA_platform_gbm,EGL_MESA_platform_surfaceless, successfuleglInitialize, and Mesa DRI loading from/usr/local/spike-emu/mesa-dri/kms_swrast_dri.so. - Native path verification on 2026-07-07: forcing
SPIKE3_DISPLAY_BACKEND=nativereachesOpenGL ES 3.1 Mesa 22.3.6/renderer: softpipe, but/games/gamerepeatedly logsfailed to make image from buffer objectandfailed to create framebuffer; native is therefore not the default auto presentation path. - Native llvmpipe verification on 2026-07-14:
SPIKE3_GRAPHICS_STACK=debian-llvmpipe SPIKE3_CAPTURE_TARGET_LOGS=1 emulation/scripts/run-macos.shbooted the ARM64 guest with HVF and the Cocoa display. The target log reportsOpenGL API OpenGL ES 3.2 Mesa 22.3.6 - Compatibility - Using Device: Mesa/X.org - llvmpipe (LLVM 15.0.6, 128 bits). QEMU monitorscreendumpproducedemulation/work/llvmpipe-test.ppm(1920x1080), converted toemulation/work/llvmpipe-test.png, and the capture contains a complete Pokémon game frame. A hostpssnapshot 22 seconds after launch reported QEMU at 534% CPU and 6.1% memory. This confirms functional multithreaded CPU rendering; it is not Apple-GPU acceleration. Repeatedfailed to queue page flip: Invalid argumentmessages remain in the serial log but did not prevent the captured frame. Because live animation was smooth,run-macos.shnow selectsdebian-llvmpipeby default;SPIKE3_GRAPHICS_STACK=buildroot-softwareretains the former behavior when explicitly requested. - fbdev/surfaceless verification on 2026-07-07: forcing
SPIKE3_DISPLAY_BACKEND=fbdevuses the real Mesa surfaceless display and an implicit pbuffer (eglMakeCurrent ... draw=<pbuffer> read=<pbuffer>). This clears theeglChooseConfig num=0retry path, but the target-created framebuffer remains incomplete. The serial log showsfailed framebuffer check for created target buffer,failed to create framebuffer, and repeatedGL_INVALID_FRAMEBUFFER_OPERATIONfromglClear,glDrawArrays, andglDrawElements. The captureemulation/work/game-fbdev-implicit-pbuffer.ppmis nonblank but still only shows the fallback color bands because no valid target swap/copy occurs. - Netbridge wiring verification on 2026-07-07:
python3 -m unittest discover -s analysis/netbridge-emulator, Python bytecode compilation, andbash -nover the changed Docker/QEMU shell scripts all pass. The fullSPIKE3_NETBRIDGE_EMU=1Docker boot has not yet been run, so guest acceptance of the emulated/dev/ttyAMA5path remains an open validation item.
Historical native-display blocker (superseded by the later Mesa dependency and fbdev work above):
/games/spike3/bin/boot_displayis reached and the old connector/EGL-loader fatals are cleared, but its EGL display creation still fails before it can allocate framebuffer BOs.- The current shim trace confirms
LD_PRELOADis loaded andgbm_create_device(fd=3)succeeds on thekms_swrast/softpipedefault path, returning a non-null GBM device pointer. The subsequenteglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gbm_device, NULL)returnsNULLwitheglGetError() == 0x300c(EGL_BAD_PARAMETER). - Shim fallbacks also return
NULLwithEGL_BAD_PARAMETERforEGL_PLATFORM_SURFACELESS_MESA, coreeglGetPlatformDisplay, andeglGetDisplay(NULL)even withEGL_PLATFORM=surfaceless. - Forcing
MESA_LOADER_DRIVER_OVERRIDE=zinkandGALLIUM_DRIVER=zinkwithVK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.aarch64.jsonmakesgbm_create_devicereturnNULL, so that is not the default path to leave enabled. - After EGL display creation fails,
boot_displaylater reports shader compile errors and then loops onfailed to lock front buffer: gbm_buffer[0] is null (frame=0). This appears to be an uninitialized app framebuffer array after the failed window/context setup, not a directgbm_surface_lock_front_bufferfailure. - The implemented
fbdevfallback bypasses this native EGL/GBM failure enough to produce nonblank QEMU VNC frames and let analysis proceed to the next deterministic target failure. - With fallback enabled, the next deterministic front-end blocker is no longer black VNC, missing ALSA default PCM, or zero GL varying limits.
spike-menuandgameproceed through real Mesa initialization and hardware-facing bridge/audio failures. The current main-game display blocker is the EGLImage/FBO attachment path: the target-created framebuffer is incomplete, producingGL_INVALID_FRAMEBUFFER_OPERATIONand preventing a real game frame from being copied to/dev/fb0.
Limitations and Next Steps
Confirmed current limitations:
- The dashboard event path is only a guest-visible event log. It does not yet inject events into the game’s SPIKE nodebus model.
- The optional
LD_PRELOADstub is intentionally broad and inert. It can move early hardware probing forward, but any deterministic game failure still needs specific static/dynamic analysis before adding a targeted shim. - QEMU
virtio-gpuis not Raspberry Pi VC4/V3D. It is the right first display target because the binary asks for standard DRM/GBM/EGL surfaces, but shader/extension mismatches may still require a display interposer or patch. - Docker Desktop on macOS runs this under QEMU TCG without KVM acceleration, so startup may be slow.
Next implementation targets:
- Fix or fake the EGLImage/FBO attachment path used for the target-created framebuffer under fbdev/surfaceless Mesa. The current evidence is
failed framebuffer check for created target buffer,failed to create framebuffer, and MesaGL_INVALID_FRAMEBUFFER_OPERATIONerrors aftereglMakeCurrentsucceeds with a real pbuffer. - Run
SPIKE3_NETBRIDGE_EMU=1 SPIKE3_AUTOSTART=game docker compose -f emulation/compose.yaml up --buildand verify whether the bridge thread accepts the emulated/dev/ttyAMA5path. If OpenOCD still blocks startup before UART use, stub or bypass the OpenOCD programming step while preserving the UART protocol model. - Investigate HEVC/V4L2 decode initialization errors after display setup is stable.
- Map dashboard switch events into a first SPIKE nodebus emulator, reusing recovered node command evidence from
main-game-behavior.mdandtopper-serial-key-emulator.md.