Memory Management

Scenome Scripting Language uses C++ style memory management. The most important thing to know is that new and delete are paired, which means if you use new, you must use delete, or you must transfer ownership of the object (and therefore its memory) to another object. When you're working with <Node> objects, you will typically create them and then transfer ownership to the Scenome document (<Model3D>) via a call to Model.AddNode(). In that sense, you can think of the <Model3D> as a sink for <Node> objects. We refer to <Model3D> and 'document' somewhat interchangeably, although the 'document' is a bit more all-encompassing than the <Model3D>. Finally, you cannot add anything but a <Node> object to the <Model3D>.

Note that Scenome supports RAII for primitive types. Smart pointers for object types are supported via the auto keyword. Using auto mkes it quite easy to manage memory most of the time, but there are some important caveats that we'll explain below.

For those who may be new to memory management concepts, here are a few brief explanations.

Table 1.1. Concepts

Concept Explanation
Ownership The concept that one object, which might be a <Model3D>, <Node>, <List>, or array, owns a particular block of memory. Memory is freed when the owner requests destruction of the memory, or when the owner is destroyed. In most cases ownership means that an object is responsible for deleting memory, and will do so in its destructor or other places.
Single Ownership The concept that one object, and only one object, can own a particular block of memory. This is an important concept in Scenome Scripting Language.
Multiple Ownership The concept that more than one object can own a particular block of memory. This is unsupported in Scenome Scripting Language.
Owning Pointer A pointer that owns a resource. In Group a_oGroup = new Group, we say that a_oGroup is a pointer that owns the memory allocated for the <Group>. You can delete the memory by calling delete a_oGroup.
Non-Owning Pointer A pointer that refers to a resource, but does not own it. In Group a_oGroup = new Group, we say that a_oGroup is an owning pointer. However, calling Model.AddNode( a_oGroup, Model, -1 ) transfers ownership to the <Model3D>. Calling delete a_oGroup will cause a crash (because of a double free, if the <Node> is owned by the <Model3D>. In that case, since <Model3D> owns a_oGroup, you need to call Model.DeleteNode( a_oGroup ) to destroy it.
Single Ownership, Multiple Non-Owning Pointers The concept that one object, and only one object, can own a particular block of memory, but other objects may point at it. Destruction of objects that point at the memory do not result in that memory being deallocated.

In an effort to protect your data, fights over object ownership usually result in a very swift crash. When working with Scenome Scripting Language, it's best to assume that multiple ownership is impossible—single ownership with multiple non-owning pointers is best practice.

Table 1.2. Common Allocation Methods

Methods Details
LibCreate.New<T> Must use delete or you must transfer ownership ( unless object is qualified with auto ).
new Must use delete or you must transfer ownership ( unless object is qualified with auto ).
Clone Must use delete or you must transfer ownership ( unless object is qualified with auto ).
CreateObject Must use delete or you must transfer ownership ( unless object is qualified with auto ).
ValidateNodeCreateContext.CreateNodes Creates a <Node> and transfers ownership to a <TypeBuffer> or the <Model3D>.

Table 1.4. Common Ownership Transfer Methods

Method Details
Model.AddNode( node, parent, index ) Transfers ownership of a single <Node> to the <Model3D>.
Group.AddChild( node ) Transfers ownership of a single <Node> to the <Group>.
List.Owns( node ) Transfers ownership of a single <Node> to the <List> object.

Table 1.4. Common Memory Destruction Methods

Method Details
delete Deletes the object which causes memory deallocation.
List.Del( object ) Deletes an object owned by a <List> which causes memory deallocation.
Model.DeleteNode( node ) Deletes a <Node> from the <Model3D> which causes memory deallocation.
Group.DeleteChild( node ) Deletes a <Node> from the <Group> object's child list, provided neither <Group> nor <Node> are owned by the <Model3D>. Causes memory deallocation.

Using Lists

In the past, it was common to use <List> objects to form collections of <Node> objects. Please note that the <NodeBuffer> and <TypeBuffer> are now recommended instead of <List>. For the purposes of supporting legacy code, this documentation includes full coverage of <List>. As of December 2022, <List> is used infrequently in the Scenome Scripting Language codebase.

There are two main ways of managing ownership with <List> objects: Add and AddRef. If you plan on adding a <Node> object to the document, then you should use AddRef so that you can later transfer ownership to the <Model3D>. If you use Add, you'll have to explicitly remove the <Node> from the <List> ( which means that no one owns it ) and then make sure to transfer ownership to the <Model3D>. For this reason, it's just easier to use AddRef.

Sample Code 1.1.

auto List a_lNodes;
Group a_oGroup = new Group;
a_lNodes.AddRef( a_oGroup );
Model.AddNode( a_oGroup, Model, -1 ); // Add a_oGroup to the Model as the last node in the Model's child list. Model now owns memory allocated for a_oGroup.
// a_lNodes goes out of scope here, but it doesn't own anything so we're fine.

Sometimes you want to use a <Node> as a temporary object, and you want the <List> object to have ownership of the <Node> objects. This means that the memory allocated for the <Node> will be freed when the <List> object goes out of scope.

Using <NodeBuffer> or <TypeBuffer>

In Scenome Scripting Language it is recommended to use <NodeBuffer> or <TypeBuffer> objects to form collections of <Node> objects and objects derived from <Type> ( such as <Image> ). <NodeBuffer> and <TypeBuffer> have features designed to prevent frequent memory allocations and deallocations, and it is important to understand what this means for memory management.

<TypeBuffer> objects can store collections of pointers to objects derived from <Type>. In practice, this includes many, but not all API objects. You can use <TypeBuffer> to store <Node> objects, but you should prefer <NodeBuffer> for this purpose.

There are two main ways of managing ownership with <TypeBuffer> and <NodeBuffer> objects: Add or its overload Owns and AddRef or its overload Refers. In simple use cases, if you plan on adding a <Node> object to the document, then you should use AddRef or Refers so that you can later transfer ownership to the <Model3D>. In more complex cases, including cases with tricky logic or cases where you could have an unplanned exit, you should allow the <NodeBuffer> or <TypeBuffer> to own the object. If you use Add/Owns, you'll still have to explicitly remove the <Node> from the container (which means that no one owns it) and then make sure to transfer ownership to the <Model3D> or elsewhere.

If a <NodeBuffer> or <TypeBuffer> owns an object, you can call Remove or RemoveObj which will return a pointer to the object, and replaces the entry in the <NodeBuffer> or <TypeBuffer> with a nullptr. This makes it easy to allocate a <NodeBuffer> or <TypeBuffer> and then reuse the memory without having to go through constant allocation/deallocation cycles while the collection is resized. You can call Compact at any time to remove any empty indices in the collection.

Stable sizing can be very useful, but it can also make some algorithms much more difficult. For this reason, you can call Del, which will remove the item from the collection and perform any allocations and operations necessary to ensure the collection is free of nullptrs.

Sample Code 1.2.

// Add a_oGroup to the Model as the last node
// in the Model's child list. Model now owns
// memory allocated for a_oGroup. a_apNodes goes
// out of scope here, but it doesn't own anything so we're fine.

auto NodeBuffer a_apNodes;
Group a_oGroup = new Group;
a_apNodes.Refers( a_oGroup );
Model.AddNode( a_oGroup, Model, -1 );

// Add a_oChild to the Model as the last node in the
// Model's child list. Model now owns memory allocated
// for a_oGroup. a_apNodes goes out of scope here,
// but it doesn't own anything so we're fine.

auto NodeBuffer a_apNodes;
Group a_oGroup = new Group;
a_apNodes.Owns( a_oGroup );
// Do work.
Group a_oChild = (Group)a_apNodes.RemoveAt( 0 );
Model.AddNode( a_oChild, Model, -1 );

Sometimes you want to use a <Node> as a temporary object, and you want the container object to have ownership of the <Node> objects. This means that the memory allocated for the <Node> will be freed when the container object goes out of scope.

Sample Code 1.3.

{
   auto NodeBuffer a_apNodes;
   Group a_oGroup = new Group;
   a_apNodes.Owns( a_oGroup ); // a_apNodes now owns a_oGroup.
   // Do something with our Group...
   a_oGroup.Name = "MyGroup";
   // a_apNodes goes out of scope here, and a_oGroup is destroyed.
}

{
   // All in all it would be better to use...
   auto NodeBuffer a_apNodes;
   auto Group a_oGroup = new Group;
   a_apNodes.Refers( a_oGroup );
   // Everything is automatically destroyed.
}

Another common case involves using a container object to store a set of <Node> objects that are already owned by the <Model3D>. In that case, you must use Refers, or the resulting ownership fight will cause a crash.

Sample Code 1.4.

auto NodeBuffer a_apChildNodes;
for( int i = 0; i < Model.ChildCount; ++i )
{
   a_apChildNodes.Refers( Model.Children[i] ); // Adds to a_apChildNodes a non-owning pointer to each child Node of the Model.
}
// a_apChildNodes goes out of scope here, but it doesn't own anything.
// The Model still owns its child nodes.

For performance reasons you'll want to know the difference between Model.AddNode() and Group.AddChild(). The first expression always works for <Node> objects allocated with new, but it can incur performance penalties because of dependency editing and undo/redo action management that must take place when a <Node> is added to the <Model3D>. The second expression only works if both the <Group> and the child <Node> have not yet been added to the <Model3D>, but the performance is far superior if you are adding lots of <Node> objects.

Sample Code 1.5.

Group a_oGroupA = new Group; // Not added to Model.
Group a_oGroupB = new Group; // Not added to Model.
Model.AddNode( a_oGroupB, a_GroupA, -1 ); // Update dependencies and undo/redo actions even though the Groups aren't in the document yet.
Model.AddNode( a_GroupA, Model, -1 ); // Update dependencies yet again... better to do this once.

Group a_oGroupC = new Group; // Not added to Model.
Group a_oGroupD = new Group; // Not added to Model.
a_oGroupC.AddChild( a_oGroupD ); // a_GroupC now owns a_oGroupD. Much faster than using Model.AddNode( a_oGroupD, a_GroupC, -1 );
Model.AddNode( a_GroupC, Model, -1 );

Once a <Node> has been added to the <Model3D> you must use Model.DeleteNode() to remove it.

Sample Code 1.6.

Group a_oGroup = new Group;
Model.AddNode( a_Group, a_oModel, -1 );
Model.DeleteNode( a_oGroup ); // Detaches a_oGroup from the Model, updates Model dependencies, and frees its memory.

However, if you use AddChild() you must use DeleteChild()

Sample Code 1.7.

Group a_oGroupA = new Group; // Not added to Model.
Group a_oGroupB = new Group; // Not added to Model.
a_oGroupA.AddChild( a_oGroupB ); // a_GroupA now owns a_oGroupB.
// Maybe we didn't mean to do that...
a_oGroupA.DeleteChild( a_oGroupB );

So far we've talked about how to manage ownership between <Nodes>, <List> objects, <NodeBuffer> objects, and the <Model3D>. However, you will also find yourself managing ownership by allocating objects with new and then transferring ownership to a <Node>. The same rules apply as above.

Sample Code 1.8.

Float32Node a_oFloat32Node = (Float32Node)Model.GetFirstSelectedNode().GetNode(); // The user has selected a Float32Node.
MaterialDataCaptureShininess a_oCapture = new MaterialDataCaptureShininess; // Allocate a new object that is NOT a Node.
a_oCapture.FindMaterial( Model ); // Find the first Material node in the document and connects it to a_oCapture...
a_oFloat32Node.Capture.Add( a_oCapture ); // Transfer ownership of a_oCapture to the Float32Node's DataCapture data member.

Very often when working in Scenome Scripting Language you'll find yourself 'setting data sources'. In simple terms this means that you'll set a pointer from <Node> A to <Node> B. This does not change ownership.

Sample Code 1.9.

Material m = FindMaterial(); // Find the first Material node in the document...
Program p = FindProgram(); // Find the first shader Program node in the document...
m.Program = p; // Set the Material's Program object pointer to point at Program p.
Model.DeleteNode( m ); // Deleting Material m does not delete Program p;

As with modern C++, most memory management is automatic, but it's necessary to know and understand ownership semantics for Scenome Scripting Language.