Specialized components
Visual scripting
Execution engine (VisualScriptEngine & Fiber)
18 min
overview the execution engine is the runtime system that brings visual scripts to life it transforms the static structure of nodes, sockets, and links into dynamic, running programs by managing execution flow, resolving data dependencies, and coordinating asynchronous operations the ir engine's execution engine consists of two primary components the visualscriptengine, which orchestrates the overall execution process, and fibers, which represent individual execution paths through the graph this chapter explores the concept, structure, and implementation of the execution engine within the ir engine core concepts runtime execution while the visual script graph defines the structure of a program, the execution engine determines how that program runs initialization the engine prepares the graph for execution by setting up event nodes event handling when events occur, the engine initiates execution flows from corresponding event nodes sequential processing the engine follows the connections between nodes to determine execution order data resolution before executing a node, the engine ensures all required input data is available asynchronous management the engine handles operations that take time without blocking the entire script this runtime system enables visual scripts to respond to events, process data, and perform actions in a controlled, predictable manner execution components the execution engine consists of two primary components that work together visualscriptengine the central coordinator that manages the overall execution process maintains references to all nodes in the graph initializes event nodes handles asynchronous operations coordinates multiple execution paths fiber an individual execution path that represents a single thread of execution through the graph moves step by step from one node to the next resolves input data for nodes triggers node execution follows output connections to determine the next node these components work together to ensure that visual scripts execute correctly, with nodes running in the proper sequence and with the necessary data implementation visualscriptengine the visualscriptengine class serves as the central coordinator for script execution // simplified from src/engine/execution/visualscriptengine ts export class visualscriptengine { public readonly eventnodes ieventnode\[] = \[]; public readonly asyncnodes iasyncnode\[] = \[]; private fiberqueue fiber\[] = \[]; constructor(public readonly nodes record\<string, inode>) { // find all event nodes in the graph object values(nodes) foreach((node) => { if (iseventnode(node)) { this eventnodes push(node); } }); // initialize all event nodes this eventnodes foreach((eventnode) => { eventnode init(this); }); } // start a new execution path from an event or async node public committonewfiber( node inode, outputflowsocketname string, oncomplete? () => void ) void { const outputsocket = node outputs find(s => s name === outputflowsocketname); if (outputsocket && outputsocket links length > 0) { const link = outputsocket links\[0]; const fiber = new fiber(this, link, oncomplete); this fiberqueue push(fiber); } } // execute all pending fibers synchronously public executeallsync(maxsteps number = infinity) number { let stepsexecuted = 0; while (stepsexecuted < maxsteps && this fiberqueue length > 0) { const currentfiber = this fiberqueue\[0]; stepsexecuted += currentfiber executestep(); if (currentfiber iscompleted()) { this fiberqueue shift(); } } return stepsexecuted; } // register an async operation public registerasyncoperation( node iasyncnode, oncomplete () => void ) void { this asyncnodes push(node); // set up completion callback const originaloncomplete = oncomplete; oncomplete = () => { // remove from async nodes list const index = this asyncnodes indexof(node); if (index >= 0) { this asyncnodes splice(index, 1); } // call original callback originaloncomplete(); }; // start the async operation node startasync(oncomplete); } } the visualscriptengine collects and initializes all event nodes during construction provides methods to start new execution paths ( committonewfiber ) manages the queue of active fibers executes fibers in sequence handles registration and completion of asynchronous operations fiber the fiber class represents a single execution path through the graph // simplified from src/engine/execution/fiber ts export class fiber { private stepsexecuted number = 0; constructor( public readonly engine visualscriptengine, public nexteval link | null, private oncomplete? () => void ) {} // execute one step in this fiber public executestep() number { if (!this nexteval) { this complete(); return 0; } const link = this nexteval; this nexteval = null; const targetnode = this engine nodes\[link nodeid]; const inputsocketname = link socketname; // resolve all data inputs for the target node let stepsforinputs = 0; targetnode inputs foreach(inputsocket => { if (inputsocket valuetypename !== 'flow') { stepsforinputs += resolvesocketvalue(this engine, inputsocket); } }); // execute the node if (isflownode(targetnode)) { targetnode triggered(this, inputsocketname); } else if (isasyncnode(targetnode)) { this engine registerasyncoperation(targetnode, () => { // when async operation completes, continue from its output this engine committonewfiber( targetnode, targetnode getoutputflowsocketname(), this oncomplete ); }); } this stepsexecuted++; return stepsforinputs + 1; } // called by a flow node to specify the next node to execute public commit(node inode, outputsocketname string) void { const outputsocket = node outputs find(s => s name === outputsocketname); if (outputsocket && outputsocket links length > 0) { this nexteval = outputsocket links\[0]; } else { this complete(); } } // check if this fiber has completed public iscompleted() boolean { return this nexteval === null; } // mark this fiber as complete and call the completion callback private complete() void { if (this oncomplete) { this oncomplete(); } } } the fiber tracks the next link to evaluate resolves input data for nodes before execution executes nodes and follows their output connections provides a commit method for nodes to specify the next execution step handles completion of the execution path data resolution before executing a node, the system must ensure all its input data is available this is handled by the resolvesocketvalue function // simplified from src/engine/execution/resolvesocketvalue ts export function resolvesocketvalue( engine visualscriptengine, inputsocket socket ) number { // if the socket has no incoming links, use its current value if (inputsocket links length === 0) { return 0; } // get the source of the input value const sourcelink = inputsocket links\[0]; const sourcenode = engine nodes\[sourcelink nodeid]; const sourcesocket = sourcenode outputs find(s => s name === sourcelink socketname); // if the source is a function node, execute it to get the value if (isfunctionnode(sourcenode)) { // recursively resolve inputs for the function node let stepsforfunctioninputs = 0; sourcenode inputs foreach(fninputsocket => { stepsforfunctioninputs += resolvesocketvalue(engine, fninputsocket); }); // execute the function node sourcenode exec(); // copy the output value to the input socket inputsocket value = sourcesocket value; return stepsforfunctioninputs + 1; } else { // for other node types, just copy the current output value inputsocket value = sourcesocket value; return 0; } } this function checks if the input socket has an incoming connection if connected to a function node, recursively resolves its inputs and executes it copies the resulting value to the input socket returns the number of execution steps performed execution workflow let's examine the execution process for a simple "hello, world!" script graph td a\[event on game start] > b\[action print "hello"] b > c\[async action wait 1 sec] c > d\[action print "world!"] the execution follows these steps initialization the visualscriptengine is created with the graph it finds the on game start event node and initializes it the event node sets up listeners for the game start event event triggering the game starts, triggering the on game start node the node calls engine committonewfiber() with its output socket first fiber creation the engine creates a new fiber starting at the link to print "hello" the fiber is added to the engine's queue executing "print hello" the engine calls fiber executestep() the fiber resolves any inputs for the print "hello" node the fiber calls node triggered() on the print node the print node displays "hello" and calls fiber commit() with its output socket the fiber updates its nexteval to the link to wait 1 sec executing "wait 1 sec" the engine calls fiber executestep() again the fiber resolves inputs for the wait 1 sec node the fiber identifies it as an async node and registers it with the engine the wait node starts its timer the current fiber is effectively completed (no nexteval ) asynchronous waiting the engine continues processing other fibers or tasks after 1 second, the wait node's timer completes the wait node's completion callback is triggered new fiber creation the completion callback calls engine committonewfiber() with the wait node's output the engine creates a new fiber starting at the link to print "world!" executing "print world!" the engine processes the new fiber the fiber resolves inputs for the print "world!" node the fiber calls node triggered() on the print node the print node displays "world!" and calls fiber commit() with no further connections, the fiber completes this sequence demonstrates how the execution engine manages both synchronous flow (from one node directly to the next) and asynchronous operations (waiting for a timer) node execution different types of nodes interact with the execution engine in specific ways event node execution // simplified concept for event nodes class eventnode extends node\<nodetype event> { init(engine visualscriptengine) void { // set up event listener const eventsource = geteventsource(); eventsource addeventlistener('event', () => { // when event occurs, start execution from this node engine committonewfiber(this, 'output'); }); } } event nodes initialize by setting up event listeners start new execution paths when events occur have no input execution sockets, only output execution sockets flow node execution // simplified concept for flow nodes class flownode extends node\<nodetype flow> { triggered(fiber fiber, inputsocketname string) void { // read input values const inputvalue = this readinput\<string>('message'); // perform node specific action console log(inputvalue); // continue execution to the next node fiber commit(this, 'next'); } } flow nodes are triggered by a fiber through their input execution socket read input values, perform their action, and determine the next step call fiber commit() to specify which output execution path to follow async node execution // simplified concept for async nodes class asyncnode extends node\<nodetype async> { triggered(engine visualscriptengine, inputsocketname string, oncomplete () => void) void { // read input values const duration = this readinput\<number>('duration'); // register the async operation engine registerasyncoperation(this, oncomplete); } startasync(oncomplete () => void) void { // start the asynchronous operation settimeout(oncomplete, this readinput\<number>('duration') 1000); } getoutputflowsocketname() string { // specify which output socket to follow when complete return 'completed'; } } async nodes register themselves with the engine for asynchronous handling start their operation (e g , timer, file loading) without blocking execution specify which output path to follow when the operation completes function node execution // simplified concept for function nodes class functionnode extends node\<nodetype function> { exec() void { // read input values const a = this readinput\<number>('a'); const b = this readinput\<number>('b'); // perform calculation const result = a + b; // write to output this writeoutput\<number>('sum', result); } } function nodes are executed on demand when their output is needed read input values, perform calculations, and write results to outputs do not participate directly in execution flow next steps with an understanding of how the execution engine processes visual scripts at runtime, the next chapter explores how the system manages the available node types and value types through the node and value registry next node & value registry docid\ n9797kg1c bjmdzhzjcss