Introduction
Blender's .blend
file format is famously robust, allowing new Blender releases to open files from decades ago and even older versions to open (with limitations) files from newer releases. This is achieved through Blender's ingenious DNA/RNA system, which makes .blend
files semi-self-descriptive. In this article, we dive deep into the technical structure of Blender's file format and explain how DNA (Dynamic/Structure DNA) and RNA (Runtime Navigable Access) work together to maintain backward and forward compatibility. We'll explore the .blend
file structure (headers, block layout, the SDNA section) and how Blender uses DNA and RNA for schema versioning, data introspection, and cross-version compatibility. Code snippets from Blender's open-source codebase and text-based diagrams will help illustrate these concepts for developers and Blender technical users.
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 actual data. The file header is 12 bytes long and provides basic information for interpreting the rest of the file:
- 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.,
300
for Blender 3.0 or448
for 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
Scene
struct, Blender knows to interpret the data using theScene
structure 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
Among the file blocks, one is extremely special: the DNA1 block. This block contains the Structure DNA (often called SDNA) - essentially a machine-readable description of all the data structure types used in the file. The DNA1 block is typically located near the end of the file (often just before ENDB
). It is the key to Blender's compatibility magic, because it allows Blender to understand how to read the data even if the exact struct definitions have changed between versions. In other words, the .blend
file carries its own schema with it.
Let's break down the contents of the DNA1 block. It's a binary blob, but its layout is defined and consistent. The block begins with a header and then 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.
Let's clarify this with what these sections mean:
- 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
.blend
files 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.0). 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 DNA schema in the file is the common language that an old file and a new Blender, or vice versa, use to communicate. Each Blender build has knowledge of struct layouts for many past versions, and the DNA parsing is flexible. For instance, if a struct field was moved to a different struct in a later version, the DNA index and name ensure the loader can still find it. If data types changed (say an int
became a float
), Blender may do a simple conversion or run a version patch. The fact that all structure definitions are in the file (even those not used, potentially) means even a drastically old Blender file (e.g., from 20+ years ago) can be parsed by a modern Blender - and indeed such demonstrations have been made, opening a Blender v1.0 file in Blender 2.48 with correct interpretation.
Runtime Navigable Access (RNA) and Data Introspection
Parallel to DNA, Blender's RNA system deals with how data is accessed and manipulated at runtime (for example, through the Python API or UI). While DNA is about storing and reading structured binary data, RNA is a reflection layer on top of Blender's data, providing a uniform way to access properties, regardless of the underlying C struct implementation. The analogy to biology is that DNA defines the raw data structure (like genetic code), and RNA translates that into functional access (like proteins doing work). In practice, RNA provides introspection and a dynamic API.
Key features of RNA include:
-
Automatic Introspection of Structures: Blender's source code uses the RNA definitions (written in C/C++ in the
makesrna
module) to declare what properties exist on each data type. For example, for theScene
struct (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_property
and 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 also defines default values for each property (which correspond to DNA defaults for new fields). These default values are used when an old file doesn't have a certain property - after loading, Blender can initialize the missing fields to the defaults defined in RNA. For instance, if a new boolean flag is added to Mesh in version 4.5 (defaulting to true), an older file won't have it, but once loaded, RNA knows the default and sets it accordingly. This is complementary to versioning code - simple default-able values might be handled by static defaults, whereas complex migrations are done in code. The RNA definitions thus contribute to compatibility by ensuring that missing data is initialized properly, as mentioned in the loading steps.
To illustrate RNA in action, consider a snippet from Blender's RNA definitions (in a hypothetical sense):
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: Both DNA and RNA contribute to introspection. DNA makes the file self-describing, and RNA makes the live program data self-describing. A developer can query an RNA struct at runtime to get a list of its properties and types. This is how the UI knows to build panels, how serialization to other formats can be generalized, and even how Blender's ID property system (for custom user-defined properties) can attach arbitrary data without changing DNA. The ID Property system is a way to attach dynamic key-value data to Blender data-blocks; these are stored in a generic format in the .blend
and are largely forward-compatible by nature (unknown ID properties can be kept and written back out by older Blender versions, since they are just user data). While not the focus of this article, ID properties provide an extension mechanism that further cushions forward compatibility: if new Blender versions use ID properties for new features, older versions will simply treat them as opaque data and not discard them when saving.
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
Blender's file format is a brilliant example of a self-describing binary format. The DNA system (Structure DNA stored in the file) means each .blend
carries a blueprint of the data structures used to save it. This allows Blender to adapt when reading the file: unknown newer fields are safely skipped or stored, and missing older fields are added with defaults. Combined with explicit version patching code, this mechanism has yielded extraordinary backward compatibility - Blender can still open files from the 1990s - and reasonable forward flexibility. The RNA system complements this by providing a stable, introspective API layer: it maps those DNA structures to high-level properties used by the UI and scripts, allowing Blender's internal data model to evolve without breaking addons or user workflows more than necessary.
In practice, if you're a developer integrating with Blender or writing a custom exporter/importer, understanding the DNA layout is crucial. You can parse a .blend
by reading the DNA1 block to know all struct layouts (there are even community libraries that do this). Likewise, if you contribute to Blender's codebase by adding a new data field, Blender's build system will update the DNA accordingly, and you should add corresponding RNA definitions and possibly versioning code to handle old files. The separation of concerns is clear: DNA handles low-level data schema (ensuring files self-describe their content), and RNA handles runtime access to data (ensuring the rest of Blender knows about these properties in a uniform way). Together, they form the genetic code of Blender's files, balancing change with continuity.
By focusing on reading old data and making the file format rich with metadata, Blender's developers have achieved a rare feat in software: continuous improvement with minimal obsolete-compatibility baggage in the saved files themselves. The .blend
you save today is not encumbered by explicit backward-compatible hacks - it's a raw snapshot of Blender's state - yet future Blender versions will very likely read it just fine. As a developer or technical artist, you can appreciate that under the hood, Blender is doing a lot of work (through DNA/RNA and versioning code) to make that just work principle a reality. This design has served the Blender community well, enabling long-term projects to span many versions of the software, and it's a model that other software could take inspiration from for sustainable file format evolution.