From 214c77052e8256bc9fe6c3d3a0fbcbc8dcfbead9 Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Sun, 14 Jun 2026 19:25:51 +0000 Subject: [PATCH] libretro: include turbo toggle phase in savestate (fixes #75) The libretro driver's per-frame turbo toggle counters (turbo_button_toggle[MAX_PLAYERS][TURBO_BUTTONS]) are advanced in the input poll while the turbo button is held, and the modular phase of that counter drives when the underlying NES button fires inside the turbo cycle. These counters were module-level state with no savestate involvement: not serialized, not reset on retro_reset, not reset on retro_unserialize. In netplay, the host and client tick this counter independently from their own local input. When the host snapshots state and ships it to the client to align them, the chunk parser restores every NES hardware register, mapper register, and core-level setting - but both ends' turbo counters keep whatever local phase they had. As long as the turbo button stays held, the two ends compute different turbo-fired-this-frame bits, which produce different NES input sequences, which produce divergent game state. Issue #75 has been open on this since 2017. Two-line fix: 1. After FCEUI_LoadGame returns successfully in retro_load_game, the core has just finished populating SFMDATA[] with all mapper chunks via AddExState. Append one more chunk (AddExState(turbo_button_toggle, sizeof(...), 0, "TBTG")) so the toggle counters save and restore alongside everything else. The "TBTG" tag is unused by any known mapper. ReadStateChunk's skip-unknown logic handles older states that predate this chunk - the array stays at the value retro_reset zero-initialised it to, which is now the correct post-load default. 2. memset turbo_button_toggle to zero in retro_reset. Without this, a held turbo button would resume mid-cycle on Reset and produce a different first-frame NES input bit than a fresh boot - small but observably non-deterministic, and the same surface that #75 targets. Verified by inspecting the serialized state binary: - Pre-fix: no TBTG chunk present. - Post-fix, after 5 frames of held turbo with delay=3: TBTG @ offset 5624, size 8, payload 02 00 00 00 00 00 00 00 (counter for player-0 turbo-A = 2, as expected from 5 increments mod (delay+1) = mod 4) ReadStateChunk + CheckS handle the restore via the standard tag-match path (state.c:277), which is bit-for-bit identical to how every mapper chunk gets reloaded. No additional load-side wiring needed. --- src/drivers/libretro/libretro.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index 5275b8a..3f2673e 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -1972,6 +1972,12 @@ void retro_deinit (void) void retro_reset(void) { + /* Reset clears the turbo toggle phase so the rapid-fire cycle starts + * at the same point every reset, matching how the core's other + * input-poll state behaves on power-cycle. Without this, a held + * turbo button would resume mid-cycle after Reset and produce a + * different first-frame input than a fresh boot. */ + memset(turbo_button_toggle, 0, sizeof(turbo_button_toggle)); ResetNES(); } @@ -3853,6 +3859,19 @@ bool retro_load_game(const struct retro_game_info *info) return false; } + /* Piggyback the libretro-side turbo toggle phase into the core's + * savestate (#75). FCEUI_LoadGame just finished populating SFMDATA[] + * with the mapper's chunks via AddExState; appending one more chunk + * here for the turbo counters makes them save/restore alongside + * everything else. Without this, the toggle phase isn't part of + * the state - host and client in a netplay session can drift on + * the counter even after exchanging savestates, producing input + * desync whenever the turbo button is held. The "TBTG" tag is + * unused by any mapper, and ReadStateChunk's skip-unknown logic + * handles older states gracefully (the array stays at retro_reset's + * zero initialisation, which is the post-#75-fix steady state). */ + AddExState(turbo_button_toggle, sizeof(turbo_button_toggle), 0, "TBTG"); + if (palette_switch_enabled) environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ps); else