Skip to content
Mel

Mel

A virtual pet with a real brain.

Created on 20th September 2026

•

Mel

Mel

A virtual pet with a real brain.

The problem Mel solves

Every virtual pet ever made is a lookup table. Tilt the toy, and a canned
animation plays. FlyBuddy is a fruit fly whose behaviour comes from a real
fruit fly's brain.

Three parts, all in this repo:

  1. The connectome engine. The published Shiu et al. leaky integrate-and-fire
    model — all 127,400 FlyWire v630 neurons and 14,687,178 traced synapses —
    reimplemented on Apple Silicon with custom Metal kernels. One second of
    brain time costs 0.29 s of wall clock, against 2.07 s for the same
    model in Brian2 on the same machine. Same parameters, same tick ordering,
    bit-identical spike trains. A 30-trial experiment that took a minute now
    takes nine seconds, so a parameter sweep is something you run over lunch
    instead of overnight.

  2. The creature. An ESP32-S3 handheld running a fly rendered at 30 fps
    with no framebuffer — 240x296 would be 142 KB, so it draws into one 15 KB
    band at a time. The fly breathes, grooms its legs, trails its antennae
    behind a head turn, and reads the accelerometer honestly: tilt past ~35°,
    spin it or shake it and it loses its footing and flies; hold it still and it
    lands on whichever edge is now the floor.

  3. Learning. It listens to whatever music is playing, recognises it from
    its own fingerprints first, and only pays for a Shazam call when
    memory fails. A song is paid for once in its life.

Who it's for. Neuroscientists get a connectome simulator fast enough to
iterate with, plus a demonstration that the wiring is the thing: on a shuffled
connectome that preserves every neuron's in/out degree and every synapse sign
and weight, the sugar signal never reaches MN9 — it fires 67.30 Hz on the real
wiring and zero on the shuffle, with identical input spikes. Everyone else
gets the first pet whose moods are a real nervous system answering real sensors,
not an if statement.

Challenges I ran into

11 fps was not 30 fps. The renderer is analytic, not sprites — a rotated
ellipse solves its own conic for each scanline, so every edge is antialiased
from the true distance to the curve and turning the board costs nothing. It was
also doing a sqrt and a float divide per pixel. Three fixes:

  • Kill the square root. Rewrote every shader to take q squared. Nothing
    actually needed q: the rim term is q⁴, the sphere normal wants 1−q², and near
    the edge — the only place it matters — q−1 ≈ (q²−1)/2 to well under a pixel.
    One 512-entry table absorbed the last root.
  • Kill the divide. The stroke code divided by segment length per pixel, but
    that only depends on the segment. Hoisted it. The facet-crowding divide became
    a linear approximation.
  • Give the panel its own core. The SPI push isn't DMA — the CPU feeds the
    FIFO, so it genuinely costs a core, which is exactly why it should have the
    idle one. Core 0 pushes band N while core 1 draws band N+1. Panel wait went to
    0.8 ms/frame.

Result: 30 fps, 27 ms draw, 58.5 KB static RAM, free heap flat at 304,804 B.
build_opt.h pins -O3; at Arduino's default -Os it runs at half speed.

The fly looked wrong and I couldn't see why on a 2.4" screen. So I built
preview/ — the renderer and the whole fly compiled on the Mac against a stub
accelerometer, writing PNGs. It immediately caught things the board never would
have shown me: a green cast on the wings (RGB565 truncation at low alpha keeps
only the 6-bit green channel), contour rings where a smooth alpha ramp
quantised into ~10 steps, and compound eyes that read as golf balls. Every one
fixed without a reflash.

Orientation was turning into a pile of special cases. Portrait, landscape,
upside down, mid-spin. I threw it out and derived everything — where the floor
is, which way is up, how big the fly can be — from the gravity vector alone.
180° and a full 360° then needed no code of their own; through a full turn it
just stays airborne until you stop. 25 minutes sitting undisturbed on a desk
produced zero false takeoffs.

The board is borrowed. GPIO 2 is the power-off latch, arduino-cli upload
rewrites the bootloader and partition table, and erase_flash would have ended
the project. The whole flash procedure got narrowed to one command that only
ever writes 0x10000, with the rules at the top of the README in bold.

Discussion

Builders also viewed

See more projects on Devfolio