Files
pokemon_pinball_wiki/docker-qemu-emulation.md
2026-07-10 22:39:27 -05:00

21 KiB
Raw Permalink Blame History

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/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: 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-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/<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/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.
  • fbdev/surfaceless verification on 2026-07-07: forcing SPIKE3_DISPLAY_BACKEND=fbdev uses the real Mesa surfaceless display and an implicit pbuffer (eglMakeCurrent ... draw=<pbuffer> read=<pbuffer>). 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 games 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.