Skip to content

Universal Saves Format

This is version 0.3 of this specification, and it is not yet stabilized. Expect breaking changes; until 1.0 they happen in place, under the same bundle tag.

Newest first. Nothing in a file says which version wrote it before 1.0, so this list is the only record that the rules moved. A version covers the format, the common types it leans on, and the extension keys these specifications own; a key belonging to a producer changes on that producer’s schedule instead.

  • 0.3, 2026-09-18. Breaking: shapes are explicit. A bundle names one in required header key 0, and created_at moved to key 6. Breaking: an x.1sav.dirent time is a reading and an optional UTC offset rather than a bare instant, because no console here records the zone its clock was set to. Supersedes 0.2, which carried the first of those and was withdrawn before anything depended on it.
  • 0.1, 2026-08-12. First published version.

A retro save is a bare binary dump with no agreed way to say what game it belongs to, what wrote it, or what else belongs with it. Every emulator invented its own convention: the game is implied by a filename or a per-game directory, a clock reading or a completion percentage has nowhere to live beside the bytes, save states and screenshots become sidecar files named differently by every tool, and multi-part media like N64 controller paks or PS1 memory cards each got their own layout.

This document specifies one portable container file, extension .1saves, holding typed binary parts plus whatever identifying context and interpretation a producer has. Everything but the parts is optional: a system slug and a chunk of bytes is a valid bundle. It is for moving saves around, not for an emulator to read in place.

The encoding is CBOR (RFC 8949), which keeps bytes as bytes and has a defined deterministic form.

  • Extension: .1saves

  • MIME: application/vnd.1saves+cbor

  • Framing: the whole file is a single CBOR value: the array envelope wrapped in one application CBOR tag. There are no non-CBOR bytes, so any conforming CBOR reader can walk the structure even without knowing the application tag.

    [0..5] 0xDA 0x31 0x53 0x41 0x56 bundle tag #827539798, ASCII "1SAV" from byte 1
    [5..] CBOR array envelope
    • The envelope CBOR tag is 827539798 (0x31534156), whose 5-byte encoding is the file magic: 1SAV in a hex dump, starting at byte 1. Byte 0 is the tag’s head byte 0xDA. Reading as ASCII magic follows RFC 9277, except that this format omits the self-describe tag #55799 that convention puts in front, so the magic sits at byte 0. A bundle never carries #55799.
    • The major version lives in the tag number. A future breaking format uses a different tag, so an old decoder rejects a new-major file on its own, with no separate version field to check.
    • Minor, additive changes happen inside the bundle, as new optional integer keys in the header or part maps; see Additive integer keys.
    • The only other tags a bundle carries are tag 1 on an epoch field and the hash value tags in rom_hashes.
  • Determinism: RFC 8949 section 4.2, with no deviations from it. Definite-length items, preferred serialization for every head (so integer map keys and floats both encode shortest), map keys sorted bytewise on their encodings, no duplicate keys. A generic deterministic-CBOR validator accepts a conforming bundle as-is, with no exceptions to unteach it.

    Three further rules sit on top, and a generic validator will not check them: text is in NFC, parts are sorted by ascending id, and rom_hashes entries follow the ordering rule on hash values. What deterministic encoding does not settle at all is a data model that can say one thing two ways; see One encoding per bundle.

What a bundle preserves is what the hardware reads, not the bytes it was read from. Two dumps of one card that differ only where nothing looks are the same save, and a bundle written from either says the same thing.

That is why so much of this format declines to carry what it could. Block positions are not stored, because the number is wrong the moment the save lands on another card. Block counts are not stored, because the payload’s length and the format’s block size already give them. A writer regenerates every chain, table, mirror and checksum rather than reading them back, and capacity counts the data area rather than the length of a dump. None of those is a space optimisation. Each is a field that could disagree with the bytes, removed so that it cannot.

A producer that wants the original file kept anyway attaches it as a part, where it gets a digest and can be compressed or resolved from a store like any other payload: a card-image for a whole card, an aux part with a content_type for a container this format has no name for. That is archival, riding alongside, and a consumer that rebuilds from the parts never has to consult it.

Where bytes are carried verbatim, it is because nothing has decoded them and so nobody can say which of them are read. dirent and system_area are that case, not exceptions to this rule: the format holds them whole precisely because it cannot tell what inside them matters.

A bundle’s content hash is the identifier a store keys on and the value a bundle part carries, so it has to be a function of what the bundle says and not of how a producer chose to say it. Deterministic encoding gets most of the way: it fixes how a given value is written. What it cannot fix is a format that offers two ways to say the same thing.

So this one offers one, everywhere:

  • Nothing is absence. An optional container has exactly one encoding of empty, and that is not being there. An empty game, game_id or rom_hashes is malformed, and so is an empty parts array.
  • A default is absence. Where a field has a default, the default is written by leaving the key out, and spelling it out is malformed. A part’s kind means save when absent, its role means primary, its path means the role is the whole address, and its encoding means "none".
  • One type per value. A field that could take an integer or a float takes the integer, because RFC 8949 section 4.2.2 declines to choose between them. Every epoch field here is whole seconds.

Each is a rule a producer follows and a decoder enforces. Breaking one is malformed rather than merely unusual, because a consumer that accepted both spellings would compute two content hashes for one save.

Integer keys belong to this specification, in every map it defines. Text keys belong to producers; that is the extension mechanism, and the two never meet.

So a decoder that meets an integer key it does not recognize MUST ignore it and round-trip it unchanged. There is only one thing an unfamiliar integer key can be: a field a later minor version assigned after this decoder shipped.

A later minor version assigns the next free key in whatever map needs it. A change that cannot be expressed that way takes a new bundle tag and a new major version, which old decoders reject on the tag alone.

Wherever this format names a category, the field says which of three kinds of name it holds, and there is nothing more to it than that.

  • Free-form text, where the value is a human’s to write: a description, a rom_filename.
  • A slug, where the value comes from a list this spec or a registry keeps: a system, a card format, a role, a device_kind. A producer takes a name from the list rather than inventing one, and the list grows by a PR.
  • A reverse-DNS name, where the value is the producer’s own to mint with nobody to ask: an extension key, a source’s app, a part’s kind it invented.

The two named types are normative and their pages carry the rules, including what a consumer does with a name it has never seen. Each field in Bundle says which kind it holds.

shape names one of four, and each has a document that defines what its parts mean:

ShapeThe bundle isIts parts are
saveone game’s statethe regions or files that state is made of
cardone memory cardone bundle part per save, and the card’s own
deviceone console’s storage, read wholeone bundle part per component, each with a role
collectionseveral bundles in one fileone bundle part per entry, any shape but a collection

A decoder reads shape rather than inferring it from what the header happens to carry. The vocabulary is spec-owned, and a later minor version assigns a new one with a document to go with it.

A shape is a slug and follows that page’s rules, which is what keeps a new one additive. A decoder meeting a shape it does not know MUST parse the bundle and round-trip it unchanged, exactly as it does for an integer key a later version assigned. It MUST NOT guess what the shape means, and MUST decline any operation that depends on knowing: walking the parts, restoring a save, rebuilding a card. Refusing to act on a bundle is not the same as rejecting it, and only the second loses the bytes.

The schema is the narrower of the two on purpose. It pins the four shapes this version defines, so validating against it says “this is a conforming 0.3 bundle” rather than “some decoder can read this”, the same asymmetry Additive integer keys describes.

A bundle names its shape and a decoder reads it; what the parts then mean is that shape’s document, and Bundle is where every field those documents refer to is defined.

The spec owns the integer-keyed namespace of every map it defines. Everything else attaches through extension keys: map entries whose key is a text string in reverse-DNS form (x.1sav.rtc, com.1retro.forge). This is how a clock reading, parser-extracted gameplay tags, emulator-native structures and third-party annotations ride along, without the spec needing to know anything about them.

The header and a Part both take them, flat, alongside their integer keys. The rules:

  • An extension key MUST be a well-formed reverse-DNS name, and there is no unnamespaced alternative. A producer with no domain to reverse takes a name in the x tree. The namespace is the whole of what stops two producers meaning different things by one key.
  • What sits under the key is entirely the producer’s. Any CBOR value of any shape: a map, a text string, a byte string, a number. The spec never looks inside one, imposes nothing on it, and the rule that integer keys belong to the spec stops at the key.
  • Decoders MUST round-trip an unknown extension key unchanged, value included. Never drop one just because you don’t recognize it.
  • The integer namespace belongs to the spec, so an integer key is never an extension. Unknown ones are handled by Additive integer keys.
  • A field that becomes first-class moves from an extension key to an integer key in a later minor version.

A producer with several fields to attach SHOULD put a map under one key rather than take a key each. The name is a namespace, not a field label, so paying for it once buys single-byte integer keys inside a space nobody else can reach:

"com.1retro.forge": { 0: "ff7", 1: { 0: "Aeris", 1: 42 } }

Whether a key is listed is a social matter and not a structural one. A catalogued key and one a producer invented this morning are the same thing on the wire, and no decoder can tell them apart; the catalogue exists so a producer finds an existing key instead of inventing one that overlaps.

The spec deliberately does not cover:

  • Encryption. The bundle is plaintext CBOR. Confidentiality is the transport’s responsibility (TLS) or a separate envelope. Unbinding a bound payload is out too: decryption, where it is even possible, needs keys this format does not carry.
  • Compression at the file level. Compression is per-part, via a part’s encoding field; the outer file is never wrapped in another compression layer.
  • Authentication / signing. No signatures inside the bundle. If signed bundles are ever needed, they belong in a new outer CBOR tag that wraps this one.
  • On-disk canonicalization for emulator save directories. Devices that natively read .sav / .srm continue to do so; the bundle is for transport and archival, not for in-place emulator consumption.
  • Per-part deduplication across bundles. Two bundles that share an identical part each carry a full copy. Deduplication, if you want it, is a storage-layer concern that works from each part’s sha256.
  • Migration from any specific legacy save layout. How a producer or consumer chooses to coexist with raw .sav/.srm/emulator-native formats is a deployment concern, not part of the format.