No. 75 / project of 147 on the ladder

AtomVM Tetris

introduces — 16-entry host input FIFO; Erlang process input messages; chip-side Tetris

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

P75 answers the obvious question after P74: yes, the host should just send events, and the chip-side program should own the game. The pygame viewer is now only a controller and framebuffer window. Tetris state lives in an Erlang process running under AtomVM.

AtomVM on P75 (Tetris)
Found startup beam: game.beam
P75 AtomVM Tetris starting
rendered frames: 1
input events: 6
input fifo status: 0
P75 AtomVM Tetris PASS

What changed

P74 had a one-entry input latch. P75 replaces it with a 16-entry MMIO FIFO:

layernew piece
hostpygame writes event words into captured/input.fifo
harnesswaits for host_input_ready before popping host events
RTL16-entry input FIFO, count field, saturated drop counter
AtomVMchip:input/0, chip:input_status/0, chip:draw_tetris_frame/9
ErlangTetris process receives {input, Event} messages

The current Erlang game loop pumps pending chip input into its own mailbox once per rendered frame, then drains {input, Event} messages before advancing gravity and rendering. That is not a true interrupt or port-driver path yet, but it is the right process boundary: host events become Erlang messages, and the game process owns state changes.

Smoothness pass

The first playable Tetris run was visibly slow. The problem was not the pygame viewer: the chip-side Erlang renderer was making many NIF calls per frame to fill individual rectangles. That is expensive when the whole AtomVM system is already running inside Verilator.

P75 now renders the board through one coarse chip:draw_tetris_frame/9 NIF. Erlang still owns the game state, but the hot drawing loop is C code next to the framebuffer. The render NIF is deliberately lenient about off-board piece coordinates so a transient spawn/rotation state cannot kill the game process.

The live run before this change hit the 120M-cycle budget after about 78 rendered frames. After the fast renderer, the interactive run quit cleanly at 71,030,364 post-load cycles with 138 rendered frames, 106 input events, one cleared line, and score 150.

Controls

keyaction
Left / Amove left
Right / Dmove right
Down / Ssoft drop
Up / Wrotate
Spacehard drop
qquit from the chip side
Escapeclose the host window

Run It

cd projects/75_atomvm_tetris/test
make all
make verilator-run-input-smoke

For the live game:

make view-interactive

What is not proven

F/D architectural compliance was NOT RUN. LibreLane hardening was NOT RUN. The game is playable in Verilator, but input wakeup is still polling. The next cleanup is a real AtomVM event source or port driver so host input wakes the Erlang scheduler instead of being pulled once per frame.

What just happened?

The demo crossed from “interactive framebuffer toy” to “chip-side game.” The host does not run Tetris. It sends controls; AtomVM Erlang updates the board; the chip-side framebuffer comes back out to the host window.