As we use more C++ and move to a more extensible architecture, the future design of DNA is one of the biggest unknowns. The goal of this document is to start a more concrete discussion about where we want to go with DNA and how to get there. For that purpose, there is a concrete initial proposal for how DNA structs could be defined in the future. Feedback that describes problems with the proposal or proposes other solutions is welcome.
Goals
There are two main goals:
- Working with DNA structs should feel more ânaturalâ in C++ code.
- It should be straight forward to define methods on them including constructors and destructors.
- One should be able to work with containers in DNA similarly to other C++ containers.
- Use a more extensible architecture.
- It should be possible to define DNA structs whereever the struct actually belongs instead of putting many unrelated structs into the same header.
- For example, each node with storage could have its own header that defines the corresponding storage struct.
- DNA structs should be able to exist in arbitrary namespaces, not just in the global namespace.
- It should be possible to define DNA structs whereever the struct actually belongs instead of putting many unrelated structs into the same header.
An important requirement for every solution is that the .blend file format must not change just to achieve these goals. Also we still want to be able to simply dump structs instead of having more complex serialization code.
Proposal
There are quite a few ways to achieve these goals independently and together. We already tried and use different solutions for better C++ integration, but there is no prevalent solution yet. Existing solutions include:
- Define raw struct in
makesdna
and inherit from that in another type that has more C++ semantics (e.g.CurvesGeometry
andbUUID
). - Add methods to structs directly in
makesdna
(e.g.AssetWeakReference
andbNode
).
Below I show a new approach that achieves both goals and feels doable, even though it does require some more significant refactoring compared to the existing approaches. Itâs easiest to show the new approach with examples. Iâll use node storage as example because it illustrates the points well. The same ideas should apply to all other structs.
Simple Example
As a first example we look at the node storage of the Mesh to Points
node because itâs very simple. Currently, NodeGeometryMeshToPoints
is defined in DNA_node_types.h
as follows:
typedef struct NodeGeometryMeshToPoints {
/* GeometryNodeMeshToPointsMode */
uint8_t mode;
} NodeGeometryMeshToPoints;
typedef enum GeometryNodeMeshToPointsMode {
GEO_NODE_MESH_TO_POINTS_VERTICES = 0,
GEO_NODE_MESH_TO_POINTS_EDGES = 1,
GEO_NODE_MESH_TO_POINTS_FACES = 2,
GEO_NODE_MESH_TO_POINTS_CORNERS = 3,
} GeometryNodeMeshToPointsMode;
For better extensibility, this definition is moved to a new node-specific header: nodes/geometry/include/NOD_geo_mesh_to_points.hh
.
namespace blender::nodes {
enum class GeometryNodeMeshToPointsMode {
Vertices = 0,
Edges = 1,
Faces = 2,
Corners = 3,
};
struct NodeGeometryMeshToPoints {
struct DNA {
uint8_t mode;
} dna;
GeometryNodeMeshToPointsMode mode() const;
};
} // namespace blender::nodes
There are a few things to note here:
- The
NodeGeometryMeshToPoints
struct is in theblender::nodes
namespace. From the perspective of the .blend file format, the struct is still just calledNodeGeometryMeshToPoints
though. - Since this is a normal C++ header now, we can also use
enum class
to for better naming. - The actual
DNA
struct is embedded into theNodeGeometryMeshToPoints
struct.-
NodeGeometryMeshToPoints::DNA
must be a trivial type, like any existing DNA type. -
NodeGeometryMeshToPoints
contains adna
member that is trivial, but doesnât have to be trivial itself.
-
-
NodeGeometryMeshToPoints
has amode
accessor that returns the enum value with the correct type.- Note that the method has the same name as the dna member. This is quite common and one reason for separating the pure dna and wrapper struct (enums canât be used in DNA structs directly, because their size is not very well defined; maybe we can use them with C++ now though?) .
More Complex Example
The next example uses the âRepeat Outputâ node storage. Itâs more complex because it contains non-trivial data. It has an array of items and each item has a name.
namespace blender::nodes {
struct NodeRepeatItem {
struct DNA {
char *name;
short socket_type;
char _pad[2];
int identifier;
} dna;
NodeRepeatItem(StringRef name, eNodeSocketDatatype type, int identifier);
NodeRepeatItem(const NodeRepeatItem &other);
~NodeRepeatItem();
static void relocate(NodeRepeatItem *src, NodeRepeatItem *dst);
void content_blend_write(BlendWriter *writer) const;
void content_blend_read_data(BlendDataReader *reader);
StringRefNull name() const;
eNodeSocketDatatype type() const;
};
struct NodeGeometryRepeatOutput {
struct DNA {
NodeRepeatItem *items;
int items_num;
int active_index;
int next_identifier;
char _pad[4];
} dna;
NodeGeometryRepeatOutput();
NodeGeometryRepeatOutput(const NodeGeometryRepeatOutput &other);
~NodeGeometryRepeatOutput();
static void relocate(NodeGeometryRepeatOutput *src, NodeGeometryRepeatOutput *dst);
void blend_write(BlendWriter *writer) const;
void blend_read_data(BlendDataReader *reader);
Span<NodeRepeatItem> items() const;
CArray<NodeRepeatItem, int> items();
};
} // namespace blender::nodes
Some notes:
- Both structs have a normal constructor, a copy constructor and a destructor.
- A move constructor or assignment operators do not exist. Instead, there is the
relocate
function.- Those could be implemented but itâs unclear if it is worth it for most types.
- We generally donât have a defined âmoved-fromâ state for DNA structs.
- Relocation is a move construction followed by a destruction of the old value. This concept mimics what we do with DNA much better. DNA structs are trivially relocatable in all cases Iâm aware of, i.e. relocation is the same as
memcpy
(self referential structs are not trivially relocatable, i.e. when a struct contains a pointer to a value within the same struct).
- File IO can simply be added as methods.
- The
content
incontent_blend_write
means that the struct itself is not written, but only the data it references. This is a common pattern for structs that are usually stored in an array.
- The
- The non-const
items
method returns a newCArray<T, SizeT>
type. This type is designed to wrap a C array with a pointer and size.- In this case the
CArray
contains aNodeRepeatItem **
to&dna.items
and anint *
to&dna.items_num
. - The
CArray
has common methods for appending and removing elements. Changing it automatically updates the data stored indna
.
- In this case the
Challenges
There are quite a few challenges when implementing this approach. I could get Blender to compile with the simple NodeGeometryMeshToPoints
struct defined in a namespace but the full implementation likely requires major refactors of makesdna
and makesrna
.
Some of the obvious and less challenges I found so far:
-
makesdna.cc
needs to scan files outside of themakesdna
folder.- To avoid scanning all files, we likely need to build list of all files to scan with cmake.
-
makesdna.cc
needs to figure out which structs to analyse and which not.- With the proposal above, no extra syntax would be needed, because one could just search for
struct DNA {
to find all the right places.
- With the proposal above, no extra syntax would be needed, because one could just search for
- The
dna_rename_defs_ensure
function that checks whether DNA renames defined indna_rename_defs.h
are valid needs to be changed.- Itâs harder (and maybe unpreferable) to include all headers that include dna structs. Itâs even worse if we want to be able to define DNA structs in
.cc
files or even at run-time. - Might be worth to consider to add information about renames directly in the dna struct definitions using macros or custom C++ attributes.
- Itâs harder (and maybe unpreferable) to include all headers that include dna structs. Itâs even worse if we want to be able to define DNA structs in
- The way
dna_verify.c
works also likely has to change.- Same reason as above, itâs not a good idea to include all headers in one file.
-
makesrna.cc
currently assumes that all DNA structs are in global namespace.- Itâs likely a good idea if it knows which namespace each dna struct is in so that it can also generate forward declarations correctly.
- Itâs not clear whether the generated rna code should generally deal with the e.g.
NodeRepeatItem
orNodeRepeatItem::DNA
.- Long term it might be better to use
NodeRepeatItem
because itâs more flexible and theDNA
subtype really only exists to have very explicit serialization.
- Long term it might be better to use
- The DNA defaults system uses syntax that is not compatible with C++. It likely would have to change as well.
One good thing is that it should be possible to have all the existing DNA structs in the makesdna
directory and only gradually move the structs elsewhere if it is benefitial. So itâs not necessary to refactor all existing DNA structs to make progress.
Next Steps
All of these challenges seem solvable given enough refactoring. But before starting with that it would be good to agree where we want to go exactly. The proposal above is not the only solution that achieves the goals. Other suggestions are welcome.
Another open question is whether we want to address decentralization of RNA at the same time. It might make sense when we have to refactor/rewrite makesrna.c
anyway. My initial experiments suggest that the way we define rna structs can stay centralized even if dna is decentralized though.