Files
pokemon_pinball_wiki/docker-qemu-emulation.md
2026-07-07 07:13:01 -05:00

11 KiB
Raw 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.

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.

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.

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

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.
  • 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.
  • 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.

The dashboard:

  • Links to noVNC and the existing conagent backend emulator.
  • Reads host logs from emulation/work/logs/.
  • 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_SKIP_PREPARE=1 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; captures from the current boot-display runs were still all black (1360x768, RGB extrema all zero).

Current blocker:

  • /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.
  • A nonblank SPIKE frame is not yet confirmed. The next display pass should either make the GBM EGL platform work with QEMU's non-virgl virtio_gpu, or bypass the app's EGL/GBM path with a targeted display interposer that supplies a fake EGL context plus framebuffer BOs or streams rendered/uploaded image data directly to the browser.

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 the remaining boot_display EGL display creation failure (EGL_BAD_PARAMETER from eglGetPlatformDisplayEXT on the GBM device) so the app can allocate framebuffer BOs and produce a nonblank noVNC frame.
  2. Return to spike-menu; its connector blocker is cleared, but it still depends on the same GBM/EGL buffer path and also hits expected netbridge/OpenOCD hardware failures.
  3. Run game and capture the first deterministic hardware/display failure from serial logs and /connectivity/dump/log/spike-emu/.
  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.
  5. Add targeted stubs only for confirmed failing APIs or device paths.