Exit a running cart from the pause menu (IN-6) - #36
Open
aldwinhermanudin wants to merge 2 commits into
Open
aldwinhermanudin wants to merge 2 commits into
aldwinhermanudin wants to merge 2 commits into
Conversation
Tapping MENU in a running cart now offers "exit to launcher", which returns to
the cover-art carousel. No reboot: the frame loop breaks and app_main re-enters
the carousel in the same millisecond, with the panel, SD and audio left up.
The vendored fake-08 is untouched -- zero modified files, gitlink unmoved. Three
stock APIs do the work: Vm::ExecuteLua registers menuitem(5, ...) in the cart
sandbox (no cart edit), the callback pokes a magic byte into 0x5fff inside
PICO-8's GPIO area (the documented cart<->host channel), and main.cpp's frame
loop reads it through the already-public Vm::getPicoRam().
main.cpp runs its own copy of Vm::GameLoop() because GameLoop owns the frame and
never yields, so there is nowhere else to notice the byte. Copying keeps the
submodule byte-identical to upstream -- the same trade the GC_MANUAL loop makes.
Two things that are easy to get wrong, both now handled and documented:
- The cart sandbox does not exist at LoadCart time. It is created inside
__z8_run_cart on the first frame, so registering earlier fails with
"__cart_sandbox is not a table". The registration runs after one Step(), and
checks its return value -- a silent failure here would remove the exit route
with no symptom beyond the item being absent.
- The exit byte is cleared when a cart starts, so a stale poke cannot eject the
next cart. Address plus magic value rather than a bare flag, because GPIO is
cart-writable by design.
This replaces two earlier designs. A hold-MENU gesture in components/input was
built and tested, then dropped: undiscoverable, it rebooted the board (~1.8 s,
and esp_restart discards cart save state), and holding MENU re-toggled PICO-8's
pause 5-30 times during the hold. A fake-08 fork change (extcmd("exit_to_host")
plus a Vm quit flag) also worked and allowed bottom placement in the menu, but
review found extcmd is reachable by any cart -- on stock platforms a downloaded
cart could quit the program -- and it changed a vendored dependency for what is
ultimately cosmetic gain.
Both premises the first design rested on were false, and the worklog records
that: fake-08 already renders a pause menu offering an exit (the //todo cited as
evidence is inside a commented-out function), and GameLoop() does return -- our
own Host::shouldQuit() just returns false.
Verified on the P4: two launch/exit laps in one boot, the item re-registering on
the second cart, no rst: line. Menu shown by framebuffer render, not a photo.
Both boards build.
Follow-up to the pause-menu exit: drop main.cpp's copy of Vm::GameLoop(). The copy existed so the app had somewhere to notice the cart's exit poke, which meant routing around the seam fake-08 already provides -- "if (_host->shouldQuit()) break;" -- purely because implementing it meant touching the submodule. That trade was wrong. A divergent copy of the frame loop silently rots as upstream changes; four lines in an ESP32-only platform file do not. ESP32Host::shouldQuit() (fake-08 PR #7) now answers from a byte the app arms with esp32host_watch_exit(), pointing at the same GPIO-area address the menu item pokes. main.cpp calls upstream's GameLoop() unmodified and checks the byte once after it returns, consuming it so the next cart does not exit immediately. Scope of the fork change: platform/esp32/ESP32Host.cpp only. No shared core, so the other 13 platforms cannot be affected, and the watch is unset by default. The rejected extcmd design touched vm.cpp, vm.h, p8GlobalLuaFunctions.h and the sandbox whitelist, and let any cart quit the program. The GPIO byte stays -- it is still how Lua reaches C, since the menu callback is Lua -- but it is now an implementation detail inside the platform host rather than a protocol main.cpp polls every frame. Verified on the P4: two launch/exit laps in one boot, no rst:, running upstream's loop. Both boards build.
aldwinhermanudin
force-pushed
the
in6-exit-menu
branch
from
August 9, 2026 19:56
497e126 to
bbab559
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Tapping MENU in a running cart now offers
exit to launcher, returning to the cover-art carousel. No reboot — the frame loop breaks andapp_mainre-enters the carousel in the same millisecond, panel/SD/audio left up.The vendored fake-08 is untouched. Zero modified files, gitlink unmoved. Three stock APIs:
Vm::ExecuteLuaregistersmenuitem(5, "exit to launcher", …)in the cart sandbox — no cart editpokes a magic byte to0x5fff, inside PICO-8's GPIO area — the documented cart↔host channelmain.cpp's frame loop reads it via the already-publicVm::getPicoRam()main.cppruns its own copy ofVm::GameLoop()because GameLoop owns the frame and never yields, so there's nowhere else to notice the byte. Copying keeps the submodule byte-identical — the same tradeGC_MANUALalready makes.Two easy things to get wrong
LoadCarttime — it's created in__z8_run_carton the first frame, so registering earlier fails with__cart_sandbox is not a table. Registration runs after oneStep()and checks its return value; a silent failure would remove the exit route with no symptom beyond the item being absent.This replaces two earlier designs
components/input— built and tested, then dropped. Undiscoverable, rebooted the board (~1.8 s, andesp_restart()discards cart save state), and holding MENU re-toggled PICO-8's pause 5–30× so the menu strobed. Supersedes Exit a running cart back to the launcher (IN-6) #35.extcmd("exit_to_host")+ aVmquit flag) — worked, and allowed bottom placement in the menu. Review foundextcmdis reachable by any cart, so on stock platforms a downloaded cart could quit the program. Dropped: a vendored dependency changed for cosmetic gain. Supersedes vm: let an embedding host take control back from GameLoop fake-08#6.Both original premises were false
The worklog records this rather than quietly fixing it: fake-08 already renders a pause menu with an exit (the
//todocited as evidence sits inside a commented-out function), andGameLoop()does return — our ownHost::shouldQuit()just returnsfalse.Verified
Two launch/exit laps in one boot, item re-registering on the second cart, no
rst:. The menu screenshot in the worklog is a framebuffer render (FB_DUMP→ zlib → PNG), not a camera capture. Both boards build.Not verified: touch (driven over serial throughout); a cart using all five
menuitemslots would displace ours; a cart poking0x5fffitself would false-trigger.Worklog:
docs/worklog/2026-08-08-exit-cart-to-launcher.md