Skip to content

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.

format fixes two numbers, both in bytes: the length of a dirent, and the size of a block.

FormatdirentBlockNotes
ps1-mc1288192The 128 is one directory frame, and a block is 64 of them.
ps2-mc5121024One entry for the save’s directory, one per file inside it. A block is a cluster of two 512-byte pages.
n64-cpak32256The pak’s own word for a block is a page.
gc-mc648192
vmu32512
neogeo-mc464Cards run from 2 KiB to 16 KiB and every one of them uses the same block.
saturn-bup3064The 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 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.

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 .mcs importer parse the filename and the filesize “while other fields should be ignored”, reconstructing the rest against the destination card.
  • Dreamcast’s .dci specification says outright that “the ‘location of first block’ field should be ignored when reading”.
  • MPKEdit writes 0xCAFE over 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.

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 dirent ahead 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 (0xCC for a mini-game, 0x33 for 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.

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.

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 -Sxx number 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-S07 sits in directory slot 1. path carries the first, slot the second, and they are independent.
  • All three US discs share one save name. The discs are SCUS-94163, SCUS-94164 and SCUS-94165, but the save filename always carries the disc-1 code so every disc reads the same saves. A serial taken from a card filename is the save’s product code, not necessarily the code of the disc that wrote it.

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.

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.

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.

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.