Metanode Versioning
A description of how metanode versioning and migrations work to ensure cross-version compatibility
Metanodes describe the data structure used to represent nodes in the tree. Over time, the software evolves and new properties can be added, old properties removed, and the meaning of properties can change. In order to maintain maximum compatibility between different versions of Visionary Render, metanodes that undergo change can define migrations that describe the nature of that change, allowing a version of the software to be able to translate data into a format understandable by a different (usually older) version.
We refer to this as cross-compatibility, using terms such as backward-compatible and forward-compatible.
Visionary Render is considered backward-compatible when it is able to use data from previous versions, which is achieved through the implementation of migrations. It is considered forward-compatible to an extent by virtue of the VRText/VRNative file format, which uses XML descriptions of nodes allowing older versions of Visionary Render to load newer scenes, limited by the older version's understanding of the node properties saved in the file.
Version Specification
Metanodes are given version numbers when their version exceeds 0
. These version numbers are appended to the name of the metanode. A metanode without a version number is considered the "base" version from which future versions are derived. It is up to the code that defines the metanode to append the version number.
For example,
Assembly.1
(a version 1 Assembly)
Collision
(a version 0 Collision)
Version numbers are validated against the number of migrations that are registered against the metanode. For every version increment, there must be a corresponding migration implemented and registered.
Versioned vs. Unversioned Metanode Names
When working with API functions, either directly with the C API or via Lua, it is important to understand the difference between versioned and unversioned metanode names.
Metanode versions are suffixed to the end of the name, creating what we refer to as the "Versioned" metanode name. This is the name that is reported when using any functions that query the name of a metanode or a type of node instance.
"Unversioned" names are the metanode names without the version suffix.
The Lua API functions that accept metanode name parameters can accept either versioned or unversioned names.
The C API functions that accept metanode names expect versioned names, with the exception of VRCurrentMetaNodeVersion, which takes an unversioned name and returns the versioned name of the "current" metanode (that is, the version defined outside of any migrations).
Migrations
When a metanode version exceeds 0
, migrations are required.
Some migrations are simple. Others are complicated. Their complexity depends on the nature of the upgrade.
Each migration can consist of up to 4 functions:
Up
for upgrading the metanode definition to the next version
Down
for downgrading the metanode definition to the previous version
Upgrade
for upgrading node instance properties to the next version of the metanode
Downgrade
for downgrading node instance properties to the previous version of the metanode
For the simplest case of simply adding or removing a property with no side-effects, only the Up
and Down
functions are required. The node instances are migrated automatically in this case because there are no specific property value changes required.
Simple Migration
Given this example metanode definition (using the vrtree-linker
C macros):
This metanode currently has no properties and no migrations. If we want to add a Position property, we add it to the definition along with a migration registration:
struct Meta_MyMetaNode {
VR_META_METHODS("MyMetaNode.1"); //we append a version number starting at 1
enum { Idx_Position }; //we added this convenience enum for property indexing
static void Register() {
HMeta metaNode = VRCreateMetaNode(Name());
VRAddPropertyVec3f(metaNode, "Position", 0, 0, 0); //we added this property definition
//register our migration for this change
VRAddMigration(metaNode, &Meta_MyMetaNode::MigrateAddPosition::reg);
VRFinishMetaNode(metaNode);
}
//implementing the migration inline for example convenience
struct MigrateAddPosition {
static int up(HMigration m, HMeta meta) {
//we make the same changes here that we just made to the definition
return VRAddPropertyVec3f(meta, "Position", 0, 0, 0);
}
static int down(HMigration m, HMeta meta) {
//to downgrade our property addition, we remove it instead
return VRRemoveProperty(meta, "Position");
}
static int reg(HMigration m) {
//this function is called when the metanode is ready to register migrations, so now we give it the functions we implemented above
VRSetMigrationUp(m, &up);
VRSetMigrationDown(m, &down);
}
};
};
Complex Migration
When a migration requires changes to values on node instances, the Upgrade
and Downgrade
functions are required, meaning the default implementation to migrate simple changes no longer applies.
In most cases, the majority of the default logic is still required - you just want to change the value of one of the properties and leave the rest alone. For this, the boilerplate is as follows:
static HNode upgrade(HMigration m, HNode old)
{
// Trigger the previous migration in the chain.
// This ensures that the old node is at the version we expect in this migration.
// We can also manually seek back through the chain and apply any migration we wish,
// if, for some reason, we need to perform a direct migration from a specific version.
old = VRMigrationPrepareNode(m, old);
// Create a node at this version, as a sibling of the old node, so tree order is preserved when old is deleted.
HNode newNode = VRMigrationCreateCurrentNode(m, old);
// This is a mostly trivial migration, so most known properties can be copied directly
VRMigrationCopyKnownProperties(m, old, newNode);
// -----------------------------------------
// This is where you would perform your own upgrade logic.
// -----------------------------------------
// For our example metanode above, we can do something trivial such as looking for a child node to copy a value from
if(vrtree_cpp::HNodeR child = VRFindChildPooled(old, "MetaDataFloat3", "Position")) {
float vec[3];
VRGetPropertyValue(child, "Value", vec, sizeof(vec));
VRSetPropertyValue(newNode, "Position", vec, sizeof(vec));
VRDeleteNode(child);
}
// -----------------------------------------
// We are done with this migration, so finish it, cleaning up the old node
VRMigrationFinish(m, old, newNode);
// Return the new node, so the caller (or another migration) may continue to operate on it
return newNode;
}
This function (and its corresponding downgrade, which uses identical boilerplate) should be registered in the same way as our
Up
and
Down
functions, except using
VRSetMigrationUpgradeNode and
VRSetMigrationDowngradeNode