No. 73 / project of 147 on the ladder

AtomVM framebuffer graphics

introduces — AtomVM-rendered framebuffer frames; scanline platform NIF; host GIF artifact from chip-written RGB565 memory

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

P72 proved Erlang could call chip-local platform NIFs. P73 uses that bridge for the thing we wanted next: AtomVM renders frames into chip memory and the host viewer turns those bytes into an animation.

Headline: Erlang now renders eight 128x96 RGB565 frames on the chip, pulses the frame-ready bridge, and produces host-viewable framebuffer artifacts.

AtomVM on P73 (framebuffer graphics demo)
Starting AtomVM revision 0.8.0-dev+git.b7503e6
Found startup beam: graphics.beam
P73 AtomVM framebuffer starting
framebuffer base: 15728640
rendered frames: 8
render delta ms: 588
P73 AtomVM framebuffer PASS
Return value: ok
AtomVM exited result=0

What changed

P73 adds one practical graphics NIF on top of the P72 chip I/O surface:

NIFeffect
chip:hline/4writes a horizontal RGB565 span into the framebuffer

The Erlang renderer in erlang/graphics.erl computes the scene one scanline at a time. The C NIF performs the tight memory write, then Erlang pulses chip:frame_ready/1 after each finished frame. The host harness dumps eight 24576 byte frame files from the scratch framebuffer window at 0x00f00000.

The frame content is intentionally simple: a gradient background, a moving sprite, and a progress meter. The important part is ownership: the image is decided by AtomVM-running Erlang, not by a C framebuffer program.

Verification

cd projects/73_atomvm_framebuffer/test
make all
make verilator-run-fb
uv run --with pillow ../viewer.py --gif captured/atomvm-framebuffer.gif captured/frames 128 96 --scale 4 --fps 8

For live viewing while Verilator is running:

cd projects/73_atomvm_framebuffer/test
make view-live

That starts a pygame window first, clears captured/frames/, then runs the Verilator harness. Each chip:frame_ready/1 pulse writes a new frame file, and the viewer displays it immediately. The live viewer also supports pause, frame scrubbing, follow-latest mode, and PNG snapshots.

Harness result:

[harness] frame 0 dumped (24576 bytes)
[harness] frame 1 dumped (24576 bytes)
[harness] frame 2 dumped (24576 bytes)
[harness] frame 3 dumped (24576 bytes)
[harness] frame 4 dumped (24576 bytes)
[harness] frame 5 dumped (24576 bytes)
[harness] frame 6 dumped (24576 bytes)
[harness] frame 7 dumped (24576 bytes)
[harness] === run ended after 24288424 post-load cycles ===
[harness] halted=1, halt_code=0x00000001

The viewer wrote atomvm-framebuffer.gif and atomvm-framebuffer-final.png under test/captured/.

What is not proven

F/D architectural compliance was NOT RUN. LibreLane hardening was NOT RUN. The framebuffer still uses a demo scratch window at 0x00f00000; if this becomes a reusable graphics API, the memory map should graduate from convention to documented platform contract.

What just happened?

The framebuffer path now belongs to AtomVM. Erlang computes a frame, the chip writes it into RAM, Verilator dumps it when Erlang pulses frame-ready, and the host viewer turns the dumps into an animation.