Understanding the Ceres Port System and NodePort References in Ceres Graphs
The Ceres Port system provides typed data containers for visual scripting graphs, while NodePort references use weak references to NodeReference handles that resolve target nodes at runtime without preventing garbage collection.
The Ceres Port system forms the backbone of data flow inside the akikurisu/ceres visual scripting framework. Every node field that should appear as a wired connection in the visual editor is implemented as a port—a typed container that either receives values as an input or provides values as an output. Understanding how the generic CeresPort<T> class functions, and specifically how NodePort references maintain graph connections via weak references, is essential for building robust node-based execution flows.
Core Components of the Ceres Port System
CeresPort Generic Container
The foundation of the system is CeresPort<T>, a generic container defined in Runtime/Core/Models/Graph/Ports/CeresPort.cs. This class holds the actual value of type T, a default fallback value, and the internal logic that resolves the final value from a linked source port. When a port has no incoming connection, it returns its default value; when linked, it reads from the source or an adapter that performs type conversion.
Input and Output Attributes
Ports are declared declaratively using attributes from the Ceres.Annotations namespace. The InputPortAttribute marks a field as a receiving connector, while the OutputPortAttribute marks a field as a source connector. The visual editor scans for these attributes during node construction to generate the interactive connection points in the graph UI.
Port Linking and Type Compatibility
Connections between ports are established through the CeresPort<T>.Link method. When two ports are wired together in the editor, the target port’s getter is redirected to read from the source port. The system supports cross-type linking via CeresPort<T>.MakeCompatibleTo<TTarget>(), which registers a conversion delegate. For example, an int port can feed a float port if a compatible conversion has been registered.
How NodePort References Work
NodePort as CeresPort
NodePort is a specialized implementation where the generic argument is NodeReference rather than a primitive data type. Defined in Runtime/Core/Models/Graph/Ports/CeresPort.cs (lines 64‑77), NodePort inherits from CeresPort<NodeReference> and stores a weak reference to a target CeresNode. This design allows the graph to maintain a logical link to another node without preventing that node from being garbage collected if it is removed from the graph.
Weak Reference Resolution
Internally, NodePort maintains a WeakReference<CeresNode> accessible via the Node property. When execution logic calls NodePort.Get(), the method attempts to resolve the target via WeakReference.TryGetTarget. If the target node has been destroyed or is no longer available, the method returns null, allowing the graph to fail gracefully rather than throwing exceptions or maintaining dangling pointers.
Runtime Execution Flow
Flow nodes utilize NodePort fields to determine execution continuation. Consider the implementation in Runtime/Flow/Models/Nodes/Utilities/FlowNode_SwitchString.cs:
public class FlowNode_SwitchString : ForwardNode, IPortArrayNode
{
[InputPort, CeresLabel("Selection")]
public CeresPort<string> sourceValue;
[OutputPort(false)]
public NodePort[] outputs;
[OutputPort(false), CeresLabel("Default")]
public NodePort defaultOutput;
public override void Execute(ExecutionContext ctx)
{
var selected = sourceValue.Value;
var targetPort = FindMatchingPort(selected) ?? defaultOutput;
var nextNode = targetPort?.Get();
nextNode?.Execute(ctx);
}
}
In this pattern, the Get() call resolves the weak reference to obtain the next CeresNode instance, and execution continues only if the reference is valid. This mechanism applies to single NodePort fields as well as arrays like NodePort[] outputs.
Practical Implementation Examples
Defining a Typed Input Port
public class MyNode : CeresNode
{
[InputPort]
public CeresPort<float> speed;
}
The [InputPort] attribute instructs the editor to render a connection point. At runtime, speed.Value returns either the linked input or the port’s configured default.
Registering Cross-Type Compatibility
// Allow linking int ports to float ports
CeresPort<int>.MakeCompatibleTo<float>(i => (float)i);
Once registered, a CeresPort<float> can accept connections from CeresPort<int> nodes, with the conversion applied automatically during value resolution.
Manual Port Wiring
CeresPort<float> source = nodeA.speed;
CeresPort<float> target = nodeB.targetSpeed;
source.Link(target);
This pattern is useful for custom editor scripts or programmatic graph construction, directly invoking the linking logic found in CeresPort.cs.
Using NodePort for Execution Branching
public class JumpNode : CeresNode
{
[OutputPort]
public NodePort next;
public override void Execute(ExecutionContext ctx)
{
var target = next?.Get();
target?.Execute(ctx);
}
}
The next.Get() call demonstrates the weak reference resolution pattern, safely retrieving the target node if it exists.
Summary
CeresPort<T>inRuntime/Core/Models/Graph/Ports/CeresPort.csprovides the generic foundation for typed data flow, holding values and default fallbacks.NodePortspecializes this system to storeNodeReferencevalues as weak references, enabling safe graph connections that do not prevent garbage collection.- Input and output attributes drive the visual editor’s port rendering and connection capabilities.
MakeCompatibleTo<T>()allows heterogeneous port types to link via registered conversion delegates.- Weak reference resolution via
NodePort.Get()ensures runtime safety when nodes are destroyed or disconnected.
Frequently Asked Questions
What is the difference between CeresPort and NodePort?
CeresPort<T> is a generic container for any data type (floats, strings, custom objects), while NodePort is a concrete subclass where T is specifically NodeReference. NodePort adds weak reference semantics to track other nodes without strong references, whereas standard CeresPort<T> instances manage data values directly.
How does the Ceres graph handle deleted nodes with active NodePort connections?
The graph handles deletions safely because NodePort stores a WeakReference<CeresNode> rather than a direct pointer. When Get() is called, it attempts TryGetTarget; if the node was destroyed, the call returns null. Execution logic can then branch or terminate gracefully without null reference exceptions.
Can CeresPort types be linked if they have different generic arguments?
Yes, as long as a compatible conversion is registered. Call CeresPort<TSource>.MakeCompatibleTo<TTarget>(converter) to register a delegate that transforms the source type into the target type. The conversion runs automatically when the target port resolves its value.
Where is the port linking logic implemented in the source code?
The core linking implementation resides in Runtime/Core/Models/Graph/Ports/CeresPort.cs, specifically within the CeresPort<T>.Link method and the compatibility resolution logic. The visual editor’s UI representation of ports is handled separately in Editor/Core/UIElements/Graph/Ports/CeresPortView.cs, which determines connection capacities (single vs. multi) based on field types.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →