Import a .sav file as a save state, and export the cartridge's save as one - #19
Open
macabeus wants to merge 9 commits into
Open
Import a .sav file as a save state, and export the cartridge's save as one#19macabeus wants to merge 9 commits into
.sav file as a save state, and export the cartridge's save as one#19macabeus wants to merge 9 commits into
Conversation
The EEPROM array was byte-reversed within each 8-byte word against every real `.sav`: the 64-bit word goes out most significant byte first and the GBA is little-endian, so the byte a game sends first is the last of the eight in memory. A retail Klonoa save written into the array straight was read as garbage and the game reformatted over it. Detection now reads the SDK string the build embeds rather than asking whether one is roughly there: word-aligned, longest prefix first, three version digits. `SRAM_F_V102` cartridges get their SRAM for the first time, and the ROM's declaration is kept so a caller can name it. `readBackup` and `writeBackup` are a `.sav` in and out, the EEPROM taking the address width its size implies — auto-detection latches 6 bits at the first transfer and never revises, so a 64 Kbit save that waits for it is misaddressed for the rest of the run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`importSaveState` builds a power-on machine of this ROM with the file already in its cartridge and encodes it at frame 0 — the convention VBA-M's `Import battery file` and mGBA's `Load alternate save game` follow, and what makes the result independent of wherever the session is stopped. It builds that on a machine of its own, so nothing about the one being debugged moves. `cartridge-save.ts` is the single home of which file belongs in which chip and of every message that says why one belongs nowhere, so the debug adapter and the in-process transport agree by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file crosses base64-encoded in an ordinary request; at 64 KB worst case it is smaller than the symbol files `gba-kit/importLabels` already carries. The state is written under a name no state has, so importing the same file twice keeps both, and neither request touches execution, so no client sees a stop it did not cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A ⋯ button beside `Save state` opens a menu with the two actions. Where the file dialog lives is the host's business: `pickFile` and `saveFile` join `openText` as optional capabilities on `Transport`, VS Code serves them with the editor's own dialogs the way `gba-kit.importLabels` does, and the webapp with an `<input type="file">` and a download. An item whose host can serve neither is not shown at all, so an embedder that passes no options sees the bar exactly as it was. Neither item is gated on `stopped`, unlike the button beside them: the import builds its snapshot on a machine of its own and the export copies the backing arrays, so neither borrows the machine's execution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whether the 0x0E window has a chip behind it is the cartridge's to say, like the ROM itself: a state carries the field so the format is unchanged but no longer overrules the ROM with it, which is what would otherwise take an `SRAM_F_V` cartridge's SRAM away again on every state loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found three ways an import could report success on a save the game would never read. **The address width belongs to the cartridge, not the file.** `EEPROM_V` does not say whether a chip is 4 Kbit or 64 Kbit, and neither does a file: a 4 Kbit save padded out to 8 KB — what several emulators write — put the chip in 14-bit addressing, where the game's 6-bit requests desynced the serial line for the rest of the run. The width now comes from the length of the transfer the game makes, which is what carries it: a read's address phase ends when the game turns around and reads, so `read` closes it and counts the bits that came. A file only suggests a width until then. This also settles the 64 Kbit case auto-detection never could, since it latched 6 bits at the sixth bit of any address and never revised. **A flash cartridge is refused in both directions.** `FLASH1M_V` was refused for having two banks, but the reason was only half of it: gba-kit backs the 0x0E window with plain memory and emulates no flash chip at all. Sonic Advance 1, 2 and 3 each write the identify sequence, read save bytes where a chip ID should be, give up, and never read the save again — leaving two command bytes in the save data that would go out in the user's next export. Refusing says so. **A `.sav` picked and then dismissed no longer wedges the web page.** The browser's `pickFile` listened for `change` alone, so a cancelled dialog never settled its promise and the save-state bar stayed disabled for good. Along with them: an import numbers a repeat of a name too long for its file, a mis-picked file is turned away by its length before it is encoded, the trigger of the ⋯ menu closes it on Escape, `gba-kit/exportSave` drops two fields no client read, and `serveTransport` is told how to put a capability's answer on the wire rather than sniffing it for a `bytes` field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ncoding it Six findings from the second review round. The address width no longer comes from an imported file's size at all. It could only ever be a guess — a 4 Kbit save padded out to 8 KB is a file several emulators write — and a guess the cartridge could not revise: a read settles the width, but a write has 64 data bits behind its address and no turnaround to end the address phase, so it takes the width as given. A game whose first EEPROM access was a write therefore lost that write, and every transfer after it, purely because a `.sav` had been imported. The chip now keeps only the file's length, which answers for how big an export is until a read settles the real width; `eepromAddrBits` on the bus becomes `eepromSaveBytes`, and `saveFileSize` takes that instead of the width. A read that turns around at a bit count no chip addresses with leaves the line carrying something it lost the framing of, so the chip goes idle for the next start bit rather than staying in an address phase that swallows every request after it. Stimulus that desynced the line recovered 396 times in 400 before this change was made and 162 after it; it recovers 396 again. `pickFile` carries the caller's `maxBytes`, and every implementation refuses a bigger file where its bytes already are. The extension host used to base64 a picked file before anything looked at its length: 64 MB cost it 3.8 s and 1.2 GB of RSS, and 200 MB was a fatal heap OOM that would have taken every extension down with it. In the browser, a file the page cannot read now reports that instead of never settling and leaving the save-state bar disabled for good, and the pre-`cancel` focus fallback is armed only on a browser that needs it — on a modern one it answered "dismissed" for a user who was still choosing and dropped their file. `safeName` kept a trailing digit run so a `(2)` survives truncation, but with no bound on how long that run could be: a name ending in 240 digits came back longer than it went in, and the import failed with ENAMETOOLONG. Only a run short enough to be a mark is kept now. Two imports of one name sent without waiting both found the name free and the second wrote over the first, in the adapter and in the in-process transport alike. Imports queue behind one another in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `.sav` work moved the EEPROM's address width, the state files' paths and the save-state actions, and left comments standing that described the shape each of them had before. Every comment here now reads against the code beneath it: the chip's width comes from the read the game makes, a snapshot without an installed length restores as 0 the way `frameCount` does, `importSave` gives out a state path like the two beside it, and only the actions that change the state list refresh it. The byte order the EEPROM keeps its words in is said where both loops that depend on it can be read, and the `id` that makes a transport message one the host answers is said where it is asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…carries The changeset left out two things it should state. `@gba-kit/gba-node` has a hand-written snapshot serializer that now enumerates `installedBytes`, so it belongs in the bump list beside the emulator whose snapshot it writes. The export side had no paragraph of its own: a `.sav` comes out at the size the cartridge really has rather than the size of the array behind it, and an EEPROM that has told nobody its width is refused rather than guessed at 4 Kbit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
.savfile — the raw battery-backup dump every other GBA emulator and every flashcart reads and writes — can be brought into gba-kit as a save state, and the save
sitting in the machine right now can be written back out as one.
A ⋯ button beside
Save statein the Screen panel's save-state bar opens a menu withImport from a .sav fileandExport to a .sav file. An import is a power-on machineof this ROM with the file already in its cartridge, at frame 0: load it, press
continue, and the ROM boots into its own save. That is what VBA-M's
File > Import > Battery file…and mGBA'sLoad alternate save gameboth do, and itmakes the result independent of wherever the session happened to be stopped. The import
adds the state and opens the drawer; it does not load it, and the machine being debugged
is not touched — no event is emitted, the revision and epoch do not move, and a session
replaying history keeps replaying.
Over DAP and in-process the requests are
gba-kit/importSaveandgba-kit/exportSave.The file dialog is a host capability (
pickFile/saveFileonTransport), so VS Codeserves it with
showOpenDialogandworkspace.fsthe waygba-kit.importLabelsalreadydoes, and the webapp serves it with
<input type="file">and a download. A menu itemwhose host cannot serve it is not shown.
The real import, end to end
kleod.gbadeclaresEEPROM_V121and nothing else; the retail save is 512 bytes andbegins
4b5f4b4c4f4e4f41,"K_KLONOA". Two boots of 900 frames, one with the saveimported and one without:
The control finds a blank chip, zeroes the header and formats a fresh save from word 0
upward. The imported run leaves the header standing and goes straight to the words it
wants, then writes the save back rather than replacing it. The two runs end on different
screens. That is the game reading the save, not 512 bytes sitting in an array.
What it refuses, and why
Which chip a file belongs in follows from the ROM's declared save type and the file's
size together. A file those two cannot account for is refused by a message naming
both — never padded or truncated into the wrong chip, where it would silently do nothing:
Flash is refused in both directions. gba-kit backs the cartridge with plain memory
and emulates no flash chip, and flash chip emulation is out of scope here — so the
question was whether a flash game can read an imported save anyway. It cannot. Sonic
Advance 1, 2 and 3 each write the identify sequence
AA/55/90, read where a chip IDshould be, get the imported save's own bytes back, write the reset command, and never
touch the save again — 0 data reads in 1200 frames — while the command bytes they
wrote land in the save as data. Accepting the file would have been a silent corruption
dressed up as a feature. 1 Mbit flash is doubly out: 128 KB cannot be represented in the
single 64 KB window at all, and bank switching is not emulated either.
An EEPROM that has not said how wide it is cannot be exported. The array is always
8 KB in memory but a 4 Kbit chip's
.savis 512 bytes, so exporting before the game hasread or written its save would be a guess. It is refused with instructions instead: run
the game until it touches its save, or import a
.savfirst.Three emulator fixes came with it
was byte-reversed within each 8-byte word against every real
.sav.written before this, of a game that uses EEPROM, comes back with its in-game save
byte-swapped. The state format is unchanged and still loads, but that game's save
inside it will not be read — re-import the
.sav.SRAM_F_Vcartridges get their SRAM. Detection looked for the literalSRAM_V,which
SRAM_F_V102does not contain, so those games had no working save at all.the sixth bit of any address and never revised, so a 64 Kbit cartridge read the wrong
words for the whole run. It now comes from the length of the read the game makes, which
is what actually carries it.
Deliberately left alone
addressing. A write carries no length — 64 data bits follow the address with nothing
marking where it ended — so settling the width from one would need the DMA transfer's
word count plumbed to the chip, which is how mGBA and VBA-M do it. That is
main'sbehaviour, not something this branch introduced, and no ROM on hand exercises it. The
changeset says so plainly rather than implying it is fixed.
is a file several emulators write; trusting its length forced 14-bit addressing on a
4 Kbit cartridge and desynced the line. The cartridge's own first read decides, and the
file's length only answers for how big an export is until then. Both reviewers instead
suggested sniffing the file's contents — rejected, since a file's bytes are no more
evidence about the cartridge than its length is.
.savis the right game's save. A file of a legal size forthe declared type is accepted whatever is in it. Nothing in the format says what it is.
the file, with a name already taken getting
(2).🤖 Generated with Claude Code