diff --git a/VIXEN/documentation/GraphArchitecture/Extracted_Operations_Architecture.md b/VIXEN/documentation/GraphArchitecture/Extracted_Operations_Architecture.md new file mode 100644 index 00000000..f67d6ea1 --- /dev/null +++ b/VIXEN/documentation/GraphArchitecture/Extracted_Operations_Architecture.md @@ -0,0 +1,836 @@ +# Extracted Operations: Avoiding Logic Duplication + +## The Core Question + +**Q:** If we create nodes that operate on GraphData (AddNodeToGraphNode, ConnectNodesNode, etc.), and RenderGraph already has methods that do the same things (AddNode(), ConnectNodes()), won't we duplicate all the logic? + +**A:** No - we **extract** the core operations into shared implementations that both RenderGraph methods AND node wrappers can use. + +--- + +## The Architecture: Three Layers + +``` +┌─────────────────────────────────────────────────────────┐ +│ INTERFACE LAYER 1: RenderGraph (Orchestrator API) │ +│ • graph.AddNode(name) │ +│ • graph.ConnectNodes(src, slot, dst, slot) │ +│ • graph.Compile() │ +│ (Convenience API for direct programmatic use) │ +└────────────────────┬────────────────────────────────────┘ + │ calls ↓ +┌─────────────────────────────────────────────────────────┐ +│ CORE OPERATIONS LAYER (Shared Implementation) │ +│ namespace GraphOperations { │ +│ void AddNode(GraphData&, NodeDescriptor); │ +│ void ConnectNodes(GraphData&, ConnectionDescriptor);│ +│ ValidationResult Validate(const GraphData&); │ +│ void Compile(GraphData&, RenderGraph*); │ +│ } │ +│ (Pure logic, no interface dependencies) │ +└────────────────────┬────────────────────────────────────┘ + │ called by ↓ +┌─────────────────────────────────────────────────────────┐ +│ INTERFACE LAYER 2: Operation Nodes (Data Flow API) │ +│ • AddNodeToGraphNode (transforms GraphData) │ +│ • ConnectNodesInGraphNode (transforms GraphData) │ +│ • ValidateGraphNode (validates GraphData) │ +│ (Node-based API for graph-as-data workflows) │ +└─────────────────────────────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ DATA LAYER: GraphData (Pure Data Structures) │ +│ • std::vector nodes │ +│ • std::vector connections │ +│ • metadata, version, etc. │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Detailed Design + +### Layer 1: Pure Data Structures + +```cpp +// VIXEN/libraries/RenderGraph/include/Data/GraphData.h +namespace VIXEN { + +// Pure data - no methods, no logic +struct NodeDescriptor { + std::string typeName; + std::string instanceName; + std::map parameters; + + // Editor metadata (ignored during compilation) + Vec2 position{0, 0}; + Color color{255, 255, 255}; +}; + +struct ConnectionDescriptor { + std::string sourceNode; + uint32_t sourceSlot; + std::string targetNode; + uint32_t targetSlot; + ConnectionType type = ConnectionType::Direct; +}; + +struct GraphData { + std::vector nodes; + std::vector connections; + std::string name; + std::map metadata; +}; + +} // namespace VIXEN +``` + +--- + +### Layer 2: Core Operations (Shared Logic) + +```cpp +// VIXEN/libraries/RenderGraph/include/Operations/GraphOperations.h +namespace VIXEN::GraphOperations { + +// ============================================================================ +// Core operations (single source of truth for graph manipulation logic) +// ============================================================================ + +struct OperationResult { + bool success; + std::string errorMessage; + std::vector warnings; +}; + +// Add node to graph data +OperationResult AddNode( + GraphData& graph, + const NodeDescriptor& node, + const NodeTypeRegistry* registry = nullptr // For validation +) { + // Validation + if (node.instanceName.empty()) { + return OperationResult{false, "Node instance name cannot be empty"}; + } + + // Check for duplicate names + for (const auto& existing : graph.nodes) { + if (existing.instanceName == node.instanceName) { + return OperationResult{false, "Node '" + node.instanceName + "' already exists"}; + } + } + + // Validate node type exists (if registry provided) + if (registry && !registry->IsTypeRegistered(node.typeName)) { + return OperationResult{false, "Unknown node type: " + node.typeName}; + } + + // Add node + graph.nodes.push_back(node); + + return OperationResult{true, ""}; +} + +// Remove node from graph data +OperationResult RemoveNode( + GraphData& graph, + const std::string& instanceName +) { + // Find node + auto nodeIt = std::find_if(graph.nodes.begin(), graph.nodes.end(), + [&](const NodeDescriptor& n) { return n.instanceName == instanceName; }); + + if (nodeIt == graph.nodes.end()) { + return OperationResult{false, "Node '" + instanceName + "' not found"}; + } + + // Remove node + graph.nodes.erase(nodeIt); + + // Remove all connections involving this node + auto& connections = graph.connections; + connections.erase( + std::remove_if(connections.begin(), connections.end(), + [&](const ConnectionDescriptor& c) { + return c.sourceNode == instanceName || c.targetNode == instanceName; + }), + connections.end() + ); + + return OperationResult{true, ""}; +} + +// Connect nodes in graph data +OperationResult ConnectNodes( + GraphData& graph, + const ConnectionDescriptor& connection, + const NodeTypeRegistry* registry = nullptr // For type validation +) { + // Validate source node exists + auto sourceIt = std::find_if(graph.nodes.begin(), graph.nodes.end(), + [&](const NodeDescriptor& n) { return n.instanceName == connection.sourceNode; }); + if (sourceIt == graph.nodes.end()) { + return OperationResult{false, "Source node '" + connection.sourceNode + "' not found"}; + } + + // Validate target node exists + auto targetIt = std::find_if(graph.nodes.begin(), graph.nodes.end(), + [&](const NodeDescriptor& n) { return n.instanceName == connection.targetNode; }); + if (targetIt == graph.nodes.end()) { + return OperationResult{false, "Target node '" + connection.targetNode + "' not found"}; + } + + // Type validation (if registry provided) + if (registry) { + auto validationResult = ValidateConnection(connection, *sourceIt, *targetIt, registry); + if (!validationResult.success) { + return validationResult; + } + } + + // Check for duplicate connections + for (const auto& existing : graph.connections) { + if (existing.sourceNode == connection.sourceNode && + existing.sourceSlot == connection.sourceSlot && + existing.targetNode == connection.targetNode && + existing.targetSlot == connection.targetSlot) { + return OperationResult{false, "Connection already exists"}; + } + } + + // Add connection + graph.connections.push_back(connection); + + return OperationResult{true, ""}; +} + +// Disconnect nodes in graph data +OperationResult DisconnectNodes( + GraphData& graph, + const std::string& sourceNode, + uint32_t sourceSlot, + const std::string& targetNode, + uint32_t targetSlot +) { + auto& connections = graph.connections; + auto it = std::find_if(connections.begin(), connections.end(), + [&](const ConnectionDescriptor& c) { + return c.sourceNode == sourceNode && c.sourceSlot == sourceSlot && + c.targetNode == targetNode && c.targetSlot == targetSlot; + }); + + if (it == connections.end()) { + return OperationResult{false, "Connection not found"}; + } + + connections.erase(it); + return OperationResult{true, ""}; +} + +// Validate graph structure +struct ValidationResult { + bool isValid; + std::vector errors; + std::vector warnings; +}; + +ValidationResult ValidateGraph( + const GraphData& graph, + const NodeTypeRegistry* registry +) { + ValidationResult result{true, {}, {}}; + + // Check for cycles + if (HasCycles(graph)) { + result.isValid = false; + result.errors.push_back("Graph contains cycles"); + } + + // Validate all connections + for (const auto& conn : graph.connections) { + auto sourceIt = std::find_if(graph.nodes.begin(), graph.nodes.end(), + [&](const NodeDescriptor& n) { return n.instanceName == conn.sourceNode; }); + auto targetIt = std::find_if(graph.nodes.begin(), graph.nodes.end(), + [&](const NodeDescriptor& n) { return n.instanceName == conn.targetNode; }); + + if (registry) { + auto connValidation = ValidateConnection(conn, *sourceIt, *targetIt, registry); + if (!connValidation.success) { + result.isValid = false; + result.errors.push_back(connValidation.errorMessage); + } + } + } + + // Check required slots are connected + if (registry) { + for (const auto& node : graph.nodes) { + auto requiredSlots = registry->GetRequiredInputSlots(node.typeName); + for (uint32_t slotIndex : requiredSlots) { + bool connected = std::any_of(graph.connections.begin(), graph.connections.end(), + [&](const ConnectionDescriptor& c) { + return c.targetNode == node.instanceName && c.targetSlot == slotIndex; + }); + + if (!connected) { + result.warnings.push_back( + "Node '" + node.instanceName + "' has unconnected required slot " + + std::to_string(slotIndex) + ); + } + } + } + } + + return result; +} + +// Helper: Check for cycles +bool HasCycles(const GraphData& graph) { + // Build adjacency list + std::map> adjList; + for (const auto& node : graph.nodes) { + adjList[node.instanceName] = {}; + } + for (const auto& conn : graph.connections) { + adjList[conn.sourceNode].push_back(conn.targetNode); + } + + // DFS cycle detection + std::set visited; + std::set recStack; + + std::function hasCycleDFS = [&](const std::string& node) -> bool { + visited.insert(node); + recStack.insert(node); + + for (const auto& neighbor : adjList[node]) { + if (recStack.count(neighbor)) { + return true; // Cycle detected + } + if (!visited.count(neighbor) && hasCycleDFS(neighbor)) { + return true; + } + } + + recStack.erase(node); + return false; + }; + + for (const auto& [nodeName, _] : adjList) { + if (!visited.count(nodeName)) { + if (hasCycleDFS(nodeName)) { + return true; + } + } + } + + return false; +} + +// Compile GraphData into a RenderGraph instance +OperationResult CompileToRenderGraph( + const GraphData& graphData, + RenderGraph& targetGraph, + const NodeTypeRegistry* registry +) { + // Validate first + auto validation = ValidateGraph(graphData, registry); + if (!validation.isValid) { + return OperationResult{false, "Graph validation failed: " + validation.errors[0]}; + } + + // Create node instances + std::map nodeHandles; + for (const auto& nodeDesc : graphData.nodes) { + if (!registry) { + return OperationResult{false, "NodeTypeRegistry required for compilation"}; + } + + // Create node instance using registry factory + NodeInstance* instance = registry->CreateNodeInstance(nodeDesc.typeName); + if (!instance) { + return OperationResult{false, "Failed to create node of type: " + nodeDesc.typeName}; + } + + // Apply parameters + ApplyParameters(instance, nodeDesc.parameters); + + // Add to graph + NodeHandle handle = targetGraph.AddNode(nodeDesc.instanceName, instance); + nodeHandles[nodeDesc.instanceName] = handle; + } + + // Create connections + for (const auto& connDesc : graphData.connections) { + NodeHandle srcHandle = nodeHandles[connDesc.sourceNode]; + NodeHandle dstHandle = nodeHandles[connDesc.targetNode]; + + targetGraph.ConnectNodes(srcHandle, connDesc.sourceSlot, dstHandle, connDesc.targetSlot); + } + + // Compile the graph + try { + targetGraph.Compile(); + } catch (const std::exception& e) { + return OperationResult{false, std::string("Compilation failed: ") + e.what()}; + } + + return OperationResult{true, ""}; +} + +} // namespace VIXEN::GraphOperations +``` + +--- + +### Layer 3: RenderGraph Orchestrator (Uses Core Operations) + +```cpp +// VIXEN/libraries/RenderGraph/include/Core/RenderGraph.h +namespace VIXEN { + +class RenderGraph { +public: + RenderGraph() = default; + + // ======================================================================== + // EXISTING API (unchanged externally, refactored internally) + // ======================================================================== + + template + NodeHandle AddNode(const std::string& instanceName) { + // Create node descriptor + NodeDescriptor nodeDesc; + nodeDesc.typeName = NodeType::GetTypeName(); + nodeDesc.instanceName = instanceName; + + // Use core operation + auto result = GraphOperations::AddNode(graphData_, nodeDesc, registry_); + if (!result.success) { + throw std::runtime_error("AddNode failed: " + result.errorMessage); + } + + // Create actual node instance (runtime state) + auto nodeInstance = std::make_unique>(); + NodeHandle handle = static_cast(nodeInstances_.size()); + nodeInstances_.push_back(std::move(nodeInstance)); + + // Map name to handle + nameToHandle_[instanceName] = handle; + + return handle; + } + + void ConnectNodes( + NodeHandle sourceHandle, uint32_t sourceSlot, + NodeHandle targetHandle, uint32_t targetSlot + ) { + // Get node names from handles + std::string sourceName = GetNodeName(sourceHandle); + std::string targetName = GetNodeName(targetHandle); + + // Create connection descriptor + ConnectionDescriptor connDesc{sourceName, sourceSlot, targetName, targetSlot}; + + // Use core operation + auto result = GraphOperations::ConnectNodes(graphData_, connDesc, registry_); + if (!result.success) { + throw std::runtime_error("ConnectNodes failed: " + result.errorMessage); + } + + // Update runtime state (connections tracked separately for execution) + RuntimeConnection runtimeConn{sourceHandle, sourceSlot, targetHandle, targetSlot}; + runtimeConnections_.push_back(runtimeConn); + } + + void Compile() { + // Validate using core operation + auto validation = GraphOperations::ValidateGraph(graphData_, registry_); + if (!validation.isValid) { + throw std::runtime_error("Graph validation failed: " + validation.errors[0]); + } + + // Perform compilation (existing logic) + AnalyzeDependencies(); + AllocateResources(); + GeneratePipelines(); + BuildExecutionOrder(); + } + + void Execute() { + // Execute nodes in compiled order (existing logic) + for (NodeHandle handle : executionOrder_) { + nodeInstances_[handle]->Execute(); + } + } + + // ======================================================================== + // NEW API: Direct GraphData access + // ======================================================================== + + const GraphData& GetGraphData() const { return graphData_; } + + void LoadFromGraphData(const GraphData& data) { + // Clear existing state + nodeInstances_.clear(); + runtimeConnections_.clear(); + nameToHandle_.clear(); + + // Use core compilation operation + auto result = GraphOperations::CompileToRenderGraph(data, *this, registry_); + if (!result.success) { + throw std::runtime_error("Failed to load graph: " + result.errorMessage); + } + + graphData_ = data; + } + +private: + // Data representation (NEW - extracted from implementation) + GraphData graphData_; + + // Runtime state (EXISTING - unchanged) + std::vector> nodeInstances_; + std::vector runtimeConnections_; + std::map nameToHandle_; + std::vector executionOrder_; + NodeTypeRegistry* registry_ = nullptr; + + // Existing private methods (unchanged) + void AnalyzeDependencies(); + void AllocateResources(); + void GeneratePipelines(); + void BuildExecutionOrder(); + + std::string GetNodeName(NodeHandle handle) const { + for (const auto& [name, h] : nameToHandle_) { + if (h == handle) return name; + } + return ""; + } +}; + +} // namespace VIXEN +``` + +--- + +### Layer 4: Operation Nodes (Also Use Core Operations) + +```cpp +// VIXEN/libraries/RenderGraphEditor/include/Nodes/GraphOperationNodes.h +namespace VIXEN::Editor { + +// ============================================================================ +// AddNodeToGraphNode - Wraps GraphOperations::AddNode +// ============================================================================ + +CONSTEXPR_NODE_CONFIG(AddNodeToGraphNodeConfig, 2, 2) { + INPUT_SLOT(INPUT_GRAPH, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_DESCRIPTOR, NodeDescriptor, 1, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(OUTPUT_GRAPH, GraphData, 0, Required, WriteOnly); + OUTPUT_SLOT(OPERATION_RESULT, GraphOperations::OperationResult, 1, Required, WriteOnly); +}; + +class AddNodeToGraphNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + // Get inputs + GraphData graph = In(ctx); + NodeDescriptor node = In(ctx); + + // Use SAME core operation as RenderGraph::AddNode() + auto result = GraphOperations::AddNode(graph, node, registry_); + + // Output results + Out(ctx) = graph; + Out(ctx) = result; + } + +private: + NodeTypeRegistry* registry_ = nullptr; // Injected +}; + +// ============================================================================ +// ConnectNodesInGraphNode - Wraps GraphOperations::ConnectNodes +// ============================================================================ + +CONSTEXPR_NODE_CONFIG(ConnectNodesInGraphNodeConfig, 2, 2) { + INPUT_SLOT(INPUT_GRAPH, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CONNECTION, ConnectionDescriptor, 1, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(OUTPUT_GRAPH, GraphData, 0, Required, WriteOnly); + OUTPUT_SLOT(OPERATION_RESULT, GraphOperations::OperationResult, 1, Required, WriteOnly); +}; + +class ConnectNodesInGraphNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + GraphData graph = In(ctx); + ConnectionDescriptor conn = In(ctx); + + // Use SAME core operation as RenderGraph::ConnectNodes() + auto result = GraphOperations::ConnectNodes(graph, conn, registry_); + + Out(ctx) = graph; + Out(ctx) = result; + } + +private: + NodeTypeRegistry* registry_ = nullptr; +}; + +// ============================================================================ +// ValidateGraphNode - Wraps GraphOperations::ValidateGraph +// ============================================================================ + +CONSTEXPR_NODE_CONFIG(ValidateGraphNodeConfig, 1, 1) { + INPUT_SLOT(INPUT_GRAPH, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(VALIDATION_RESULT, GraphOperations::ValidationResult, 0, Required, WriteOnly); +}; + +class ValidateGraphNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + const GraphData& graph = In(ctx); + + // Use SAME core operation + auto result = GraphOperations::ValidateGraph(graph, registry_); + + Out(ctx) = result; + } + +private: + NodeTypeRegistry* registry_ = nullptr; +}; + +} // namespace VIXEN::Editor +``` + +--- + +## Key Benefits of This Architecture + +### 1. **Zero Duplication** +- Core logic implemented ONCE in `GraphOperations` namespace +- RenderGraph methods call these operations +- Operation nodes call these operations +- Single source of truth + +### 2. **Backward Compatibility** +- Existing RenderGraph API unchanged +- Existing code continues to work +- `graph.AddNode()` still works exactly as before + +### 3. **Separation of Concerns** + +| Component | Responsibility | +|-----------|----------------| +| **GraphData** | Pure data structures | +| **GraphOperations** | Pure logic (validation, manipulation) | +| **RenderGraph** | Orchestrator + runtime state management | +| **Operation Nodes** | Data flow interface to operations | + +### 4. **Testability** +- Test `GraphOperations` functions independently +- Pure functions: input → output +- No dependencies on RenderGraph or nodes + +### 5. **Flexibility** +- Use RenderGraph directly (programmatic) +- Use operation nodes (graph-based workflows) +- Use GraphOperations directly (custom tools) + +--- + +## Usage Examples + +### Example 1: Traditional RenderGraph Usage (Unchanged) + +```cpp +// Existing code continues to work +RenderGraph graph; + +auto window = graph.AddNode("main_window"); +auto swapChain = graph.AddNode("swapchain"); + +graph.ConnectNodes(window, 0, swapChain, 1); +graph.Compile(); +graph.Execute(); +``` + +Internally, this now uses `GraphOperations`, but externally identical. + +--- + +### Example 2: Direct GraphData Manipulation + +```cpp +// Create graph as data +GraphData graph; + +// Add nodes using core operations +NodeDescriptor windowNode{ + .typeName = "WindowNode", + .instanceName = "main_window" +}; +GraphOperations::AddNode(graph, windowNode); + +NodeDescriptor swapChainNode{ + .typeName = "SwapChainNode", + .instanceName = "swapchain" +}; +GraphOperations::AddNode(graph, swapChainNode); + +// Add connection +ConnectionDescriptor conn{ + .sourceNode = "main_window", + .sourceSlot = 0, + .targetNode = "swapchain", + .targetSlot = 1 +}; +GraphOperations::ConnectNodes(graph, conn); + +// Validate +auto validation = GraphOperations::ValidateGraph(graph, registry); + +// Compile to executable graph +RenderGraph executableGraph; +GraphOperations::CompileToRenderGraph(graph, executableGraph, registry); +executableGraph.Execute(); +``` + +--- + +### Example 3: Node-Based Graph Manipulation + +```cpp +// Editor application graph +RenderGraph editorGraph; + +auto graphStateNode = editorGraph.AddNode("state"); +auto addNodeOpNode = editorGraph.AddNode("add_node_op"); +auto validateNode = editorGraph.AddNode("validate"); + +// Graph state → Add node operation → Validation +editorGraph.ConnectNodes( + graphStateNode, GraphStateNodeConfig::OUTPUT_GRAPH, + addNodeOpNode, AddNodeToGraphNodeConfig::INPUT_GRAPH +); +editorGraph.ConnectNodes( + addNodeOpNode, AddNodeToGraphNodeConfig::OUTPUT_GRAPH, + validateNode, ValidateGraphNodeConfig::INPUT_GRAPH +); + +editorGraph.Compile(); +editorGraph.Execute(); // Processes graph data through operations +``` + +--- + +## What Needs to Be Extracted? + +### From RenderGraph to GraphOperations: + +✅ **Extract:** +- Add node validation logic +- Remove node logic +- Connection validation logic (type checking, nullability, etc.) +- Cycle detection +- Topological sort +- Required slot checking +- Graph serialization/deserialization + +❌ **Keep in RenderGraph:** +- Runtime state management (NodeInstance ownership) +- Resource allocation (VkImage, VkBuffer creation) +- Compilation to Vulkan pipelines +- Execution scheduling +- Frame synchronization + +### Why This Split? + +**GraphOperations** handles **graph structure** (nodes, connections, validation) +**RenderGraph** handles **runtime execution** (resources, pipelines, scheduling) + +Graph structure is **data** (can be serialized, edited, transformed). +Runtime execution is **imperative** (must run, allocate, synchronize). + +--- + +## Migration Path + +### Phase 1: Extract GraphData +- [x] Define `GraphData`, `NodeDescriptor`, `ConnectionDescriptor` +- [ ] Add `graphData_` member to RenderGraph +- [ ] Populate `graphData_` in existing `AddNode()` and `ConnectNodes()` + +### Phase 2: Extract Core Operations +- [ ] Create `GraphOperations` namespace +- [ ] Implement `AddNode()`, `RemoveNode()`, `ConnectNodes()` as pure functions +- [ ] Implement `ValidateGraph()`, `HasCycles()` +- [ ] Implement `CompileToRenderGraph()` + +### Phase 3: Refactor RenderGraph Internals +- [ ] Modify `RenderGraph::AddNode()` to call `GraphOperations::AddNode()` +- [ ] Modify `RenderGraph::ConnectNodes()` to call `GraphOperations::ConnectNodes()` +- [ ] Add `RenderGraph::LoadFromGraphData()` method +- [ ] Test backward compatibility (existing code should work unchanged) + +### Phase 4: Create Operation Nodes +- [ ] Implement `AddNodeToGraphNode` +- [ ] Implement `ConnectNodesInGraphNode` +- [ ] Implement `RemoveNodeFromGraphNode` +- [ ] Implement `ValidateGraphNode` +- [ ] Implement `GraphCompilerNode` + +### Phase 5: Build Editor +- [ ] Create `GraphStateNode` (stores GraphData) +- [ ] Wire operation nodes in editor graph +- [ ] Test round-trip: GraphData → RenderGraph → Execute + +--- + +## Final Architecture Diagram + +``` +User Application (e.g., Editor) + ↓ +RenderGraph::AddNode() ← (Convenience API) + ↓ +GraphOperations::AddNode() ← (Core logic, single implementation) + ↑ +AddNodeToGraphNode::Execute() ← (Data flow API) + ↓ +GraphData (pure data) +``` + +**Key Points:** +1. **GraphOperations** = Single source of truth (no duplication) +2. **RenderGraph** = Orchestrator (uses GraphOperations internally) +3. **Operation Nodes** = Data flow interface (also use GraphOperations) +4. **GraphData** = Pure data (flows through nodes) + +--- + +## Conclusion + +**Q: Do we need node wrappers for every RenderGraph operation?** +**A: Yes, but they don't duplicate logic - they share it.** + +**Q: How do we avoid duplication?** +**A: Extract core operations into `GraphOperations` namespace. Both RenderGraph methods AND operation nodes call these shared functions.** + +**Q: Does RenderGraph become pure data?** +**A: No - RenderGraph stays as orchestrator (manages runtime state). GraphData is the pure data representation.** + +**Q: Who uses what?** +- **Normal applications**: Use RenderGraph API directly (unchanged) +- **Editor application**: Uses operation nodes that transform GraphData +- **Both**: Use shared GraphOperations underneath (zero duplication) + +This architecture: +- ✅ Keeps RenderGraph backward compatible +- ✅ Avoids all logic duplication +- ✅ Enables graph-as-data workflows +- ✅ Provides flexibility (API, nodes, or direct operations) +- ✅ Maintains clear separation of concerns diff --git a/VIXEN/documentation/GraphArchitecture/Graph_As_Data_Refactoring.md b/VIXEN/documentation/GraphArchitecture/Graph_As_Data_Refactoring.md new file mode 100644 index 00000000..8af09f62 --- /dev/null +++ b/VIXEN/documentation/GraphArchitecture/Graph_As_Data_Refactoring.md @@ -0,0 +1,604 @@ +# Graph-as-Data: Transforming RenderGraph from Orchestrator to Data Structure + +## The Key Architectural Insight + +To build a graph editor **as a RenderGraph application**, we need a fundamental shift in perspective: + +**Current Architecture:** +- `RenderGraph` is an **orchestrator** (you call methods on it) +- Nodes are **inside** the graph +- Operations are **methods** on RenderGraph: `AddNode()`, `ConnectNodes()`, `Compile()` + +**New Architecture (Two-Level System):** +- **Level 1 (Runtime)**: Editor application graph - uses RenderGraph as orchestrator +- **Level 2 (Data)**: Graph being edited - represented as **data structures** flowing through Level 1 +- Graph operations become **nodes** that transform graph data + +--- + +## The Fundamental Transformation + +### Before: RenderGraph as Orchestrator + +```cpp +// RenderGraph is the runtime, you operate ON it +RenderGraph renderGraph; + +// Methods modify the orchestrator +auto nodeHandle = renderGraph.AddNode("tex_diffuse"); +renderGraph.ConnectNodes(node1, slot1, node2, slot2); +renderGraph.Compile(); +renderGraph.Execute(); +``` + +**Problem:** Can't represent "add a node" as a node itself - it's a method on the orchestrator. + +--- + +### After: Graph as Data Structure + +```cpp +// GraphData is a data structure (not an orchestrator) +struct GraphData { + std::vector nodes; + std::vector connections; + std::map metadata; +}; + +struct NodeDescriptor { + std::string typeName; + std::string instanceName; + std::map parameters; +}; + +struct ConnectionDescriptor { + std::string sourceNode; + uint32_t sourceSlot; + std::string targetNode; + uint32_t targetSlot; +}; + +// Now "add node" can be a NODE that operates on graph data +class AddNodeToGraphNode : public TypedNode<...> { + void Execute(ExecuteContext& ctx) override { + GraphData graphData = In(ctx); + NodeDescriptor newNode = In(ctx); + + // Pure data transformation + graphData.nodes.push_back(newNode); + + Out(ctx) = graphData; + } +}; +``` + +**Solution:** Graph structure is data, operations on graphs are nodes. + +--- + +## Two-Level Architecture + +### Level 1: Runtime Graph (The Editor Application) + +This is a **running RenderGraph** that implements the editor: + +```cpp +RenderGraph editorGraph; // This is the orchestrator + +// Add editor nodes (these ARE executing) +auto graphStateNode = editorGraph.AddNode("graph_state"); +auto addNodeNode = editorGraph.AddNode("add_node_operation"); +auto nodeRendererNode = editorGraph.AddNode("node_renderer"); + +// Connect them +editorGraph.ConnectNodes(addNodeNode, OUTPUT_GRAPH, graphStateNode, INPUT_GRAPH); + +// Compile and run the editor +editorGraph.Compile(); +editorGraph.Execute(); // Editor is running! +``` + +**This level uses RenderGraph as orchestrator** - normal usage, no changes needed. + +--- + +### Level 2: Data Graph (Being Edited) + +This is **graph data** flowing through the Level 1 nodes: + +```cpp +// Inside GraphStateNode (a Level 1 node) +class GraphStateNode : public TypedNode<...> { + void Execute(ExecuteContext& ctx) override { + // currentGraphData_ is the graph being EDITED (Level 2) + // It's just data, not a running graph + GraphData currentGraphData_; + + // Process commands that modify the data + if (auto* addNodeCmd = GetCommandOfType()) { + currentGraphData_.nodes.push_back({ + .typeName = addNodeCmd->nodeTypeName, + .instanceName = addNodeCmd->instanceName, + .parameters = addNodeCmd->parameters + }); + } + + // Output the graph data (for other nodes to consume) + Out(ctx) = currentGraphData_; + } + +private: + GraphData currentGraphData_; // The graph being edited (data!) +}; +``` + +**This level is pure data** - no execution, no orchestration, just structures. + +--- + +## Required Refactoring: Meta Operations → Data Nodes + +### 1. Graph Manipulation Operations + +**Current (methods on RenderGraph):** +```cpp +NodeHandle AddNode(const std::string& name, NodeInstance* instance); +void ConnectNodes(NodeHandle src, uint32_t srcSlot, NodeHandle dst, uint32_t dstSlot); +void RemoveNode(NodeHandle handle); +void Compile(); +void Execute(); +``` + +**New (data structures + transformation nodes):** + +```cpp +// Data structures +struct GraphData { + std::vector nodes; + std::vector connections; + GraphMetadata metadata; +}; + +// Transformation nodes +class AddNodeToGraphNode { + CONSTEXPR_NODE_CONFIG(AddNodeToGraphNodeConfig, 2, 1) { + INPUT_SLOT(INPUT_GRAPH, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_TO_ADD, NodeDescriptor, 1, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(OUTPUT_GRAPH, GraphData, 0, Required, WriteOnly); + }; + + void Execute(ExecuteContext& ctx) override { + GraphData graph = In(ctx); + NodeDescriptor node = In(ctx); + + graph.nodes.push_back(node); + + Out(ctx) = graph; + } +}; + +class ConnectNodesInGraphNode { + CONSTEXPR_NODE_CONFIG(ConnectNodesInGraphNodeConfig, 2, 1) { + INPUT_SLOT(INPUT_GRAPH, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CONNECTION, ConnectionDescriptor, 1, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(OUTPUT_GRAPH, GraphData, 0, Required, WriteOnly); + }; + + void Execute(ExecuteContext& ctx) override { + GraphData graph = In(ctx); + ConnectionDescriptor conn = In(ctx); + + graph.connections.push_back(conn); + + Out(ctx) = graph; + } +}; + +class RemoveNodeFromGraphNode { + CONSTEXPR_NODE_CONFIG(RemoveNodeFromGraphNodeConfig, 2, 1) { + INPUT_SLOT(INPUT_GRAPH, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_NAME, std::string, 1, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(OUTPUT_GRAPH, GraphData, 0, Required, WriteOnly); + }; + + void Execute(ExecuteContext& ctx) override { + GraphData graph = In(ctx); + std::string nodeName = In(ctx); + + // Remove node and all its connections + auto& nodes = graph.nodes; + nodes.erase( + std::remove_if(nodes.begin(), nodes.end(), + [&](const NodeDescriptor& n) { return n.instanceName == nodeName; }), + nodes.end() + ); + + // Remove connections involving this node + auto& connections = graph.connections; + connections.erase( + std::remove_if(connections.begin(), connections.end(), + [&](const ConnectionDescriptor& c) { + return c.sourceNode == nodeName || c.targetNode == nodeName; + }), + connections.end() + ); + + Out(ctx) = graph; + } +}; +``` + +--- + +### 2. Graph Compilation: Data → Runtime + +The bridge between Level 2 (data) and actual execution is a **compiler node**: + +```cpp +class GraphCompilerNode { + CONSTEXPR_NODE_CONFIG(GraphCompilerNodeConfig, 2, 2) { + INPUT_SLOT(GRAPH_DATA, GraphData, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(COMPILE_TRIGGER, bool, 1, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(COMPILED_GRAPH, std::shared_ptr, 0, Optional, WriteOnly); + OUTPUT_SLOT(COMPILATION_RESULT, CompilationResult, 1, Required, WriteOnly); + }; + + void Execute(ExecuteContext& ctx) override { + if (!In(ctx)) return; + + GraphData graphData = In(ctx); + + // Create a NEW RenderGraph instance (Level 1 orchestrator) + auto compiledGraph = std::make_shared(); + + try { + // Transform data → runtime + std::map nodeHandles; + + // Add nodes + for (const auto& nodeDesc : graphData.nodes) { + NodeInstance* instance = CreateNodeInstance(nodeDesc.typeName); + ApplyParameters(instance, nodeDesc.parameters); + + NodeHandle handle = compiledGraph->AddNode(nodeDesc.instanceName, instance); + nodeHandles[nodeDesc.instanceName] = handle; + } + + // Add connections + for (const auto& connDesc : graphData.connections) { + compiledGraph->ConnectNodes( + nodeHandles[connDesc.sourceNode], connDesc.sourceSlot, + nodeHandles[connDesc.targetNode], connDesc.targetSlot + ); + } + + // Compile the actual graph + compiledGraph->Compile(); + + Out(ctx) = compiledGraph; + Out(ctx) = CompilationResult{.success = true}; + + } catch (const std::exception& e) { + Out(ctx) = CompilationResult{ + .success = false, + .errorMessage = e.what() + }; + } + } + +private: + NodeInstance* CreateNodeInstance(const std::string& typeName); + void ApplyParameters(NodeInstance* instance, const std::map& params); +}; +``` + +**Key insight:** GraphCompilerNode **creates a RenderGraph instance** from data. It transforms Level 2 (data) into a Level 1 (runtime) graph. + +--- + +### 3. Graph State Management + +Instead of RenderGraph being a singleton orchestrator, graph state becomes **data flowing through nodes**: + +```cpp +class GraphStateNode { + CONSTEXPR_NODE_CONFIG(GraphStateNodeConfig, 1, 1) { + INPUT_SLOT(COMMAND_STREAM, CommandBuffer, 0, Required, Execute, ReadOnly, TaskLevel); + OUTPUT_SLOT(GRAPH_DATA, GraphData, 0, Required, WriteOnly); + }; + + void Execute(ExecuteContext& ctx) override { + auto& commands = In(ctx); + + // Process each command (pure data transformations) + for (auto& cmd : commands.commands) { + if (auto* addCmd = dynamic_cast(cmd.get())) { + currentGraphData_.nodes.push_back({ + .typeName = addCmd->nodeTypeName, + .instanceName = addCmd->instanceName, + .parameters = {} + }); + } + else if (auto* connectCmd = dynamic_cast(cmd.get())) { + currentGraphData_.connections.push_back({ + .sourceNode = connectCmd->sourceNode, + .sourceSlot = connectCmd->sourceSlot, + .targetNode = connectCmd->targetNode, + .targetSlot = connectCmd->targetSlot + }); + } + else if (auto* deleteCmd = dynamic_cast(cmd.get())) { + // Remove from data structure + auto& nodes = currentGraphData_.nodes; + nodes.erase( + std::remove_if(nodes.begin(), nodes.end(), + [&](const NodeDescriptor& n) { + return n.instanceName == deleteCmd->nodeName; + }), + nodes.end() + ); + } + } + + Out(ctx) = currentGraphData_; + } + +private: + GraphData currentGraphData_; // Persistent state (the graph being edited) +}; +``` + +--- + +## Migration Strategy: What Needs to Change + +### Core RenderGraph (NO CHANGES) + +The existing RenderGraph orchestrator **remains unchanged**: +- Still used to run applications (including the editor) +- Still manages node lifecycle +- Still compiles and executes +- **Zero changes to existing codebase** + +--- + +### New Additions: Graph Data Structures + +Add lightweight data structures to represent graphs: + +```cpp +// VIXEN/libraries/RenderGraph/include/Data/GraphData.h +#pragma once + +namespace VIXEN { + +struct NodeDescriptor { + std::string typeName; // "WindowNode", "TextureLoaderNode", etc. + std::string instanceName; // "main_window", "tex_diffuse", etc. + std::map parameters; + + // Editor metadata (not part of compiled graph) + Vec2 position; + Color color; + std::map editorMetadata; +}; + +struct ConnectionDescriptor { + std::string sourceNode; + uint32_t sourceSlot; + std::string targetNode; + uint32_t targetSlot; + + // Connection type + enum class Type { + Direct, + Array, + Constant, + Variadic, + FieldExtraction + } type = Type::Direct; + + // Type-specific data + std::optional arrayIndex; + std::optional fieldPath; + std::optional constantValue; +}; + +struct GraphData { + std::vector nodes; + std::vector connections; + + // Graph metadata + std::string name; + std::string description; + uint32_t version = 1; + std::map metadata; + + // Serialization + std::string ToJSON() const; + static GraphData FromJSON(const std::string& json); +}; + +} // namespace VIXEN +``` + +--- + +### New Additions: Graph Operation Nodes + +Create nodes that operate on `GraphData`: + +```cpp +// VIXEN/libraries/RenderGraphEditor/include/Nodes/GraphOperationNodes.h + +namespace VIXEN::Editor { + +// Add node operation +class AddNodeToGraphNode; +CONSTEXPR_NODE_CONFIG(AddNodeToGraphNodeConfig, 2, 1) { ... }; + +// Connect nodes operation +class ConnectNodesInGraphNode; +CONSTEXPR_NODE_CONFIG(ConnectNodesInGraphNodeConfig, 2, 1) { ... }; + +// Remove node operation +class RemoveNodeFromGraphNode; +CONSTEXPR_NODE_CONFIG(RemoveNodeFromGraphNodeConfig, 2, 1) { ... }; + +// Validate graph operation +class ValidateGraphNode; +CONSTEXPR_NODE_CONFIG(ValidateGraphNodeConfig, 1, 1) { ... }; + +// Compile graph operation +class GraphCompilerNode; +CONSTEXPR_NODE_CONFIG(GraphCompilerNodeConfig, 2, 2) { ... }; + +// Serialize graph operation +class SerializeGraphNode; +CONSTEXPR_NODE_CONFIG(SerializeGraphNodeConfig, 1, 1) { ... }; + +// Deserialize graph operation +class DeserializeGraphNode; +CONSTEXPR_NODE_CONFIG(DeserializeGraphNodeConfig, 1, 1) { ... }; + +} // namespace VIXEN::Editor +``` + +--- + +## Complete Example: Editor Graph Operating on Graph Data + +```cpp +// The editor application (Level 1: Runtime) +RenderGraph editorGraph; + +// ==== Model Nodes (manage graph data) ==== +auto graphStateNode = editorGraph.AddNode("graph_state"); + +// ==== Controller Nodes (process user input) ==== +auto inputNode = editorGraph.AddNode("input"); +auto commandNode = editorGraph.AddNode("commands"); + +// ==== View Nodes (render graph data) ==== +auto nodeRendererNode = editorGraph.AddNode("node_renderer"); + +// ==== Utility Nodes (compile graph data) ==== +auto compilerNode = editorGraph.AddNode("compiler"); + +// Connections +ConnectionBatch batch(editorGraph); + +// Input → Commands → GraphState +batch.Connect(inputNode, InputHandlerNodeConfig::OUTPUT_EVENTS, + commandNode, CommandProcessorNodeConfig::INPUT_EVENTS); +batch.Connect(commandNode, CommandProcessorNodeConfig::OUTPUT_COMMAND_STREAM, + graphStateNode, GraphStateNodeConfig::INPUT_COMMAND_STREAM); + +// GraphState → Renderer (renders the graph being edited) +batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_GRAPH_DATA, + nodeRendererNode, NodeRendererNodeConfig::INPUT_GRAPH_DATA); + +// GraphState → Compiler (compiles graph data to executable graph) +batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_GRAPH_DATA, + compilerNode, GraphCompilerNodeConfig::INPUT_GRAPH_DATA); + +batch.RegisterAll(); + +// Run the editor +editorGraph.Compile(); + +while (running) { + editorGraph.Execute(); + + // The GRAPH_DATA flowing through graphStateNode represents + // the graph being edited (Level 2) + + // When user clicks "Run", compilerNode transforms GRAPH_DATA + // into an actual RenderGraph instance that can execute +} +``` + +--- + +## Key Transformations Summary + +| Aspect | Before (Orchestrator) | After (Data + Nodes) | +|--------|----------------------|---------------------| +| **Graph Representation** | `RenderGraph` instance | `GraphData` struct | +| **Add Node** | `graph.AddNode(name)` | `AddNodeToGraphNode` processes `GraphData` | +| **Connect** | `graph.ConnectNodes(...)` | `ConnectNodesInGraphNode` processes `GraphData` | +| **Compile** | `graph.Compile()` | `GraphCompilerNode` transforms `GraphData` → `RenderGraph*` | +| **Validation** | Method on graph | `ValidateGraphNode` processes `GraphData` | +| **Serialization** | External serializer | `SerializeGraphNode` processes `GraphData` | +| **State** | Managed by `RenderGraph` | Flows through nodes as data | + +--- + +## Benefits of This Refactoring + +### 1. **Pure Data Transformations** +- Graph operations become functional (input graph → output graph) +- No side effects on global state +- Easier to reason about, test, and debug + +### 2. **Composable Operations** +- Chain graph operations as node graphs +- Example: `AddNode → ValidateGraph → SerializeGraph` + +### 3. **Undo/Redo for Free** +- Every operation produces a new `GraphData` +- Store old `GraphData` for undo +- No need to reverse operations + +### 4. **Multiple Graphs** +- Editor can work on multiple graphs simultaneously +- Each `GraphData` is independent +- No singleton orchestrator limiting you to one graph + +### 5. **Self-Hosting** +- Editor graph (Level 1) can load and modify itself +- Load editor's own graph data, modify it, compile it, run it +- True meta-circular design + +### 6. **Clear Separation** +- **RenderGraph**: Orchestrator for running graphs (unchanged) +- **GraphData**: Data structures representing graphs (new) +- **Editor Nodes**: Operations on GraphData (new) + +--- + +## Migration Checklist + +- [ ] Define `GraphData`, `NodeDescriptor`, `ConnectionDescriptor` structs +- [ ] Implement serialization: `GraphData::ToJSON()`, `GraphData::FromJSON()` +- [ ] Create graph operation nodes: + - [ ] `AddNodeToGraphNode` + - [ ] `RemoveNodeFromGraphNode` + - [ ] `ConnectNodesInGraphNode` + - [ ] `DisconnectNodesInGraphNode` + - [ ] `ValidateGraphNode` +- [ ] Create compilation node: + - [ ] `GraphCompilerNode` (GraphData → RenderGraph*) +- [ ] Create state management node: + - [ ] `GraphStateNode` (stores current GraphData) +- [ ] Create serialization nodes: + - [ ] `SerializeGraphNode` (GraphData → JSON) + - [ ] `DeserializeGraphNode` (JSON → GraphData) +- [ ] Build editor application using these nodes +- [ ] Test meta-circular editing (editor edits itself) + +--- + +## Conclusion + +**The key insight:** To build a graph editor as a graph, we need to treat **graphs as data** that flows through nodes, rather than as orchestrators that contain nodes. + +This requires: +1. **GraphData** - Lightweight data structures representing graphs +2. **Operation Nodes** - Nodes that transform GraphData (add, remove, connect, etc.) +3. **Compiler Node** - Bridge from GraphData to RenderGraph instance +4. **Two-Level Architecture** - Runtime graph (editor) operates on data graph (being edited) + +**RenderGraph itself doesn't change** - it remains the orchestrator for running applications. We just add the ability to represent graphs as data and operate on them with nodes. + +This is the refactoring needed to make the graph editor meta-circular and self-hosting. diff --git a/VIXEN/documentation/GraphArchitecture/Graph_Editor_As_Application_Graph.md b/VIXEN/documentation/GraphArchitecture/Graph_Editor_As_Application_Graph.md new file mode 100644 index 00000000..2ebe3f2f --- /dev/null +++ b/VIXEN/documentation/GraphArchitecture/Graph_Editor_As_Application_Graph.md @@ -0,0 +1,981 @@ +# Graph Editor as a RenderGraph Application + +## Core Insight + +The MCP (Model-View-Controller) pattern is **only needed for the graph editor application**, not for the core RenderGraph framework itself. More importantly, the graph editor application should be **built using RenderGraph nodes**, creating a meta-circular design where the framework is used to build tools for editing itself. + +--- + +## Architecture Philosophy + +``` +┌─────────────────────────────────────────────────────────────┐ +│ RenderGraph Framework (Core - Unchanged) │ +│ • NodeInstance, RenderGraph, TypedNode, Connections │ +│ • Resource management, Compilation, Execution │ +│ • All existing 45+ node types │ +└─────────────────────────────────────────────────────────────┘ + ↑ Uses +┌─────────────────────────────────────────────────────────────┐ +│ Graph Editor Application (Built with RenderGraph) │ +│ │ +│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐│ +│ │ Model Nodes │ │ View Nodes │ │ Control Nodes ││ +│ │ (Data) │ │ (Rendering) │ │ (Processing) ││ +│ ├────────────────┤ ├────────────────┤ ├────────────────┤│ +│ │ GraphStateNode │→ │ NodeRenderer │← │ InputHandler ││ +│ │ NodeRegistryN. │→ │ ConnectionRend.│← │ ValidationNode ││ +│ │ SelectionNode │→ │ UIRenderer │← │ CommandNode ││ +│ └────────────────┘ └────────────────┘ └────────────────┘│ +└─────────────────────────────────────────────────────────────┘ +``` + +**Key Principles:** + +1. **Core RenderGraph** - Pure data flow framework (no MCP, no GUI dependencies) +2. **Editor Application** - Composed from RenderGraph nodes +3. **Meta-Circular** - Use the framework to build tools for itself +4. **Separation** - MCP concerns only in editor nodes, not in core + +--- + +## Editor Application Node Types + +### Category 1: Model Nodes (Data Storage) + +These nodes store and manage the state of the graph being edited. + +#### GraphStateNode + +**Purpose:** Stores the current graph being edited + +```cpp +CONSTEXPR_NODE_CONFIG(GraphStateNodeConfig, 2, 5) { + // Inputs + INPUT_SLOT(COMMAND_STREAM, CommandBuffer, 0, Optional, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(SERIALIZED_DATA, std::string, 1, Optional, Dependency, ReadOnly, NodeLevel); + + // Outputs + OUTPUT_SLOT(NODE_LIST, std::vector, 0, Required, ReadOnly); + OUTPUT_SLOT(CONNECTION_LIST, std::vector, 1, Required, ReadOnly); + OUTPUT_SLOT(SELECTION_STATE, SelectionState, 2, Required, ReadOnly); + OUTPUT_SLOT(GRAPH_DIRTY, bool, 3, Required, ReadOnly); + OUTPUT_SLOT(VALIDATION_RESULT, ValidationResult, 4, Required, ReadOnly); +}; + +struct EditorNodeData { + std::string instanceName; + std::string nodeTypeName; + Vec2 position; + Color color; + std::map parameters; +}; + +struct EditorConnectionData { + std::string sourceNode; + uint32_t sourceSlot; + std::string targetNode; + uint32_t targetSlot; +}; + +class GraphStateNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + // Process incoming commands (add node, delete node, etc.) + if (auto* commandBuffer = In(ctx)) { + ProcessCommands(*commandBuffer); + } + + // Update outputs + Out(ctx) = currentNodes_; + Out(ctx) = currentConnections_; + Out(ctx) = selectionState_; + Out(ctx) = isDirty_; + } + +private: + std::vector currentNodes_; + std::vector currentConnections_; + SelectionState selectionState_; + bool isDirty_ = false; + + void ProcessCommands(const CommandBuffer& commands); +}; +``` + +--- + +#### NodeRegistryNode + +**Purpose:** Stores metadata about available node types (the "palette") + +```cpp +CONSTEXPR_NODE_CONFIG(NodeRegistryNodeConfig, 0, 2) { + // No inputs (static data) + + // Outputs + OUTPUT_SLOT(NODE_TYPE_DESCRIPTORS, std::vector, 0, Required, ReadOnly); + OUTPUT_SLOT(CATEGORY_MAP, std::map>, 1, Required, ReadOnly); +}; + +class NodeRegistryNode : public TypedNode { + void Compile(CompileContext& ctx) override { + // Load all registered node types + LoadNodeTypeDescriptors(); + + Out(ctx) = descriptors_; + Out(ctx) = categoryMap_; + } + +private: + std::vector descriptors_; + std::map> categoryMap_; + + void LoadNodeTypeDescriptors(); +}; +``` + +--- + +#### SelectionNode + +**Purpose:** Manages node/connection selection state + +```cpp +CONSTEXPR_NODE_CONFIG(SelectionNodeConfig, 2, 3) { + // Inputs + INPUT_SLOT(MOUSE_EVENTS, MouseEventStream, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_BOUNDS, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(SELECTED_NODES, std::vector, 0, Required, ReadOnly); + OUTPUT_SLOT(SELECTED_CONNECTIONS, std::vector, 1, Required, ReadOnly); + OUTPUT_SLOT(SELECTION_BOX, std::optional, 2, Required, ReadOnly); +}; + +class SelectionNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& mouseEvents = In(ctx); + auto& nodeBounds = In(ctx); + + // Process mouse events for selection + for (const auto& event : mouseEvents) { + if (event.type == MouseEventType::Click) { + HandleClickSelection(event.position, nodeBounds); + } else if (event.type == MouseEventType::DragStart) { + BeginBoxSelection(event.position); + } else if (event.type == MouseEventType::Drag) { + UpdateBoxSelection(event.position); + } + } + + Out(ctx) = selectedNodes_; + Out(ctx) = selectedConnections_; + Out(ctx) = currentSelectionBox_; + } + +private: + std::vector selectedNodes_; + std::vector selectedConnections_; + std::optional currentSelectionBox_; +}; +``` + +--- + +### Category 2: View Nodes (Rendering) + +These nodes handle visual representation of the graph editor. + +#### NodeRendererNode + +**Purpose:** Renders nodes visually on the canvas + +```cpp +CONSTEXPR_NODE_CONFIG(NodeRendererNodeConfig, 5, 1) { + // Inputs + INPUT_SLOT(NODE_LIST, std::vector, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(SELECTED_NODES, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CAMERA_TRANSFORM, Mat3, 2, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(RENDER_TARGET, VkImage, 3, Required, Execute, ReadWrite, TaskLevel); + INPUT_SLOT(COMMAND_BUFFER, VkCommandBuffer, 4, Required, Execute, ReadWrite, TaskLevel); + + // Outputs + OUTPUT_SLOT(NODE_BOUNDS, std::vector, 0, Required, ReadOnly); +}; + +class NodeRendererNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& nodeList = In(ctx); + auto& selectedNodes = In(ctx); + auto& cameraTransform = In(ctx); + auto* commandBuffer = In(ctx); + + std::vector bounds; + + for (size_t i = 0; i < nodeList.size(); ++i) { + const auto& nodeData = nodeList[i]; + + // Transform to screen space + Vec2 screenPos = TransformPoint(cameraTransform, nodeData.position); + + // Determine visual state + bool isSelected = std::find(selectedNodes.begin(), selectedNodes.end(), i) + != selectedNodes.end(); + + // Render node + RenderNodeVisual(commandBuffer, nodeData, screenPos, isSelected); + + // Store bounds for hit testing + bounds.push_back(Rect(screenPos, screenPos + Vec2(200, 150))); + } + + Out(ctx) = bounds; + } + +private: + void RenderNodeVisual(VkCommandBuffer* cmd, const EditorNodeData& node, + Vec2 position, bool selected); +}; +``` + +--- + +#### ConnectionRendererNode + +**Purpose:** Renders connection wires + +```cpp +CONSTEXPR_NODE_CONFIG(ConnectionRendererNodeConfig, 5, 0) { + // Inputs + INPUT_SLOT(CONNECTION_LIST, std::vector, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_LIST, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(SELECTED_CONNECTIONS, std::vector, 2, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CAMERA_TRANSFORM, Mat3, 3, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(COMMAND_BUFFER, VkCommandBuffer, 4, Required, Execute, ReadWrite, TaskLevel); + + // No outputs (pure rendering) +}; + +class ConnectionRendererNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& connections = In(ctx); + auto& nodes = In(ctx); + auto& selectedConnections = In(ctx); + auto& cameraTransform = In(ctx); + auto* commandBuffer = In(ctx); + + for (size_t i = 0; i < connections.size(); ++i) { + const auto& conn = connections[i]; + + // Find source and target node positions + Vec2 sourcePos = FindNodePosition(nodes, conn.sourceNode); + Vec2 targetPos = FindNodePosition(nodes, conn.targetNode); + + // Transform to screen space + sourcePos = TransformPoint(cameraTransform, sourcePos); + targetPos = TransformPoint(cameraTransform, targetPos); + + // Determine visual state + bool isSelected = std::find(selectedConnections.begin(), + selectedConnections.end(), i) + != selectedConnections.end(); + + // Render Bezier curve + RenderBezierWire(commandBuffer, sourcePos, targetPos, isSelected); + } + } + +private: + void RenderBezierWire(VkCommandBuffer* cmd, Vec2 start, Vec2 end, bool selected); +}; +``` + +--- + +#### UIRendererNode + +**Purpose:** Renders UI panels (toolbar, inspector, node palette) + +```cpp +CONSTEXPR_NODE_CONFIG(UIRendererNodeConfig, 4, 0) { + // Inputs + INPUT_SLOT(SELECTED_NODES, std::vector, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_REGISTRY, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(RENDER_TARGET, VkImage, 2, Required, Execute, ReadWrite, TaskLevel); + INPUT_SLOT(COMMAND_BUFFER, VkCommandBuffer, 3, Required, Execute, ReadWrite, TaskLevel); +}; + +class UIRendererNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& selectedNodes = In(ctx); + auto& nodeRegistry = In(ctx); + auto* commandBuffer = In(ctx); + + // Render toolbar + RenderToolbar(commandBuffer); + + // Render node palette + RenderNodePalette(commandBuffer, nodeRegistry); + + // Render inspector (if node selected) + if (!selectedNodes.empty()) { + RenderInspector(commandBuffer, selectedNodes[0]); + } + + // Render status bar + RenderStatusBar(commandBuffer); + } + +private: + void RenderToolbar(VkCommandBuffer* cmd); + void RenderNodePalette(VkCommandBuffer* cmd, const std::vector& registry); + void RenderInspector(VkCommandBuffer* cmd, uint32_t selectedNodeIndex); + void RenderStatusBar(VkCommandBuffer* cmd); +}; +``` + +--- + +### Category 3: Controller Nodes (Input & Processing) + +These nodes handle user input and orchestrate operations. + +#### InputHandlerNode + +**Purpose:** Processes mouse and keyboard input + +```cpp +CONSTEXPR_NODE_CONFIG(InputHandlerNodeConfig, 2, 3) { + // Inputs + INPUT_SLOT(WINDOW_HANDLE, void*, 0, Required, Execute, ReadOnly, NodeLevel); + INPUT_SLOT(CAMERA_TRANSFORM, Mat3, 1, Required, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(MOUSE_EVENTS, MouseEventStream, 0, Required, ReadOnly); + OUTPUT_SLOT(KEYBOARD_EVENTS, KeyboardEventStream, 1, Required, ReadOnly); + OUTPUT_SLOT(CAMERA_DELTA, CameraDelta, 2, Required, ReadOnly); +}; + +struct MouseEventStream { + std::vector events; +}; + +struct KeyboardEventStream { + std::vector events; +}; + +struct CameraDelta { + Vec2 panDelta; + float zoomDelta; +}; + +class InputHandlerNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + // Poll input from window + MouseEventStream mouseEvents; + KeyboardEventStream keyboardEvents; + CameraDelta cameraDelta; + + PollInputEvents(In(ctx), mouseEvents, keyboardEvents); + + // Process camera controls + ProcessCameraInput(keyboardEvents, mouseEvents, cameraDelta); + + Out(ctx) = mouseEvents; + Out(ctx) = keyboardEvents; + Out(ctx) = cameraDelta; + } + +private: + void PollInputEvents(void* windowHandle, MouseEventStream& mouse, KeyboardEventStream& keyboard); + void ProcessCameraInput(const KeyboardEventStream& keys, const MouseEventStream& mouse, + CameraDelta& delta); +}; +``` + +--- + +#### ValidationNode + +**Purpose:** Validates graph structure and connections + +```cpp +CONSTEXPR_NODE_CONFIG(ValidationNodeConfig, 3, 1) { + // Inputs + INPUT_SLOT(NODE_LIST, std::vector, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CONNECTION_LIST, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(NODE_REGISTRY, std::vector, 2, Required, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(VALIDATION_RESULT, ValidationResult, 0, Required, ReadOnly); +}; + +struct ValidationResult { + bool isValid; + std::vector errors; + std::vector warnings; +}; + +class ValidationNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& nodes = In(ctx); + auto& connections = In(ctx); + auto& registry = In(ctx); + + ValidationResult result; + + // Check for cycles + if (HasCycles(nodes, connections)) { + result.errors.push_back({"Graph contains cycles"}); + } + + // Validate connection types + for (const auto& conn : connections) { + if (!AreTypesCompatible(conn, nodes, registry)) { + result.errors.push_back({ + "Type mismatch: " + conn.sourceNode + " -> " + conn.targetNode + }); + } + } + + // Check required slots + for (const auto& node : nodes) { + if (!AllRequiredSlotsConnected(node, connections, registry)) { + result.warnings.push_back({ + "Node '" + node.instanceName + "' has unconnected required slots" + }); + } + } + + result.isValid = result.errors.empty(); + Out(ctx) = result; + } + +private: + bool HasCycles(const std::vector& nodes, + const std::vector& connections); + bool AreTypesCompatible(const EditorConnectionData& conn, + const std::vector& nodes, + const std::vector& registry); +}; +``` + +--- + +#### CommandProcessorNode + +**Purpose:** Processes user commands (create node, delete node, connect, etc.) + +```cpp +CONSTEXPR_NODE_CONFIG(CommandProcessorNodeConfig, 3, 1) { + // Inputs + INPUT_SLOT(MOUSE_EVENTS, MouseEventStream, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(KEYBOARD_EVENTS, KeyboardEventStream, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(UI_INTERACTIONS, UIInteractionStream, 2, Required, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(COMMAND_STREAM, CommandBuffer, 0, Required, ReadOnly); +}; + +struct CommandBuffer { + std::vector> commands; +}; + +class CommandProcessorNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& mouseEvents = In(ctx); + auto& keyboardEvents = In(ctx); + auto& uiInteractions = In(ctx); + + CommandBuffer commandBuffer; + + // Process drag-and-drop (create node) + for (const auto& interaction : uiInteractions.events) { + if (interaction.type == UIInteractionType::NodePaletteDrag) { + commandBuffer.commands.push_back( + std::make_unique( + interaction.nodeTypeName, + interaction.dropPosition + ) + ); + } + } + + // Process delete key (delete selected) + for (const auto& keyEvent : keyboardEvents.events) { + if (keyEvent.key == Key::Delete && keyEvent.action == KeyAction::Press) { + commandBuffer.commands.push_back( + std::make_unique() + ); + } + } + + // Process connection drag + ProcessConnectionDrag(mouseEvents, commandBuffer); + + Out(ctx) = commandBuffer; + } + +private: + void ProcessConnectionDrag(const MouseEventStream& mouse, CommandBuffer& buffer); +}; +``` + +--- + +#### UndoRedoNode + +**Purpose:** Manages command history for undo/redo + +```cpp +CONSTEXPR_NODE_CONFIG(UndoRedoNodeConfig, 2, 1) { + // Inputs + INPUT_SLOT(COMMAND_STREAM, CommandBuffer, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(KEYBOARD_EVENTS, KeyboardEventStream, 1, Required, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(PROCESSED_COMMANDS, CommandBuffer, 0, Required, ReadOnly); +}; + +class UndoRedoNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& incomingCommands = In(ctx); + auto& keyboardEvents = In(ctx); + + CommandBuffer outputCommands; + + // Check for undo/redo hotkeys + for (const auto& keyEvent : keyboardEvents.events) { + if (keyEvent.key == Key::Z && keyEvent.modifiers & Modifier::Ctrl) { + if (keyEvent.modifiers & Modifier::Shift) { + Redo(outputCommands); + } else { + Undo(outputCommands); + } + } + } + + // Process incoming commands + for (auto& command : incomingCommands.commands) { + commandHistory_.push_back(std::move(command)); + historyIndex_ = commandHistory_.size(); + } + + // Forward commands (or undo/redo results) + Out(ctx) = std::move(outputCommands.commands.empty() + ? incomingCommands + : outputCommands); + } + +private: + std::vector> commandHistory_; + size_t historyIndex_ = 0; + + void Undo(CommandBuffer& output); + void Redo(CommandBuffer& output); +}; +``` + +--- + +### Category 4: Compilation & Execution Nodes + +#### GraphCompilerNode + +**Purpose:** Compiles the edited graph into an actual RenderGraph + +```cpp +CONSTEXPR_NODE_CONFIG(GraphCompilerNodeConfig, 3, 2) { + // Inputs + INPUT_SLOT(NODE_LIST, std::vector, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CONNECTION_LIST, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(COMPILE_TRIGGER, bool, 2, Required, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(COMPILED_GRAPH, RenderGraph*, 0, Optional, WriteOnly); + OUTPUT_SLOT(COMPILATION_RESULT, CompilationResult, 1, Required, ReadOnly); +}; + +class GraphCompilerNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + if (!In(ctx)) { + return; // No compilation requested + } + + auto& nodes = In(ctx); + auto& connections = In(ctx); + + // Create new RenderGraph + auto graph = std::make_unique(); + + CompilationResult result; + + try { + // Add nodes to graph + for (const auto& nodeData : nodes) { + AddNodeToGraph(*graph, nodeData); + } + + // Add connections + for (const auto& connData : connections) { + AddConnectionToGraph(*graph, connData); + } + + // Compile the graph + graph->Compile(); + + result.success = true; + Out(ctx) = graph.release(); + + } catch (const std::exception& e) { + result.success = false; + result.errorMessage = e.what(); + } + + Out(ctx) = result; + } + +private: + void AddNodeToGraph(RenderGraph& graph, const EditorNodeData& nodeData); + void AddConnectionToGraph(RenderGraph& graph, const EditorConnectionData& connData); +}; +``` + +--- + +### Category 5: Serialization Nodes + +#### GraphSerializerNode + +**Purpose:** Saves/loads graphs to/from files + +```cpp +CONSTEXPR_NODE_CONFIG(GraphSerializerNodeConfig, 3, 1) { + // Inputs + INPUT_SLOT(NODE_LIST, std::vector, 0, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(CONNECTION_LIST, std::vector, 1, Required, Execute, ReadOnly, TaskLevel); + INPUT_SLOT(SAVE_PATH, std::filesystem::path, 2, Optional, Execute, ReadOnly, TaskLevel); + + // Outputs + OUTPUT_SLOT(SERIALIZED_DATA, std::string, 0, Required, ReadOnly); +}; + +class GraphSerializerNode : public TypedNode { + void Execute(ExecuteContext& ctx) override { + auto& nodes = In(ctx); + auto& connections = In(ctx); + + // Serialize to JSON + nlohmann::json j; + j["version"] = 1; + j["nodes"] = SerializeNodes(nodes); + j["connections"] = SerializeConnections(connections); + + std::string serialized = j.dump(2); + + // Save to file if path provided + if (auto* savePath = In(ctx)) { + std::ofstream file(*savePath); + file << serialized; + } + + Out(ctx) = serialized; + } + +private: + nlohmann::json SerializeNodes(const std::vector& nodes); + nlohmann::json SerializeConnections(const std::vector& connections); +}; +``` + +--- + +## Complete Graph Editor Application Graph + +Now let's compose these nodes into the actual editor application: + +```cpp +// GraphEditorApplication.cpp +class GraphEditorApplication { +public: + void InitializeEditorGraph() { + RenderGraph editorGraph; + + // ==================================================================== + // RESOURCE NODES + // ==================================================================== + auto windowNode = editorGraph.AddNode("editor_window"); + auto deviceNode = editorGraph.AddNode("editor_device"); + auto swapChainNode = editorGraph.AddNode("editor_swapchain"); + + // ==================================================================== + // MODEL NODES (Data Storage) + // ==================================================================== + auto graphStateNode = editorGraph.AddNode("graph_state"); + auto nodeRegistryNode = editorGraph.AddNode("node_registry"); + auto selectionNode = editorGraph.AddNode("selection"); + auto cameraNode = editorGraph.AddNode("editor_camera"); + + // ==================================================================== + // CONTROLLER NODES (Input & Processing) + // ==================================================================== + auto inputHandlerNode = editorGraph.AddNode("input_handler"); + auto commandProcessorNode = editorGraph.AddNode("command_processor"); + auto undoRedoNode = editorGraph.AddNode("undo_redo"); + auto validationNode = editorGraph.AddNode("validation"); + + // ==================================================================== + // VIEW NODES (Rendering) + // ==================================================================== + auto nodeRendererNode = editorGraph.AddNode("node_renderer"); + auto connectionRendererNode = editorGraph.AddNode("connection_renderer"); + auto uiRendererNode = editorGraph.AddNode("ui_renderer"); + + // ==================================================================== + // SERIALIZATION NODES + // ==================================================================== + auto serializerNode = editorGraph.AddNode("serializer"); + + // ==================================================================== + // COMPILATION NODE + // ==================================================================== + auto compilerNode = editorGraph.AddNode("graph_compiler"); + + // ==================================================================== + // CONNECTIONS + // ==================================================================== + ConnectionBatch batch(editorGraph); + + // Window → SwapChain + batch.Connect(windowNode, WindowNodeConfig::OUTPUT_SURFACE, + swapChainNode, SwapChainNodeConfig::INPUT_SURFACE); + + // InputHandler → Camera updates + batch.Connect(inputHandlerNode, InputHandlerNodeConfig::OUTPUT_CAMERA_DELTA, + cameraNode, CameraNodeConfig::INPUT_DELTA); + + // InputHandler → Selection + batch.Connect(inputHandlerNode, InputHandlerNodeConfig::OUTPUT_MOUSE_EVENTS, + selectionNode, SelectionNodeConfig::INPUT_MOUSE_EVENTS); + + // InputHandler → CommandProcessor + batch.Connect(inputHandlerNode, InputHandlerNodeConfig::OUTPUT_MOUSE_EVENTS, + commandProcessorNode, CommandProcessorNodeConfig::INPUT_MOUSE_EVENTS); + batch.Connect(inputHandlerNode, InputHandlerNodeConfig::OUTPUT_KEYBOARD_EVENTS, + commandProcessorNode, CommandProcessorNodeConfig::INPUT_KEYBOARD_EVENTS); + + // CommandProcessor → UndoRedo → GraphState + batch.Connect(commandProcessorNode, CommandProcessorNodeConfig::OUTPUT_COMMAND_STREAM, + undoRedoNode, UndoRedoNodeConfig::INPUT_COMMAND_STREAM); + batch.Connect(undoRedoNode, UndoRedoNodeConfig::OUTPUT_PROCESSED_COMMANDS, + graphStateNode, GraphStateNodeConfig::INPUT_COMMAND_STREAM); + + // GraphState → Validation + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_NODE_LIST, + validationNode, ValidationNodeConfig::INPUT_NODE_LIST); + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_CONNECTION_LIST, + validationNode, ValidationNodeConfig::INPUT_CONNECTION_LIST); + + // GraphState + Camera → NodeRenderer + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_NODE_LIST, + nodeRendererNode, NodeRendererNodeConfig::INPUT_NODE_LIST); + batch.Connect(cameraNode, CameraNodeConfig::OUTPUT_VIEW_MATRIX, + nodeRendererNode, NodeRendererNodeConfig::INPUT_CAMERA_TRANSFORM); + + // GraphState + Camera → ConnectionRenderer + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_CONNECTION_LIST, + connectionRendererNode, ConnectionRendererNodeConfig::INPUT_CONNECTION_LIST); + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_NODE_LIST, + connectionRendererNode, ConnectionRendererNodeConfig::INPUT_NODE_LIST); + batch.Connect(cameraNode, CameraNodeConfig::OUTPUT_VIEW_MATRIX, + connectionRendererNode, ConnectionRendererNodeConfig::INPUT_CAMERA_TRANSFORM); + + // Selection → Renderers + batch.Connect(selectionNode, SelectionNodeConfig::OUTPUT_SELECTED_NODES, + nodeRendererNode, NodeRendererNodeConfig::INPUT_SELECTED_NODES); + batch.Connect(selectionNode, SelectionNodeConfig::OUTPUT_SELECTED_CONNECTIONS, + connectionRendererNode, ConnectionRendererNodeConfig::INPUT_SELECTED_CONNECTIONS); + + // NodeRegistry → UIRenderer + batch.Connect(nodeRegistryNode, NodeRegistryNodeConfig::OUTPUT_NODE_TYPE_DESCRIPTORS, + uiRendererNode, UIRendererNodeConfig::INPUT_NODE_REGISTRY); + + // GraphState → Serializer + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_NODE_LIST, + serializerNode, GraphSerializerNodeConfig::INPUT_NODE_LIST); + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_CONNECTION_LIST, + serializerNode, GraphSerializerNodeConfig::INPUT_CONNECTION_LIST); + + // GraphState → Compiler + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_NODE_LIST, + compilerNode, GraphCompilerNodeConfig::INPUT_NODE_LIST); + batch.Connect(graphStateNode, GraphStateNodeConfig::OUTPUT_CONNECTION_LIST, + compilerNode, GraphCompilerNodeConfig::INPUT_CONNECTION_LIST); + + batch.RegisterAll(); + + // ==================================================================== + // COMPILE & RUN + // ==================================================================== + editorGraph.Compile(); + + // Main loop + while (!ShouldClose(windowNode)) { + editorGraph.Execute(); + } + } +}; +``` + +--- + +## Data Flow Example: User Creates a Node + +``` +1. User drags "TextureLoaderNode" from palette to canvas + ↓ +2. UIRendererNode detects drag → emits UIInteraction event + ↓ +3. CommandProcessorNode receives UIInteraction + → Creates CreateNodeCommand("TextureLoaderNode", position) + → Outputs to COMMAND_STREAM + ↓ +4. UndoRedoNode receives command + → Adds to history + → Forwards to PROCESSED_COMMANDS + ↓ +5. GraphStateNode receives command + → Adds EditorNodeData to currentNodes_ + → Outputs updated NODE_LIST + ↓ +6. NodeRendererNode receives updated NODE_LIST + → Renders new node on next Execute() + ↓ +7. ValidationNode receives updated NODE_LIST + → Validates (may show warning if slots unconnected) + ↓ +User sees new node on canvas! +``` + +--- + +## Benefits of This Approach + +### 1. **Meta-Circular Design** +- The editor is built using the framework it edits +- Self-hosting: Can edit the editor's own graph +- Dogfooding: Using your own framework exposes design issues + +### 2. **No Framework Pollution** +- Core RenderGraph remains pure (no GUI dependencies) +- MCP concerns isolated to editor application nodes +- Separation of concerns at node granularity + +### 3. **Composability** +- Editor features are nodes that can be mixed and matched +- Want a read-only viewer? Remove CommandProcessorNode +- Want a headless compiler? Remove all View nodes +- Want a validation service? Use only ValidationNode + +### 4. **Testability** +- Each node is independently testable +- Mock input nodes for unit testing +- Validate output nodes for correctness + +### 5. **Extensibility** +- Add new features by adding nodes +- Example: Add ProfilingNode to track performance +- Example: Add CollaborationNode for multi-user editing + +### 6. **Performance** +- Leverages RenderGraph's compilation and execution optimizations +- Parallel execution of independent nodes +- Resource management handled by existing framework + +### 7. **Consistency** +- Same mental model as the graphs you're editing +- Same debugging tools work on the editor +- Same profiling tools work on the editor + +--- + +## Comparison: Traditional MCP vs. Node-Based Editor + +### Traditional MCP Architecture: +``` +GraphModel ← GraphController → GraphEditorView + ↕ ↕ ↕ +NodeModel ← NodeController → NodeView + ↕ ↕ ↕ + ... ... ... +``` +- **Pros**: Well-understood pattern, clear separation +- **Cons**: Separate framework, not self-hosting, framework pollution + +### Node-Based Editor Architecture: +``` +RenderGraph (Editor Application) + ├─ GraphStateNode (Model) + ├─ CommandProcessorNode (Controller) + ├─ NodeRendererNode (View) + ├─ ValidationNode (Controller) + └─ ... + +RenderGraph (Being Edited) + ├─ WindowNode + ├─ TextureLoaderNode + └─ ... +``` +- **Pros**: Self-hosting, no pollution, composable, same tools +- **Cons**: More initial design work, need good editor node library + +--- + +## Implementation Strategy + +### Phase 1: Core Editor Nodes +1. Implement `GraphStateNode` (model data storage) +2. Implement `NodeRegistryNode` (node type metadata) +3. Implement basic `InputHandlerNode` +4. Implement simple `CommandProcessorNode` + +### Phase 2: Basic Rendering +1. Implement `NodeRendererNode` (simple box rendering) +2. Implement `ConnectionRendererNode` (straight lines) +3. Implement `CameraNode` (pan/zoom) + +### Phase 3: Interaction +1. Implement `SelectionNode` +2. Extend `CommandProcessorNode` (create, delete, connect) +3. Implement `UndoRedoNode` + +### Phase 4: Validation & Compilation +1. Implement `ValidationNode` +2. Implement `GraphCompilerNode` +3. Implement execution preview + +### Phase 5: Persistence +1. Implement `GraphSerializerNode` +2. Implement `GraphDeserializerNode` +3. Add save/load UI + +### Phase 6: Advanced Features +1. Implement `UIRendererNode` (inspector panel) +2. Add node palette +3. Add performance profiling +4. Add multi-graph support + +--- + +## Conclusion + +By building the graph editor **as a RenderGraph application**, we achieve: + +✅ **Pure Framework** - RenderGraph stays clean (no MVC/GUI dependencies) +✅ **Self-Hosting** - Use the framework to build tools for itself +✅ **MVC via Nodes** - Model/View/Controller as node categories +✅ **Composability** - Mix and match editor features as nodes +✅ **Same Tools** - Debug/profile the editor like any other graph +✅ **Extensibility** - Add features by adding nodes + +This is the most elegant approach: **the graph editor is itself a graph**. diff --git a/VIXEN/documentation/GraphArchitecture/MCP_Architecture_Design.md b/VIXEN/documentation/GraphArchitecture/MCP_Architecture_Design.md new file mode 100644 index 00000000..976dc489 --- /dev/null +++ b/VIXEN/documentation/GraphArchitecture/MCP_Architecture_Design.md @@ -0,0 +1,1388 @@ +# MCP Architecture for VIXEN RenderGraph GUI Editor + +## Executive Summary + +This document outlines the transformation of the VIXEN RenderGraph system into a Model-View-Controller (MVC) architecture suitable for a GUI graph editor application. The design separates concerns into three layers while preserving the existing compile-time type safety and runtime efficiency. + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VIEW LAYER │ +│ (GUI Graph Editor - User Interaction & Visualization) │ +├─────────────────────────────────────────────────────────────┤ +│ • GraphEditorView - Canvas, panning, zooming │ +│ • NodeView - Visual node representation │ +│ • ConnectionView - Visual edge/wire representation │ +│ • SlotView - Input/output port visualization │ +│ • InspectorView - Node property panel │ +│ • ToolbarView - Node palette, actions │ +└─────────────────────────────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────────────────────────────┐ +│ CONTROLLER LAYER │ +│ (Orchestration - User Actions → Model Updates) │ +├─────────────────────────────────────────────────────────────┤ +│ • GraphController - Graph CRUD operations │ +│ • NodeController - Node lifecycle management │ +│ • ConnectionController - Connection validation/create │ +│ • ResourceController - Resource allocation/tracking │ +│ • CompilationController - Graph compilation workflow │ +│ • ExecutionController - Graph execution management │ +│ • SerializationController - Save/load graph state │ +│ • ValidationController - Real-time type checking │ +└─────────────────────────────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────────────────────────────┐ +│ MODEL LAYER │ +│ (Core Data - Graph Structure & Resources) │ +├─────────────────────────────────────────────────────────────┤ +│ • GraphModel - RenderGraph state wrapper │ +│ • NodeModel - NodeInstance + metadata wrapper │ +│ • ConnectionModel - Edge data + validation state │ +│ • ResourceModel - Resource ownership & lifetime │ +│ • ConfigModel - Node configuration schemas │ +│ • TopologyModel - Graph structure & analysis │ +│ • ExecutionModel - Compiled execution plan │ +└─────────────────────────────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────────────────────────────┐ +│ EXISTING CORE │ +│ (VIXEN RenderGraph - No Changes) │ +├─────────────────────────────────────────────────────────────┤ +│ • RenderGraph, NodeInstance, NodeType, GraphTopology │ +│ • Resource, ResourceSlot, ConnectionBatch │ +│ • All Node Implementations (45+ types) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Layer 1: MODEL LAYER (Data & Business Logic) + +### Purpose +- **Single Source of Truth** for graph state +- Wraps existing RenderGraph core with observable properties +- Handles resource ownership and lifetime management +- Provides type-safe interfaces to underlying graph + +### Key Components + +#### 1.1 GraphModel +```cpp +class GraphModel { + // Core graph reference + std::unique_ptr renderGraph; + + // Observable state + Observable state; // Building, Compiling, Executing, Failed + Observable> nodes; + Observable> connections; + Observable lastCompilationResult; + + // Metadata + std::string graphName; + UUID graphId; + GraphMetadata metadata; + + // Lifecycle management + GraphState GetState() const; + bool IsDirty() const; + void MarkDirty(); + + // Query interface + NodeModel* FindNode(const std::string& name) const; + std::vector GetConnectionsForNode(NodeHandle) const; + TopologyModel GetTopology() const; + + // Validation + ValidationResult Validate() const; + bool CanCompile() const; +}; +``` + +**Key Responsibilities:** +- Owns the RenderGraph instance +- Maintains node and connection registries +- Tracks graph state transitions +- Provides query interface for topology analysis +- Emits change notifications for View updates + +--- + +#### 1.2 NodeModel +```cpp +class NodeModel { + // Core node reference + NodeInstance* nodeInstance; // Non-owning (RenderGraph owns) + + // Identity + NodeHandle handle; + std::string instanceName; + std::string nodeTypeName; + + // Configuration + const NodeConfig* config; // Compile-time config schema + std::map parameters; + + // Slot metadata + std::vector inputSlots; + std::vector outputSlots; + + // Visual metadata (for GUI) + Vec2 position; + Vec2 size; + Color color; + bool collapsed; + std::string category; // "Resource", "Pipeline", "Rendering", etc. + + // State + Observable state; // Unconnected, Ready, Compiled, Error + Observable> errors; + Observable> warnings; + + // Query interface + const SlotModel& GetInputSlot(uint32_t index) const; + const SlotModel& GetOutputSlot(uint32_t index) const; + bool IsSlotConnected(uint32_t index, bool isInput) const; + std::vector GetConnections(uint32_t slotIndex, bool isInput) const; + + // Validation + bool CanConnect(uint32_t outputSlot, NodeModel* target, uint32_t inputSlot) const; +}; +``` + +**Key Responsibilities:** +- Wraps NodeInstance with GUI-specific metadata +- Stores visual properties (position, size, color) +- Maintains slot connection state +- Provides type-safe connection validation +- Tracks compilation errors per-node + +--- + +#### 1.3 SlotModel +```cpp +struct SlotModel { + uint32_t index; + std::string name; + std::string typeName; // "VkImage", "VkBuffer", etc. + + // From ResourceSlot + SlotNullability nullability; + SlotRole role; + SlotMutability mutability; + SlotScope scope; + + // Connection state + bool isConnected; + ConnectionModel* connection; // For inputs (single connection) + std::vector connections; // For outputs (multi-connection) + + // Visual hints + Color typeColor; // Color-code by type + std::string tooltip; + + // Validation + bool IsCompatibleWith(const SlotModel& other) const; +}; +``` + +**Key Responsibilities:** +- Encapsulates slot metadata from compile-time config +- Tracks connection state per-slot +- Provides type compatibility checking +- Stores visual hints for rendering + +--- + +#### 1.4 ConnectionModel +```cpp +class ConnectionModel { + UUID connectionId; + + // Source + NodeModel* sourceNode; + uint32_t sourceSlotIndex; + + // Target + NodeModel* targetNode; + uint32_t targetSlotIndex; + + // Connection type + ConnectionType type; // Direct, Array, Constant, Variadic, FieldExtraction + + // Array connection metadata + std::optional targetArrayIndex; + + // Field extraction metadata + std::optional fieldPath; // e.g., "transform.position.x" + + // Constant connection metadata + std::optional constantValue; + + // Validation state + Observable isValid; + Observable errorMessage; + + // Visual state + Color wireColor; + float wireThickness; + bool isSelected; + + // Lifecycle + bool IsRegistered() const; + void Validate(); +}; +``` + +**Key Responsibilities:** +- Represents a single edge in the graph +- Stores source and target node/slot references +- Tracks connection type and metadata +- Validates connection legality +- Stores visual properties for rendering + +--- + +#### 1.5 ResourceModel +```cpp +class ResourceModel { + Resource* resource; // Non-owning (RenderGraph owns) + + // Identity + UUID resourceId; + std::string resourceName; + ResourceType type; + + // Ownership + NodeModel* owner; // Node that created this resource + std::vector consumers; // Nodes that read this resource + + // Lifetime + ResourceLifetime lifetime; // Transient or Persistent + bool isAllocated; + + // Usage tracking + std::set usageFlags; + + // Dependencies + std::vector dependsOn; + std::vector requiredBy; + + // Visual representation + std::string displayName; + std::string description; + + // Query interface + size_t GetMemoryUsage() const; + bool IsShared() const; + std::vector GetAllDependents() const; +}; +``` + +**Key Responsibilities:** +- Wraps Resource with ownership tracking +- Maintains producer/consumer relationships +- Tracks resource dependencies for cleanup ordering +- Provides memory usage metrics + +--- + +#### 1.6 ConfigModel (Registry) +```cpp +class ConfigModelRegistry { + // Registered node types + std::map nodeTypes; + + struct NodeTypeDescriptor { + std::string typeName; + std::string category; + std::string description; + std::vector inputs; + std::vector outputs; + std::function factoryFunction; + + // Visual hints + Color defaultColor; + std::string iconPath; + }; + + struct SlotDescriptor { + std::string name; + std::string typeName; + SlotNullability nullability; + SlotRole role; + SlotMutability mutability; + SlotScope scope; + }; + + // Registration + void RegisterNodeType(const NodeTypeDescriptor& descriptor); + + // Query + const NodeTypeDescriptor* GetNodeType(const std::string& typeName) const; + std::vector GetAllNodeTypes() const; + std::vector GetNodeTypesByCategory(const std::string& category) const; + + // Validation + bool IsTypeRegistered(const std::string& typeName) const; +}; +``` + +**Key Responsibilities:** +- Central registry of all node types +- Provides factory functions for node creation +- Stores compile-time configuration as runtime metadata +- Enables dynamic node palette generation + +--- + +#### 1.7 TopologyModel +```cpp +class TopologyModel { + GraphTopology* topology; // Non-owning + + // Cached analysis results + std::vector rootNodes; + std::vector executionOrder; + std::map> dependencyMap; + + // Validation + bool hasCycles; + std::vector> cycles; + std::vector unreachableNodes; + + // Query interface + bool IsReachable(NodeModel* from, NodeModel* to) const; + std::vector GetDependencies(NodeModel* node) const; + std::vector GetDependents(NodeModel* node) const; + int GetDepth(NodeModel* node) const; // Distance from root + + // Analysis + void Analyze(); + std::vector FindCriticalPath() const; + std::map GetStatistics() const; +}; +``` + +**Key Responsibilities:** +- Analyzes graph structure +- Detects cycles and connectivity issues +- Provides execution order preview +- Enables graph visualization algorithms (layering, routing) + +--- + +#### 1.8 ExecutionModel +```cpp +class ExecutionModel { + // Compilation state + Observable state; + Observable> errors; + Observable> warnings; + + struct CompilationError { + NodeModel* node; + std::string message; + ErrorSeverity severity; + }; + + // Execution state + Observable executionState; + Observable frameCount; + Observable lastFrameTime; + + // Performance tracking + std::map nodeMetrics; + + struct PerformanceMetrics { + double avgExecutionTime; + double maxExecutionTime; + uint64_t executionCount; + }; + + // Control + void StartExecution(); + void PauseExecution(); + void StopExecution(); + void StepFrame(); + + // Query + const std::vector& GetErrors() const; + bool CanExecute() const; +}; +``` + +**Key Responsibilities:** +- Tracks compilation and execution state +- Collects performance metrics per-node +- Provides execution control (play/pause/step) +- Stores validation results + +--- + +## Layer 2: CONTROLLER LAYER (Orchestration) + +### Purpose +- Translates user actions into model updates +- Enforces business rules and constraints +- Coordinates multi-step operations +- Maintains graph invariants + +### Key Components + +#### 2.1 GraphController +```cpp +class GraphController { + GraphModel* model; + + // Commands (undoable) + CommandResult CreateGraph(const std::string& name); + CommandResult LoadGraph(const std::filesystem::path& path); + CommandResult SaveGraph(const std::filesystem::path& path); + + // Lifecycle + void Compile(); + void Execute(); + void Cleanup(); + void Reset(); + + // State management + void BeginBatch(); // Suspend change notifications + void EndBatch(); // Emit batched changes + void MarkDirty(); + + // Undo/Redo + void Undo(); + void Redo(); + bool CanUndo() const; + bool CanRedo() const; + + // Events + Event onStateChanged; + Event onCompilationFinished; + Event onValidationFinished; +}; +``` + +--- + +#### 2.2 NodeController +```cpp +class NodeController { + GraphModel* graphModel; + ConfigModelRegistry* registry; + + // Node operations (undoable commands) + CommandResult CreateNode(const std::string& nodeTypeName, const std::string& instanceName); + CommandResult DeleteNode(NodeHandle handle); + CommandResult DuplicateNode(NodeHandle handle); + CommandResult RenameNode(NodeHandle handle, const std::string& newName); + + // Node properties + CommandResult SetNodePosition(NodeHandle handle, Vec2 position); + CommandResult SetNodeParameter(NodeHandle handle, const std::string& paramName, const std::any& value); + + // Bulk operations + CommandResult DeleteNodes(const std::vector& handles); + CommandResult MoveNodes(const std::vector& handles, Vec2 delta); + + // Validation + ValidationResult ValidateNodeName(const std::string& name) const; + bool CanDeleteNode(NodeHandle handle) const; + + // Events + Event onNodeCreated; + Event onNodeDeleted; + Event onNodeModified; +}; +``` + +--- + +#### 2.3 ConnectionController +```cpp +class ConnectionController { + GraphModel* graphModel; + ValidationController* validator; + + // Connection operations (undoable) + CommandResult CreateConnection( + NodeHandle source, uint32_t sourceSlot, + NodeHandle target, uint32_t targetSlot + ); + CommandResult CreateArrayConnection( + NodeHandle source, uint32_t sourceSlot, + NodeHandle target, uint32_t targetSlot, uint32_t arrayIndex + ); + CommandResult CreateConstantConnection( + NodeHandle target, uint32_t targetSlot, + const std::any& constantValue + ); + CommandResult DeleteConnection(UUID connectionId); + + // Batch operations + CommandResult CreateConnectionBatch(const std::vector& connections); + CommandResult DeleteConnectionsForNode(NodeHandle handle); + + // Validation (real-time) + ConnectionValidation ValidateConnection( + NodeHandle source, uint32_t sourceSlot, + NodeHandle target, uint32_t targetSlot + ) const; + + struct ConnectionValidation { + bool isValid; + std::string errorMessage; + CompatibilityReason reason; + }; + + // Wire routing + std::vector ComputeWirePath(ConnectionModel* connection) const; + + // Events + Event onConnectionCreated; + Event onConnectionDeleted; + Event onConnectionValidated; +}; +``` + +--- + +#### 2.4 ResourceController +```cpp +class ResourceController { + GraphModel* graphModel; + + // Resource tracking + void OnCompilationStarted(); + void OnCompilationFinished(); + void OnResourceAllocated(Resource* resource, NodeModel* owner); + void OnResourceDeallocated(Resource* resource); + + // Query + std::vector GetAllResources() const; + std::vector GetResourcesOwnedBy(NodeHandle handle) const; + std::vector GetResourcesConsumedBy(NodeHandle handle) const; + size_t GetTotalMemoryUsage() const; + + // Dependency analysis + std::vector GetCleanupOrder() const; + std::vector GetResourceDependents(ResourceModel* resource) const; + + // Visualization + ResourceDependencyGraph BuildDependencyGraph() const; +}; +``` + +--- + +#### 2.5 CompilationController +```cpp +class CompilationController { + GraphModel* graphModel; + ValidationController* validator; + + // Compilation workflow + void StartCompilation(); + void CancelCompilation(); + + // Steps + void ValidateGraph(); + void AnalyzeTopology(); + void AllocateResources(); + void CompileNodes(); + void BuildExecutionOrder(); + + // Incremental compilation + void RecompileDirtyNodes(const std::vector& dirtyNodes); + void PartialCleanup(const std::vector& nodesToRemove); + + // Progress tracking + Observable progress; + + struct CompilationProgress { + CompilationPhase phase; + uint32_t currentNode; + uint32_t totalNodes; + std::string statusMessage; + }; + + // Events + Event onPhaseChanged; + Event onCompilationFinished; + Event onErrorOccurred; +}; +``` + +--- + +#### 2.6 ExecutionController +```cpp +class ExecutionController { + GraphModel* graphModel; + ExecutionModel* executionModel; + + // Execution control + void Play(); + void Pause(); + void Stop(); + void Step(); // Execute one frame + + // Performance tracking + void BeginNodeExecution(NodeHandle handle); + void EndNodeExecution(NodeHandle handle); + void RecordFrameTime(double deltaTime); + + // Profiling + void EnableProfiling(bool enabled); + PerformanceReport GeneratePerformanceReport() const; + + struct PerformanceReport { + std::map nodeMetrics; + double avgFrameTime; + double minFrameTime; + double maxFrameTime; + uint64_t totalFrames; + }; + + // Events + Event onExecutionStateChanged; + Event onFrameCompleted; +}; +``` + +--- + +#### 2.7 SerializationController +```cpp +class SerializationController { + GraphModel* graphModel; + + // Serialization formats + enum class Format { + JSON, + Binary, + YAML + }; + + // Save/Load + Result SaveGraph(const std::filesystem::path& path, Format format = Format::JSON); + Result LoadGraph(const std::filesystem::path& path); + + // Export/Import + Result ExportToJSON(std::ostream& stream); + Result ImportFromJSON(std::istream& stream); + + // Clipboard operations + std::string SerializeSelection(const std::vector& nodes); + Result DeserializeAndPaste(const std::string& serialized, Vec2 offset); + + // Versioning + uint32_t GetFormatVersion() const; + bool CanLoadVersion(uint32_t version) const; + + // Schema + struct GraphSchema { + std::string name; + UUID id; + std::vector nodes; + std::vector connections; + }; +}; +``` + +--- + +#### 2.8 ValidationController +```cpp +class ValidationController { + ConfigModelRegistry* registry; + + // Real-time validation + ValidationResult ValidateGraph(const GraphModel* graph) const; + ValidationResult ValidateNode(const NodeModel* node) const; + ValidationResult ValidateConnection(const ConnectionModel* connection) const; + + struct ValidationResult { + bool isValid; + std::vector errors; + std::vector warnings; + }; + + // Type checking + bool AreTypesCompatible(const std::string& sourceType, const std::string& targetType) const; + bool CanConnectSlots(const SlotModel& source, const SlotModel& target) const; + + // Constraint checking + bool CheckNullability(const ConnectionModel* connection) const; + bool CheckMutability(const SlotModel& slot, const ConnectionModel* connection) const; + bool CheckScope(const SlotModel& slot) const; + + // Graph-level validation + bool HasCycles(const GraphModel* graph) const; + bool AllRequiredSlotsConnected(const NodeModel* node) const; + std::vector FindUnreachableNodes(const GraphModel* graph) const; +}; +``` + +--- + +## Layer 3: VIEW LAYER (GUI Presentation) + +### Purpose +- Renders the graph visually +- Handles user input (mouse, keyboard) +- Provides interactive editing experience +- Observes model changes and updates UI + +### Key Components + +#### 3.1 GraphEditorView +```cpp +class GraphEditorView : public Widget { + GraphController* graphController; + GraphModel* graphModel; + + // Child views + std::vector nodeViews; + std::vector connectionViews; + CanvasView* canvas; + + // Camera/Transform + Vec2 cameraPosition; + float zoomLevel; + Mat3 worldToScreen; + + // Rendering + void Render() override; + void RenderGrid(); + void RenderNodes(); + void RenderConnections(); + void RenderSelectionBox(); + + // Input handling + void OnMouseDown(MouseEvent event) override; + void OnMouseMove(MouseEvent event) override; + void OnMouseUp(MouseEvent event) override; + void OnKeyPress(KeyEvent event) override; + void OnScroll(ScrollEvent event) override; + + // Interaction modes + enum class Mode { + Select, + Pan, + CreateConnection, + BoxSelect + }; + Mode currentMode; + + // Selection + std::set selectedNodes; + std::set selectedConnections; + + void SelectNode(NodeModel* node, bool additive = false); + void DeselectAll(); + void DeleteSelection(); + + // Connection creation + SlotModel* connectionStartSlot; + Vec2 connectionDragPosition; + + // Camera control + void Pan(Vec2 delta); + void Zoom(float delta, Vec2 focus); + void FrameAll(); + void FrameSelection(); + + // Model observation + void OnNodeAdded(NodeModel* node); + void OnNodeRemoved(NodeModel* node); + void OnConnectionAdded(ConnectionModel* connection); + void OnConnectionRemoved(ConnectionModel* connection); +}; +``` + +--- + +#### 3.2 NodeView +```cpp +class NodeView : public Widget { + NodeModel* model; + NodeController* controller; + + // Visual state + Vec2 position; // Synced with model + Vec2 size; + Color backgroundColor; + bool isSelected; + bool isHovered; + + // Child views + std::vector inputSlots; + std::vector outputSlots; + HeaderView* header; + BodyView* body; + + // Rendering + void Render() override; + void RenderFrame(); + void RenderHeader(); + void RenderBody(); + void RenderSlots(); + void RenderErrorBadge(); + + // Layout + void UpdateLayout(); + Rect GetBounds() const; + + // Input handling + void OnMouseDown(MouseEvent event) override; + void OnMouseMove(MouseEvent event) override; + void OnDragStart(DragEvent event); + void OnDrag(DragEvent event); + void OnDragEnd(DragEvent event); + + // State + void SetSelected(bool selected); + void SetHovered(bool hovered); + void UpdateFromModel(); + + // Slot lookup + SlotView* GetSlotAtPosition(Vec2 position) const; + SlotView* GetInputSlot(uint32_t index) const; + SlotView* GetOutputSlot(uint32_t index) const; +}; +``` + +--- + +#### 3.3 ConnectionView +```cpp +class ConnectionView : public Widget { + ConnectionModel* model; + ConnectionController* controller; + + // Visual state + Color wireColor; + float wireThickness; + bool isSelected; + bool isHovered; + + // Layout + Vec2 startPosition; // Computed from source slot + Vec2 endPosition; // Computed from target slot + std::vector controlPoints; // Bezier curve + + // Rendering + void Render() override; + void RenderWire(); + void RenderArrow(); + void RenderSelectionHighlight(); + + // Wire routing + void UpdatePath(); + std::vector ComputeBezierPath() const; + std::vector ComputeOrthogonalPath() const; + + // Interaction + bool HitTest(Vec2 position, float tolerance = 5.0f) const; + void OnMouseDown(MouseEvent event) override; + void OnMouseEnter() override; + void OnMouseLeave() override; + + // State + void SetSelected(bool selected); + void UpdateFromModel(); +}; +``` + +--- + +#### 3.4 SlotView +```cpp +class SlotView : public Widget { + SlotModel* model; + NodeView* parentNode; + + // Visual state + Vec2 position; // Relative to parent node + float radius; + Color color; // Type-based color + bool isHovered; + bool isConnected; + + // Rendering + void Render() override; + void RenderCircle(); + void RenderLabel(); + void RenderTooltip(); + void RenderTypeIndicator(); + + // Layout + Vec2 GetWorldPosition() const; + Rect GetBounds() const; + + // Interaction + void OnMouseEnter() override; + void OnMouseLeave() override; + void OnMouseDown(MouseEvent event) override; + void OnDragStart(DragEvent event); // Begin connection creation + + // State + void UpdateFromModel(); + bool CanAcceptConnection(const SlotModel& other) const; + void ShowCompatibilityIndicator(bool compatible); +}; +``` + +--- + +#### 3.5 InspectorView +```cpp +class InspectorView : public Panel { + NodeModel* selectedNode; + ValidationController* validator; + + // Sections + PropertyGridView* properties; + SlotListView* inputs; + SlotListView* outputs; + ErrorListView* errors; + + // Rendering + void Render() override; + void RenderNodeInfo(); + void RenderProperties(); + void RenderSlots(); + void RenderErrors(); + + // Property editing + void OnPropertyChanged(const std::string& propertyName, const std::any& value); + void OnParameterChanged(const std::string& paramName, const std::any& value); + + // State + void SetSelectedNode(NodeModel* node); + void ClearSelection(); + void UpdateFromModel(); +}; +``` + +--- + +#### 3.6 ToolbarView +```cpp +class ToolbarView : public Panel { + GraphController* graphController; + ConfigModelRegistry* registry; + + // Sections + NodePaletteView* nodePalette; + ActionBarView* actionBar; + + // Node palette + void RenderNodePalette(); + void RenderNodeCategory(const std::string& category); + void RenderNodeButton(const NodeTypeDescriptor& descriptor); + + // Actions + void OnCompileClicked(); + void OnExecuteClicked(); + void OnStopClicked(); + void OnSaveClicked(); + void OnLoadClicked(); + + // Search + void RenderSearchBox(); + void FilterNodes(const std::string& searchQuery); + std::vector filteredNodes; + + // Drag-and-drop + void OnNodeDragStart(const NodeTypeDescriptor& descriptor); +}; +``` + +--- + +## Data Flow Examples + +### Example 1: User Creates a Node + +``` +[USER] Drags "TextureLoaderNode" from palette to canvas + ↓ +[VIEW] ToolbarView detects drag-and-drop + ↓ +[VIEW] GraphEditorView.OnDrop(nodeTypeName="TextureLoaderNode", position=Vec2(100, 200)) + ↓ +[CONTROLLER] NodeController.CreateNode("TextureLoaderNode", "texture_diffuse_1") + ↓ +[CONTROLLER] Validates name uniqueness + ↓ +[MODEL] GraphModel.AddNode(nodeModel) + ↓ +[MODEL] GraphModel.nodes.Notify(NodeAdded) + ↓ +[VIEW] GraphEditorView.OnNodeAdded(nodeModel) + ↓ +[VIEW] Creates NodeView and adds to canvas + ↓ +[VIEW] Renders updated graph +``` + +--- + +### Example 2: User Connects Two Nodes + +``` +[USER] Drags from output slot of Node A + ↓ +[VIEW] SlotView.OnDragStart(sourceSlot) + ↓ +[VIEW] GraphEditorView enters "CreateConnection" mode + ↓ +[VIEW] Renders temporary wire following cursor + ↓ +[USER] Drops on input slot of Node B + ↓ +[VIEW] SlotView.OnDrop(targetSlot) + ↓ +[CONTROLLER] ConnectionController.CreateConnection(nodeA, slot0, nodeB, slot1) + ↓ +[CONTROLLER] ValidationController.ValidateConnection(...) + ↓ +[CONTROLLER] If valid: Create ConnectionModel + ↓ +[MODEL] GraphModel.AddConnection(connectionModel) + ↓ +[MODEL] GraphModel.MarkDirty() + ↓ +[MODEL] GraphModel.connections.Notify(ConnectionAdded) + ↓ +[VIEW] GraphEditorView.OnConnectionAdded(connectionModel) + ↓ +[VIEW] Creates ConnectionView and renders wire +``` + +--- + +### Example 3: User Compiles Graph + +``` +[USER] Clicks "Compile" button + ↓ +[VIEW] ToolbarView.OnCompileClicked() + ↓ +[CONTROLLER] CompilationController.StartCompilation() + ↓ +[CONTROLLER] CompilationController.ValidateGraph() + ↓ +[CONTROLLER] ValidationController returns errors (if any) + ↓ +[CONTROLLER] If valid: CompilationController.AnalyzeTopology() + ↓ +[MODEL] TopologyModel analyzes graph structure + ↓ +[CONTROLLER] CompilationController.AllocateResources() + ↓ +[MODEL] ResourceController tracks resource creation + ↓ +[CONTROLLER] CompilationController.CompileNodes() + ↓ +[MODEL] RenderGraph.Compile() called + ↓ +[MODEL] Each NodeInstance.Compile() executed + ↓ +[MODEL] ExecutionModel updated with results + ↓ +[MODEL] ExecutionModel.state.Notify(CompilationFinished) + ↓ +[VIEW] ToolbarView shows "Compiled Successfully" indicator + ↓ +[VIEW] NodeViews update visual state (errors highlighted) +``` + +--- + +## Implementation Strategy + +### Phase 1: Model Layer Foundation +1. Create `GraphModel` wrapping `RenderGraph` +2. Implement `NodeModel` with observable properties +3. Implement `ConnectionModel` with validation +4. Build `ConfigModelRegistry` from existing node configs +5. Create `ResourceModel` tracking system + +**Deliverables:** +- `RenderGraphModel.h/cpp` +- `NodeModel.h/cpp` +- `ConnectionModel.h/cpp` +- `ConfigModelRegistry.h/cpp` +- `ResourceModel.h/cpp` +- Unit tests for each model class + +--- + +### Phase 2: Controller Layer +1. Implement `GraphController` with command pattern +2. Implement `NodeController` with undo/redo +3. Implement `ConnectionController` with real-time validation +4. Implement `CompilationController` with progress tracking +5. Implement `ValidationController` with type checking + +**Deliverables:** +- `GraphController.h/cpp` +- `NodeController.h/cpp` +- `ConnectionController.h/cpp` +- `CompilationController.h/cpp` +- `ValidationController.h/cpp` +- Command pattern infrastructure (`Command.h`) +- Integration tests + +--- + +### Phase 3: View Layer (Basic) +1. Set up GUI framework (Dear ImGui or custom) +2. Implement `GraphEditorView` with pan/zoom +3. Implement `NodeView` with basic rendering +4. Implement `ConnectionView` with Bezier curves +5. Implement `SlotView` with hit testing + +**Deliverables:** +- `GraphEditorView.h/cpp` +- `NodeView.h/cpp` +- `ConnectionView.h/cpp` +- `SlotView.h/cpp` +- Rendering utilities (`WireRouter.h`, `NodeLayoutEngine.h`) + +--- + +### Phase 4: View Layer (Advanced) +1. Implement `InspectorView` with property editing +2. Implement `ToolbarView` with node palette +3. Add drag-and-drop support +4. Add selection and multi-select +5. Add keyboard shortcuts + +**Deliverables:** +- `InspectorView.h/cpp` +- `ToolbarView.h/cpp` +- `DragDropManager.h/cpp` +- `SelectionManager.h/cpp` +- `KeyboardShortcutManager.h/cpp` + +--- + +### Phase 5: Serialization & Persistence +1. Implement JSON serialization for `GraphModel` +2. Implement save/load functionality +3. Add clipboard support (copy/paste nodes) +4. Add template/preset system +5. Add version migration + +**Deliverables:** +- `SerializationController.h/cpp` +- `GraphSerializer.h/cpp` +- `ClipboardManager.h/cpp` +- `TemplateManager.h/cpp` + +--- + +### Phase 6: Execution & Debugging +1. Implement `ExecutionController` with play/pause +2. Add performance profiling per-node +3. Add visual execution indicator (current executing node) +4. Add breakpoint support +5. Add step-through execution + +**Deliverables:** +- `ExecutionController.h/cpp` +- `PerformanceProfiler.h/cpp` +- `DebugVisualization.h/cpp` + +--- + +## Key Design Principles + +### 1. Separation of Concerns +- **Model**: Knows nothing about GUI (no rendering, no input) +- **View**: Knows nothing about business logic (only rendering and input) +- **Controller**: Mediates between Model and View + +### 2. Observable Pattern +- Models emit change notifications +- Views observe models and update automatically +- No manual synchronization required + +### 3. Command Pattern +- All user actions are commands +- Commands are undoable/redoable +- Commands validate before execution + +### 4. Type Safety +- Compile-time type checking preserved +- Runtime type validation for connections +- No type erasure except where necessary (PassThroughStorage) + +### 5. Zero Overhead Abstraction +- Models wrap existing classes (no duplication) +- No performance penalty for GUI layer +- Existing RenderGraph performance unchanged + +--- + +## Technology Recommendations + +### GUI Framework Options + +#### Option 1: Dear ImGui (Recommended) +**Pros:** +- Immediate mode (simple state management) +- Mature node editor extension (imnodes) +- Low overhead +- Easy integration + +**Cons:** +- Less polished visuals +- Requires custom styling for professional look + +#### Option 2: Qt +**Pros:** +- Native widgets +- Mature ecosystem +- Professional appearance + +**Cons:** +- Heavyweight +- Licensing considerations +- Steeper learning curve + +#### Option 3: Custom (OpenGL/Vulkan) +**Pros:** +- Full control +- Perfect Vulkan integration +- Lightweight + +**Cons:** +- High development cost +- Requires custom UI framework + +--- + +### Serialization Format + +**Recommended: JSON** +```json +{ + "version": 1, + "graph": { + "name": "MyRenderGraph", + "id": "a1b2c3d4-...", + "nodes": [ + { + "handle": 0, + "type": "WindowNodeType", + "instance": "main_window", + "position": [100.0, 200.0], + "parameters": { + "width": 1920, + "height": 1080 + } + } + ], + "connections": [ + { + "source": {"node": 0, "slot": 2}, + "target": {"node": 1, "slot": 0}, + "type": "direct" + } + ] + } +} +``` + +--- + +## File Structure + +``` +VIXEN/ +├── libraries/ +│ ├── RenderGraph/ # Existing (no changes) +│ │ ├── include/ +│ │ └── src/ +│ │ +│ └── RenderGraphEditor/ # New MCP GUI layer +│ ├── include/ +│ │ ├── Model/ +│ │ │ ├── GraphModel.h +│ │ │ ├── NodeModel.h +│ │ │ ├── ConnectionModel.h +│ │ │ ├── ResourceModel.h +│ │ │ ├── ConfigModelRegistry.h +│ │ │ ├── TopologyModel.h +│ │ │ └── ExecutionModel.h +│ │ │ +│ │ ├── Controller/ +│ │ │ ├── GraphController.h +│ │ │ ├── NodeController.h +│ │ │ ├── ConnectionController.h +│ │ │ ├── ResourceController.h +│ │ │ ├── CompilationController.h +│ │ │ ├── ExecutionController.h +│ │ │ ├── SerializationController.h +│ │ │ └── ValidationController.h +│ │ │ +│ │ └── View/ +│ │ ├── GraphEditorView.h +│ │ ├── NodeView.h +│ │ ├── ConnectionView.h +│ │ ├── SlotView.h +│ │ ├── InspectorView.h +│ │ ├── ToolbarView.h +│ │ └── Utilities/ +│ │ ├── WireRouter.h +│ │ ├── NodeLayoutEngine.h +│ │ └── RenderingUtils.h +│ │ +│ └── src/ +│ ├── Model/ +│ ├── Controller/ +│ └── View/ +│ +└── applications/ + └── GraphEditor/ # Standalone GUI application + ├── main.cpp + └── GraphEditorApplication.h +``` + +--- + +## Next Steps + +1. **Review & Approval**: Review this architecture with team +2. **Prototype**: Build Phase 1 (Model Layer) as proof-of-concept +3. **Iterate**: Refine based on prototype learnings +4. **Implement**: Execute phases sequentially +5. **Test**: Comprehensive testing at each phase +6. **Document**: API documentation and user guide + +--- + +## Conclusion + +This MCP architecture provides: + +✅ **Clean Separation**: Model (data), View (GUI), Controller (logic) +✅ **Type Safety**: Compile-time + runtime validation +✅ **Undo/Redo**: Command pattern throughout +✅ **Real-time Validation**: Immediate feedback on errors +✅ **Observable Updates**: Automatic UI synchronization +✅ **Zero Overhead**: No performance penalty for existing RenderGraph +✅ **Extensibility**: Easy to add new node types +✅ **Testability**: Each layer independently testable + +The architecture leverages the existing RenderGraph system while adding a robust, maintainable GUI layer suitable for professional graph editing applications. diff --git a/VIXEN/documentation/GraphArchitecture/MCP_Implementation_Example.md b/VIXEN/documentation/GraphArchitecture/MCP_Implementation_Example.md new file mode 100644 index 00000000..65a2f3c5 --- /dev/null +++ b/VIXEN/documentation/GraphArchitecture/MCP_Implementation_Example.md @@ -0,0 +1,1159 @@ +# MCP Architecture Implementation Example + +This document provides concrete implementation examples for the MCP architecture, focusing on a complete workflow: creating a node, connecting it, and compiling the graph. + +--- + +## Example: Implementing NodeController as a Controller Point + +### Model Layer: NodeModel + +```cpp +// File: RenderGraphEditor/include/Model/NodeModel.h +#pragma once + +#include +#include +#include +#include +#include "Observable.h" +#include "RenderGraph/Core/NodeInstance.h" +#include "SlotModel.h" + +namespace VIXEN::GraphEditor { + +enum class NodeState { + Unconnected, // Not all required slots connected + Ready, // All required slots connected, not compiled + Compiled, // Successfully compiled + Error // Compilation or validation error +}; + +class NodeModel { +public: + // Constructor wraps existing NodeInstance + NodeModel(NodeInstance* instance, const std::string& typeName); + + // Identity + NodeHandle GetHandle() const { return handle_; } + const std::string& GetInstanceName() const { return instanceName_; } + const std::string& GetTypeName() const { return typeName_; } + + // Configuration + const std::vector& GetInputSlots() const { return inputSlots_; } + const std::vector& GetOutputSlots() const { return outputSlots_; } + const SlotModel& GetInputSlot(uint32_t index) const; + const SlotModel& GetOutputSlot(uint32_t index) const; + + // Visual properties + Vec2 GetPosition() const { return position_.Get(); } + void SetPosition(Vec2 position) { position_.Set(position); } + + Vec2 GetSize() const { return size_.Get(); } + void SetSize(Vec2 size) { size_.Set(size); } + + Color GetColor() const { return color_; } + void SetColor(Color color) { color_ = color; } + + // State management + NodeState GetState() const { return state_.Get(); } + void SetState(NodeState state) { state_.Set(state); } + + const std::vector& GetErrors() const { return errors_.Get(); } + void AddError(const std::string& error); + void ClearErrors(); + + // Parameters (runtime configuration) + bool HasParameter(const std::string& name) const; + std::any GetParameter(const std::string& name) const; + void SetParameter(const std::string& name, const std::any& value); + + // Connection tracking + void MarkSlotConnected(uint32_t slotIndex, bool isInput, bool connected); + bool IsSlotConnected(uint32_t slotIndex, bool isInput) const; + bool AreAllRequiredSlotsConnected() const; + + // Underlying node instance + NodeInstance* GetNodeInstance() const { return nodeInstance_; } + + // Observables (Views subscribe to these) + Observable& PositionObservable() { return position_; } + Observable& SizeObservable() { return size_; } + Observable& StateObservable() { return state_; } + Observable>& ErrorsObservable() { return errors_; } + +private: + // Core data + NodeInstance* nodeInstance_; // Non-owning (RenderGraph owns) + NodeHandle handle_; + std::string instanceName_; + std::string typeName_; + + // Slot metadata + std::vector inputSlots_; + std::vector outputSlots_; + + // Visual properties (observable) + Observable position_; + Observable size_; + Color color_; + + // State (observable) + Observable state_; + Observable> errors_; + + // Runtime parameters + std::map parameters_; + + // Connection state + std::vector inputSlotConnected_; + std::vector outputSlotConnected_; +}; + +} // namespace VIXEN::GraphEditor +``` + +--- + +### Controller Layer: NodeController + +```cpp +// File: RenderGraphEditor/include/Controller/NodeController.h +#pragma once + +#include "Model/GraphModel.h" +#include "Model/ConfigModelRegistry.h" +#include "Command/Command.h" +#include "Event.h" +#include + +namespace VIXEN::GraphEditor { + +class NodeController { +public: + NodeController(GraphModel* graphModel, ConfigModelRegistry* registry); + + // Node creation + CommandResult CreateNode( + const std::string& nodeTypeName, + const std::string& instanceName, + Vec2 position = Vec2(0, 0) + ); + + // Node deletion + CommandResult DeleteNode(NodeHandle handle); + CommandResult DeleteNodes(const std::vector& handles); + + // Node modification + CommandResult SetNodePosition(NodeHandle handle, Vec2 position); + CommandResult SetNodeParameter( + NodeHandle handle, + const std::string& paramName, + const std::any& value + ); + CommandResult RenameNode(NodeHandle handle, const std::string& newName); + + // Node duplication + CommandResult DuplicateNode(NodeHandle handle, Vec2 offset = Vec2(50, 50)); + + // Bulk operations + CommandResult MoveNodes(const std::vector& handles, Vec2 delta); + + // Validation + ValidationResult ValidateNodeName(const std::string& name) const; + bool CanDeleteNode(NodeHandle handle) const; + bool NodeExists(const std::string& name) const; + + // Undo/Redo + void Undo(); + void Redo(); + bool CanUndo() const; + bool CanRedo() const; + + // Events + Event onNodeCreated; + Event onNodeDeleted; + Event onNodeModified; + +private: + GraphModel* graphModel_; + ConfigModelRegistry* registry_; + CommandHistory commandHistory_; + + // Helper methods + std::string GenerateUniqueNodeName(const std::string& baseName) const; + NodeModel* CreateNodeModel( + const std::string& nodeTypeName, + const std::string& instanceName + ); +}; + +// Command implementations +class CreateNodeCommand : public Command { +public: + CreateNodeCommand( + GraphModel* graphModel, + ConfigModelRegistry* registry, + const std::string& nodeTypeName, + const std::string& instanceName, + Vec2 position + ); + + void Execute() override; + void Undo() override; + std::string GetDescription() const override; + +private: + GraphModel* graphModel_; + ConfigModelRegistry* registry_; + std::string nodeTypeName_; + std::string instanceName_; + Vec2 position_; + NodeHandle createdHandle_; // Stored for undo +}; + +class DeleteNodeCommand : public Command { +public: + DeleteNodeCommand(GraphModel* graphModel, NodeHandle handle); + + void Execute() override; + void Undo() override; + std::string GetDescription() const override; + +private: + GraphModel* graphModel_; + NodeHandle handle_; + + // Store state for undo + std::unique_ptr deletedNode_; + std::vector deletedConnections_; +}; + +class SetNodePositionCommand : public Command { +public: + SetNodePositionCommand(GraphModel* graphModel, NodeHandle handle, Vec2 newPosition); + + void Execute() override; + void Undo() override; + std::string GetDescription() const override; + +private: + GraphModel* graphModel_; + NodeHandle handle_; + Vec2 newPosition_; + Vec2 oldPosition_; // Stored during Execute +}; + +} // namespace VIXEN::GraphEditor +``` + +--- + +### Controller Implementation + +```cpp +// File: RenderGraphEditor/src/Controller/NodeController.cpp +#include "Controller/NodeController.h" +#include "Model/NodeModel.h" +#include + +namespace VIXEN::GraphEditor { + +NodeController::NodeController(GraphModel* graphModel, ConfigModelRegistry* registry) + : graphModel_(graphModel) + , registry_(registry) +{ +} + +CommandResult NodeController::CreateNode( + const std::string& nodeTypeName, + const std::string& instanceName, + Vec2 position +) { + // Validate node type exists + if (!registry_->IsTypeRegistered(nodeTypeName)) { + return CommandResult::Error("Node type '" + nodeTypeName + "' not registered"); + } + + // Validate name uniqueness + auto validationResult = ValidateNodeName(instanceName); + if (!validationResult.isValid) { + return CommandResult::Error(validationResult.errorMessage); + } + + // Create command + auto command = std::make_unique( + graphModel_, + registry_, + nodeTypeName, + instanceName, + position + ); + + // Execute command + command->Execute(); + + // Add to history + commandHistory_.Add(std::move(command)); + + return CommandResult::Success(); +} + +CommandResult NodeController::DeleteNode(NodeHandle handle) { + // Validate node exists + NodeModel* node = graphModel_->FindNode(handle); + if (!node) { + return CommandResult::Error("Node not found"); + } + + // Check if node can be deleted + if (!CanDeleteNode(handle)) { + return CommandResult::Error("Node cannot be deleted (critical node)"); + } + + // Create command + auto command = std::make_unique(graphModel_, handle); + + // Execute command + command->Execute(); + + // Add to history + commandHistory_.Add(std::move(command)); + + return CommandResult::Success(); +} + +CommandResult NodeController::SetNodePosition(NodeHandle handle, Vec2 position) { + // Validate node exists + NodeModel* node = graphModel_->FindNode(handle); + if (!node) { + return CommandResult::Error("Node not found"); + } + + // Create command + auto command = std::make_unique( + graphModel_, + handle, + position + ); + + // Execute command + command->Execute(); + + // Add to history + commandHistory_.Add(std::move(command)); + + // Emit event + onNodeModified.Emit(node); + + return CommandResult::Success(); +} + +ValidationResult NodeController::ValidateNodeName(const std::string& name) const { + // Check if name is empty + if (name.empty()) { + return ValidationResult::Error("Node name cannot be empty"); + } + + // Check if name already exists + if (NodeExists(name)) { + return ValidationResult::Error("Node name '" + name + "' already exists"); + } + + // Check for invalid characters + if (name.find(' ') != std::string::npos) { + return ValidationResult::Error("Node name cannot contain spaces"); + } + + return ValidationResult::Success(); +} + +bool NodeController::CanDeleteNode(NodeHandle handle) const { + // Example: Prevent deletion of root nodes (e.g., DeviceNode) + NodeModel* node = graphModel_->FindNode(handle); + if (!node) return false; + + // Check if node is a critical system node + if (node->GetTypeName() == "DeviceNode" || + node->GetTypeName() == "WindowNode") { + return false; + } + + return true; +} + +bool NodeController::NodeExists(const std::string& name) const { + return graphModel_->FindNode(name) != nullptr; +} + +std::string NodeController::GenerateUniqueNodeName(const std::string& baseName) const { + std::string candidateName = baseName; + int suffix = 1; + + while (NodeExists(candidateName)) { + std::ostringstream oss; + oss << baseName << "_" << suffix++; + candidateName = oss.str(); + } + + return candidateName; +} + +void NodeController::Undo() { + if (CanUndo()) { + commandHistory_.Undo(); + } +} + +void NodeController::Redo() { + if (CanRedo()) { + commandHistory_.Redo(); + } +} + +bool NodeController::CanUndo() const { + return commandHistory_.CanUndo(); +} + +bool NodeController::CanRedo() const { + return commandHistory_.CanRedo(); +} + +// ============================================================================ +// CreateNodeCommand Implementation +// ============================================================================ + +CreateNodeCommand::CreateNodeCommand( + GraphModel* graphModel, + ConfigModelRegistry* registry, + const std::string& nodeTypeName, + const std::string& instanceName, + Vec2 position +) + : graphModel_(graphModel) + , registry_(registry) + , nodeTypeName_(nodeTypeName) + , instanceName_(instanceName) + , position_(position) + , createdHandle_(INVALID_NODE_HANDLE) +{ +} + +void CreateNodeCommand::Execute() { + // Get node type descriptor + const NodeTypeDescriptor* descriptor = registry_->GetNodeType(nodeTypeName_); + if (!descriptor) { + throw std::runtime_error("Node type not found: " + nodeTypeName_); + } + + // Create node instance using factory + NodeInstance* nodeInstance = descriptor->factoryFunction(); + + // Add to RenderGraph + createdHandle_ = graphModel_->GetRenderGraph()->AddNode( + instanceName_, + nodeInstance + ); + + // Create NodeModel wrapper + auto nodeModel = std::make_unique(nodeInstance, nodeTypeName_); + nodeModel->SetPosition(position_); + nodeModel->SetColor(descriptor->defaultColor); + + // Add to GraphModel + graphModel_->AddNode(std::move(nodeModel)); + + // Mark graph dirty + graphModel_->MarkDirty(); +} + +void CreateNodeCommand::Undo() { + // Remove from GraphModel + graphModel_->RemoveNode(createdHandle_); + + // Remove from RenderGraph (if supported) + // Note: Current RenderGraph doesn't support removal during Build phase + // This would require extending RenderGraph API + + // Mark graph dirty + graphModel_->MarkDirty(); + + createdHandle_ = INVALID_NODE_HANDLE; +} + +std::string CreateNodeCommand::GetDescription() const { + return "Create node '" + instanceName_ + "' of type '" + nodeTypeName_ + "'"; +} + +// ============================================================================ +// DeleteNodeCommand Implementation +// ============================================================================ + +DeleteNodeCommand::DeleteNodeCommand(GraphModel* graphModel, NodeHandle handle) + : graphModel_(graphModel) + , handle_(handle) +{ +} + +void DeleteNodeCommand::Execute() { + // Store node state for undo + NodeModel* node = graphModel_->FindNode(handle_); + if (!node) { + throw std::runtime_error("Node not found"); + } + + // Store connections that will be deleted + deletedConnections_ = graphModel_->GetConnectionsForNode(handle_); + + // Remove connections first + for (auto* connection : deletedConnections_) { + graphModel_->RemoveConnection(connection->GetId()); + } + + // Remove node + deletedNode_ = graphModel_->RemoveNode(handle_); + + // Mark graph dirty + graphModel_->MarkDirty(); +} + +void DeleteNodeCommand::Undo() { + // Re-add node + graphModel_->AddNode(std::move(deletedNode_)); + + // Re-add connections + for (auto* connection : deletedConnections_) { + graphModel_->AddConnection(connection); + } + + // Mark graph dirty + graphModel_->MarkDirty(); +} + +std::string DeleteNodeCommand::GetDescription() const { + return "Delete node (handle: " + std::to_string(handle_) + ")"; +} + +// ============================================================================ +// SetNodePositionCommand Implementation +// ============================================================================ + +SetNodePositionCommand::SetNodePositionCommand( + GraphModel* graphModel, + NodeHandle handle, + Vec2 newPosition +) + : graphModel_(graphModel) + , handle_(handle) + , newPosition_(newPosition) +{ +} + +void SetNodePositionCommand::Execute() { + NodeModel* node = graphModel_->FindNode(handle_); + if (!node) { + throw std::runtime_error("Node not found"); + } + + // Store old position for undo + oldPosition_ = node->GetPosition(); + + // Set new position + node->SetPosition(newPosition_); +} + +void SetNodePositionCommand::Undo() { + NodeModel* node = graphModel_->FindNode(handle_); + if (!node) { + throw std::runtime_error("Node not found"); + } + + // Restore old position + node->SetPosition(oldPosition_); +} + +std::string SetNodePositionCommand::GetDescription() const { + return "Move node (handle: " + std::to_string(handle_) + ")"; +} + +} // namespace VIXEN::GraphEditor +``` + +--- + +### View Layer: NodeView + +```cpp +// File: RenderGraphEditor/include/View/NodeView.h +#pragma once + +#include "Model/NodeModel.h" +#include "Controller/NodeController.h" +#include "View/SlotView.h" +#include +#include + +namespace VIXEN::GraphEditor { + +class NodeView { +public: + NodeView(NodeModel* model, NodeController* controller); + ~NodeView(); + + // Rendering + void Render(ImDrawList* drawList, const Mat3& worldToScreen); + + // Layout + Rect GetBounds() const; + Vec2 GetScreenPosition() const { return screenPosition_; } + + // Input handling + bool HitTest(Vec2 screenPosition) const; + void OnMouseDown(Vec2 position, MouseButton button); + void OnMouseMove(Vec2 position); + void OnMouseUp(Vec2 position, MouseButton button); + void OnDragStart(Vec2 position); + void OnDrag(Vec2 delta); + void OnDragEnd(); + + // State + void SetSelected(bool selected); + bool IsSelected() const { return isSelected_; } + void SetHovered(bool hovered); + bool IsHovered() const { return isHovered_; } + + // Slot access + SlotView* GetSlotAtPosition(Vec2 screenPosition) const; + SlotView* GetInputSlot(uint32_t index) const; + SlotView* GetOutputSlot(uint32_t index) const; + + // Model access + NodeModel* GetModel() const { return model_; } + +private: + NodeModel* model_; + NodeController* controller_; + + // Visual state + Vec2 screenPosition_; + Vec2 size_; + bool isSelected_; + bool isHovered_; + bool isDragging_; + + // Child views + std::vector> inputSlots_; + std::vector> outputSlots_; + + // Rendering helpers + void RenderFrame(ImDrawList* drawList); + void RenderHeader(ImDrawList* drawList); + void RenderBody(ImDrawList* drawList); + void RenderSlots(ImDrawList* drawList); + void RenderSelectionHighlight(ImDrawList* drawList); + void RenderErrorBadge(ImDrawList* drawList); + + // Layout helpers + void UpdateLayout(); + void UpdateSlotPositions(); + + // Model observation + void OnPositionChanged(Vec2 newPosition); + void OnStateChanged(NodeState newState); + void OnErrorsChanged(const std::vector& errors); + + // Subscriptions (stored for cleanup) + std::vector observerHandles_; +}; + +} // namespace VIXEN::GraphEditor +``` + +--- + +### View Implementation + +```cpp +// File: RenderGraphEditor/src/View/NodeView.cpp +#include "View/NodeView.h" +#include + +namespace VIXEN::GraphEditor { + +NodeView::NodeView(NodeModel* model, NodeController* controller) + : model_(model) + , controller_(controller) + , isSelected_(false) + , isHovered_(false) + , isDragging_(false) +{ + // Subscribe to model changes + observerHandles_.push_back( + model_->PositionObservable().Subscribe([this](Vec2 pos) { + OnPositionChanged(pos); + }) + ); + + observerHandles_.push_back( + model_->StateObservable().Subscribe([this](NodeState state) { + OnStateChanged(state); + }) + ); + + observerHandles_.push_back( + model_->ErrorsObservable().Subscribe([this](const std::vector& errors) { + OnErrorsChanged(errors); + }) + ); + + // Create slot views + for (size_t i = 0; i < model_->GetInputSlots().size(); ++i) { + inputSlots_.push_back( + std::make_unique(&model_->GetInputSlots()[i], this, true) + ); + } + + for (size_t i = 0; i < model_->GetOutputSlots().size(); ++i) { + outputSlots_.push_back( + std::make_unique(&model_->GetOutputSlots()[i], this, false) + ); + } + + // Initial layout + UpdateLayout(); +} + +NodeView::~NodeView() { + // Unsubscribe from model (handles cleaned up automatically) +} + +void NodeView::Render(ImDrawList* drawList, const Mat3& worldToScreen) { + // Transform world position to screen space + Vec2 worldPos = model_->GetPosition(); + screenPosition_ = TransformPoint(worldToScreen, worldPos); + + // Update layout + UpdateLayout(); + + // Render node + RenderFrame(drawList); + RenderHeader(drawList); + RenderBody(drawList); + RenderSlots(drawList); + + // Render state indicators + if (isSelected_) { + RenderSelectionHighlight(drawList); + } + + if (!model_->GetErrors().empty()) { + RenderErrorBadge(drawList); + } +} + +void NodeView::RenderFrame(ImDrawList* drawList) { + Rect bounds = GetBounds(); + + // Background color based on state + Color backgroundColor; + switch (model_->GetState()) { + case NodeState::Unconnected: + backgroundColor = Color(60, 60, 60, 255); + break; + case NodeState::Ready: + backgroundColor = Color(70, 70, 70, 255); + break; + case NodeState::Compiled: + backgroundColor = Color(50, 80, 50, 255); + break; + case NodeState::Error: + backgroundColor = Color(100, 50, 50, 255); + break; + } + + // Draw background + drawList->AddRectFilled( + ImVec2(bounds.min.x, bounds.min.y), + ImVec2(bounds.max.x, bounds.max.y), + ImGui::ColorConvertFloat4ToU32(backgroundColor.ToImVec4()), + 4.0f // Corner radius + ); + + // Draw border + Color borderColor = isHovered_ ? Color(200, 200, 200, 255) : Color(100, 100, 100, 255); + drawList->AddRect( + ImVec2(bounds.min.x, bounds.min.y), + ImVec2(bounds.max.x, bounds.max.y), + ImGui::ColorConvertFloat4ToU32(borderColor.ToImVec4()), + 4.0f, // Corner radius + 0, + isSelected_ ? 3.0f : 1.5f // Border thickness + ); +} + +void NodeView::RenderHeader(ImDrawList* drawList) { + Rect bounds = GetBounds(); + float headerHeight = 30.0f; + + // Header background + drawList->AddRectFilled( + ImVec2(bounds.min.x, bounds.min.y), + ImVec2(bounds.max.x, bounds.min.y + headerHeight), + ImGui::ColorConvertFloat4ToU32(model_->GetColor().ToImVec4()), + 4.0f, + ImDrawFlags_RoundCornersTop + ); + + // Node name text + drawList->AddText( + ImVec2(bounds.min.x + 10, bounds.min.y + 8), + IM_COL32(255, 255, 255, 255), + model_->GetInstanceName().c_str() + ); +} + +void NodeView::RenderBody(ImDrawList* drawList) { + // Node type text + Rect bounds = GetBounds(); + drawList->AddText( + ImVec2(bounds.min.x + 10, bounds.min.y + 40), + IM_COL32(180, 180, 180, 255), + model_->GetTypeName().c_str() + ); +} + +void NodeView::RenderSlots(ImDrawList* drawList) { + // Render input slots + for (auto& slot : inputSlots_) { + slot->Render(drawList); + } + + // Render output slots + for (auto& slot : outputSlots_) { + slot->Render(drawList); + } +} + +void NodeView::RenderSelectionHighlight(ImDrawList* drawList) { + Rect bounds = GetBounds(); + drawList->AddRect( + ImVec2(bounds.min.x - 4, bounds.min.y - 4), + ImVec2(bounds.max.x + 4, bounds.max.y + 4), + IM_COL32(255, 200, 0, 255), // Orange highlight + 6.0f, + 0, + 2.0f + ); +} + +void NodeView::RenderErrorBadge(ImDrawList* drawList) { + Rect bounds = GetBounds(); + Vec2 badgePos(bounds.max.x - 20, bounds.min.y + 5); + + // Error badge (red circle with exclamation mark) + drawList->AddCircleFilled( + ImVec2(badgePos.x, badgePos.y), + 10.0f, + IM_COL32(200, 50, 50, 255) + ); + + drawList->AddText( + ImVec2(badgePos.x - 3, badgePos.y - 8), + IM_COL32(255, 255, 255, 255), + "!" + ); +} + +bool NodeView::HitTest(Vec2 screenPosition) const { + Rect bounds = GetBounds(); + return bounds.Contains(screenPosition); +} + +void NodeView::OnMouseDown(Vec2 position, MouseButton button) { + if (button == MouseButton::Left) { + // Begin drag + OnDragStart(position); + } +} + +void NodeView::OnDragStart(Vec2 position) { + isDragging_ = true; +} + +void NodeView::OnDrag(Vec2 delta) { + if (isDragging_) { + Vec2 newPosition = model_->GetPosition() + delta; + controller_->SetNodePosition(model_->GetHandle(), newPosition); + } +} + +void NodeView::OnDragEnd() { + isDragging_ = false; +} + +void NodeView::UpdateLayout() { + // Calculate node size based on slot count + float headerHeight = 30.0f; + float slotHeight = 20.0f; + float padding = 10.0f; + + size_t maxSlots = std::max(inputSlots_.size(), outputSlots_.size()); + float bodyHeight = maxSlots * slotHeight + padding * 2; + + size_ = Vec2(200.0f, headerHeight + bodyHeight); + + // Update slot positions + UpdateSlotPositions(); +} + +void NodeView::UpdateSlotPositions() { + float slotHeight = 20.0f; + float headerHeight = 30.0f; + float startY = screenPosition_.y + headerHeight + 10.0f; + + // Position input slots (left side) + for (size_t i = 0; i < inputSlots_.size(); ++i) { + Vec2 slotPos(screenPosition_.x, startY + i * slotHeight); + inputSlots_[i]->SetPosition(slotPos); + } + + // Position output slots (right side) + for (size_t i = 0; i < outputSlots_.size(); ++i) { + Vec2 slotPos(screenPosition_.x + size_.x, startY + i * slotHeight); + outputSlots_[i]->SetPosition(slotPos); + } +} + +Rect NodeView::GetBounds() const { + return Rect(screenPosition_, screenPosition_ + size_); +} + +void NodeView::OnPositionChanged(Vec2 newPosition) { + // Position updated, layout will be recalculated on next Render() +} + +void NodeView::OnStateChanged(NodeState newState) { + // State updated, visual appearance will change on next Render() +} + +void NodeView::OnErrorsChanged(const std::vector& errors) { + // Errors updated, badge will appear on next Render() +} + +void NodeView::SetSelected(bool selected) { + isSelected_ = selected; +} + +void NodeView::SetHovered(bool hovered) { + isHovered_ = hovered; +} + +SlotView* NodeView::GetSlotAtPosition(Vec2 screenPosition) const { + // Check input slots + for (auto& slot : inputSlots_) { + if (slot->HitTest(screenPosition)) { + return slot.get(); + } + } + + // Check output slots + for (auto& slot : outputSlots_) { + if (slot->HitTest(screenPosition)) { + return slot.get(); + } + } + + return nullptr; +} + +SlotView* NodeView::GetInputSlot(uint32_t index) const { + if (index < inputSlots_.size()) { + return inputSlots_[index].get(); + } + return nullptr; +} + +SlotView* NodeView::GetOutputSlot(uint32_t index) const { + if (index < outputSlots_.size()) { + return outputSlots_[index].get(); + } + return nullptr; +} + +} // namespace VIXEN::GraphEditor +``` + +--- + +## Complete Workflow Example + +### User Creates and Connects Two Nodes + +```cpp +// Application code example +#include "Controller/GraphController.h" +#include "Controller/NodeController.h" +#include "Controller/ConnectionController.h" +#include "View/GraphEditorView.h" + +int main() { + // Initialize MCP architecture + auto graphModel = std::make_unique(); + auto registry = std::make_unique(); + + // Register all node types + registry->RegisterAllNodeTypes(); + + // Create controllers + auto graphController = std::make_unique(graphModel.get()); + auto nodeController = std::make_unique(graphModel.get(), registry.get()); + auto connectionController = std::make_unique(graphModel.get()); + + // Create view + auto editorView = std::make_unique( + graphController.get(), + nodeController.get(), + connectionController.get(), + graphModel.get() + ); + + // ======================================================================== + // WORKFLOW: User creates WindowNode + // ======================================================================== + + // User drags WindowNode from palette to canvas at position (100, 100) + auto result1 = nodeController->CreateNode("WindowNode", "main_window", Vec2(100, 100)); + if (!result1.success) { + std::cerr << "Failed to create WindowNode: " << result1.errorMessage << std::endl; + return 1; + } + + // View automatically receives notification via Observer pattern + // GraphEditorView::OnNodeAdded() creates NodeView and renders it + + // ======================================================================== + // WORKFLOW: User creates SwapChainNode + // ======================================================================== + + // User drags SwapChainNode from palette to canvas at position (400, 100) + auto result2 = nodeController->CreateNode("SwapChainNode", "swapchain_main", Vec2(400, 100)); + if (!result2.success) { + std::cerr << "Failed to create SwapChainNode: " << result2.errorMessage << std::endl; + return 1; + } + + // ======================================================================== + // WORKFLOW: User connects WindowNode.OUTPUT_SURFACE to SwapChainNode.INPUT_SURFACE + // ======================================================================== + + // Get node handles + NodeModel* windowNode = graphModel->FindNode("main_window"); + NodeModel* swapChainNode = graphModel->FindNode("swapchain_main"); + + // User drags from WindowNode output slot 2 (SURFACE) to SwapChainNode input slot 1 (SURFACE) + auto connectionResult = connectionController->CreateConnection( + windowNode->GetHandle(), + 2, // WindowNodeConfig::OUTPUT_SURFACE + swapChainNode->GetHandle(), + 1 // SwapChainNodeConfig::INPUT_SURFACE + ); + + if (!connectionResult.success) { + std::cerr << "Failed to create connection: " << connectionResult.errorMessage << std::endl; + return 1; + } + + // View automatically receives notification + // GraphEditorView::OnConnectionAdded() creates ConnectionView and renders wire + + // ======================================================================== + // WORKFLOW: User compiles graph + // ======================================================================== + + // User clicks "Compile" button + graphController->Compile(); + + // CompilationController performs: + // 1. ValidationController validates graph + // 2. TopologyModel analyzes dependencies + // 3. RenderGraph.Compile() called + // 4. ExecutionModel updated with results + + // If errors occur, they're displayed in NodeView error badges + + // ======================================================================== + // WORKFLOW: User executes graph + // ======================================================================== + + // User clicks "Play" button + graphController->Execute(); + + // Main render loop + while (true) { + // Process input + editorView->ProcessInput(); + + // Update and render + editorView->Render(); + + // Execute graph (if playing) + if (graphController->IsExecuting()) { + graphModel->GetRenderGraph()->Execute(); + } + + // Present frame + // ... + } + + return 0; +} +``` + +--- + +## Key Takeaways + +### 1. **Model Layer**: Single Source of Truth +- `NodeModel` wraps `NodeInstance` with observable properties +- Views subscribe to model changes automatically +- No manual synchronization required + +### 2. **Controller Layer**: Command Pattern + Validation +- All user actions are commands (undoable) +- Controllers validate before modifying model +- Events emitted for cross-cutting concerns + +### 3. **View Layer**: Passive Rendering +- Views observe models and render state +- Input handling translates to controller calls +- No business logic in views + +### 4. **Workflow**: User Action → Controller → Model → View Update +``` +User drags node + → NodeView.OnDragStart() + → NodeController.SetNodePosition() + → CreateCommand + Execute + → NodeModel.SetPosition() + → NodeModel.position_.Notify() + → NodeView.OnPositionChanged() + → NodeView.Render() (updated position) +``` + +This architecture ensures: +- ✅ Clear separation of concerns +- ✅ Automatic UI synchronization +- ✅ Full undo/redo support +- ✅ Type-safe validation +- ✅ Testability (each layer independent) +- ✅ Extensibility (easy to add new node types) + +--- + +## Next Steps + +1. Implement Observable pattern (`Observable`) +2. Implement Command pattern (`Command`, `CommandHistory`) +3. Implement Event system (`Event`) +4. Prototype NodeModel + NodeController + NodeView +5. Test complete workflow end-to-end diff --git a/VIXEN/documentation/GraphArchitecture/Node_Config_To_Controller_Mapping.md b/VIXEN/documentation/GraphArchitecture/Node_Config_To_Controller_Mapping.md new file mode 100644 index 00000000..126210f1 --- /dev/null +++ b/VIXEN/documentation/GraphArchitecture/Node_Config_To_Controller_Mapping.md @@ -0,0 +1,710 @@ +# Transforming Node Configs into Controller Points + +This document demonstrates how to transform every existing node configuration into a controller point within the MCP architecture, enabling GUI-driven graph editing. + +--- + +## Overview: Config as Controller Metadata + +In the existing system: +- **Compile-time**: `CONSTEXPR_NODE_CONFIG` defines node structure +- **Runtime**: `NodeInstance` uses config for type-safe slot access + +In the MCP system: +- **Model**: Config becomes runtime metadata (NodeTypeDescriptor) +- **Controller**: Config drives validation and connection logic +- **View**: Config drives visual representation (slot layout, colors) + +--- + +## Transformation Pipeline + +``` +Compile-Time Config → Runtime Descriptor → Controller Logic → View Representation +``` + +### Step 1: Extract Config to Descriptor + +For each node config (e.g., `WindowNodeConfig`), create a runtime descriptor: + +```cpp +// Existing compile-time config +CONSTEXPR_NODE_CONFIG(WindowNodeConfig, 1, 5) { + // Input: Instance + INPUT_SLOT(INSTANCE, VkInstance, 0, Required, Dependency, ReadOnly, NodeLevel); + + // Outputs + OUTPUT_SLOT(SURFACE, VkSurfaceKHR, 0, Required, WriteOnly); + OUTPUT_SLOT(WIDTH, uint32_t, 1, Required, WriteOnly); + OUTPUT_SLOT(HEIGHT, uint32_t, 2, Required, WriteOnly); + OUTPUT_SLOT(WINDOW_HANDLE, void*, 3, Required, WriteOnly); + OUTPUT_SLOT(SHOULD_CLOSE, bool, 4, Required, WriteOnly); +}; +``` + +**Transforms to:** + +```cpp +// Runtime descriptor for MCP architecture +NodeTypeDescriptor windowDescriptor { + .typeName = "WindowNode", + .category = "Resource", + .description = "Creates a windowed surface for rendering", + + .inputs = { + SlotDescriptor { + .index = 0, + .name = "Instance", + .typeName = "VkInstance", + .nullability = SlotNullability::Required, + .role = SlotRole::Dependency, + .mutability = SlotMutability::ReadOnly, + .scope = SlotScope::NodeLevel, + .typeColor = Color(100, 150, 255), // Blue for Vulkan types + .tooltip = "Vulkan instance handle" + } + }, + + .outputs = { + SlotDescriptor { + .index = 0, + .name = "Surface", + .typeName = "VkSurfaceKHR", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = Color(100, 150, 255), + .tooltip = "Vulkan surface for presentation" + }, + SlotDescriptor { + .index = 1, + .name = "Width", + .typeName = "uint32_t", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = Color(255, 200, 100), // Orange for primitives + .tooltip = "Window width in pixels" + }, + // ... more outputs + }, + + .factoryFunction = []() -> NodeInstance* { + return new TypedNode(); + }, + + .defaultColor = Color(80, 120, 180), + .iconPath = "icons/window.png" +}; +``` + +--- + +## Step 2: Config Registry as Controller Point + +The `ConfigModelRegistry` acts as the central controller point for all node types: + +```cpp +class ConfigModelRegistry { +public: + // Registration (called at startup) + void RegisterAllNodeTypes() { + RegisterWindowNode(); + RegisterDeviceNode(); + RegisterSwapChainNode(); + RegisterComputePipelineNode(); + // ... register all 45+ node types + } + + // Query interface (used by controllers) + const NodeTypeDescriptor* GetNodeType(const std::string& typeName) const; + std::vector GetAllNodeTypes() const; + std::vector GetNodeTypesByCategory(const std::string& category) const; + + // Validation (used by ConnectionController) + bool AreTypesCompatible(const std::string& sourceType, const std::string& targetType) const; + bool CanConnectSlots(const SlotDescriptor& source, const SlotDescriptor& target) const; + +private: + std::map nodeTypes_; + std::map compatibilityRules_; + + // Registration helpers + void RegisterWindowNode(); + void RegisterDeviceNode(); + void RegisterSwapChainNode(); + // ... one method per node type +}; +``` + +--- + +## Step 3: Per-Node Registration Functions + +Each node type gets its own registration function that serves as a "controller point": + +### Example: WindowNode Controller Point + +```cpp +void ConfigModelRegistry::RegisterWindowNode() { + NodeTypeDescriptor descriptor; + descriptor.typeName = "WindowNode"; + descriptor.category = "Resource"; + descriptor.description = "Creates a windowed surface for rendering"; + + // Input slots (from WindowNodeConfig) + descriptor.inputs.push_back({ + .index = 0, + .name = "Instance", + .typeName = "VkInstance", + .nullability = SlotNullability::Required, + .role = SlotRole::Dependency, + .mutability = SlotMutability::ReadOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkInstance"), + .tooltip = "Vulkan instance handle", + .defaultValue = std::nullopt + }); + + // Output slots + descriptor.outputs = { + { + .index = 0, + .name = "Surface", + .typeName = "VkSurfaceKHR", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkSurfaceKHR"), + .tooltip = "Vulkan surface for presentation" + }, + { + .index = 1, + .name = "Width", + .typeName = "uint32_t", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("uint32_t"), + .tooltip = "Window width in pixels" + }, + { + .index = 2, + .name = "Height", + .typeName = "uint32_t", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("uint32_t"), + .tooltip = "Window height in pixels" + }, + { + .index = 3, + .name = "WindowHandle", + .typeName = "void*", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("void*"), + .tooltip = "Platform-specific window handle" + }, + { + .index = 4, + .name = "ShouldClose", + .typeName = "bool", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("bool"), + .tooltip = "True if window close requested" + } + }; + + // Factory function + descriptor.factoryFunction = []() -> NodeInstance* { + return new TypedNode(); + }; + + // Visual metadata + descriptor.defaultColor = Color(80, 120, 180); + descriptor.iconPath = "icons/window.png"; + + // Runtime parameters (editable in Inspector) + descriptor.parameters = { + { + .name = "title", + .type = "std::string", + .defaultValue = std::string("VIXEN Window"), + .description = "Window title bar text" + }, + { + .name = "width", + .type = "uint32_t", + .defaultValue = uint32_t(1920), + .description = "Initial window width" + }, + { + .name = "height", + .type = "uint32_t", + .defaultValue = uint32_t(1080), + .description = "Initial window height" + }, + { + .name = "resizable", + .type = "bool", + .defaultValue = true, + .description = "Allow window resizing" + } + }; + + // Register + RegisterNodeType(descriptor); +} +``` + +--- + +### Example: ComputePipelineNode Controller Point + +```cpp +void ConfigModelRegistry::RegisterComputePipelineNode() { + NodeTypeDescriptor descriptor; + descriptor.typeName = "ComputePipelineNode"; + descriptor.category = "Pipeline"; + descriptor.description = "Creates a compute pipeline for GPU computation"; + + // Inputs (from ComputePipelineNodeConfig) + descriptor.inputs = { + { + .index = 0, + .name = "Device", + .typeName = "VkDevice", + .nullability = SlotNullability::Required, + .role = SlotRole::Dependency, + .mutability = SlotMutability::ReadOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkDevice"), + .tooltip = "Logical device handle" + }, + { + .index = 1, + .name = "ShaderModule", + .typeName = "VkShaderModule", + .nullability = SlotNullability::Required, + .role = SlotRole::Dependency, + .mutability = SlotMutability::ReadOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkShaderModule"), + .tooltip = "Compiled shader module" + }, + { + .index = 2, + .name = "DescriptorSetLayout", + .typeName = "VkDescriptorSetLayout", + .nullability = SlotNullability::Optional, + .role = SlotRole::Dependency, + .mutability = SlotMutability::ReadOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkDescriptorSetLayout"), + .tooltip = "Descriptor set layout (optional)" + } + }; + + // Outputs + descriptor.outputs = { + { + .index = 0, + .name = "Pipeline", + .typeName = "VkPipeline", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkPipeline"), + .tooltip = "Compute pipeline handle" + }, + { + .index = 1, + .name = "PipelineLayout", + .typeName = "VkPipelineLayout", + .nullability = SlotNullability::Required, + .mutability = SlotMutability::WriteOnly, + .scope = SlotScope::NodeLevel, + .typeColor = GetTypeColor("VkPipelineLayout"), + .tooltip = "Pipeline layout handle" + } + }; + + // Factory + descriptor.factoryFunction = []() -> NodeInstance* { + return new TypedNode(); + }; + + // Visual + descriptor.defaultColor = Color(150, 100, 180); // Purple for pipelines + descriptor.iconPath = "icons/compute_pipeline.png"; + + // Parameters + descriptor.parameters = { + { + .name = "entryPoint", + .type = "std::string", + .defaultValue = std::string("main"), + .description = "Shader entry point function name" + }, + { + .name = "workgroupSizeX", + .type = "uint32_t", + .defaultValue = uint32_t(8), + .description = "Workgroup size in X dimension" + }, + { + .name = "workgroupSizeY", + .type = "uint32_t", + .defaultValue = uint32_t(8), + .description = "Workgroup size in Y dimension" + }, + { + .name = "workgroupSizeZ", + .type = "uint32_t", + .defaultValue = uint32_t(1), + .description = "Workgroup size in Z dimension" + } + }; + + RegisterNodeType(descriptor); +} +``` + +--- + +## Step 4: Type Compatibility Rules as Controller Logic + +The registry also defines how types can connect (controller logic): + +```cpp +class ConfigModelRegistry { +private: + void InitializeTypeCompatibilityRules() { + // Direct type matching + AddCompatibilityRule("VkInstance", "VkInstance", CompatibilityType::Exact); + AddCompatibilityRule("VkDevice", "VkDevice", CompatibilityType::Exact); + AddCompatibilityRule("VkImage", "VkImage", CompatibilityType::Exact); + + // Implicit conversions + AddCompatibilityRule("uint32_t", "int32_t", CompatibilityType::ImplicitCast); + AddCompatibilityRule("float", "double", CompatibilityType::ImplicitCast); + + // PassThroughStorage compatibility + AddCompatibilityRule("PassThroughStorage", "*", CompatibilityType::TypeErasure); + + // Array element compatibility + AddCompatibilityRule("VkImage", "VkImage[]", CompatibilityType::ArrayElement); + + // Struct field extraction + AddCompatibilityRule("CameraData", "Mat4", CompatibilityType::FieldExtraction, "viewMatrix"); + AddCompatibilityRule("CameraData", "Vec3", CompatibilityType::FieldExtraction, "position"); + } + +public: + bool AreTypesCompatible( + const std::string& sourceType, + const std::string& targetType, + CompatibilityReason* outReason = nullptr + ) const { + // Exact match + if (sourceType == targetType) { + if (outReason) *outReason = CompatibilityReason::ExactMatch; + return true; + } + + // Check compatibility rules + auto key = std::make_pair(sourceType, targetType); + auto it = compatibilityRules_.find(key); + if (it != compatibilityRules_.end()) { + if (outReason) *outReason = it->second.reason; + return true; + } + + // PassThroughStorage accepts any type + if (targetType == "PassThroughStorage") { + if (outReason) *outReason = CompatibilityReason::TypeErasure; + return true; + } + + if (outReason) *outReason = CompatibilityReason::Incompatible; + return false; + } + + bool CanConnectSlots(const SlotDescriptor& source, const SlotDescriptor& target) const { + // Type compatibility + if (!AreTypesCompatible(source.typeName, target.typeName)) { + return false; + } + + // Mutability check (can't write to ReadOnly) + if (target.mutability == SlotMutability::ReadOnly) { + return false; + } + + // Role check (Dependency slots must connect before Execute slots) + if (source.role == SlotRole::Execute && target.role == SlotRole::Dependency) { + return false; + } + + // Scope compatibility + if (source.scope != target.scope && target.scope != SlotScope::NodeLevel) { + return false; + } + + return true; + } +}; +``` + +--- + +## Step 5: Auto-Generate Registration from Existing Configs + +To avoid manual translation, create a code generator: + +```cpp +// Tool: ConfigToDescriptorGenerator +// Reads existing node config headers and generates registration functions + +class ConfigToDescriptorGenerator { +public: + void GenerateRegistrationCode(const std::filesystem::path& configHeaderPath) { + // Parse CONSTEXPR_NODE_CONFIG macro + ConfigParseResult parseResult = ParseNodeConfig(configHeaderPath); + + // Generate registration function + std::ofstream output("Generated_" + parseResult.nodeName + "_Registration.cpp"); + + output << "void ConfigModelRegistry::Register" << parseResult.nodeName << "() {\n"; + output << " NodeTypeDescriptor descriptor;\n"; + output << " descriptor.typeName = \"" << parseResult.nodeName << "\";\n"; + output << " descriptor.category = \"" << InferCategory(parseResult) << "\";\n"; + output << "\n"; + + // Generate input slots + output << " // Input slots\n"; + for (const auto& slot : parseResult.inputSlots) { + output << " descriptor.inputs.push_back({\n"; + output << " .index = " << slot.index << ",\n"; + output << " .name = \"" << slot.name << "\",\n"; + output << " .typeName = \"" << slot.typeName << "\",\n"; + output << " .nullability = SlotNullability::" << slot.nullability << ",\n"; + output << " .role = SlotRole::" << slot.role << ",\n"; + output << " .mutability = SlotMutability::" << slot.mutability << ",\n"; + output << " .scope = SlotScope::" << slot.scope << ",\n"; + output << " .typeColor = GetTypeColor(\"" << slot.typeName << "\"),\n"; + output << " .tooltip = \"" << GenerateTooltip(slot) << "\"\n"; + output << " });\n"; + } + + // Generate output slots + output << "\n // Output slots\n"; + for (const auto& slot : parseResult.outputSlots) { + // Similar generation + } + + // Generate factory + output << "\n // Factory function\n"; + output << " descriptor.factoryFunction = []() -> NodeInstance* {\n"; + output << " return new TypedNode<" << parseResult.nodeTypeName + << ", " << parseResult.configName << ">();\n"; + output << " };\n"; + + // Visual metadata + output << "\n // Visual metadata\n"; + output << " descriptor.defaultColor = " << InferColor(parseResult) << ";\n"; + output << " descriptor.iconPath = \"icons/" << parseResult.nodeName << ".png\";\n"; + + output << "\n RegisterNodeType(descriptor);\n"; + output << "}\n"; + } +}; +``` + +### Usage: + +```bash +# Generate registration code for all node configs +./ConfigToDescriptorGenerator \ + --input VIXEN/libraries/RenderGraph/include/Data/Nodes/*.h \ + --output VIXEN/libraries/RenderGraphEditor/src/Generated/NodeRegistrations.cpp +``` + +--- + +## Step 6: Controller Usage Example + +Now that all configs are transformed to descriptors, controllers use them: + +### ConnectionController using Registry + +```cpp +CommandResult ConnectionController::CreateConnection( + NodeHandle sourceHandle, + uint32_t sourceSlotIndex, + NodeHandle targetHandle, + uint32_t targetSlotIndex +) { + // Get nodes + NodeModel* sourceNode = graphModel_->FindNode(sourceHandle); + NodeModel* targetNode = graphModel_->FindNode(targetHandle); + + if (!sourceNode || !targetNode) { + return CommandResult::Error("Node not found"); + } + + // Get slot descriptors + const SlotModel& sourceSlot = sourceNode->GetOutputSlot(sourceSlotIndex); + const SlotModel& targetSlot = targetNode->GetInputSlot(targetSlotIndex); + + // Validate using registry (CONTROLLER POINT) + if (!registry_->CanConnectSlots(sourceSlot, targetSlot)) { + CompatibilityReason reason; + registry_->AreTypesCompatible( + sourceSlot.typeName, + targetSlot.typeName, + &reason + ); + + return CommandResult::Error( + "Cannot connect " + sourceSlot.typeName + " to " + targetSlot.typeName + + " (reason: " + ToString(reason) + ")" + ); + } + + // Create connection + auto connection = std::make_unique( + sourceNode, sourceSlotIndex, + targetNode, targetSlotIndex + ); + + // Register in graph model + graphModel_->AddConnection(std::move(connection)); + + return CommandResult::Success(); +} +``` + +--- + +## Step 7: View Layer Uses Descriptors + +Views query the registry to render node palettes and tooltips: + +```cpp +class ToolbarView { +private: + void RenderNodePalette() { + // Group nodes by category + for (const auto& category : registry_->GetAllCategories()) { + if (ImGui::TreeNode(category.c_str())) { + // Get nodes in this category + auto nodeTypes = registry_->GetNodeTypesByCategory(category); + + for (const auto& nodeTypeName : nodeTypes) { + const NodeTypeDescriptor* descriptor = registry_->GetNodeType(nodeTypeName); + + // Render node button with icon and color + ImGui::PushID(nodeTypeName.c_str()); + + // Color indicator + ImGui::ColorButton("##color", descriptor->defaultColor.ToImVec4(), + ImGuiColorEditFlags_NoTooltip); + ImGui::SameLine(); + + // Node button + if (ImGui::Selectable(nodeTypeName.c_str())) { + // Begin drag-and-drop + BeginNodeDrag(descriptor); + } + + // Tooltip + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", descriptor->description.c_str()); + ImGui::Separator(); + ImGui::Text("Inputs: %zu", descriptor->inputs.size()); + ImGui::Text("Outputs: %zu", descriptor->outputs.size()); + ImGui::EndTooltip(); + } + + ImGui::PopID(); + } + + ImGui::TreePop(); + } + } + } +}; +``` + +--- + +## Complete Transformation Example + +### Before (Compile-Time Only): + +```cpp +// WindowNodeConfig.h +CONSTEXPR_NODE_CONFIG(WindowNodeConfig, 1, 5) { + INPUT_SLOT(INSTANCE, VkInstance, 0, Required, Dependency, ReadOnly, NodeLevel); + OUTPUT_SLOT(SURFACE, VkSurfaceKHR, 0, Required, WriteOnly); + OUTPUT_SLOT(WIDTH, uint32_t, 1, Required, WriteOnly); + OUTPUT_SLOT(HEIGHT, uint32_t, 2, Required, WriteOnly); + OUTPUT_SLOT(WINDOW_HANDLE, void*, 3, Required, WriteOnly); + OUTPUT_SLOT(SHOULD_CLOSE, bool, 4, Required, WriteOnly); +}; + +// Usage (compile-time) +auto windowNode = renderGraph.AddNode("main_window"); +``` + +### After (MCP Architecture): + +```cpp +// Model: Runtime representation +NodeModel* windowNode = graphModel->FindNode("main_window"); +windowNode->GetOutputSlot(0); // SURFACE slot + +// Controller: Validation and operations +nodeController->CreateNode("WindowNode", "main_window", Vec2(100, 100)); +connectionController->CreateConnection(windowNode->GetHandle(), 0, swapChainNode->GetHandle(), 1); + +// View: Visual representation +nodeView->Render(drawList, worldToScreen); +slotView->RenderTooltip(); // Shows "Vulkan surface for presentation" +``` + +**Result:** +- ✅ Compile-time config preserved (no changes to existing code) +- ✅ Runtime descriptor generated (enables GUI) +- ✅ Controller validates using descriptor metadata +- ✅ View renders using descriptor visual hints +- ✅ All 45+ node types become controller points + +--- + +## Summary: Every Config is Now a Controller Point + +| Component | Role | Example | +|-----------|------|---------| +| **Config (Compile-Time)** | Type-safe slot definitions | `WindowNodeConfig` with `INPUT_SLOT` macros | +| **Descriptor (Runtime)** | Model metadata | `NodeTypeDescriptor` with slot metadata | +| **Registry (Controller)** | Validation + Factory | `ConfigModelRegistry::RegisterWindowNode()` | +| **NodeController (Controller)** | CRUD operations | `CreateNode("WindowNode", "main_window")` | +| **ConnectionController (Controller)** | Connection validation | `CanConnectSlots(sourceSlot, targetSlot)` | +| **NodeView (View)** | Visual rendering | Renders node with slots based on descriptor | + +**Every node config is transformed into:** +1. A runtime descriptor (model) +2. A registration function (controller point) +3. Factory function (instantiation controller) +4. Validation rules (connection controller) +5. Visual metadata (view hints) + +This architecture enables **full GUI graph editing** while preserving the existing compile-time type safety and zero-overhead runtime performance.