How Pyrite64's Undo/Redo System Manages Scene State and History Internally
Pyrite64 implements undo/redo by serializing the entire scene state into JSON snapshots stored in a dual-stack history system, enabling full scene restoration while tracking object selection changes.
Pyrite64 is an open-source Nintendo 64 homebrew engine and editor that provides robust tooling for scene manipulation. Understanding how its undo/redo system internally manages state is crucial for developers extending the editor or debugging history-related issues. The system operates at the full-scene level, capturing complete serialized states rather than individual command objects.
Core Architecture and Data Structures
The undo/redo implementation resides in src/editor/undoRedo.h and src/editor/undoRedo.cpp, centered around two primary structures: the Entry snapshot container and the History manager class.
The Entry Structure
Each Editor::UndoRedo::Entry represents a single point in history containing:
state: A serialized JSON string of the entire scene (captured viascene->serialize(true))description: A human-readable label for UI display (e.g., "Moved selected object")selection: A vector of selected object UUIDs to restore the user's context
The Entry class also provides getMemoryUsage() to report its individual memory footprint for diagnostics.
The History Class
The History class maintains the state machine through several critical members:
undoStackandredoStack: Vectors ofunique_ptr<Entry>storing past and future statesmaxHistorySize: Configurable limit to prevent unbounded memory growthsnapshotSceneandsnapshotSelUUID: Pointers tracking the "in-flight" modification state betweenbegin()andend()callsnextChangedReason: String buffer holding the description set bymarkChanged()
The Undo/Redo Lifecycle
The system follows a transactional pattern where modifications are bracketed by begin() and end() calls, with explicit change notification in between.
Starting a Modification with begin()
When History::begin() is invoked:
- The system retrieves the currently loaded scene via
ctx.project->getScenes().getLoadedScene() - If the undo stack is empty, it automatically stores an "Initial State" snapshot to ensure the user can return to the original configuration
- It saves a pointer to the scene (
snapshotScene) and the currently selected object UUID (snapshotSelUUID)
This captures the baseline state before any modifications occur.
Capturing Changes with markChanged()
During the edit operation, the editor calls History::markChanged("description") to set nextChangedReason. This labels the upcoming snapshot with a human-readable description for the undo menu. If end() is called without markChanged(), no entry is created, filtering out no-op operations.
Finalizing with end()
The History::end() method completes the transaction:
- Validates that
nextChangedReasonis set; otherwise returns early - Clears the
redoStack(new actions invalidate previous redo history) - Serializes the current scene state using
scene->serialize(true) - Creates a new
Entrywith the serialized state, description, and current selection UUIDs - Compares the new state against the previous stack entry to avoid duplicate consecutive snapshots
- Pushes the entry onto
undoStackand enforcesmaxHistorySizeby trimming old entries
Executing Undo and Redo Operations
Undo (History::undo()):
- Pops the top entry from
undoStack(the state to revert from) and pushes it toredoStack - Takes the new top of
undoStack(the previous state) and deserializes it intosnapshotSceneusingsnapshotScene->deserialize(prevCmd->state) - Restores the selection by finding the first UUID from
prevCmd->selectionthat still exists in the scene
Redo (History::redo()):
- Pops the top entry from
redoStack, deserializes its state into the scene, and pushes it back ontoundoStack - Restores the selection similarly to the undo operation
Memory Management and Limits
The system implements explicit memory accounting to prevent unbounded growth during long editing sessions. Each Entry implements getMemoryUsage() to report its string size and overhead. The History class aggregates this via getMemoryUsage() to provide total consumption of both stacks.
The maxHistorySize configuration acts as a hard limit. When the undo stack exceeds this count, the oldest entries are removed from the bottom of the stack. This trades infinite history for predictable memory usage, essential for an editor handling large Nintendo 64 scenes.
Implementation Example
The following pattern demonstrates the standard integration used throughout Pyrite64's editor tools:
// Begin a user-driven edit operation
Editor::UndoRedo::getHistory().begin();
// ... user manipulates objects, calls various editor actions ...
// Signal that a change occurred, with a description for the UI
Editor::UndoRedo::getHistory().markChanged("Moved selected object");
// End the operation – snapshot is stored automatically
Editor::UndoRedo::getHistory().end();
// Later, UI button triggers undo
if (Editor::UndoRedo::getHistory().canUndo())
Editor::UndoRedo::getHistory().undo();
// Redo button
if (Editor::UndoRedo::getHistory().canRedo())
Editor::UndoRedo::getHistory().redo();
Summary
- Pyrite64's undo/redo system operates on complete scene serialization, storing JSON snapshots rather than command objects.
- The
Historyclass insrc/editor/undoRedo.hmanages two stacks (undoStackandredoStack) with configurable limits viamaxHistorySize. - Transactions bracket modifications using
begin(),markChanged(), andend()to capture state changes with human-readable descriptions. - Undo and redo operations deserialize stored snapshots back into the live scene via
Project::Scene::deserialize()and restore previous object selections. - Explicit memory accounting through
getMemoryUsage()enables monitoring and automatic pruning of old history entries.
Frequently Asked Questions
How does Pyrite64's undo/redo system handle memory usage for large scenes?
The system tracks memory consumption explicitly through the Entry::getMemoryUsage() method, which reports the size of the serialized JSON state string. The History class aggregates this across both stacks via getMemoryUsage() and enforces a hard limit using maxHistorySize. When the undo stack exceeds this limit, the oldest entries are automatically removed from the bottom, ensuring memory usage remains predictable even when editing complex Nintendo 64 scenes with numerous objects.
What happens if an editor action does not call markChanged() before end()?
If History::end() is called without a preceding markChanged() call, the system returns early and does not create a history entry. This behavior acts as a filter to prevent no-op operations or cancelled actions from polluting the undo stack with meaningless snapshots. The nextChangedReason string must be populated with a description for the entry to be committed, ensuring every recorded state has a human-readable label for the UI.
How does the system restore object selection during undo operations?
When performing an undo, the system retrieves the selection vector from the target Entry, which contains the UUIDs of objects that were selected at that point in history. It then iterates through these UUIDs and restores the first one that still exists in the current scene. This approach gracefully handles cases where objects have been deleted since the snapshot was taken, falling back to the next valid UUID to maintain user context during the undo/redo workflow.
Why does Pyrite64 serialize the entire scene rather than using command objects?
Pyrite64 uses full scene serialization (storing complete JSON snapshots via scene->serialize(true)) rather than command objects because it provides a simpler, more robust mechanism for an editor handling complex hierarchical scenes. This approach eliminates the need to implement inverse operations for every possible edit action and ensures that any type of modification—whether to object transforms, materials, or scene hierarchy—can be captured and restored consistently. The trade-off is higher memory usage per entry, which is mitigated by the configurable maxHistorySize limit and explicit memory tracking.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →