Skip to content

Launcher features, USB link (console, test control, SD drive, OTA), emulink - #18

Open
finger563 wants to merge 6 commits into
mainfrom
feat/launcher-features
Open

finger563 wants to merge 6 commits into
mainfrom
feat/launcher-features

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Summary

  • Launcher: games grouped by console, with a console badge; settings are saved, and the last game can be resumed.
  • USB link: the USB-C port is now an espp::UsbDevice composite device.
    • CDC console: the logs, teed to UART0.
    • Vendor interface: framed services.
      • OTA, through a small updater app in ota_1, so the 9 MB image can be replaced over USB.
      • Core dump, system info and reboot (including into the ROM download mode), and a log monitor.
      • The emulator's test control: status, launch and quit, press and hold buttons, save and load states, screenshots, and SD card ls, push, pull, rm and mkdir.
    • MSC: the SD card as a USB drive. It goes to the PC only on request (the launcher's switch, or the link), and comes back once the PC has ejected it.
  • tools/emulink: the Python client (python -m emulink ...) for scripted tests and for getting content onto the card. The same copy is in esp-box-emu.
  • MGS at full speed with USB up. The drive function's buffers take about 8 KB of main SRAM. That pushed psyz's CLUT caches into the P4's slow LP SRAM.
    • psyz (esp-cpp/psyz@f3aebd9) now allocates the caches only from main SRAM or TCM, when the game starts.
    • The SD driver has a permanent 16 KB PSRAM bounce buffer, so reads into unaligned buffers no longer need DMA-capable internal RAM, and they now move 16 KB per command.
    • MGS intro frames at 91 s: 1017–1042 before, 1238–1260 after (1231 without the drive function at all).
  • JK: file staging goes through cache-aligned PSRAM, and card reads use stdio, so a failed read is an error instead of an abort. The menu also loads screenshots with stdio.

espp/usb_device comes from the esp-cpp/espp#898 branch for take_msc_from_ejected_host(). Switch back to the registry once that's released.

Test plan

  • OTA, launch and quit, press, save and load, screenshot, ls, push, pull and rm, drive on and off, and log, all through emulink.
  • Five MGS launch and quit cycles: no internal RAM leak (66.1 KB free in the menu each time), and the same speed every time. Each cycle did a 1 MB SD round trip while the game was running.
  • MGS save and load state.
  • Jedi Knight, Phantom Menace, Minish Cap, Chrono Trigger, Sonic 3 and Doom: each launched and did an SD round trip while playing.
  • Three drive hand-overs: a file the Mac wrote to the drive is read back identically on the device.
  • Box-side changes, in their own esp-box-emu PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY

finger563 and others added 3 commits November 10, 2026 21:04
…sume

- The game list is grouped under a header per console (SNES, Game Boy
  Advance, Genesis, PlayStation, PC, PICO-8), metadata.csv order within one.
- The selected game's console is shown above its boxart, on the launcher
  only: boxart/consoles/<key>.jpg from the SD card if present, else a badge
  drawn from LVGL objects (a generic icon and the name, no logos).
- Settings survive a power cycle in /sdcard/settings.bin (espp
  persistent_data, versioned): volume, mute, brightness, video scaling,
  resume, the last game started (selected again at boot) and the save slot
  last used per game. Written once launcher changes stop, at each launch and
  on the way back from a game (pause-menu changes).
- "Resume last save" (on by default): starting a game loads the slot it was
  last saved to / loaded from, for the emulator cores (Genesis, SNES, GBA,
  PICO-8 after its first second); the native ports start fresh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
…), OTA updater, emulink

The USB-C port becomes an espp::UsbDevice with three functions:

- CDC console: the logs (teed to UART0)
- a vendor interface carrying framed services: OTA (through a separate
  updater app in ota_1, so the 9 MB main image can be replaced), the core
  dump, system info / reboot, a log monitor, and the emulator's own test
  control (status, launch / quit, press / hold buttons, save / load states,
  screenshots, and SD card file list / push / pull / rm / mkdir)
- MSC: the SD card as a USB drive, handed to the PC only on request (the
  launcher's switch or the link) and taken back once the PC ejected it

tools/emulink is the Python client (python -m emulink ...), shared with
esp-box-emu, for scripting tests and getting content onto the card.

Also: JK stages files through cache-aligned PSRAM (internal DMA memory is
scarce with USB up) and reads the card with stdio, so a failed read is an
error instead of an abort; the menu loads screenshots with stdio too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
The USB drive function's buffers take ~8 KB of main SRAM. With that gone,
the rasterizer's per-core CLUT caches landed in the P4's LP SRAM (slow to
read): sprites took 4.7x longer and busy scenes ran ~40% slower.

- psyz: allocate the caches only from main SRAM or TCM, and take them
  when the game starts (Draw_ClutCacheInit()), after the statics reset
- SD card: a permanent 16 KB PSRAM bounce buffer for reads into unaligned
  buffers. The driver otherwise mallocs one from DMA-capable internal RAM
  per call, which failed once the caches had the main SRAM, and moved one
  sector per command

MGS intro, frames drawn 91 s after launch: 1017-1042 before, 1238-1260
now (1231 with no drive function at all). Stress tested: five MGS
launch / quit cycles (no leak, same speed each time), save / load, JK,
TPM, GBA, SNES, Genesis and Doom with 1 MB SD round trips while playing,
and three drive hand-overs with Mac writes verified on the device.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
Copilot AI balanced review requested due to automatic review settings November 11, 2026 22:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical issues allow silent transfer data loss, unsafe SD-card handovers, and recovery reboot loops after interrupted updates.

14 open issues
What changed in this PR

This PR expands the launcher and adds a composite USB connection for console access, scripted emulator control, SD-card transfers, and firmware updates through a dedicated updater app.

Changes:

  • Groups games by console and persists launcher settings, last-game selection, and resume slots.
  • Adds USB services and the Python emulink client for device control, diagnostics, file transfers, and OTA updates.
  • Adjusts memory placement and SD buffering to preserve game performance with USB enabled.
File Description
updater/​sdkconfig.defaults Configures the updater’s USB interfaces and rollback support.
updater/​main/​updater.cpp Implements USB OTA service and idle return to the emulator.
updater/​main/​idf_component.yml Declares updater service dependencies.
updater/​main/​CMakeLists.txt Registers updater sources and dependencies.
updater/​CMakeLists.txt Defines the standalone updater project.
tools/​emulink/​emulink/​link.py Implements USB discovery, transport, and request matching.
tools/​emulink/​emulink/​frame.py Adds framed-message encoding and parsing.
tools/​emulink/​emulink/​espp_tools.py Locates external espp host clients.
tools/​emulink/​emulink/​control.py Provides emulator-control and SD-file APIs.
tools/​emulink/​emulink/​cli.py Adds commands for control, transfers, diagnostics, and updates.
tools/​emulink/​emulink/​__main__.py Enables execution with python -m emulink.
tools/​emulink/​emulink/​__init__.py Exports the client API and describes USB services.
sdkconfig.defaults Enables composite USB interfaces and OTA rollback.
partitions.csv Allocates emulator, updater, and OTA metadata partitions.
main/​snes_cart.hpp Enables automatic save-state resume for SNES.
main/​remote_control.hpp Implements remote commands and main-task dispatch.
main/​pico8_cart.hpp Enables delayed save-state resume for PICO-8.
main/​mgs_cart.hpp Exposes MGS frame counts for performance monitoring.
main/​main.cpp Integrates settings, USB services, remote control, and resume.
main/​genesis_cart.hpp Enables automatic save-state resume for Genesis.
main/​gba_cart.hpp Enables automatic save-state resume for GBA.
main/​cart.hpp Adds host save/load, deferred quit, and resume handling.
components/​tab5-emu/​src/​usb_msc.cpp Removes the standalone mass-storage implementation.
components/​tab5-emu/​src/​usb_hid.cpp Explicitly selects the USB host controller.
components/​tab5-emu/​src/​usb_device.cpp Implements composite USB and SD ownership transfers.
components/​tab5-emu/​src/​tab5-emu.cpp Adds PSRAM SD buffering and remote button input.
components/​tab5-emu/​src/​coredump.cpp Retains crash dumps for USB retrieval.
components/​tab5-emu/​linker.lf Places USB software buffers in PSRAM.
components/​tab5-emu/​include/​tab5-emu.hpp Exposes composite USB and remote-input interfaces.
components/​tab5-emu/​idf_component.yml Uses the USB dependency branch supporting explicit take-back.
components/​tab5-emu/​CMakeLists.txt Registers the USB buffer linker fragment.
components/​settings/​src/​settings.cpp Implements settings persistence and save-slot tracking.
components/​settings/​include/​settings.hpp Defines versioned settings and synchronized access.
components/​settings/​idf_component.yml Adds the persistent-data dependency.
components/​settings/​CMakeLists.txt Registers the settings component.
components/​rom_info/​src/​rom_info.cpp Maps games to console groups and display metadata.
components/​rom_info/​include/​rom_info.hpp Defines console identities and badge attributes.
components/​mgs/​src/​mgs.cpp Initializes palette caches during game startup.
components/​menu/​src/​menu.cpp Uses checked stdio reads for screenshots.
components/​jk/​src/​jk.cpp Prefers cache-aligned PSRAM for file staging.
components/​gui/​src/​gui.cpp Adds console grouping, badges, settings, and ownership display.
components/​gui/​src/​console_art.cpp Draws console badges and locates optional artwork.
components/​gui/​include/​gui.hpp Declares launcher state and reload interfaces.
components/​gui/​include/​console_art.hpp Declares console artwork helpers.
components/​gui/​CMakeLists.txt Links the launcher to settings persistence.
components/​devlink/​src/​devlink.cpp Registers USB services and preserves panic details.
components/​devlink/​include/​devlink.hpp Defines service integration and guarded flash operations.
components/​devlink/​idf_component.yml Declares diagnostic and dispatcher dependencies.
components/​devlink/​CMakeLists.txt Registers services and panic-handler wrappers.
CMakeLists.txt Includes settings and device-link components.

Review effort: Balanced

Comment on lines +152 to +154
std::lock_guard<std::recursive_mutex> lk(lvgl_mutex());
std::error_code ec;
if (!usb_device_->set_msc_owner(0, espp::UsbDevice::MscOwner::Host, ec)) {
Comment thread main/remote_control.hpp
Comment on lines +353 to +355
fseek(f, offset, SEEK_SET);
body.resize(fread(body.data(), 1, length, f));
fclose(f);
Comment thread main/remote_control.hpp
}
void close_file(const std::string &only_if_path = "") {
if (write_file_ && (only_if_path.empty() || only_if_path == write_path_)) {
fclose(write_file_);
from espp_ota.client import OtaClient # type: ignore

image = open(args.binary, "rb").read()
link = _open(args)
Comment on lines +170 to +171
if not chunk:
break
Comment thread main/remote_control.hpp
auto e = fmt::format("{}{{\"name\":{},\"path\":{},\"platform\":\"{}\",\"console\":{}}}",
i > offset ? "," : "", quote(r.name), quote(r.rom_path), r.platform,
quote(console_info(console_of(r.platform)).name));
if (j.size() + e.size() + 8 > espp::stream_frame::kMaxPayloadSize) break; // the host asks for the rest
Comment thread main/remote_control.hpp
}
const std::string match(text);
bool found = false;
on_main([&] { found = select(match) && (gui_.ready_to_play(true), true); }, std::chrono::seconds(2));
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
try:
with Link(args.serial).open(5) as link:
Comment on lines +339 to +346
with serial.Serial(port, 115200, timeout=0.2) as s:
while end is None or time.monotonic() < end:
data = s.read(4096)
if data:
write(data)
(out or sys.stdout.buffer).flush()
if args.until and args.until.encode() in data:
return 0
Comment thread tools/emulink/emulink/control.py Outdated

# -- files on the SD card --------------------------------------------------
def ls(self, path: str = "") -> List[dict]:
return self._call(FILE_LIST, self.remote(path).encode())["entries"]
finger563 and others added 2 commits November 11, 2026 17:30
…console

- The link's SCREENSHOT no longer goes through the SD card: the main loop
  takes the game's frame between two frames and holds the game still
  (Cart::capture_hold() pauses the cores with their own tasks: MGS, JK,
  TPM, PICO-8) while SCREENSHOT_READ reads the pixels, so nothing is
  allocated or copied. emulink converts them with a lookup table: a
  screenshot takes 0.3-1.2 s (JK's 640x480 the longest).
- espp routes only stdout to the CDC console: stderr goes there too now.
- espp/usb_device pinned to the #898 merge commit (the branch is gone).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
For performance and memory tests of any core: frames_presented and
frames_dropped (the BSP's counters: displayed fps for every emulator, not
just the ones that count their own frames), internal_min_free and
psram_min_free.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
@finger563

Copy link
Copy Markdown
Contributor Author

New commits since the PR was opened:

  • Screenshots: read straight from the game's frame buffer while the game is held still (Cart::capture_hold() pauses MGS, JK, TPM and PICO-8). No SD card, no allocation, 0.3–1.2 s.
  • stderr now reaches the USB console too.
  • espp/usb_device is pinned to the feat(usb_device): take_msc_from_ejected_host() for devices that stay attached espp#898 merge commit; the branch was deleted.
  • Link status gains frames_presented, frames_dropped, internal_min_free and psram_min_free.

Sweep on the Tab5 (displayed fps over 10 s):

Game fps
Sonic 2, Sonic 3 59.5–59.7
Minish Cap, Mario Kart SC 53.7–55.5
SNES (SMW, DKC, Doom) 29–33
JK 41.7
TPM 18.3
MGS, intro window 9.6

No resets, and menu internal RAM stays at 63–64 KB throughout. Menu PSRAM drops after MGS (about 0.7 MB) and JK (about 1.6 MB) and then stays there; the Tab5 has PSRAM to spare.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

ESP-IDF Size Report for 'Tab5 Emu'

Metric Base PR Delta
FLASH 0 bytes (0.00%) 0 bytes (0.00%) 0 bytes (0.00%)
DRAM 268,638 bytes (60.87%) 267,826 bytes (60.69%) ⬇️ -812 bytes (-0.18%)
IRAM 0 bytes 0 bytes 0 bytes
RAM (DRAM+IRAM) 268,638 bytes 267,826 bytes ⬇️ -812 bytes (-0.18%)

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.

- FILE_LIST pages like LIST_ROMS (path\0offset, reply "next"), so emulink
  ls lists a whole directory instead of stopping at ~69 entries.
- femto8: cart buffers only while they are needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY
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.

2 participants