# 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:

```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/<name>`.

## 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.
- 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 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`.
