You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds PICO-8 as a new core (components/pico8), hosting femto8 (Ben Baker's C reimplementation for embedded systems: fixed-point Lua 5.2, lodepng for .p8.png). This replaces the cursor/add-pico-8-emulator-support-d887 branch (#107), which was a skeleton against an imagined femto8 API with no emulator source; #107 can be closed in favour of this.
See components/pico8/VENDOR.md for the vendored tree, every change made and the measured limits.
What's in it
components/pico8/ — femto8 at f9e5097 with a third platform FEMTO8_ESPBOX beside its SDL and DA1470x targets, plus the box glue:
The cart's p8_run() loop runs in its own task; frames are 128×128 palette indices scaled by the emulator (1:1 "original", 240×240 "fit"); audio rendered on core 1 at 44.1 kHz.
Pause/quit happen in femto8's Lua instruction hook (the VM is at a safe point, no locks held): quit unwinds through femto8's own p8_quit() longjmp, no reboot; statics reset via linker SURROUND symbols for relaunch.
Save states: PICO-8 RAM + overlay + audio channels + the cart's Lua heap serialized with Eris (as fake-08 does), carried out by the cart task at a frame boundary. Host-tested round trip (closures/upvalues, metatables, captured library functions, coroutines, replaced globals).
PICO-8 compatibility fixes verified against femto8's desktop build: _ENV-override API fallback (for _ENV in all(objs) do circfill(...)), lenient split(), load("#bbs_id") of sibling carts (multi-cart games), load() relative to the cart's folder, gamepad "press any key".
Performance: sspr() clipped to the visible area (an O(dw·dh) loop looked like a hang), span-based circfill/ovalfill/hline, memsetcls (all pixel-identical to the old code on a test cart); pooled Lua allocator (internal SRAM first); VM core in IRAM; PICO8_PROFILE build option reports Lua instructions/s, GC share and per-API cycles.
D-pad; A/Y = O, B/X = X; START = PICO-8's pause menu; carts' cartdata() saves go to pico8/cdata/.
Status (BOX-3)
Celeste at a locked 30 fps; emulator pause/resume, quit → relaunch, save/load state verified; memory returns to baseline across relaunches.
Measured envelope: the interpreter runs ~1.3 M Lua instructions/s with its heap in PSRAM (GC ~1 %, drawing API ~5–15 % of a frame) — carts near PICO-8's CPU limit (e.g. Cattle Crisis in play, Mossmoss) run at a few fps. Known: Pico Ball shows a blank screen in upstream femto8 too; no mouse/keyboard.
Test plan
Build (IDF 6.1), flash BOX-3
Celeste: play, pause/resume, quit, relaunch; colors and input correct
Save state → play on → load; quit → relaunch → load
Vendors femto8 (benbaker76/femto8 @ f9e5097) as components/pico8 with an
esp-box-emu platform: the cart runs in its own task, frames are palette
indices scaled by the emulator, audio is rendered on core 1, and the Lua
instruction hook is the pause/quit point so the core stops cleanly and
relaunches without a reboot (statics reset through linker SURROUND symbols).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
…; backtrace on a stuck cart
The audio task rendered before p8_init() had allocated PICO-8 RAM (a
NULL-base LoadProhibited at 0x5f2f). femto8's keyboard-only "press any
key" loop now takes a button and honours the emulator's stop request.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
…plit()
Carts that override _ENV (`for _ENV in all(objs) do circfill(...) end`,
`function f(_ENV)`) could not reach the API: a string-keyed lookup that
comes back nil now falls back to a snapshot of the API globals taken
before the cart ran (the same approach as fake-08's z8lua). split() no
longer raises on a non-string argument, as PICO-8 does not.
Reproduced and verified with femto8's desktop build: all 7 test carts
run (Mossmoss and Pico Ball failed before).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
… audio gate on the kill path
- draw_scaled_sprite walked every destination pixel of a scaled sprite
before clipping: a cart scaling a sprite to thousands of pixels spent
seconds per frame on the S3 and looked hung (Cattle Crisis). The loops
now cover only the clip rectangle.
- load(): no working-directory check on the box, the cart's own folder
is the base, and "#bbs_id" (multi-cart games) resolves to a sibling
bbs_id.p8.png / .p8.
- The _ENV fallback caches its table pointers instead of interning the
registry key on every nil lookup.
- A cart killed while wedged left the audio task armed; the next launch
rendered before p8_init() and crashed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
The 64KB PICO-8 RAM (screen, sprites, map, draw state) is read a dozen
times per pixel drawn; it came from the PSRAM heap. A multi-cart menu
retrying load() every frame no longer floods the console.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
The interpreter, not the drawing API, dominates the slow carts (Cattle
Crisis in play: >90% Lua; Mossmoss: ~86%), and a string creation cost
~12k cycles through the locked system heap. Lua's small objects now come
from size-class free lists over 64KB PSRAM slabs (src/platform/
p8_lua_alloc.c, host-tested under ASan); larger blocks still use the
heap. PICO8_PROFILE wraps every API function with a cycle counter and
reports the top entries with the fps line.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
…internal SRAM first; VM core in IRAM
circfill cost ~0.5ms per call (a third of some carts' frame): the octant
walk drew every scanline several times over, each pixel through the full
pixel_set. The fill now records one extent per row and draws it once;
draw_hline writes screen nibbles directly when no fill pattern, secondary
palette, mask or per-call attributes are in play; cls is a memset.
Verified pixel-identical against the previous code with a test cart on
the desktop build (circles of every radius, camera/clip, ovals, fillp).
The interpreter itself is the limit for heavy carts (~1.3M Lua
instructions/s from PSRAM, GC 1%): its small objects now come from
internal SRAM first up to a budget, lvm/ltable/lstring run from IRAM, and
PICO-8 RAM goes back to the default heap (measured ~10% of a frame).
PICO8_PROFILE also reports instructions/s and GC share.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
A state is PICO-8's RAM, the overlay, the frame counters, the audio
channels and the cart's Lua heap, serialized with Eris (Lua 5.2's
persistence library, as fake-08 does; vendored from its z8lua tree with
the fixed-point number I/O and the populateperms helper). Everything the
cart created or replaced in _G is persisted as a graph; the API functions
and library tables recorded before the cart ran are permanents referenced
by name. The menu's save/load request is carried out by the cart task at
its next frame boundary, where no cart code is on the Lua stack.
Host-tested: a round trip through a fresh Lua state keeps closures with
upvalues, metatables, captured library functions, a suspended coroutine,
replaced originals, and clears globals created after the save.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
… is open
A save/load runs the cart for one frame with the menu up, and the button
that closed the menu was still held on the first frame after resume.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Unresolved buffer-ownership races, unsafe cartridge decompression, and platform-specific audio defects can cause corruption or incorrect runtime behavior.
FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available. DRAM/IRAM usage does not include memory used by the heap allocator at runtime. This report was generated by esp-idf-size-delta.
The video task keeps the frame pointer it dequeued for the whole LCD
write and nothing signals completion, so with two buffers the cart could
render into one still being scanned out; three buffers put the reuse two
full frames out. fix32_rotl/rotr shifted a signed value, and by 32 for a
rotate count of 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
The audio task could pass its s_audioReady check and be preempted before
render_sounds(); clearing the flag and sleeping did not cover that. It now
re-checks the flag and renders under a mutex, and the cart task takes
that mutex once after clearing the flag, before p8_shutdown() frees
femto8's memory.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
Left-shifting a negative int, or a positive value whose result is not representable, is undefined behavior. PICO-8 conversion relies on 32-bit wrapping, so perform the shift on the unsigned bit pattern before converting back.
This issue also appears on line 129 of the same file.
…(review)
Both .p8.png code decompressors took the back-reference offset and
length straight from the cart stream: the legacy one memcpy'd overlapping
ranges, neither checked that the source lies inside the data produced so
far or that the copy fits the output. A malformed cart now fails to load
instead of reading before the buffer or writing past it. fix32's
from_int / shl / abs shift the unsigned bit pattern (PICO-8 numbers wrap).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 23af3a2: both decompressors take the code section's length (PNG payload minus cart memory); every read past it fails the decode, the PXA path also rejects a comp_len larger than the input, and the legacy raw-text path is bounded by the section and the output.
…n (review)
Both .p8.png code decompressors read the cart stream with no end check,
so a cart declaring more output than its payload encodes read on past the
decoded image. They now take the code section's length, every read past
it fails the decode, and the legacy raw-text path is bounded as well. The
last-frame pointer and the frame counter are written by the cart task and
read by the emulator task; they are atomics now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds PICO-8 as a new core (
components/pico8), hosting femto8 (Ben Baker's C reimplementation for embedded systems: fixed-point Lua 5.2, lodepng for.p8.png). This replaces thecursor/add-pico-8-emulator-support-d887branch (#107), which was a skeleton against an imagined femto8 API with no emulator source; #107 can be closed in favour of this.See
components/pico8/VENDOR.mdfor the vendored tree, every change made and the measured limits.What's in it
components/pico8/— femto8 atf9e5097with a third platformFEMTO8_ESPBOXbeside its SDL and DA1470x targets, plus the box glue:p8_run()loop runs in its own task; frames are 128×128 palette indices scaled by the emulator (1:1 "original", 240×240 "fit"); audio rendered on core 1 at 44.1 kHz.p8_quit()longjmp, no reboot; statics reset via linker SURROUND symbols for relaunch._ENV-override API fallback (for _ENV in all(objs) do circfill(...)), lenientsplit(),load("#bbs_id")of sibling carts (multi-cart games),load()relative to the cart's folder, gamepad "press any key".sspr()clipped to the visible area (an O(dw·dh) loop looked like a hang), span-basedcircfill/ovalfill/hline,memsetcls(all pixel-identical to the old code on a test cart); pooled Lua allocator (internal SRAM first); VM core in IRAM;PICO8_PROFILEbuild option reports Lua instructions/s, GC share and per-API cycles.main/pico8_cart.hpp,carts.hpp,rom_info(.p8/.p8.png), top-level CMake (PICO8_COMPONENTS/ENABLE_PICO8), README table + setup section, static-analysis exclusion/suppression for the vendored tree.Controls
D-pad; A/Y = O, B/X = X; START = PICO-8's pause menu; carts'
cartdata()saves go topico8/cdata/.Status (BOX-3)
Test plan
load(),_ENVcarts (Mossmoss, Pico Ball load and run), lenientsplit🤖 Generated with Claude Code
https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1