Skip to content

NES: MMC3/MMC6 battery-save dumping, save-fill warning, reload survival#28

Merged
pathawks merged 3 commits into
mainfrom
nes-saves
Jun 13, 2026
Merged

NES: MMC3/MMC6 battery-save dumping, save-fill warning, reload survival#28
pathawks merged 3 commits into
mainfrom
nes-saves

Conversation

@pathawks

Copy link
Copy Markdown
Owner

Three device-agnostic improvements, independent of any one dumper.

  • MMC3 / MMC6 battery-save dumping. The shared MMC3 save path read $6000 with the PRG-RAM chip off the bus and returned zero fill. Give MMC3 a dumpSave that brackets the read FME-7-style — $A001 <- $C0 (enabled, write-protected) only for the read, then $40 to take the chip back off the bus — so the SRAM is exposed for as short a time as possible. MMC6 (HKROM) shares mapper 4 but keeps its 1 KiB save inside the mapper at $7000-$73FF behind a master gate ($8000 bit 5) and per-512-byte-half enables ($A001): auto-detect it when $6000 reads uniform, fingerprint it by the driven zeros a single-half read-enable produces (a response open bus can't mimic), gate detection on byte diversity rather than uniformity (open bus can read as a few capacitance-echo values), and consensus-read the 1 KiB to ride out a battery-weak array.
  • Uniform-fill save warning. A save that reads back as a solid 0x00 or 0xFF block is the electrical signature of a chip that never reached the bus or an erased region, not real data — surface the suspicion in the event log at dump time while keeping the bytes.
  • Reload survival. Reloading with a serial port open could hang navigation until the device was physically unplugged. Add Transport.closeNow() for a synchronous best-effort teardown from beforeunload/pagehide, and persist the last-connected device id so the page-load auto-reconnect resumes the device that was actually in use.

Three device-agnostic improvements, independent of any one dumper.

MMC3 / MMC6 battery-save dumping. The shared MMC3 save path read
$6000 with the PRG-RAM chip off the bus, so it returned zero fill.
Give MMC3 a dumpSave that brackets the read FME-7-style -- $A001 <- $C0
(enabled, write-protected) only for the read, then $40 to take the chip
back off the bus -- so the SRAM is exposed for as short a time as
possible. MMC6 (HKROM) shares mapper 4 but keeps its 1 KiB save inside
the mapper at $7000-$73FF behind a master gate ($8000 bit 5) and
per-512-byte-half enables ($A001): auto-detect it when $6000 reads
uniform, fingerprint it by the driven zeros a single-half read-enable
produces (a response open bus cannot mimic), gate detection on byte
diversity rather than uniformity (open bus can read as a few
capacitance-echo values), and consensus-read the 1 KiB to ride out a
battery-weak array.

Uniform-fill save warning. A save that reads back as a solid 0x00 or
0xFF block is the electrical signature of a chip that never reached the
bus or an erased region, not real data -- surface the suspicion in the
event log at dump time while keeping the bytes.

Reload survival. Reloading with a serial port open could hang the
navigation until the device was physically unplugged. Add
Transport.closeNow() for a synchronous best-effort teardown from
beforeunload/pagehide, and persist the last-connected device id so the
page-load auto-reconnect resumes the device that was actually in use.

This comment was marked as resolved.

Per PR review: dumpSave's MMC6 code paths exited with $A001 <- 0x00,
clearing the write-protect bit (6) and contradicting initMmc3's safe
park ($40 — off the bus AND write-protected, so a variant that ignores
the enable bit still can't be written). Close the MMC6 master gate
first, then park PRG-RAM control at $40 on both the MMC6-detected and
not-MMC6 exits: a no-op on a gated MMC6, the safe state on an MMC3
false-positive.

Tests now assert every dumpSave exit leaves $A001 at $40 — the not-MMC6
path directly, and the MMC6-detected path via its final write value
(the master gate forces $A001 to 0, which would otherwise mask a
regression on that path).

This comment was marked as resolved.

Two more PR-review nits on the unload path:

- The closeNow-less fallback in the unload handler floated
  transport.disconnect() (async) inside a synchronous try/catch, which
  cannot catch its rejection. Swallow it with .catch() to avoid an
  unhandled promise rejection during navigation.

- closeNow()'s docstring claimed teardown happened "in one synchronous
  burst", but releaseLock + port.close() are deliberately deferred to a
  microtask (the stream locks aren't free until cancel/abort settle, so
  releaseLock would throw this tick). Reword to describe the actual
  synchronous-cancel/abort plus deferred-close behavior.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@pathawks pathawks merged commit f419551 into main Jun 13, 2026
2 checks passed
@pathawks pathawks deleted the nes-saves branch June 13, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants