Skip to content

Savestates: force NMI to capture states on NMI-disabled sections - #488

Open
drizzt wants to merge 1 commit into
MiSTer-devel:masterfrom
MiSTer-DB9:savestate-force-nmi
Open

drizzt wants to merge 1 commit into
MiSTer-devel:masterfrom
MiSTer-DB9:savestate-force-nmi

Conversation

@drizzt

@drizzt drizzt commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

The save state engine only enters by hijacking an S-CPU NMI or IRQ vector fetch (native-mode $00FFEA / $00FFEE). Sections that run with NMI disabled (NMITIMEN bit 7 clear) and no IRQ source never fetch a vector, so a save or load request can never trigger. Title screens polling for Start via auto-joypad read are the common case.

Worse, the latched request is uncancelable: it fires at the next vector fetch once gameplay re-enables NMI, capturing or restoring a random later moment instead of the screen the user asked for.

Affected games include those tracked in #484 (Earth Defense Force, Super Ninja Kid, Operation Logic Bomb). The current workaround is a per-game ROM byte-patch to force NMI enable, which is not a general fix.

Change

Add a timer to the savestate engine (rtl/savestates.sv): when a request stays armed for ~48.8 ms (2^20 MCLK ticks, matching the sibling nmi_cycle_cnt idiom) without a hijack, assert a new ss_nmi_force. This ORs into P65_NMI_N in rtl/CPU.vhd, injecting one NMI regardless of NMITIMEN. The existing vector-fetch hijack then steals that NMI and runs the helper, so the game's own handler never executes.

  • Force drops as soon as ss_busy rises; NMI edge detection keeps it single-shot (no storm, no re-fire after the helper's RTI).
  • When no request is armed the OR term is a no-op, so existing timing is unchanged. Normal-latency saves fire on the natural vblank NMI (~16 ms) long before the timer, so it never asserts during normal play.
  • The signal threads savestates.sv -> main.v -> SNES.vhd -> CPU.vhd, following the existing SS_* port style, with := '0' defaults on the VHDL ports.

Scope

Consoles in 6502 emulation mode (NMI vector $FFFA, not hijacked) stay out of scope; the helper is native mode only, and commercial games are in native mode wherever a save makes sense.

Validation

Verilog testbench on savestates.sv (iverilog): ss_nmi_force rises after the timer on a quiet bus, the resulting $00FFEA fetch starts the walk, the force drops once ss_busy rises, and it stays low on the normal prompt-latency path (natural saves untouched).

Addresses #484.

The save state engine only enters by hijacking an S-CPU NMI or IRQ
vector fetch. Sections that run with NMI disabled (NMITIMEN bit 7 clear)
and no IRQ source, such as title screens polling for Start via
auto-joypad read, never fetch a vector, so a save or load request can
never trigger. The latched request instead fires at the next vector
fetch once gameplay re-enables NMI, capturing or restoring a random
later moment.

Add a timer to the savestate engine: when a request stays armed for
~48.8 ms (2^20 MCLK ticks) without a hijack, assert ss_nmi_force, which
ORs into P65_NMI_N and injects one edge-triggered NMI regardless of
NMITIMEN. The existing vector-fetch hijack then steals that NMI and runs
the helper, so the game's own handler never executes. The force drops as
soon as ss_busy rises, and NMI edge detection keeps it single-shot. When
no request is armed the term is a no-op, so existing timing is unchanged.

Consoles in 6502 emulation mode (NMI vector $FFFA, not hijacked) stay
out of scope; the helper is native mode only.

Addresses MiSTer-devel#484.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant