Introduction
A .blend file saved in 1998 will open in a Blender release from this year. That's unusual for a binary format built around raw memory dumps, and the reason is the DNA/RNA system, which makes .blend files semi-self-descriptive.
This article covers the file's technical structure (headers, block layout, the SDNA section) and how DNA (Structure DNA) and RNA (Runtime Navigable Access) handle schema versioning, data introspection, and cross-version compatibility. Code snippets are from Blender's own codebase.
Overview of the .blend file format
A Blender .blend file essentially contains a memory dump of Blender's data structures, annotated with metadata so different Blender versions can interpret it. Saving a scene in Blender writes the in-memory data structures directly to disk with minimal transformation, merely prefixing each chunk of data with a small header. This design makes saving very fast and ensures that the file directly reflects Blender's internal data representations. It also means .blend files are not byte-for-byte identical across platforms or Blender versions - for example, a file saved on a 32-bit little-endian system will differ from one saved on a 64-bit big-endian system due to pointer sizes and endianness. Rather than normalizing the data on save, Blender relies on a smart loading process to handle differences. Backward/forward compatibility isn't baked into the saved file; instead, conversion and adaptation are done during file load.
Backward compatibility refers to newer Blender versions loading old files, and forward compatibility means older versions loading files made by newer Blender. Blender's general policy is to maintain backward compatibility (you can open files from any earlier version) and to allow limited forward compatibility (older versions can open newer files, usually with some data loss for unsupported new features). Under the hood, the .blend format's self-descriptive DNA system is key to this. The file itself stores the Blender version that created it and even a "minimum version" required to open it. The heavy lifting is done by DNA: a complete encoded schema of Blender's data structures is written into every .blend file. Using this schema, Blender can interpret the file's data layout, even if it differs from the program's current internal structures. When loading a file, Blender will ignore unknown data (from newer versions), initialize any missing data fields with defaults, and run version patch routines to update old data to new formats.
File header and block structure
Every .blend file begins with a fixed-length file header, followed by a sequence of file blocks that contain the actual data. The header is 12 bytes:
- Identifier (7 bytes): Always the ASCII string
BLENDER, which marks the file as a Blender file. - Pointer Size (1 byte): A
'_'or'-'character indicating 32-bit or 64-bit pointers, respectively. All pointer values stored in the file are in this format (4 or 8 bytes), which is crucial since Blender dumps raw pointers to disk. - Endianness (1 byte):
'v'for little-endian or'V'for big-endian, indicating the byte order used when saving the file. Blender will swap bytes when loading if the file's endianness differs from the host system. - Version (3 bytes): The Blender version that saved the file, as a three-digit number (e.g.,
300for Blender 3.0 or448for 4.48). This helps Blender apply version-specific patching during load.
After the header, the file consists of a series of file blocks. Each block has a small header of its own, followed by the binary data for one or more Blender data structures. Think of file blocks as records in the file, each labeled with what kind of data it holds. The file blocks are 4-byte aligned and continue until a special end block is reached. Here's the general layout of a .blend file:
[ File Header (12 bytes) ]
[ Block #1: BlockHeader + Data ]
[ Block #2: BlockHeader + Data ]
... (many blocks)
[ Block 'DNA1': BlockHeader + DNA data (structure schema) ]
[ Block 'ENDB': BlockHeader (zero-length, end of file marker) ]Block Headers: Every block's header is either 20 or 24 bytes depending on pointer size. The structure of a file-block header is:
- Code (4 bytes): An identifier for the block's data type. This is typically a short code like
SC(Scene),OB(Object),ME(Mesh), etc., padded to 4 chars. For example, a scene block might have code"SC\0\0". Special codes includeDNA1(the DNA schema block, described later) andENDB(end-of-file). The code tells Blender what kind of data to expect and is used to quickly find specific data (e.g., all objects or materials) in the file. - Size (4 bytes): The length in bytes of the data chunk that follows this header. Blender uses this to know where the next block begins.
- Old Memory Address (Pointer size bytes): The memory address where this block's data resided in the originating Blender process. It's literally the pointer value from when the file was saved. This is used for pointer relocation: if other data blocks contain pointers referencing this block's data, those pointers are stored as this old memory address. During loading, Blender will map these old addresses to new pointers (more on that soon).
- SDNA Index (4 bytes): An index into the DNA structure list (in the DNA1 block) indicating which struct type this data block corresponds to. For instance, if the SDNA index is 139 and it refers to
Scenestruct, Blender knows to interpret the data using theScenestructure definition from the DNA schema. This ties the binary data to a named struct type. - Count (4 bytes): How many struct instances are packed in this block's data. Often this is 1 (one struct per block), but it can be higher if Blender saved an array of identical structs in one block.
Each block's data segment immediately follows its header and contains raw bytes for one or more structs of the type identified by the code/SDNA index. Blocks are written one after another. The last block in the file is an ENDB block which has a code ENDB and a size of 0, signaling the file's end.
Pointer Relocation: Because Blender dumps actual memory addresses into the file (the old memory addresses in each block header and any pointer fields inside the data), the loader must fix these up. When loading, Blender first reads all blocks and notes their old memory addresses and new allocated addresses. If a data structure has a pointer field, its value on disk is an old address; Blender looks up that address in a mapping of old->new addresses and replaces it with the new pointer (or NULL if the target block isn't present). This way, all internal references (like an Object's pointer to its Mesh, or a Scene's pointer to the active Camera) are updated to valid pointers in the new session. The old pointer values serve as IDs to reconnect the graph of Blender data after everything is loaded.
The Structure DNA (SDNA) block - Blender's schema
One block does the heavy lifting for compatibility: DNA1. It contains the Structure DNA (usually shortened to SDNA), a machine-readable description of every data structure type used in the file, and it sits near the end (typically just before ENDB). Because the file carries its own schema, Blender can read the data even when its own struct definitions have since changed.
The block is a binary blob with a defined, consistent layout: a header followed by a series of sub-sections.
DNA1 block data structure:
"SDNA" (4 bytes) - File identifier for DNA section.
"NAME" (4 bytes) - Start of the names subsection.
<int32> number_of_names - Count of field names to follow.
[ list of field name strings, each null-terminated ]
"TYPE" (4 bytes) - Start of the types subsection (aligned to 4 bytes).
<int32> number_of_types - Count of type names to follow.
[ list of type name strings, each null-terminated ]
"TLEN" (4 bytes) - Start of type lengths subsection (aligned).
[ list of uint16 type lengths, one for each type in the same order ]
"STRC" (4 bytes) - Start of structures subsection (aligned).
<int32> number_of_structs - Count of structures to follow.
For each structure:
uint16 type_index - Index into the type names list (which entry is this struct's name).
uint16 field_count - Number of fields in this struct.
For each field:
uint16 field_type_index - Index into the type names list for this field's type.
uint16 field_name_index - Index into the names list for this field's name.What each section holds:
- Names (NAME): A list of all struct field names (variable names) used in any struct. These include indicators for pointers and arrays. For example, a field name in this list might be
"*parent","color", or"vertex[3]". An asterisk*prefix denotes a pointer, and bracket[N]denotes an array size. Essentially, the names list contains every member name of every struct, in C-like notation. - Types (TYPE): A list of all type names. This includes both basic types (
char,int,float, etc.) and all struct names (Object,Mesh,Scene, etc.) that appear in the data. For instance,"float"and"Scene"are both entries in the type list. - Type Lengths (TLEN): For each type in the above list (in order), a 2-byte value giving the size of that type in bytes. Primitive types have their known sizes (
int= 4,float= 4, etc.), and struct types have the size of that struct. This is important because different platforms or different compilation settings could change struct padding or size - the DNA explicitly records the size used when saving. For example, TLEN for"Scene"might be 1376 bytes (as in Blender 2.48), meaning the Scene struct occupied 1376 bytes in that version. - Structures (STRC): The list of structure definitions. Each structure entry ties everything together: it says "Struct of type X with N fields: field1 of type Y with name A, field2 of type Z with name B, ...". Specifically, each struct entry starts with a type index (pointing to the struct's name in the TYPE list) and a field count, followed by that many (field type, field name) index pairs. For example, a struct entry might be: type index = 10 (which might correspond to
"Scene"in the TYPE list), field count = 5, then pairs like (field_type=4, field_name=0), (field_type=6, field_name=1), ... meaning the first field is of type TYPE[4] with name NAME[0], etc. Using the NAME and TYPE lists, these pairs can be resolved to something like "ID id;" (an ID field), "Object *camera;" (a pointer to an Object), "float cursor[3];" (an array of 3 floats), and so on.
All together, the DNA1 block fully describes the memory layout of each struct that was used when the file was saved. Essentially it's a condensed form of all the relevant C struct definitions from Blender's source at that time. Blender's own source code has all these structures defined in the DNA_*_types.h headers (in the makesdna directory) for compiling. Those same definitions (after some processing) get written into the file as the DNA block. Because the file carries its schema, Blender doesn't require an exact version match to read data - it can programmatically figure out where each field is in the file's blocks by consulting the DNA blueprint.
Note: In the DNA names list, you might see entries that look like function pointers (e.g.,
"(*doit)()") or similar. These are indeed function pointer fields embedded in structs. Blender updates those to point to the correct functions on load, but they appear in the DNA just as another field name. They're mostly internal and can be ignored for understanding data contents.
How DNA enables backward and forward compatibility
When you open a .blend file, Blender first reads the file header to adjust for pointer size and endianness, then proceeds to load each block. Critically, it locates the DNA1 block early in the loading process and parses the structure definitions out of it. This gives Blender a data schema (let's call it file-SDNA) for the file's contents. Blender also has its own compiled-in DNA (the current-SDNA) representing the program's current struct definitions (for the running Blender version). The loader will compare these two to map the file data to Blender's current data structures.
The DNA system is designed such that adding new fields or even new struct types typically won't break the ability to read old files or for old Blender to skip unknown new data. Here's how different scenarios are handled:
-
New Blender opening an old file (Backward Compatibility): This is the common, fully supported case. Blender's current version may have added or renamed some data fields relative to the file's version. The file's DNA tells the loader exactly what fields are present in the file. For each data block, Blender finds the corresponding struct in file-SDNA, then finds the matching struct in its own current-SDNA. It then maps fields by name (and type) between the old and new struct definitions. If the new Blender has added extra fields that the old file didn't have, those fields simply won't be in the file's DNA, so Blender will recognize them as "missing" and can initialize them to default values in memory. Conversely, if a field in the old file has been removed or renamed in the new Blender, the loader will either ignore it (if removed entirely) or map it via a rename table to the new field name. Blender's codebase includes a DNA rename mapping (
dna_rename_defs.h) that helps it handle renamed structs or fields without losing data. Because the file's DNA explicitly labels each field by name, Blender can still find (or ignore) the right bytes even if the struct layout changed order or size. Unknown old fields are skipped and unknown old structs (if some data block type was removed in new Blender) are ignored safely. After raw data is loaded and matched to new structures, Blender runs version patching code: a series of fixes specific to that file version to update data or fill in new defaults that might require computation. This code (located inversioning_xxx.c) can handle complex transitions - for example, converting an old physics simulation setting to a new system. We'll see an example of this shortly. -
Old Blender opening a new file (Forward Compatibility): This is trickier and not guaranteed for every new feature, but the design helps older versions not choke on new data. Since
.blendfiles include the DNA schema, an older Blender can read the file's structure definitions. It will find some struct types or fields it doesn't recognize (because they were added after that Blender's release). In general, unknown block codes (whole data-block types added in the future) are skipped entirely by an old Blender - it doesn't know what those are, so it ignores those blocks (they will be lost if the file is re-saved). If a known struct has new fields that the old Blender doesn't have in its own DNA, it will read only the part of the data it understands (the old fields) and ignore the rest. Those extra bytes for new fields are essentially dropped from memory since the older Blender has no place to put them. In some cases, Blender might preserve them in memory as an unknown chunk to rewrite out (to avoid data loss on re-save), but typically, they are lost on saving in the old version (thus "with some loss of data" is expected in forward compatibility). To mitigate major breakages, the Blender developers only introduce hard incompatibilities at designated milestones (usually when a major version number changes, e.g., 2.7 to 2.80, or 4.x to 5.x). Even then, they try to have the last release of the old cycle act as a bridge (e.g., Blender 3.6 LTS can open a 4.0 file and then save it in a form that older 3.x versions can use, like converting new meshes to old mesh format). In summary, forward compatibility exists to a degree: older Blender will not crash when encountering newer file data - thanks to DNA it knows how to skip unknown stuff - but new features will either be ignored or degraded. If you open a Blender 4.3 file (with, say, the new Grease Pencil system) in Blender 4.0, the old Blender will skip any unfamiliar GP data blocks; you might lose that part of your scene, because 4.0 doesn't know how to use or save it.
DNA as the bridge: the schema in the file is the common language between an old file and a new Blender, or the reverse. Each Blender build knows struct layouts from many past versions, and the DNA parsing is flexible enough to absorb the rest. If a field moved to a different struct in a later version, the DNA index and name let the loader find it anyway. If a type changed, say an int became a float, Blender does a conversion or runs a version patch. Because all structure definitions travel in the file, even a 20-year-old file can be parsed by a modern Blender: someone demonstrated this by opening a Blender v1.0 file in Blender 2.48 and getting correct interpretation.
Runtime Navigable Access (RNA) and data introspection
Where DNA handles storing and reading structured binary data, RNA is a reflection layer on top of Blender's live data, giving a uniform way to access properties regardless of the underlying C struct. The biology analogy is the obvious one: DNA defines the raw structure, RNA translates it into something that does work. In practice this means introspection and a dynamic API.
What RNA provides:
-
Automatic introspection of structures: Blender's source code uses the RNA definitions (written in C/C++ in the
makesrnamodule) to declare what properties exist on each data type. For example, for theScenestruct (defined in DNA), there is a corresponding RNA definition that tells Blender: "A Scene has a property calledcamera(of type Object pointer), a property calledworld(World pointer), a property calledframe_start(int), etc." The RNA definitions use macros likeRNA_def_propertyand often automatically map to DNA by struct and field name. If an RNA struct is given the same name as a DNA struct, it will by default refer to that DNA struct's fields. This means when Blender adds a new field in DNA, simply adding the corresponding RNA property will hook it up to the underlying data. The RNA system knows the offset of each field in the struct (since it can look it up from DNA or from static metadata generated at build time) and can read or write it at runtime. This is how Blender's Python API and UI panels can generically iterate over properties of (for example) a Material or Mesh - RNA provides metadata like "this property is an int, this one is an array of floats with length 3, this one is an enum with these possible values," etc., all derived from or specified alongside the DNA structure. -
Unified access and versioning: RNA serves as a stable interface for addons and scripts, even if DNA structures change. Because you can rename DNA fields and map them in RNA, Blender can maintain old RNA property names for script compatibility while the underlying DNA changes. There's an explicit facility to define alternate names or to mark properties as deprecated. For example, if a struct member is renamed in DNA, the Blender devs might keep the old RNA property name but point it to the new DNA field, so existing Python scripts continue to work. The DNA rename definitions ensure the file loads, and the RNA layer ensures the API remains consistent (or at least can throw a deprecation warning rather than break outright).
-
Defaults and UI: RNA defines default values for each property, matching the DNA defaults for new fields. When an old file lacks a property, Blender initializes the missing field from the RNA default after loading. Add a boolean flag to Mesh in 4.5 that defaults to true, and an older file that has never heard of it still gets the right value once loaded. This splits the work with versioning code: static defaults cover the simple cases, code handles the complex migrations.
Here's roughly what an RNA definition looks like:
StructRNA *srna = RNA_def_struct(brna, "Scene", NULL);
RNA_def_struct_sdna(srna, "Scene");
// ... define properties ...
PropertyRNA *prop;
prop = RNA_def_property(srna, "frame_start", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, NULL, "startframe");
RNA_def_property_range(prop, 0, MAXFRAME);
RNA_def_property_ui_text(prop, "Start Frame", "First frame of the animation timeline");In this example, RNA_def_struct_sdna(srna, "Scene") links this RNA struct to the DNA struct named Scene. Then RNA_def_property_int_sdna(prop, NULL, "startframe") says this RNA property corresponds to the C struct member called startframe inside the Scene struct. If at some point startframe was renamed to frame_start in DNA, a definition in dna_rename_defs.h would allow Blender to load old files (mapping old->new name). On the RNA side, they might keep the Python property name "frame_start" but map it to the DNA field "startframe", or vice versa, depending on what they want to expose. The result is that Python code scene.frame_start gets or sets the underlying C Scene.startframe field seamlessly.
Introspection: DNA makes the file self-describing; RNA makes the live program data self-describing. Query an RNA struct at runtime and you get its properties and their types, which is how the UI builds panels and how serialization to other formats stays generic. It's also how the ID property system attaches arbitrary key-value data to data-blocks without changing DNA. Those properties are stored in a generic format in the .blend and are forward-compatible by nature: an older Blender treats unknown ID properties as opaque data and writes them back out rather than discarding them. We covered ID properties in their own article.
Versioning code and schema evolution
Even with DNA and RNA handling a lot automatically, there are cases where explicit code is needed to convert data between versions. Blender's codebase includes versioning functions that run after basic file loading to tweak data. These are organized by Blender version - for instance, blo_do_versions_400() might handle migrations for 4.0, blo_do_versions_430() for 4.3, etc., typically consolidated in versioning_xxx.c files. When Blender opens a file, it checks the file's version, and if it's older, it will execute all the relevant versioning fixes in sequence (incrementally updating the data from the old version to the current).
Versioning code often uses DNA introspection as well. A common pattern is to check if a certain new struct or member exists in the file's DNA; if not, it implies the file is from before that feature was added, so the code will create or initialize the new data. For example, when a new data structure was introduced (like a SceneLineArt settings struct for Grease Pencil line art in Blender 2.93), the versioning code added default instances of it for old files. Here's a real snippet from Blender's versioning code illustrating this (in C):
if (!DNA_struct_find(fd->filesdna, "SceneLineArt")) {
LISTBASE_FOREACH (Scene *, sc, &bmain->scenes) {
sc->lineart.crease_threshold = DEG2RAD(140.0f);
sc->lineart.line_types |= LRT_EDGE_FLAG_ALL_TYPE;
sc->lineart.flags |= (LRT_ALLOW_DUPLI_OBJECTS | LRT_REMOVE_DOUBLES);
sc->lineart.angle_splitting_threshold = DEG2RAD(60.0f);
sc->lineart.chaining_geometry_threshold = 0.001f;
sc->lineart.chaining_image_threshold = 0.001f;
}
}In this code (from Blender's versioning_290.c for an early 2.90/2.91 development branch), DNA_struct_find(fd->filesdna, "SceneLineArt") checks if the loaded file's DNA has a struct named SceneLineArt. If it returns false, that means the .blend came from a Blender version that didn't have the new Scene.lineart settings yet. The code then iterates over all scenes in the loaded data and initializes the new lineart sub-structure with default values (here setting various thresholds and flags). This ensures that when the old file is loaded into the new Blender, the new features have valid default data. Without this step, those new fields would either be zeroed or uninitialized, which might not be desired. Blender has many such versioning patches - adding new fluid simulation settings, renaming a modifier field, changing how a camera property is stored, etc. - each guarded by conditions on the file version or presence/absence of DNA entries.
The versioning code can also convert data. For instance, if a mesh face representation changed, Blender might on load iterate over all meshes and build new polygon structures from the old face data or vice versa. The DNA schema tells it how to read the old data, and code does the transformation into the new DNA format. These transformations are applied in increasing version order, so an extremely old file will go through many small upgrades successively (this is safer and easier to maintain than having one giant leap). Over time, some old versioning code may be removed if the features were deprecated long ago (Blender's policy is to drop compatibility code for features that were removed at least 2 years prior, to keep the codebase manageable). However, the DNA mechanism means even if versioning code for a really old file is gone, Blender can still read the data - it just might not convert some legacy feature perfectly.
Finally, Blender's major version increments (like the future Blender 5.0) are times when the developers might intentionally break forward compatibility to clean up DNA (remove legacy fields, etc.). Even then, they leverage DNA to do it gracefully. For example, they might introduce a new DNA struct and drop an old one; older Blender won't know the new one and will drop that data, which is an expected break. Such changes are coordinated and documented. As of Blender 4.4 (latest in this writing), the DNA/RNA system continues to be the backbone of compatibility. The mesh data structure was revamped during 2.80 and again minorly around 4.0, but by using DNA versioning, Blender 3.6 LTS could still open files from 4.0 (it recognized the new mesh DNA and could at least read basic mesh attributes). And Blender 4.0+ can of course read all older meshes, upgrading them on the fly.
Summary and key takeaways
The trick is that each .blend carries a blueprint of the data structures used to save it. That's the DNA system, and it's what lets Blender adapt at read time: unknown newer fields get skipped, missing older fields get defaults, and explicit version patching code handles anything that needs real conversion. Blender still opens files from the 1990s. The RNA system sits above this as a stable, introspective API layer, mapping DNA structures to the high-level properties the UI and scripts use, so the internal data model can change without breaking add-ons every release.
If you're writing a custom exporter or importer, the DNA layout is what you need. Read the DNA1 block and you have every struct layout in the file; several community libraries do exactly this. If you're adding a data field to Blender itself, the build system updates the DNA for you, but you still write the RNA definitions and, often, the versioning code for old files.
The division is clean: DNA handles the low-level schema so files describe their own content, RNA handles runtime access so the rest of Blender sees properties uniformly.
What I find genuinely interesting about this design is what it doesn't do. There are no backward-compatibility hacks in the saved file. A .blend is a raw snapshot of memory, essentially the laziest possible thing to write, and all the compatibility work happens at load time in code that can be fixed, extended, and eventually deleted. Most formats solve this the other way round, by normalizing on write, and then live with the schema forever.
