Files
deargui-vpl/examples/blueprints-example/utilities/UUID_DUAL_ID_SYSTEM.md
T

13 KiB

Dual-ID System: UUIDs + Runtime IDs

The Problem

imgui-node-editor uses dynamic integer IDs for fast runtime access:

  • ed::NodeId → int (e.g., 1, 2, 3, ...)
  • ed::LinkId → int
  • ed::PinId → int

These IDs are:

  • Fast: Direct array/map indexing
  • Efficient: No string hashing
  • Dynamic: Change between sessions
  • Unstable: Not suitable for save/load

We need persistent IDs for save/load that remain stable across sessions.

The Solution: Dual-ID System

Each entity (Node/Link/Pin) has TWO IDs:

Type Used For Storage Stability
UUID (ed::Uuid64) Save/Load (persistent) JSON files Stable across sessions
Runtime ID (int/ed::NodeId) Runtime lookups (fast) Memory only Changes each session
struct Node {
    ed::NodeId ID;      // Runtime ID (dynamic) - for imgui-node-editor
    ed::Uuid64 UUID;    // Persistent ID (stable) - for save/load
    // ... other fields
};

Architecture

┌─────────────────────────────────────────────────────────────┐
│                         Application                          │
│                                                              │
│  ┌────────────────┐                ┌────────────────────┐  │
│  │  UuidIdManager │◄──────────────►│   Node/Link/Pin    │  │
│  │                │                │    Structures      │  │
│  │  UUID ↔ ID Map │                │                    │  │
│  └────────────────┘                │  - UUID (persist)  │  │
│         ▲                           │  - ID (runtime)    │  │
│         │                           └────────────────────┘  │
│         │                                     │              │
│         │                                     ▼              │
│  ┌──────┴────────────┐         ┌───────────────────────┐  │
│  │   Save/Load       │         │  imgui-node-editor    │  │
│  │  (uses UUIDs)     │         │  (uses Runtime IDs)   │  │
│  └───────────────────┘         └───────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Workflow

1. Creating New Nodes (Runtime)

// Generate runtime ID (from container ID generator)
int runtimeId = container->GetNextId();

// Generate persistent UUID
ed::Uuid64 uuid = app->m_UuidIdManager.GenerateUuid();

// Create node with both IDs
Node newNode(runtimeId, "MyNode");
newNode.UUID = uuid;

// Register mapping: UUID ↔ Runtime ID
app->m_UuidIdManager.RegisterNode(uuid, runtimeId);

// imgui-node-editor uses Runtime ID
ed::BeginNode(newNode.ID);  // Uses runtimeId

2. Saving Graph (Serialize UUIDs)

void SaveGraph(const std::string& filename)
{
    crude_json::value root;
    
    for (auto* node : nodes)
    {
        crude_json::value nodeData;
        
        // ✅ SAVE UUID (persistent, stable)
        nodeData["uuid_high"] = (double)node->UUID.high;
        nodeData["uuid_low"] = (double)node->UUID.low;
        
        // ❌ DON'T save runtime ID (changes each session)
        // nodeData["id"] = node->ID.Get();  // WRONG!
        
        nodeData["name"] = node->Name;
        nodeData["type"] = node->BlockType;
        
        // Save pins (also use UUIDs)
        for (auto& pin : node->Inputs)
        {
            crude_json::value pinData;
            pinData["uuid_high"] = (double)pin.UUID.high;
            pinData["uuid_low"] = (double)pin.UUID.low;
            pinData["name"] = pin.Name;
            // ... etc
        }
        
        root["nodes"].push_back(nodeData);
    }
    
    // Save links with PIN UUIDs (not runtime pin IDs!)
    for (auto* link : links)
    {
        crude_json::value linkData;
        
        linkData["uuid_high"] = (double)link->UUID.high;
        linkData["uuid_low"] = (double)link->UUID.low;
        
        // Find start/end pins and save their UUIDs
        Pin* startPin = FindPin(link->StartPinID);
        Pin* endPin = FindPin(link->EndPinID);
        
        linkData["start_pin_uuid_high"] = (double)startPin->UUID.high;
        linkData["start_pin_uuid_low"] = (double)startPin->UUID.low;
        linkData["end_pin_uuid_high"] = (double)endPin->UUID.high;
        linkData["end_pin_uuid_low"] = (double)endPin->UUID.low;
        
        root["links"].push_back(linkData);
    }
}

3. Loading Graph (Resolve UUIDs → Generate New Runtime IDs)

void LoadGraph(const std::string& filename)
{
    // Clear old mappings
    m_UuidIdManager.Clear();
    
    crude_json::value root = crude_json::value::parse(fileData);
    
    // ========== Load Nodes ==========
    for (auto& nodeData : root["nodes"])
    {
        // ✅ Load UUID (persistent)
        ed::Uuid64 uuid(
            (uint32_t)nodeData["uuid_high"].get<double>(),
            (uint32_t)nodeData["uuid_low"].get<double>()
        );
        
        // Generate NEW runtime ID (don't reuse old IDs!)
        int runtimeId = container->GetNextId();
        
        // Create node
        Node newNode(runtimeId, nodeData["name"].get<std::string>().c_str());
        newNode.UUID = uuid;
        newNode.BlockType = nodeData["type"].get<std::string>();
        
        // Build node structure (adds pins)
        BuildNodeStructure(newNode);
        
        // Load pin UUIDs and register them
        for (size_t i = 0; i < newNode.Inputs.size(); ++i)
        {
            ed::Uuid64 pinUuid(
                (uint32_t)nodeData["inputs"][i]["uuid_high"].get<double>(),
                (uint32_t)nodeData["inputs"][i]["uuid_low"].get<double>()
            );
            newNode.Inputs[i].UUID = pinUuid;
            
            // Register pin mapping
            m_UuidIdManager.RegisterPin(pinUuid, newNode.Inputs[i].ID.Get());
        }
        
        // Add to container
        Node* addedNode = container->AddNode(newNode);
        
        // ✅ Register mapping: UUID → Runtime ID
        m_UuidIdManager.RegisterNode(uuid, runtimeId);
    }
    
    // ========== Load Links ==========
    for (auto& linkData : root["links"])
    {
        // Load link UUID
        ed::Uuid64 linkUuid(
            (uint32_t)linkData["uuid_high"].get<double>(),
            (uint32_t)linkData["uuid_low"].get<double>()
        );
        
        // Load start/end pin UUIDs
        ed::Uuid64 startPinUuid(
            (uint32_t)linkData["start_pin_uuid_high"].get<double>(),
            (uint32_t)linkData["start_pin_uuid_low"].get<double>()
        );
        ed::Uuid64 endPinUuid(
            (uint32_t)linkData["end_pin_uuid_high"].get<double>(),
            (uint32_t)linkData["end_pin_uuid_low"].get<double>()
        );
        
        // ✅ Resolve pin UUIDs → Runtime IDs
        int startPinRuntimeId = m_UuidIdManager.GetPinRuntimeId(startPinUuid);
        int endPinRuntimeId = m_UuidIdManager.GetPinRuntimeId(endPinUuid);
        
        if (startPinRuntimeId < 0 || endPinRuntimeId < 0)
        {
            printf("[LoadGraph] WARNING: Link references missing pin\n");
            continue;  // Skip orphaned link
        }
        
        // Generate new runtime ID for link
        int linkRuntimeId = container->GetNextId();
        
        // Create link with resolved runtime pin IDs
        Link newLink(ed::LinkId(linkRuntimeId), 
                     ed::PinId(startPinRuntimeId),
                     ed::PinId(endPinRuntimeId));
        newLink.UUID = linkUuid;
        
        // Add link
        Link* addedLink = container->AddLink(newLink);
        
        // Register link mapping
        m_UuidIdManager.RegisterLink(linkUuid, linkRuntimeId);
    }
}

Key Principles

DO:

  1. Generate UUIDs on creation (not on save!)

    Node* SpawnNode() {
        int runtimeId = GetNextId();
        ed::Uuid64 uuid = m_UuidIdManager.GenerateUuid();  // ✅ Assign immediately
        Node node(runtimeId, "Name");
        node.UUID = uuid;
        m_UuidIdManager.RegisterNode(uuid, runtimeId);
        return addedNode;
    }
    
  2. Save UUIDs (persistent identifiers)

    nodeData["uuid_high"] = node->UUID.high;
    nodeData["uuid_low"] = node->UUID.low;
    
  3. Load UUIDs, generate new runtime IDs

    ed::Uuid64 uuid = LoadUuidFromJson(nodeData);
    int newRuntimeId = container->GetNextId();  // Fresh ID!
    m_UuidIdManager.RegisterNode(uuid, newRuntimeId);
    
  4. Link via UUIDs in save files (not runtime IDs)

    linkData["start_pin_uuid"] = startPin->UUID;  // ✅
    

DON'T:

  1. Don't save runtime IDs

    nodeData["id"] = node->ID.Get();  // ❌ Will change next session!
    
  2. Don't reuse runtime IDs from save file

    int runtimeId = nodeData["id"].get<int>();  // ❌ Conflicts!
    
  3. Don't reference runtime IDs in save files

    linkData["start_pin_id"] = link->StartPinID.Get();  // ❌ Unstable!
    
  4. Don't forget to register mappings

    Node node(runtimeId, "Name");
    node.UUID = uuid;
    // m_UuidIdManager.RegisterNode(uuid, runtimeId);  // ❌ FORGOT!
    

Benefits

Feature Without UUIDs With Dual-ID System
Runtime Performance Fast Fast (uses runtime IDs)
Save/Load Stability Breaks on ID changes Stable across sessions
Node Reference Integrity Can break Preserved
Merge Graphs ID conflicts UUIDs unique
Debugging IDs change UUIDs stable

Migration Strategy

Phase 1: Add UUID fields ( DONE)

  • Added UUID to Node, Link, Pin structures
  • Created UuidIdManager class

Phase 2: Generate UUIDs on creation

  • Modify SpawnBlockNode() to generate UUIDs
  • Modify SpawnParameterNode() to generate UUIDs
  • Register all UUID ↔ Runtime ID mappings

Phase 3: Update Save logic

  • Save UUIDs instead of runtime IDs
  • Use pin UUIDs for link serialization

Phase 4: Update Load logic

  • Load UUIDs from file
  • Generate fresh runtime IDs
  • Populate UUID ↔ Runtime ID mappings
  • Resolve pin UUIDs for links

Phase 5: Backward compatibility (optional)

  • Detect old format (has runtime IDs)
  • Auto-generate UUIDs for old saves
  • Resave in new format

Example: Complete Node Lifecycle

// ========== CREATION ==========
// (Happens in SpawnBlockNode)
int runtimeId = container->GetNextId();           // e.g., 5
ed::Uuid64 uuid = m_UuidIdManager.GenerateUuid(); // e.g., {0x1000000, 0x0}
Node node(runtimeId, "Add");
node.UUID = uuid;
m_UuidIdManager.RegisterNode(uuid, runtimeId);

// ========== RUNTIME ==========
// imgui-node-editor uses runtime ID
ed::BeginNode(node.ID);  // Uses 5
ed::EndNode();

// ========== SAVE ==========
nodeData["uuid_high"] = 0x1000000;
nodeData["uuid_low"] = 0x0;
// Runtime ID (5) is NOT saved

// ========== LOAD (Next Session) ==========
ed::Uuid64 loadedUuid(0x1000000, 0x0);  // Same as before ✅
int newRuntimeId = container->GetNextId();  // Could be 12 now!
Node newNode(newRuntimeId, "Add");
newNode.UUID = loadedUuid;
m_UuidIdManager.RegisterNode(loadedUuid, newRuntimeId);

// Now:
// - Node has NEW runtime ID (12)
// - Node has SAME UUID ({0x1000000, 0x0})
// - imgui-node-editor uses 12
// - Next save will use UUID again

FAQ

Q: Why not use UUIDs directly as imgui-node-editor IDs?
A: imgui-node-editor expects int IDs for performance. Converting Uuid64 to int would lose information or require hashing (slow).

Q: What happens if I forget to assign a UUID?
A: Node will have UUID{0,0} (invalid). Detect with uuid.IsValid(). Save will fail or generate UUID on-the-fly.

Q: Can I use random UUIDs instead of sequential?
A: Yes! Change GenerateUuid() to use GenerateRandom64(). Sequential is better for debugging.

Q: How do I migrate existing save files?
A: Detect old format (has "id" field), auto-generate UUIDs, register mappings, resave.

Q: Do pins need UUIDs too?
A: Yes! Links reference pins by UUID. Otherwise, link loading will break.

Summary

  • Two IDs per entity: UUID (persistent) + Runtime ID (dynamic)
  • Save UUIDs to JSON (stable across sessions)
  • Generate new runtime IDs on load (avoid conflicts)
  • UuidIdManager maintains UUID ↔ Runtime ID mapping
  • imgui-node-editor uses runtime IDs (no changes needed)
  • Result: Stable save files + fast runtime performance