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:
| layer | new piece |
|---|---|
| host | viewer.py --interactive --input-fifo captured/input.fifo |
| harness | harness_interactive.cpp reads event words and drives top-level pins |
| RTL | one-entry MMIO_INPUT_STATUS / MMIO_INPUT_DATA queue |
| AtomVM | chip:input/0 plus coarse chip:draw_game_frame/5 platform NIFs |
| Erlang | game 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:
| measurement | value |
|---|---|
| post-load cycles | 13,764,739 |
| Verilator wall speed | 1.40 MHz |
| full run wall FPS including AtomVM startup | 3.26 fps |
| steady frame cadence | about 137,700 chip cycles/frame |
| steady frame rate at this Verilator speed | about 10 fps |
| steady frame rate at a 25 MHz chip clock | about 181 fps |
| framebuffer dump cost | 0.072 ms/frame average |
| viewer read + RGB565 convert + draw | 4.77 ms/frame average |
| viewer-only frame pipeline ceiling | about 210 fps |
| input delivery in scripted smoke | 5 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.