# 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/game` imports `libEGL.so.1`, `libGLESv2.so.2`, `libdrm.so.2`, and `libgbm.so.1`; see `analysis/static-triage/game.objdump-p.txt`. - `/games/pokemon_pro/game` strings include `/sys/class/drm`, `/dev/dri/card1`, `/dev/dri/card0`, `drmModeSetCrtc fail`, `drmModePageFlip`, `gbm_bo_create`, `eglGetPlatformDisplayEXT`, and `egl resolution: %d x %d`; see `analysis/static-triage/game.strings`. - `/games/pokemon_pro/spike3/spike_menu/game` has matching GBM/EGL/DRM strings at offsets including `0x382cd0` (`/sys/class/drm`), `0x383030` (`/dev/dri/card1`), `0x383058` (`/dev/dri/card0`), and `0x383730` (`drmModeSetCrtc fail`); see `analysis/static-triage/spike_menu_game.strings`. - `/etc/init.d/S12lvds_panel` launches `/games/spike3/bin/boot_display` early, and `/etc/init.d/game_monitor` later stops `boot_display` before 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: ```sh 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: ```sh 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: ```sh 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-display` - `spike-menu` - `game` - `monitors` - `shell` ## 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.sh` to start `boot_display`, SPIKE menu, game, or monitors. - Injects an optional `LD_PRELOAD` hardware stub at `/usr/local/spike-emu/lib/libspike3emu_stub.so` when the Docker image has `aarch64-linux-gnu-gcc`. - Injects overlay Mesa DRI fallback drivers under `/usr/local/spike-emu/mesa-dri` when the Docker image has ARM64 `virtio_gpu`, `kms_swrast`, `swrast`, and `zink` drivers. - Injects matching ARM64 Debian Mesa/GLVND EGL libraries under `/usr/local/spike-emu/mesa-lib` plus `/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 rootfs `libEGL`. The overlay intentionally does not shadow the target rootfs `libstdc++.so.6`; Debian bookworm's copy lacks `GLIBCXX_3.4.31`/`GLIBCXX_3.4.32`, which `/games/game` requires. - 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 from `emulation/guest-tools/display-probe.c`, to test the guest GBM/EGL path before target launch. - Supports `SPIKE3_DISPLAY_BACKEND=auto|native|fbdev`. `auto` records the probe result under `/run/spike-emu/display-backend.env` and currently selects `fbdev` presentation by default; `native` is 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 by `emulation/work/netbridge.sock`, guest setup symlinks `/dev/ttyAMA5` to `/dev/ttyAMA1`, and the preload shim passes `/dev/ttyAMA5` through instead of opening `/dev/null`. - In `fbdev` fallback mode, `emulation/stubs/spike3emu_stub.c` uses 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.conf` that maps ALSA `default` to the null PCM, clearing the prior missing-default-PCM abort inside the emulator copy. - Injects `/usr/local/spike-emu/bin/fb-message`, compiled from `emulation/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.log` and `/connectivity/dump/log/spike-emu/control-agent.log`. - Creates a cache manifest at `emulation/work/rootfs.manifest` and cache status at `emulation/work/rootfs-cache.status`. `SPIKE3_PREPARE_MODE=auto` reuses `emulation/work/rootfs.ext4` when 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.sock` when QEMU is running. Verified APIs are `POST /api/event` and `POST /api/switch/`. ## Verification Status Commands run from the workspace root: ```sh 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/vmlinuz` and `/opt/spike3-kernel/initrd.img`; the initramfs includes `drm.ko`, `drm_kms_helper.ko`, `virtio_dma_buf.ko`, and `virtio-gpu.ko`. - QEMU boots the generated `emulation/work/rootfs.ext4` and reaches `SPIKE3 emulator init complete; autostart=boot-display` in `emulation/work/logs/qemu-serial.log`. - QEMU serial log confirms `virtio-gpu-pci detected`, `Initialized virtio_gpu`, and `fb0: 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 loaded `virtio_gpu`/`drm` modules. - noVNC responds at `http://localhost:6080/vnc.html`; the dashboard responds at `http://localhost:8090/api/status`. - `POST /api/switch/start` returns `sent_to_guest: true` and appends a switch event to `emulation/work/dashboard-events.jsonl`. - The generated `/dev/dri/card1 -> card0` symlink fixes the first `/dev/dri/card1` open failure for `boot_display` and SPIKE menu. - QEMU `virtio-gpu-pci,max_outputs=2` exposes 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 id `39` as connected with mode `1360x768`. This removes SPIKE menu's repeated `failed to get DRM connector for display_number 1` failure. - Adding `libegl1:arm64`, `libegl-mesa0:arm64`, `libgles2:arm64`, and related ARM64 Mesa/GLVND packages to the Docker image removes the prior Mesa loader fatal `did not find extension DRI_Mesa version 1`. - Adding `mesa-vulkan-drivers:arm64` to the Docker image makes the ARM64 lavapipe ICD and `libvulkan_lvp.so` available to the generated rootfs overlay. - `emulation/scripts/capture-vnc.py` captures QEMU VNC frames to PPM; earlier captures before the `fbdev` fallback 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-emu` logged `rootfs cache hit; reusing /workspace/emulation/work/rootfs.ext4`. Host `stat` showed `emulation/work/rootfs.ext4` mtime unchanged at `1783433532`. - `boot-display` verification on 2026-07-07: `docker compose -f emulation/compose.yaml up --force-recreate` reached `SPIKE3 emulator init complete; autostart=boot-display`; container-side `capture-vnc.py` wrote `emulation/work/boot-display-fbdev.ppm` with RGB extrema `[(0, 255), (0, 255), (0, 227)]`. - `spike-menu` verification on 2026-07-07: `docker compose -f emulation/compose.yaml run --name spike3-menu-test --rm --service-ports -e SPIKE3_AUTOSTART=spike-menu spike3-emu` wrote `emulation/work/spike-menu-fbdev.ppm` with RGB extrema `[(0, 255), (0, 255), (0, 203)]`. - `game` verification on 2026-07-07: `docker compose -f emulation/compose.yaml run --name spike3-game-test --rm --service-ports -e SPIKE3_AUTOSTART=game spike3-emu` wrote `emulation/work/game-fbdev.ppm` with RGB extrema `[(0, 255), (0, 255), (0, 203)]`. - Diagnostic-screen verification on 2026-07-07: after adding `fb-message` and the generated ALSA null default, `docker compose -f emulation/compose.yaml run --name spike3-game-diag --rm --service-ports -e SPIKE3_AUTOSTART=game spike3-emu` moved past the prior `Unknown PCM default` / `snd_pcm_prepare` abort. It then hit shader/video failures and a segfault; `capture-vnc.py` wrote `emulation/work/game-diagnostic.ppm`, converted to `emulation/work/game-diagnostic.png`, showing `SHADER LIBRARY INIT FAILURE` and `LOG=GAME.STDOUT.LOG`. - Fallback GLES/framebuffer verification on 2026-07-07: after adding synthetic GLES framebuffer/renderbuffer/texture wrappers, `boot-display` no longer logs `failed framebuffer check for created target buffer`, `failed to create framebuffer`, or `create_window RETRY`. `capture-vnc.py` wrote `emulation/work/boot-display-fbo-wrap.ppm` with 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 repeated `invalid GPIO 25` / `failed to set GPIO 25` spam disappeared. `capture-vnc.py` wrote `emulation/work/spike-menu-gpio-wrap.ppm` with 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.py` wrote `emulation/work/game-gl-limits.ppm` with RGB extrema `[(0, 255), (0, 255), (0, 255)]`. The old framebuffer retry is not the blocker, but Godot/Radium still reports `Too many varyings used in shader (1 used, maximum supported is 0)`, followed by HEVC/V4L2 setup errors and `Segmentation 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`, and `libxshmfence`) changed `display-probe` from `EGL_BAD_PARAMETER` to a successful native GBM/EGL initialization. The probe log reports `EGL_MESA_platform_gbm`, `EGL_MESA_platform_surfaceless`, successful `eglInitialize`, 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=native` reaches `OpenGL ES 3.1 Mesa 22.3.6` / `renderer: softpipe`, but `/games/game` repeatedly logs `failed to make image from buffer object` and `failed 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.sh` booted the ARM64 guest with HVF and the Cocoa display. The target log reports `OpenGL API OpenGL ES 3.2 Mesa 22.3.6 - Compatibility - Using Device: Mesa/X.org - llvmpipe (LLVM 15.0.6, 128 bits)`. QEMU monitor `screendump` produced `emulation/work/llvmpipe-test.ppm` (1920x1080), converted to `emulation/work/llvmpipe-test.png`, and the capture contains a complete Pokémon game frame. A host `ps` snapshot 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. Repeated `failed to queue page flip: Invalid argument` messages remain in the serial log but did not prevent the captured frame. Because live animation was smooth, `run-macos.sh` now selects `debian-llvmpipe` by default; `SPIKE3_GRAPHICS_STACK=buildroot-software` retains the former behavior when explicitly requested. - fbdev/surfaceless verification on 2026-07-07: forcing `SPIKE3_DISPLAY_BACKEND=fbdev` uses the real Mesa surfaceless display and an implicit pbuffer (`eglMakeCurrent ... draw= read=`). This clears the `eglChooseConfig num=0` retry path, but the target-created framebuffer remains incomplete. The serial log shows `failed framebuffer check for created target buffer`, `failed to create framebuffer`, and repeated `GL_INVALID_FRAMEBUFFER_OPERATION` from `glClear`, `glDrawArrays`, and `glDrawElements`. The capture `emulation/work/game-fbdev-implicit-pbuffer.ppm` is 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, and `bash -n` over the changed Docker/QEMU shell scripts all pass. The full `SPIKE3_NETBRIDGE_EMU=1` Docker boot has not yet been run, so guest acceptance of the emulated `/dev/ttyAMA5` path remains an open validation item. Historical native-display blocker (superseded by the later Mesa dependency and fbdev work above): - `/games/spike3/bin/boot_display` is 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_PRELOAD` is loaded and `gbm_create_device(fd=3)` succeeds on the `kms_swrast`/`softpipe` default path, returning a non-null GBM device pointer. The subsequent `eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gbm_device, NULL)` returns `NULL` with `eglGetError() == 0x300c` (`EGL_BAD_PARAMETER`). - Shim fallbacks also return `NULL` with `EGL_BAD_PARAMETER` for `EGL_PLATFORM_SURFACELESS_MESA`, core `eglGetPlatformDisplay`, and `eglGetDisplay(NULL)` even with `EGL_PLATFORM=surfaceless`. - Forcing `MESA_LOADER_DRIVER_OVERRIDE=zink` and `GALLIUM_DRIVER=zink` with `VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.aarch64.json` makes `gbm_create_device` return `NULL`, so that is not the default path to leave enabled. - After EGL display creation fails, `boot_display` later reports shader compile errors and then loops on `failed 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 direct `gbm_surface_lock_front_buffer` failure. - The implemented `fbdev` fallback 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-menu` and `game` proceed 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, producing `GL_INVALID_FRAMEBUFFER_OPERATION` and 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_PRELOAD` stub 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-gpu` is 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: 1. 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 Mesa `GL_INVALID_FRAMEBUFFER_OPERATION` errors after `eglMakeCurrent` succeeds with a real pbuffer. 2. Run `SPIKE3_NETBRIDGE_EMU=1 SPIKE3_AUTOSTART=game docker compose -f emulation/compose.yaml up --build` and verify whether the bridge thread accepts the emulated `/dev/ttyAMA5` path. If OpenOCD still blocks startup before UART use, stub or bypass the OpenOCD programming step while preserving the UART protocol model. 3. Investigate HEVC/V4L2 decode initialization errors after display setup is stable. 4. Map dashboard switch events into a first SPIKE nodebus emulator, reusing recovered node command evidence from `main-game-behavior.md` and `topper-serial-key-emulator.md`.