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.
Changelog
Section titled “Changelog”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_atmoved to key 6. Breaking: anx.1sav.direnttime 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.
Context
Section titled “Context”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.
Format Overview
Section titled “Format Overview”-
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:1SAVin a hex dump, starting at byte 1. Byte 0 is the tag’s head byte0xDA. 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.
- The envelope CBOR tag is
-
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, androm_hashesentries 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.
Semantics, not bytes
Section titled “Semantics, not bytes”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.
One encoding per bundle
Section titled “One encoding per bundle”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_idorrom_hashesis malformed, and so is an emptypartsarray. - 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
kindmeanssavewhen absent, itsrolemeansprimary, itspathmeans the role is the whole address, and itsencodingmeans"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.
Additive integer keys
Section titled “Additive integer keys”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, arom_filename. - A slug, where the value comes from a list this spec or a registry keeps: a
system, a cardformat, arole, adevice_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’skindit 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.
Shapes
Section titled “Shapes”shape names one of four, and each has a document that defines what its parts
mean:
| Shape | The bundle is | Its parts are |
|---|---|---|
save | one game’s state | the regions or files that state is made of |
card | one memory card | one bundle part per save, and the card’s own |
device | one console’s storage, read whole | one bundle part per component, each with a role |
collection | several bundles in one file | one 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.
Extensions
Section titled “Extensions”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
xtree. 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.
Out of scope of this specification
Section titled “Out of scope of this specification”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
encodingfield; 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/.srmcontinue 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.