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