Skip to content

Exit a running cart from the pause menu (IN-6) - #36

Open
aldwinhermanudin wants to merge 2 commits into
mainfrom
in6-exit-menu
Open

aldwinhermanudin wants to merge 2 commits into
mainfrom
in6-exit-menu

Conversation

@aldwinhermanudin

Copy link
Copy Markdown
Contributor

Tapping MENU in a running cart now offers exit to launcher, returning to the cover-art carousel. No reboot — the frame loop breaks and app_main re-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::ExecuteLua registers menuitem(5, "exit to launcher", …) in the cart sandbox — no cart edit
  • the callback pokes a magic byte to 0x5fff, inside PICO-8's GPIO area — the documented cart↔host channel
  • main.cpp's frame loop reads it via the already-public Vm::getPicoRam()

main.cpp runs its own copy of Vm::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 trade GC_MANUAL already makes.

Two easy things to get wrong

  • The cart sandbox doesn't exist at LoadCart time — it's created in __z8_run_cart on the first frame, so registering earlier fails with __cart_sandbox is not a table. Registration runs after one Step() and checks its return value; a silent failure 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 can't eject the next cart. Address + magic rather than a bare flag, since GPIO is cart-writable by design.

This replaces two earlier designs

  • A hold-MENU gesture in components/input — built and tested, then dropped. Undiscoverable, rebooted the board (~1.8 s, and esp_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.
  • A fake-08 fork change (extcmd("exit_to_host") + a Vm quit flag) — worked, and allowed bottom placement in the menu. Review found extcmd is 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 //todo cited as evidence sits inside a commented-out function), and GameLoop() does return — our own Host::shouldQuit() just returns false.

Verified

pause-menu "exit to launcher" item registered: yes
cart asked to exit — returning to the launcher
pause-menu "exit to launcher" item registered: yes
cart asked to exit — returning to the launcher
clean exits: 2    reboots: 0

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 menuitem slots would displace ours; a cart poking 0x5fff itself would false-trigger.

Worklog: docs/worklog/2026-08-08-exit-cart-to-launcher.md

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant