Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
GraphExecutionSimulator.cpp
Go to the documentation of this file.
1/**
2 * @file GraphExecutionSimulator.cpp
3 * @brief Implementation of graph execution simulator.
4 * @author Olympe Engine
5 * @date 2026-03-24
6 */
7
10#include "../TaskSystem/TaskGraphTypes.h"
11#include "../system/system_utils.h"
12#include <algorithm>
13#include <set>
14
15namespace Olympe {
16
20
24
28{
29 std::vector<ValidationError> errors;
30 outTracer.Reset();
31
32 // Initialize blackboard
34 if (!options.initialBlackboardJson.empty())
35 {
36 // TODO: Parse JSON and initialize blackboard
37 }
38
39 // Get entry point
40 if (tmpl.EntryPointID < 0)
41 {
42 errors.push_back(ValidationError(-1, "", "No entry point defined",
43 ErrorSeverity::Critical, "Simulation"));
44 outTracer.RecordError(-1, "", "No entry point defined", "Critical");
45 outTracer.RecordExecutionCompleted(false, "Graph has no entry point");
46 return errors;
47 }
48
49 // Simulate execution
50 int32_t currentNodeId = tmpl.EntryPointID;
51 m_visitCount.clear();
52 m_pathStack.clear();
54
55 outTracer.RecordNodeEntered(currentNodeId, "EntryPoint", "EntryPoint");
56
57 while (currentNodeId != NODE_INDEX_NONE && stepCount < options.maxStepsPerFrame)
58 {
59 ++stepCount;
60
61 // Get node definition
62 const TaskNodeDefinition* nodeDef = tmpl.GetNode(currentNodeId);
63 if (!nodeDef)
64 {
65 std::string msg = "Node " + std::to_string(currentNodeId) + " not found";
66 errors.push_back(ValidationError(currentNodeId, "", msg,
67 ErrorSeverity::Error, "Simulation"));
68 outTracer.RecordError(currentNodeId, "", msg);
69 break;
70 }
71
72 // Track visit count for cycle detection
73 m_visitCount[currentNodeId]++;
74 if (m_visitCount[currentNodeId] > 2)
75 {
76 std::string msg = "Potential infinite loop detected at node " +
77 (nodeDef->NodeName.empty() ? std::to_string(currentNodeId) : nodeDef->NodeName);
78 errors.push_back(ValidationError(currentNodeId, nodeDef->NodeName, msg,
79 ErrorSeverity::Warning, "Logic"));
80 outTracer.RecordError(currentNodeId, nodeDef->NodeName, msg, "Warning");
81 break;
82 }
83
84 // Simulate node execution
86
87 switch (nodeDef->Type)
88 {
90 {
91 outTracer.RecordNodeEntered(currentNodeId, "EntryPoint", "EntryPoint");
92 // EntryPoint always goes to the "Then" pin
93 nextNodeId = GetNextNodeId(tmpl, currentNodeId, "Then");
95 {
96 outTracer.RecordExecutionBlocked(currentNodeId, "EntryPoint has no outgoing connection");
97 errors.push_back(ValidationError(currentNodeId, "EntryPoint",
98 "EntryPoint has no outgoing execution link",
99 ErrorSeverity::Error, "Link"));
100 }
101 break;
102 }
103
106 break;
107
110 break;
111
114 break;
115
118 break;
119
121 {
122 outTracer.RecordNodeEntered(currentNodeId, nodeDef->NodeName, "AtomicTask");
123 nextNodeId = GetNextNodeId(tmpl, currentNodeId, "Completed");
125 {
126 outTracer.RecordExecutionBlocked(currentNodeId, "No outgoing link from AtomicTask");
127 }
128 break;
129 }
130
134 {
135 outTracer.RecordNodeEntered(currentNodeId, nodeDef->NodeName, "DataNode");
136
137 // Trace all incoming data pins recursively
138 m_tracedDataNodes.clear();
139 TraceDataPinEvaluation(currentNodeId, tmpl, outTracer);
140
141 nextNodeId = GetNextNodeId(tmpl, currentNodeId, "Then");
142 break;
143 }
144
146 {
147 outTracer.RecordNodeEntered(currentNodeId, nodeDef->NodeName, "Delay");
148 nextNodeId = GetNextNodeId(tmpl, currentNodeId, "Completed");
149 break;
150 }
151
153 {
154 outTracer.RecordNodeEntered(currentNodeId, nodeDef->NodeName, "DoOnce");
155 nextNodeId = GetNextNodeId(tmpl, currentNodeId, "Completed");
156 break;
157 }
158
160 {
161 outTracer.RecordNodeEntered(currentNodeId, nodeDef->NodeName, "SubGraph");
162
163 // Get SubGraphPath from either field or Parameters
164 std::string subGraphPath = nodeDef->SubGraphPath;
165 if (subGraphPath.empty())
166 {
167 auto it = nodeDef->Parameters.find("subgraph_path");
168 if (it != nodeDef->Parameters.end() &&
169 it->second.Type == ParameterBindingType::Literal)
170 {
171 subGraphPath = it->second.LiteralValue.to_string();
172 }
173 }
174
175 if (subGraphPath.empty())
176 {
177 outTracer.RecordError(currentNodeId, nodeDef->NodeName, "SubGraph path is empty");
178 }
179
180 nextNodeId = GetNextNodeId(tmpl, currentNodeId, "Completed");
181 break;
182 }
183
184 default:
186 break;
187 }
188
189 currentNodeId = nextNodeId;
190 }
191
192 if (stepCount >= options.maxStepsPerFrame)
193 {
194 std::string msg = "Maximum simulation steps exceeded - possible infinite loop";
195 errors.push_back(ValidationError(-1, "", msg, ErrorSeverity::Warning, "Logic"));
196 outTracer.RecordError(-1, "", msg, "Warning");
197 }
198
199 // Perform additional validation checks
200 if (options.validateBranchPaths)
201 {
202 ValidateAllBranches(tmpl, errors);
203 }
204
205 if (options.validateDataFlow)
206 {
208 }
209
210 // Check for unreachable nodes
211 std::vector<int32_t> unreachable = FindUnreachableNodes(tmpl, errors);
212
213 outTracer.RecordExecutionCompleted(errors.empty(),
214 std::to_string(stepCount) + " steps executed, " +
215 std::to_string(errors.size()) + " validation issues found");
216
217 return errors;
218}
219
221 int32_t currentNodeId,
225{
226 const TaskNodeDefinition* node = tmpl.GetNode(currentNodeId);
227 if (!node)
228 return NODE_INDEX_NONE;
229
230 tracer.RecordNodeEntered(currentNodeId, node->NodeName, "");
231 return NODE_INDEX_NONE;
232}
233
235 int32_t nodeId,
238{
239 const TaskNodeDefinition* node = tmpl.GetNode(nodeId);
240 if (!node)
241 return NODE_INDEX_NONE;
242
243 tracer.RecordNodeEntered(nodeId, node->NodeName, "Branch");
244
245 // For simulation, we'll try the true branch by default
246 // In a real scenario, we'd evaluate the condition
247 bool conditionResult = true;
248 tracer.RecordConditionEvaluated(nodeId, "condition", conditionResult);
249
250 if (conditionResult)
251 {
252 tracer.RecordBranchTaken(nodeId, "True", -1);
253 return GetNextNodeId(tmpl, nodeId, "Then");
254 }
255 else
256 {
257 tracer.RecordBranchTaken(nodeId, "False", -1);
258 return GetNextNodeId(tmpl, nodeId, "Else");
259 }
260}
261
263 int32_t nodeId,
266{
267 const TaskNodeDefinition* node = tmpl.GetNode(nodeId);
268 if (!node)
269 return NODE_INDEX_NONE;
270
271 tracer.RecordNodeEntered(nodeId, node->NodeName, "Switch");
272
273 // Get the default (first) case
274 return GetNextNodeId(tmpl, nodeId, "Default");
275}
276
278 int32_t nodeId,
281{
282 const TaskNodeDefinition* node = tmpl.GetNode(nodeId);
283 if (!node)
284 return NODE_INDEX_NONE;
285
286 tracer.RecordNodeEntered(nodeId, node->NodeName, "VSSequence");
287 return GetNextNodeId(tmpl, nodeId, "Then");
288}
289
291 int32_t nodeId,
294{
295 const TaskNodeDefinition* node = tmpl.GetNode(nodeId);
296 if (!node)
297 return NODE_INDEX_NONE;
298
299 tracer.RecordNodeEntered(nodeId, node->NodeName, "While");
300 tracer.RecordBranchTaken(nodeId, "Loop", -1);
301
302 return GetNextNodeId(tmpl, nodeId, "Loop");
303}
304
306 int32_t nodeId,
307 const std::string& pinName)
308{
309 // Find execution link from nodeId with pin name
310 const TaskNodeDefinition* node = tmpl.GetNode(nodeId);
311 if (!node)
312 return NODE_INDEX_NONE;
313
314 for (const auto& link : tmpl.ExecConnections)
315 {
316 if (link.SourceNodeID == nodeId && link.SourcePinName == pinName)
317 {
318 return link.TargetNodeID;
319 }
320 }
321
322 return NODE_INDEX_NONE;
323}
324
326 std::vector<ValidationError>& outErrors)
327{
328 bool allValid = true;
329
330 for (const auto& node : tmpl.Nodes)
331 {
332 if (node.Type == TaskNodeType::Branch)
333 {
334 // Check that both "Then" and "Else" pins have outgoing connections
335 int32_t thenTarget = GetNextNodeId(tmpl, node.NodeID, "Then");
336 int32_t elseTarget = GetNextNodeId(tmpl, node.NodeID, "Else");
337
339 {
340 outErrors.push_back(ValidationError(node.NodeID, node.NodeName,
341 "Branch node missing 'Then' connection",
342 ErrorSeverity::Error, "Link"));
343 allValid = false;
344 }
345
347 {
348 outErrors.push_back(ValidationError(node.NodeID, node.NodeName,
349 "Branch node missing 'Else' connection",
350 ErrorSeverity::Error, "Link"));
351 allValid = false;
352 }
353 }
354 }
355
356 return allValid;
357}
358
360 std::vector<ValidationError>& outErrors)
361{
362 bool allValid = true;
363
364 for (const auto& link : tmpl.DataConnections)
365 {
366 const TaskNodeDefinition* srcNode = tmpl.GetNode(link.SourceNodeID);
367 const TaskNodeDefinition* dstNode = tmpl.GetNode(link.TargetNodeID);
368
369 if (!srcNode || !dstNode)
370 {
371 outErrors.push_back(ValidationError(-1, "",
372 "Data connection references non-existent node",
373 ErrorSeverity::Error, "Link"));
374 allValid = false;
375 }
376 }
377
378 return allValid;
379}
380
382 std::vector<ValidationError>& outErrors)
383{
384 std::map<int32_t, bool> reachable;
385 for (const auto& node : tmpl.Nodes)
386 {
387 reachable[node.NodeID] = false;
388 }
389
390 // Mark entry point and all reachable nodes
391 if (tmpl.EntryPointID >= 0)
392 {
393 MarkReachableNodes(tmpl, tmpl.EntryPointID, reachable);
394 }
395
396 std::vector<int32_t> unreachable;
397 for (const auto& pair : reachable)
398 {
399 if (!pair.second && pair.first != -1)
400 {
401 unreachable.push_back(pair.first);
402
403 const TaskNodeDefinition* node = tmpl.GetNode(pair.first);
404 if (node)
405 {
406 outErrors.push_back(ValidationError(pair.first, node->NodeName,
407 "Node is unreachable from entry point",
408 ErrorSeverity::Warning, "Logic"));
409 }
410 }
411 }
412
413 return unreachable;
414}
415
417 int32_t nodeId,
418 std::map<int32_t, bool>& reachable)
419{
420 if (reachable.find(nodeId) != reachable.end() && reachable[nodeId])
421 return; // Already marked
422
423 reachable[nodeId] = true;
424
425 // Find all nodes this one connects to
426 for (const auto& link : tmpl.ExecConnections)
427 {
428 if (link.SourceNodeID == nodeId)
429 {
430 MarkReachableNodes(tmpl, link.TargetNodeID, reachable);
431 }
432 }
433}
434
436 std::vector<ValidationError>& outErrors)
437{
438 bool found = false;
439
440 for (const auto& node : tmpl.Nodes)
441 {
442 if (node.Type == TaskNodeType::While)
443 {
444 // While loops without proper exit conditions can be infinite
445 // This is a heuristic - we'd need condition analysis for certainty
446 int32_t exitTarget = GetNextNodeId(tmpl, node.NodeID, "Completed");
448 {
449 outErrors.push_back(ValidationError(node.NodeID, node.NodeName,
450 "While loop has no 'Completed' exit path",
451 ErrorSeverity::Warning, "Logic"));
452 found = true;
453 }
454 }
455 }
456
457 return found;
458}
459
461 const std::string& expression)
462{
463 // Basic validation - check for empty or invalid syntax
464 if (expression.empty())
465 return false;
466
467 return true;
468}
469
471 std::map<int32_t, bool>& reachable)
472{
473 for (const auto& node : tmpl.Nodes)
474 {
475 reachable[node.NodeID] = false;
476 }
477
478 if (tmpl.EntryPointID >= 0)
479 {
480 MarkReachableNodes(tmpl, tmpl.EntryPointID, reachable);
481 }
482}
483
485 const TaskGraphTemplate& tmpl,
487 int32_t depth)
488{
489 // Prevent infinite recursion on cyclic data dependencies
490 if (depth > 10)
491 {
492 return;
493 }
494
495 if (m_tracedDataNodes.find(nodeId) != m_tracedDataNodes.end())
496 {
497 return;
498 }
499
500 m_tracedDataNodes.insert(nodeId);
501
502 const TaskNodeDefinition* node = tmpl.GetNode(nodeId);
503 if (!node)
504 {
505 return;
506 }
507
508 // Log the data node being evaluated with depth indentation
509 std::string indent(depth * 2, ' ');
510 std::string depthMarker = (depth > 0) ? ("<- [EVAL] ") : "";
511
512 switch (node->Type)
513 {
515 {
516 tracer.RecordDataPinResolved(nodeId, "Value",
517 indent + depthMarker + "[GetBBValue] " + node->NodeName +
518 " (Key: " + node->BBKey + ")");
519 break;
520 }
521
523 {
524 // Log the MathOp node
525 tracer.RecordDataPinResolved(nodeId, "Result",
526 indent + depthMarker + "[MathOp] " + node->NodeName +
527 " (Operator: " + node->MathOperator + ")");
528
529 // Evaluate and trace both operands
532
533 for (const auto& conn : tmpl.DataConnections)
534 {
535 if (conn.TargetNodeID == nodeId)
536 {
537 if (conn.TargetPinName == "A")
538 {
539 leftSourceNodeId = conn.SourceNodeID;
540 // Trace left operand source
541 TraceDataConnection(conn.SourceNodeID, conn.SourcePinName,
542 nodeId, "A", tmpl, tracer, depth + 1);
543 }
544 else if (conn.TargetPinName == "B")
545 {
546 rightSourceNodeId = conn.SourceNodeID;
547 // Trace right operand source
548 TraceDataConnection(conn.SourceNodeID, conn.SourcePinName,
549 nodeId, "B", tmpl, tracer, depth + 1);
550 }
551 }
552 }
553 break;
554 }
555
557 {
558 tracer.RecordDataPinResolved(nodeId, "Input",
559 indent + depthMarker + "[SetBBValue] " + node->NodeName +
560 " (Key: " + node->BBKey + ")");
561
562 // Find incoming data connection
563 for (const auto& conn : tmpl.DataConnections)
564 {
565 if (conn.TargetNodeID == nodeId && conn.TargetPinName == "Value")
566 {
567 TraceDataConnection(conn.SourceNodeID, conn.SourcePinName,
568 nodeId, "Value", tmpl, tracer, depth + 1);
569 }
570 }
571 break;
572 }
573
574 default:
575 break;
576 }
577
578 // Find all data connections targeting this node (for non-MathOp nodes)
580 {
581 for (const auto& conn : tmpl.DataConnections)
582 {
583 if (conn.TargetNodeID == nodeId)
584 {
585 TraceDataConnection(conn.SourceNodeID, conn.SourcePinName,
586 conn.TargetNodeID, conn.TargetPinName,
587 tmpl, tracer, depth);
588 }
589 }
590 }
591}
592
594 const std::string& sourcePinName,
596 const std::string& targetPinName,
597 const TaskGraphTemplate& tmpl,
599 int32_t depth)
600{
602 if (!sourceNode)
603 {
604 return;
605 }
606
607 // Recursively trace dependencies of this node
609}
610
611} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Simulates graph execution without runtime side effects.
void TraceDataPinEvaluation(int32_t nodeId, const TaskGraphTemplate &tmpl, GraphExecutionTracer &tracer, int32_t depth=0)
Recursively traces data pin evaluation for pure data nodes.
int32_t GetNextNodeId(const TaskGraphTemplate &tmpl, int32_t nodeId, const std::string &pinName)
Gets the next node ID from an execution link.
std::set< int32_t > m_tracedDataNodes
Track data nodes already traced to prevent infinite recursion.
int32_t HandleWhileSimulation(const TaskGraphTemplate &tmpl, int32_t nodeId, LocalBlackboard &blackboard, GraphExecutionTracer &tracer)
Simulates a While loop execution.
void TraceDataConnection(int32_t sourceNodeId, const std::string &sourcePinName, int32_t targetNodeId, const std::string &targetPinName, const TaskGraphTemplate &tmpl, GraphExecutionTracer &tracer, int32_t depth)
Traces evaluation of a single data connection.
std::vector< int32_t > m_pathStack
Current execution path.
std::map< int32_t, int32_t > m_visitCount
Track visits per node to detect loops.
void BuildNodeReachabilityMap(const TaskGraphTemplate &tmpl, std::map< int32_t, bool > &reachable)
bool DetectPotentialInfiniteLoops(const TaskGraphTemplate &tmpl, std::vector< ValidationError > &outErrors)
Checks for potential infinite loops or cycles.
int32_t HandleSequenceSimulation(const TaskGraphTemplate &tmpl, int32_t nodeId, LocalBlackboard &blackboard, GraphExecutionTracer &tracer)
Simulates a Sequence node execution.
std::vector< ValidationError > SimulateExecution(const TaskGraphTemplate &tmpl, const SimulationOptions &options, GraphExecutionTracer &outTracer)
Simulates execution of a graph template.
int32_t SimulateStep(const TaskGraphTemplate &tmpl, int32_t currentNodeId, LocalBlackboard &blackboard, const SimulationOptions &options, GraphExecutionTracer &tracer)
int32_t HandleBranchSimulation(const TaskGraphTemplate &tmpl, int32_t nodeId, LocalBlackboard &blackboard, GraphExecutionTracer &tracer)
Simulates a Branch node execution.
bool ValidateAllBranches(const TaskGraphTemplate &tmpl, std::vector< ValidationError > &outErrors)
Validates all branch nodes in a graph.
int32_t HandleSwitchSimulation(const TaskGraphTemplate &tmpl, int32_t nodeId, LocalBlackboard &blackboard, GraphExecutionTracer &tracer)
Simulates a Switch node execution.
void MarkReachableNodes(const TaskGraphTemplate &tmpl, int32_t nodeId, std::map< int32_t, bool > &reachable)
bool ValidateConditionExpression(int32_t nodeId, const std::string &expression)
Validates a condition expression.
bool ValidateDataConnections(const TaskGraphTemplate &tmpl, std::vector< ValidationError > &outErrors)
Validates all data connections in a graph.
std::vector< int32_t > FindUnreachableNodes(const TaskGraphTemplate &tmpl, std::vector< ValidationError > &outErrors)
Checks for unreachable nodes.
Records execution trace during graph simulation.
Simple map-based blackboard for task graph runtime state.
Immutable, shareable task graph asset.
< Provides AssetID and INVALID_ASSET_ID
@ Literal
Value is embedded directly in the template.
@ AtomicTask
Leaf node that executes a single atomic task.
@ While
Conditional loop (Loop / Completed exec outputs)
@ SubGraph
Sub-graph call (SubTask)
@ DoOnce
Single-fire execution (reset via Reset pin)
@ Delay
Timer (Completed exec output after N seconds)
@ GetBBValue
Data node – reads a Blackboard key.
@ MathOp
Data node – arithmetic operation (+, -, *, /)
@ SetBBValue
Data node – writes a Blackboard key.
@ Switch
Multi-branch on value (N exec outputs)
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
@ Branch
If/Else conditional (Then / Else exec outputs)
@ VSSequence
Execute N outputs in order ("VS" prefix avoids collision with BT Sequence=1)
constexpr int32_t NODE_INDEX_NONE
Sentinel value for "no node" in node index / ID fields.
Configuration options for graph simulation.
Full description of a single node in the task graph.