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.
This commit is contained in:
libretroadmin
2026-06-14 19:25:51 +00:00
committed by U-DESKTOP-SPFP6AQ\twistedtechre
parent cf6080af28
commit 214c77052e

View File

@@ -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