No. 74 / project of 147 on the ladder

AtomVM interactive game host bridge

introduces — host keyboard events into Verilator; chip MMIO input queue; AtomVM Erlang game-server loop

harden statelast run2026-05-05
signoff
  • DRCNOT RUN
  • LVSNOT RUN
  • antennaNOT RUN

P73 made the host window a display. P74 makes it an input device too. Keyboard events from pygame now travel into Verilator, through a tiny RTL MMIO queue, into AtomVM via chip:input/0, and finally into an Erlang server process that owns the game state.

AtomVM on P74 (interactive game demo)
Found startup beam: game.beam
P74 AtomVM interactive game starting
rendered frames: 32
input events: 1
P74 AtomVM interactive game PASS

What changed

The bridge is deliberately small:

layernew piece
hostviewer.py --interactive --input-fifo captured/input.fifo
harnessharness_interactive.cpp reads event words and drives top-level pins
RTLone-entry MMIO_INPUT_STATUS / MMIO_INPUT_DATA queue
AtomVMchip:input/0 plus coarse chip:draw_game_frame/5 platform NIFs
Erlanggame server process polls input, updates state, redraws

The event format is an integer, not a protocol stack:

(Type << 16) | Code

Type is key-down or key-up. Code is left, right, up, down, action, or quit. That is enough for the host to move a square around, change its color, and prove the game loop reacts to host input while still rendering frames from chip-side Erlang.

Run It

cd projects/74_atomvm_interactive_game/test
make all
make verilator-run-input-smoke

For the actual interactive window:

make view-interactive

Use arrows or WASD to move, Space to change color, q to send quit, and Escape to close the host window.

For the jank hunt, P74 also has a profiled smoke:

make profile-interactive-smoke

It emits separate JSON for the simulated chip run, the core CPI-ish profile, and the host viewer frame pipeline.

Smoothness profile

The first interactive version felt rough because the render loop crossed from Erlang into a platform NIF around 109 times per frame. The quick fix was to leave game state in Erlang but move the demo rasterization into one coarse NIF, chip:draw_game_frame/5.

Current measured run:

measurementvalue
post-load cycles13,764,739
Verilator wall speed1.40 MHz
full run wall FPS including AtomVM startup3.26 fps
steady frame cadenceabout 137,700 chip cycles/frame
steady frame rate at this Verilator speedabout 10 fps
steady frame rate at a 25 MHz chip clockabout 181 fps
framebuffer dump cost0.072 ms/frame average
viewer read + RGB565 convert + draw4.77 ms/frame average
viewer-only frame pipeline ceilingabout 210 fps
input delivery in scripted smoke5 queued, 5 delivered

That says the jank is not primarily graphics transfer. The harness writes a full 128x96 RGB565 frame, 24 KiB, in less than a tenth of a millisecond on this run. The Python viewer is also comfortably ahead of the simulated chip. Its slowest step is the pure-Python RGB565 conversion at about 4.4 ms/frame, but that still leaves the host capable of roughly 210 fps for these tiny frames.

The limiter is the simulated CPU plus AtomVM workload. At the measured Verilator speed, the steady render loop is only about 10 fps. On a real 25 MHz version of this chip, the same 137.7k cycles/frame would be well over 60 fps, assuming memory behavior matches the simulation.

The next useful optimizations are therefore chip-side:

  • a real RTL input FIFO instead of the one-entry latch;
  • coarse drawing primitives or a tiny blitter for bulk pixels;
  • interrupt or wakeup-driven input so AtomVM does not poll from the render loop;
  • shared-memory or pipe streaming for frames only after the chip can produce frames fast enough for file writes to show up in the profile.

What is not proven

F/D architectural compliance was NOT RUN. LibreLane hardening was NOT RUN. The input bridge is a one-entry MMIO queue, which is fine for the demo but not yet a robust HID or interrupt-driven input subsystem. P74 uses a small Erlang mailbox server rather than OTP gen_server; that runtime path is still a follow-up.

What just happened?

The viewer, Verilator harness, RTL, AtomVM NIF layer, and Erlang game process now form a full interactive loop. The host sends controls in; the chip updates state; the host sees the new framebuffer frames.