Repository navigation
Conversation
…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
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical issues allow silent transfer data loss, unsafe SD-card handovers, and recovery reboot loops after interrupted updates.
14 open issues
Serialize SD access with ownership changes and close remote writers · New Reject file reads with seek or SD errors · New Propagate file close and flush failures · New Pin reconnects to the initially resolved device · New Fail pulls that end before the reported file size · New Restart only after successfully setting the OTA boot partition · New Restore the saved ROM selection after rebuilding the focus group · New Clarify that returning SD ownership requires disabling drive mode · New Poll the request queue during the soak-test loop · New Account for the full listing trailer and status in payload limits · New Reject launches while the PC owns the SD card · New Pin the drive-off reconnect check to the original device · New Match --until text across serial read boundaries · New Report or paginate truncated directory listings · New
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
emulinkclient 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
| 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)) { |
| fseek(f, offset, SEEK_SET); | ||
| body.resize(fread(body.data(), 1, length, f)); | ||
| fclose(f); |
| } | ||
| 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) |
| if not chunk: | ||
| break |
| 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 |
| } | ||
| 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: |
| 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 |
|
|
||
| # -- files on the SD card -------------------------------------------------- | ||
| def ls(self, path: str = "") -> List[dict]: | ||
| return self._call(FILE_LIST, self.remote(path).encode())["entries"] |
…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
|
New commits since the PR was opened:
Sweep on the Tab5 (displayed fps over 10 s):
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 |
ESP-IDF Size Report for 'Tab5 Emu'
FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available. |
- 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


Summary
espp::UsbDevicecomposite device.ota_1, so the 9 MB image can be replaced over USB.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.espp/usb_devicecomes from the esp-cpp/espp#898 branch fortake_msc_from_ejected_host(). Switch back to the registry once that's released.Test plan
🤖 Generated with Claude Code
https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY