Memory Cards
How a card is represented is in Cards: the card is a bundle of shape "card", its card
map holds the card’s own properties, and each save is a bundle part carrying that save’s directory entry. This page is
the other half, the part that is specific to real hardware. What a writer has to regenerate. What each format fixes.
What splitting costs, worked through on actual cards.
That representation is enough to write the saves back onto a card of the same kind. It is not enough to reproduce the
original card byte for byte, and it is not meant to be. A producer that wants byte-exact fidelity keeps a
card-image part alongside the split saves.
Per-format facts
Section titled “Per-format facts”format fixes two numbers, both in bytes: the length of a dirent, and the size of a block.
| Format | dirent | Block | Notes |
|---|---|---|---|
ps1-mc | 128 | 8192 | The 128 is one directory frame, and a block is 64 of them. |
ps2-mc | 512 | 1024 | One entry for the save’s directory, one per file inside it. A block is a cluster of two 512-byte pages. |
n64-cpak | 32 | 256 | The pak’s own word for a block is a page. |
gc-mc | 64 | 8192 | |
vmu | 32 | 512 | |
neogeo-mc | 4 | 64 | Cards run from 2 KiB to 16 KiB and every one of them uses the same block. |
saturn-bup | 30 | 64 | The entry sits at the head of the save’s own first block rather than in a directory region, and the block list that follows it is the writer’s. The 64 is the console’s internal memory; a backup cartridge is a separate device carrying its own. |
The names are the ones already in circulation for these layouts, not system slugs with a suffix, which is why ps1-mc
says ps1 where the registry says psx, and why vmu carries no system at all. Reading a
system out of one of these names is a mistake; that is what system is for.
The table covers the formats this spec has rebuild rules for, not every backup medium ever built. PC Engine internal
backup RAM, a Famicom Disk System disk, a WonderSwan’s internal EEPROM and several others have no entry yet, and adding
one means documenting its dirent length and its rebuild rules alongside the name.
Capacity and out-of-band bytes
Section titled “Capacity and out-of-band bytes”capacity is the size of the card’s data area: every byte the console can address
on it, the blocks the filesystem keeps for itself included. It is the figure the card is sold under, and it does not
move when the card fills up.
It is not the room a save has. Every format here spends part of its data area on a header, a directory and a FAT, and
some spend a great deal more: a VMU addresses 256 blocks and hands saves 200 of them, and a GameCube Memory Card 59 has
64 blocks and is named for the 59 that survive its header, its directory and its block allocation table, the last two
mirrored. capacity on those two is 131072 and 524288, the whole area in both cases. What actually fits is that number
minus what the format reserves, which is a rebuild rule rather than a field: a writer allocating blocks has to know it,
and nothing else does.
It is not necessarily the length of a dump either, because some media carry an out-of-band area that is invisible to the
filesystem. PS2 is the case that bites. An 8 MB PS2 card holds 8388608 bytes of data, and a raw dump from a hardware
reader is 8650752 bytes, because every 512-byte page carries 16 further bytes of spare area holding ECC. So capacity
on a PS2 card is 8388608, and a byte-exact card-image part of the same card is
8650752 bytes, and the two are supposed to disagree. Read the image’s length off its own size; never derive it from
capacity.
The split follows what a writer does. ECC is regenerated on write, like every other structural field below, so a save’s
payload never carries it and capacity never counts it. A card-image keeps it, because keeping the card byte for byte
is the only reason that part exists.
Why there are no block positions
Section titled “Why there are no block positions”Because no console addresses a save by block, and every tool that moves saves between cards throws that field away:
- The PS1 memory card format specifies that a
.mcsimporter parse the filename and the filesize “while other fields should be ignored”, reconstructing the rest against the destination card. - Dreamcast’s
.dcispecification says outright that “the ‘location of first block’ field should be ignored when reading”. - MPKEdit writes
0xCAFEover the start-page field when it exports an N64.note, and uses that as the file’s type magic. - Dolphin overwrites a
.gci’s stored first block on every load, allocating fresh blocks at the destination.
Consoles open saves by name and identifier instead: bu00:BASLUS-XXXXX on PS1, gamecode plus makercode plus filename on
GameCube, company and game code plus note name on N64, NGH plus sub-number on Neo Geo. Block placement is an allocator’s
business, so recording it would preserve a number that is wrong the moment the save lands anywhere else.
Block counts are a different thing, and they are still not stored, because they are derivable: a producer stores exactly the bytes the card allocated, so the count is the nested bundle’s total payload size divided by the block size for the format.
That division takes the block size from the format, which holds for every format here except saturn-bup. Its 64 is the
console’s internal memory and a backup cartridge carries its own, so a count derived this way for a cartridge is wrong.
The table stands as written for 0.3. Settling it means keying the block size on
capacity, which is required on every card map, rather than on the format alone: a
cartridge dumped by itself is a card bundle with no role anywhere in it, so the medium cannot be
read back off the role the way it can when a Saturn is read whole as a device.
What a writer regenerates
Section titled “What a writer regenerates”Everything structural: block chains, the FAT or BAT and its mirrors, every checksum, the header block, directory
ordering, and ECC where the medium has it. dirent plus the payload is sufficient input for every format here, which is
what .mcs, .psu, .note and .gci each demonstrate in production, since none of them carries more than that.
Three cases need more:
- Saturn stores its own block chain inside the save’s first block, so a writer rewrites those numbers rather than
treating the payload as opaque. It is the only format here where that is true. The
direntahead of that chain is not affected: every field in it — name, language, comment, date, length — is the same wherever the save sits, so it is carried verbatim like any other format’s and only the chain is rebuilt. - GameCube saves for Phantasy Star Online and F-Zero GX embed the destination card’s flash serial and a CRC32 over it, and the game rejects a save whose serial does not match. A writer re-signs them, as Dolphin does on every copy.
- Dreamcast mini-games must start at block 0 and stay contiguous, because they execute in place from flash. The
dirent’s type byte distinguishes them (
0xCCfor a mini-game,0x33for data), so a writer can tell without understanding the payload.
Neo Geo needs contiguity too, and stores no length of its own, so a writer allocates a run of the save’s size rather than following a chain.
What splitting a card drops
Section titled “What splitting a card drops”system_area is what a writer needs to rebuild a card’s own identity rather than
only its contents: an N64 controller pak’s 32-byte label, a Neo Geo card’s 16-byte cardholder username, a Dreamcast
VMU’s custom colour and icon shape. None of it belongs to any save, so splitting a card would otherwise drop it, and
keeping it would otherwise mean keeping a whole card-image for a few dozen bytes.
What system_area does not cover is the residue of deleted saves, whose payload blocks a card keeps intact long after
the directory stops pointing at them, and the contents of free blocks. Neither belongs to any save, so neither survives
a split, and a producer that wants them keeps a card-image too. That is the whole of what splitting a card costs.
Several saves for the same game
Section titled “Several saves for the same game”A card routinely holds more than one save for a single game, because many of them write one file per save slot. Final
Fantasy VII is the extreme case: one 8192-byte block per save, named BASCUS-94163FF7-S01 through -S15 on the US
release, which is fifteen separate directory entries and every block a PS1 card has. A card can be nothing but Final
Fantasy VII saves. Each is a separate save, and nothing about the game identifies which is which.
Two details of that naming are worth having, because they are what path and slot are each for:
- The
-Sxxnumber is the game’s slot, not the card’s. It is the slot Final Fantasy VII shows in its own menu, and it lands in whatever directory slot the card’s allocator picked. Save to the game’s slot 7 on an empty card and…FF7-S07sits in directory slot 1.pathcarries the first,slotthe second, and they are independent. - All three US discs share one save name. The discs are
SCUS-94163,SCUS-94164andSCUS-94165, but the save filename always carries the disc-1 code so every disc reads the same saves. Aserialtaken from a card filename is the save’s product code, not necessarily the code of the disc that wrote it.
Saves made of several files
Section titled “Saves made of several files”A PS2 save is a directory: BASLUS-20312/ holding icon.sys, an icon file and the game’s own data. Those are the parts
of that save’s nested bundle, distinguished by path exactly as path already
covers 3DS extdata.
The directory has a directory entry of its own, carrying its mode bits and timestamps, and it belongs to no file. That
one rides on the outer bundle part, alongside the slot. .psu is laid out the same way, with the directory’s entry
first and . and .. behind it.
Worked example: a PS1 card
Section titled “Worked example: a PS1 card”Three saves on a 128 KiB card, two of them for the same game. The Final Fantasy VII saves went to the game’s slots 1 and 3, and landed in directory slots 1 and 2 in the order they were written.
[0] header:{ 0: "card", 1: "psx", 4: { 0: "ps1-mc", 1: 131072 },}
[1] parts: { 0: 0, 1: "bundle", 2: "memcard-1", 3: "BASCUS-94163FF7-S01", 4: 1, 5: h'…128 bytes…', 9: 18540(h'…'), 11: { 3: "SCUS-94163", 4: "Final Fantasy VII" }, 12: "psx", -1: h'DA31534156…' }
{ 0: 1, 1: "bundle", 2: "memcard-1", 3: "BASCUS-94163FF7-S03", 4: 2, 5: h'…128 bytes…', 9: 18540(h'…'), 11: { 3: "SCUS-94163", 4: "Final Fantasy VII" }, 12: "psx", -1: h'DA31534156…' }
{ 0: 2, 1: "bundle", 2: "memcard-1", 3: "BASLUS-00594", 4: 3, 5: h'…128 bytes…', 9: 18540(h'…'), 11: { 3: "SLUS-00594", 4: "Metal Gear Solid" }, 12: "psx", -1: h'DA31534156…' }Each payload is a complete bundle holding one part, the save’s 8192 bytes, plus the inner bundle’s own framing. The two
Final Fantasy VII entries differ from the Metal Gear Solid one by a single byte, because their game names differ by a
character. The header carries no game, because none of the three describes the card. Each nested bundle’s size
covers every block the save occupied, so a save spanning two blocks is one part of 16384 bytes, and the fact that its
two blocks were not adjacent is not recorded.
Inside the first payload:
[0] header:{ 0: "save", 1: "psx", 2: { 3: "SCUS-94163", 4: "Final Fantasy VII" }, 5: "Midgar, before the reactor",}
[1] parts: { 0: 0, 9: 18540(h'…'), -1: h'…8192 bytes…' }The inner part carries no slot and no dirent. A Final Fantasy VII save is one file with one directory entry, and
that entry is on the outer part. That is the whole difference from the PS2 case below, where the save is a directory and
its files have entries of their own.
A card of one game
Section titled “A card of one game”Fifteen Final Fantasy VII saves fill a PS1 card, and the arrangement above still holds. Three things about that card are worth knowing before you build one.
The game map appears sixteen times: once in each nested bundle, and once per outer part as the index copy. The inner
copies are not removable. Nothing is inherited across the nesting boundary,
which is exactly what lets a save be sliced out of the card as a byte copy and still say what game it belongs to. The
outer copies are removable, since a part with no game falls back to the header’s, and dropping all fifteen of them
recovers 495 bytes out of 128505. That is 0.4%, in exchange for giving up listing the card without reading a payload.
Such a card may name Final Fantasy VII in its own header, because for once a single game does describe the whole card.
That is the exception to the rule that a card has no header game, and it is what makes the card answer “yes” to
the identified test.
The other thing the full card shows is what splitting is worth. Fifteen saves come to 128505 bytes against the 131072 of a raw card image, so splitting a card that is genuinely full saves nothing. What it buys is that each save is separately addressable, extractable and content-hashed.
Worked example: a PS2 save
Section titled “Worked example: a PS2 save”One game, three files plus the directory’s own entry, on an 8 MB card. capacity is the card’s data capacity, which is
not the length of a dump of it; see Capacity and out-of-band bytes.
[0] header:{ 0: "card", 1: "ps2", 4: { 0: "ps2-mc", 1: 8388608 },}
[1] parts: { 0: 0, 1: "bundle", 2: "memcard-1", 3: "BASLUS-20312", 4: 3, 5: h'…512 bytes…', 9: 18540(h'…'), 11: { 3: "SLUS-20312", 4: "Final Fantasy X" }, 12: "ps2", -1: h'DA31534156…' }and inside that payload, the save itself:
[0] header:{ 0: "save", 1: "ps2", 2: { 3: "SLUS-20312", 4: "Final Fantasy X" },}
[1] parts: { 0: 0, 3: "icon.sys", 4: 4, 5: h'…512 bytes…', 9: 18540(h'…'), -1: h'…' } { 0: 1, 3: "list.ico", 4: 5, 5: h'…512 bytes…', 9: 18540(h'…'), -1: h'…' } { 0: 2, 3: "BASLUS-20312", 4: 6, 5: h'…512 bytes…', 9: 18540(h'…'), -1: h'…' }The directory’s own entry is on the outer part, which is what a writer needs to recreate the directory with the right mode bits and timestamps. Each file’s entry is on its own inner part. The outer payload is the three inner payloads plus the inner bundle’s framing, most of which is the three 512-byte dirents. A card holding several PS2 saves repeats this arrangement once per save, and nothing has to infer where one save ends and the next begins. The nesting is required rather than an arrangement this page happens to choose.
Worked example: an N64 cartridge and its Controller Pak
Section titled “Worked example: an N64 cartridge and its Controller Pak”Mario Kart 64 keeps progress in the cartridge’s battery SRAM and its ghosts in a Controller Pak. Dumping both gives two storage components rather than one game’s bytes, so the bundle is a device: the pak holds notes for games the cartridge knows nothing about, and neither component is inside the other.
[0] header:{ 0: "device", 1: "n64",}
[1] parts: { 0: 0, 1: "bundle", 2: "cartridge", 9: 18540(h'…'), 11: { 3: "NUS-NKTE", 4: "Mario Kart 64" }, 12: "n64", -1: h'DA31534156…' }
{ 0: 1, 1: "bundle", 2: "controller-pak-1", 9: 18540(h'…'), 12: "n64", -1: h'DA31534156…' }The cartridge’s payload is a plain save, one part and nothing else:
[0] header:{ 0: "save", 1: "n64", 2: { 3: "NUS-NKTE", 4: "Mario Kart 64" },}
[1] parts: { 0: 0, 9: 18540(h'…'), -1: h'…512 bytes of SRAM…' }The pak’s is a card, one nested save per note, laid out like any other card:
[0] header:{ 0: "card", 1: "n64", 4: { 0: "n64-cpak", 1: 32768, 2: h'…32 bytes…' },}
[1] parts: { 0: 0, 1: "bundle", 2: "controller-pak-1", 4: 1, 5: h'…32 bytes…', 9: 18540(h'…'), 11: { 3: "NUS-NKTE", 4: "Mario Kart 64" }, 12: "n64", -1: h'DA31534156…' }The device header carries no game, because the pak’s other notes are not Mario Kart 64’s, and the outer parts carry
game and system only as an index of what each component holds.
Carry only that game’s note instead of the whole pak and there is no device at all: the cartridge save and the note are
two ordinary parts of one save, told apart by role, with nothing nested. Which of the two a
producer writes depends on what it read, not on what the game is.
This arrangement is the cartridge-and-pak case in the conformance corpus.