Skip to content

LittleFoot VM

ROLI Blocks devices run LittleFoot, a bytecode virtual machine baked into the firmware. Programs uploaded to the device execute locally at native speed, enabling LED rendering and touch processing without USB round-trips. This is how ROLI's own software achieves responsive LED feedback: the host writes pixel data to the device heap, and a LittleFoot program reads it and calls fillPixel() on each repaint cycle.

Architecture

blocksd includes a complete LittleFoot assembler that generates valid program binaries from instruction sequences. The assembler handles label resolution, function table generation, and program checksum computation.

mermaid
graph LR
    ASM[Assembly Instructions] --> ASSEMBLE[Assembler]
    ASSEMBLE --> BIN[Program Binary]
    BIN --> DC[Data Change Encoder]
    DC --> HEAP[Device Heap Upload]
    HEAP --> VM[LittleFoot VM on Device]

Program Binary Format

Offset 0-1:  uint16 LE  checksum
Offset 2-3:  uint16 LE  program size (bytes, including header)
Offset 4-5:  uint16 LE  number of functions
Offset 6-7:  uint16 LE  number of globals
Offset 8-9:  uint16 LE  heap size
Offset 10+:  function table (4 bytes each: int16 func_id + uint16 code_offset)
Remaining:   bytecode

The checksum algorithm: n = programSize; for each byte from index 2: n = (3*n + byte) & 0xFFFF. The device validates this before executing.

Native Functions

LittleFoot programs call native device functions by their hashed name (FNV1a of the signature string).

FunctionIDVerified
makeARGB/iiiii0x3F83 (16259)
fillPixel/viii0xC20B (-15861)
repaint/v0x6F8D (28557)

These functions are called via the callNative opcode with the function's 16-bit hash as the operand.

BitmapLEDProgram

The primary use case for LittleFoot in blocksd is the BitmapLEDProgram: a 94-byte repaint routine that reads RGB565 pixel data from the heap and calls fillPixel() for each of the 225 pixels on the Lightpad's 15x15 grid.

This is the same approach used by the ROLI SDK. The host writes pixel data to the device heap via SharedDataChange, and the LittleFoot program renders it on each repaint cycle.

Firmware Compatibility

WARNING

The LittleFoot program upload is currently disabled due to opcode incompatibilities with Lightpad Block M firmware v1.1.0.

Known Working Opcodes

ByteNameVerified
0x00halt
0x01jump✅ (loop back-jumps)
0x03jumpIfFalse✅ (loop conditionals)
0x05retVoid
0x07callNative✅ (makeARGB, fillPixel)
0x08drop
0x0Bpush0
0x0Cpush1
0x0Dpush8
0x0Epush16
0x10dup

Known Broken Opcodes

ByteNameResult
0x11dupOffset_01Illegal instruction, program aborted
0x40getHeapBitsIllegal instruction, BitmapLEDProgram requires this

The firmware's opcode table differs from the ROLI JUCE SDK source. Opcodes 0x11 and 0x40 are critical for the standard BitmapLEDProgram but crash the VM on firmware v1.1.0.

Workaround

blocksd currently bypasses LittleFoot entirely for LED control. Instead of uploading a repaint program and writing pixel data to the heap, the daemon uses unrolled fillPixel calls (225 per frame) via SharedDataChange. This is less efficient but works reliably on all tested firmware versions.

Key Bugs Discovered

RemoteHeap Full Heap Sync

Unknown device bytes (uint16 0x100) were mapped to 0x00 in the expected state computation. When the target was also 0x00, the diff engine skipped those bytes, leaving stale data in the device heap. Fix: map unknown bytes to target[i] ^ 0xFF, guaranteeing they always diff.

DNA Bridge Requires API Mode on All Devices

Messages addressed to a DNA-connected Lightpad weren't delivered unless the USB-connected LUMI Keys was also in API mode. The daemon already handles this by activating API mode on all devices in the topology.

Initial TOS Value

The VM initializes tos = 0 before calling repaint(). The first push operation flushes this value onto the stack, creating an extra entry that shifts all dup_offset references. Worked around with unrolled code.

Future Work

The right long-term fix is to probe the actual opcode table on each firmware version by uploading small test programs and checking for illegal instruction errors. Once the real opcode map is known, BitmapLEDProgram can be rewritten to use the correct opcodes.

Key unknowns to resolve:

  • Is getHeapByte (0x3E) available on v1.1.0?
  • What is the actual opcode for dupOffset(int8)?
  • Do loops work with the correct opcodes?
  • What is the max ops per repaint call?

Released under the ISC License.