From e1964d00cb598c3e39c48ae51ec189c0f4cb3366 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:58:39 +1000 Subject: [PATCH] =?UTF-8?q?GBCEmulatorScreen.cpp=20=E2=86=92=20examples/co?= =?UTF-8?q?mpanion=5Fradio/ui-new/,=20fingerprint=201093=20(build=2028=20w?= =?UTF-8?q?as=201071).=20Replaces=20build=2028;=20nothing=20else=20changes?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What changed Quiet period 2 s → 20 s. A background save now only happens after the cart RAM has been untouched for 20 seconds, so ordinary in-game writes during active play no longer trip it. A real menu save is followed by a pause, so it still gets written. Checksum guard. Before any save, background or on-quit, the live cart RAM is checksummed against what was last written; if the bytes are identical, nothing is written to the card. The game replaying identical state, which is what caused the save-just-after-opening, no longer touches the SD card at all. Seeded from the loaded .sav at launch, so opening a game and idling never writes. --- .vscode/extensions.json | 3 +- .../ui-new/GBCEmulatorScreen.cpp | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5c5735c6..b07714d9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "platformio.platformio-ide" ], "unwantedRecommendations": [ - "ms-vscode.cpptools-extension-pack" + "ms-vscode.cpptools-extension-pack", + "pioarduino.pioarduino-ide" ] } diff --git a/examples/companion_radio/ui-new/GBCEmulatorScreen.cpp b/examples/companion_radio/ui-new/GBCEmulatorScreen.cpp index dff2a992..da70b358 100644 --- a/examples/companion_radio/ui-new/GBCEmulatorScreen.cpp +++ b/examples/companion_radio/ui-new/GBCEmulatorScreen.cpp @@ -67,9 +67,11 @@ // after gb_init. Written in the background: every cart-RAM write by // the game marks the save dirty, and poll() (UI task, so it owns the // SD card) writes the file once the game has left its cart RAM alone -// for GBC_SAVE_QUIET_MS -- in practice a couple of seconds after the -// user saves in-game. Quit writes only if something is still dirty, -// so quitting is normally instant. (Build 9 wrote only on quit.) +// for GBC_SAVE_QUIET_MS (20 s) AND the bytes have actually changed +// since the last write (a checksum gates out the frequent identical +// rewrites the game makes). A 32 KB SD write blocks the UI loop on the +// shared SPI bus, so writes are kept rare. Quit writes only if the +// save still differs from disk. // // - Memory: cart RAM (128 KB), ROM arena (reserved at 2 MB on first launch // and retained for the life of the boot, so a big game late in a long @@ -175,7 +177,7 @@ extern void otaResumeRadio(); // main.cpp #define GBC_ROM_RESERVE 0x200000 // 2 MB arena on first launch #define GBC_FRAME_US 16742 // 59.73 Hz #define GBC_MAX_DEFICIT_US 250000 // behind by more than this = stall, resync -#define GBC_SAVE_QUIET_MS 2000 // cart RAM untouched this long -> write .sav +#define GBC_SAVE_QUIET_MS 20000 // cart RAM untouched this long -> consider a save #define GBC_STOP_WAIT_MS 200 // max wait for the task to park on quit #define GBC_TASK_STACK 8192 // bytes, static, reserved at boot // The LoRa radio and mesh loop keep running while a game plays (they live on @@ -244,6 +246,7 @@ static StaticTask_t s_task_tcb; static size_t s_save_size = 0; static volatile bool s_cram_dirty = false; // set by the game's cart-RAM writes (emulator task) static volatile uint32_t s_cram_dirty_ms = 0; // millis() of the last such write +static uint32_t s_saved_sum = 0; // checksum of cart RAM as last written to .sav static char s_save_path[96]; // ---- Core callbacks --------------------------------------------------------- @@ -495,6 +498,19 @@ static void print_heaps(const char *when) { (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)); } +// Fast additive-rotate checksum over the live save region. Two different +// save states practically never collide, which is all this needs: it +// gates out writes when the bytes have not changed since the last one. +static uint32_t gbc_cram_sum(void) { + uint32_t h = 2166136261u; + for (size_t i = 0; i < s_save_size; i++) { + h ^= s_cram[i]; + h = (h << 1) | (h >> 31); + h += 0x9E3779B9u; + } + return h; +} + // Write cart RAM to the .sav file. UI task only (the SD card is the UI // task's). Returns true if the whole file was written. static bool gbc_write_save(void) { @@ -518,6 +534,7 @@ static bool gbc_write_save(void) { Serial.printf("[GBC] save written: %u bytes to %s (remove %lu, open %lu, write %lu, close %lu ms)\n", (unsigned)put, s_save_path, (unsigned long)(t1 - t0), (unsigned long)(t2 - t1), (unsigned long)(t3 - t2), (unsigned long)(t4 - t3)); + if (put == s_save_size) s_saved_sum = gbc_cram_sum(); return put == s_save_size; } @@ -679,6 +696,7 @@ bool GBCEmulatorScreen::launch(int idx) { } } s_cram_dirty = false; // the load above is not the game writing + s_saved_sum = gbc_cram_sum(); // matches the .sav on disk; no write until it changes gb_init_lcd(s_gb, draw_line); s_gb->direct.frame_skip = 1; memset(s_fb, 0, (size_t)GB_W * GB_H * sizeof(uint16_t)); @@ -778,7 +796,8 @@ void GBCEmulatorScreen::finishStop() { bool saved = false; if (s_cram_dirty) { // normally already written in the background s_cram_dirty = false; - saved = gbc_write_save(); + if (gbc_cram_sum() != s_saved_sum) saved = gbc_write_save(); + else saved = true; // already on disk, nothing to write } // The keyboard stays in raw joypad mode, and keeps being drained by the // busy poll, until the ROM list has actually been drawn (poll() releases @@ -834,7 +853,10 @@ void GBCEmulatorScreen::poll() { // game keeps running on core 0 meanwhile). if (s_cram_dirty && (uint32_t)(millis() - s_cram_dirty_ms) >= GBC_SAVE_QUIET_MS) { s_cram_dirty = false; - gbc_write_save(); + // Only touch the card if the save actually differs from what is on + // it: the game rewrites cart RAM with identical bytes often, and a + // 32 KB SD write blocks the UI loop for the length of the write. + if (gbc_cram_sum() != s_saved_sum) gbc_write_save(); } #if GBC_SOUND if (keyboard.rawMutePressed() && s_audio_on) {