21 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
The emulator is a working analysis scaffold, not yet a playable machine emulator. It boots the generated AArch64 guest, exposes logs and a browser-visible framebuffer, and runs far enough to identify deterministic display and hardware dependencies. It does not yet produce a real game frame or model the complete SPIKE hardware environment.
| 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. |
| Target display output | Blocked | The game-created EGLImage/FBO remains incomplete. Mesa reports GL_INVALID_FRAMEBUFFER_OPERATION, so the captured image is the shim's fallback bands rather than a rendered game frame. |
| Audio startup | Partially bypassed | A null ALSA default clears the earlier missing-PCM abort; real amplifier behavior and audio output are not emulated. |
| Netbridge/nodebus | Scaffolded, not end-to-end verified | A Python netbridge model and optional /dev/ttyAMA5 wiring exist, but the full guest boot with that mode has not been validated and OpenOCD programming may block before UART exchange. |
| Switch dashboard | Logging only | Events reach the guest log but are not translated into SPIKE nodebus state. |
| Video decode | Blocked after display setup | HEVC/V4L2 initialization still expects target hardware capabilities not supplied by the current QEMU machine. |
Why It Does Not Fully Work Yet
The extracted software assumes a Raspberry Pi graphics and I/O stack, while QEMU exposes a generic virtio-gpu machine. The compatibility layer can satisfy discovery calls and create a real Mesa context, but it cannot yet reproduce the exact GBM/EGLImage-to-framebuffer path used by the game. That is the immediate reason no genuine game frame reaches noVNC.
After that display path is fixed, the game still expects several independent machine subsystems: OpenOCD-managed RP2040 netbridge firmware, SPIKE nodebus devices, TAS/MAX amplifier register behavior, physical switches/lamps/coils, and V4L2 HEVC decode. Those are separate emulation targets; success in one does not imply that the others are modeled.
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: the first emulator path uses QEMU virtio-gpu and browser-visible VNC/noVNC, not X11 forwarding.
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. - 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.