# How Pyrite64's Undo/Redo System Manages Scene State and History Internally

> Explore Pyrite64's undo redo system. Learn how it serializes scene state to JSON snapshots in a dual stack history for full scene restoration and object selection tracking.

- Repository: [Max Bebök/pyrite64](https://github.com/hailtododongo/pyrite64)
- Tags: internals
- Published: 2026-02-19

---

**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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/editor/undoRedo.h) and [`src/editor/undoRedo.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/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 via `scene->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:

- **`undoStack`** and **`redoStack`**: Vectors of `unique_ptr<Entry>` storing past and future states
- **`maxHistorySize`**: Configurable limit to prevent unbounded memory growth
- **`snapshotScene`** and **`snapshotSelUUID`**: Pointers tracking the "in-flight" modification state between `begin()` and `end()` calls
- **`nextChangedReason`**: String buffer holding the description set by `markChanged()`

## 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:

1. The system retrieves the currently loaded scene via `ctx.project->getScenes().getLoadedScene()`
2. If the undo stack is empty, it automatically stores an "Initial State" snapshot to ensure the user can return to the original configuration
3. 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:

1. Validates that `nextChangedReason` is set; otherwise returns early
2. Clears the `redoStack` (new actions invalidate previous redo history)
3. Serializes the current scene state using `scene->serialize(true)`
4. Creates a new `Entry` with the serialized state, description, and current selection UUIDs
5. Compares the new state against the previous stack entry to avoid duplicate consecutive snapshots
6. Pushes the entry onto `undoStack` and enforces `maxHistorySize` by 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 to `redoStack`
- Takes the new top of `undoStack` (the previous state) and deserializes it into `snapshotScene` using `snapshotScene->deserialize(prevCmd->state)`
- Restores the selection by finding the first UUID from `prevCmd->selection` that still exists in the scene

**Redo** (`History::redo()`):
- Pops the top entry from `redoStack`, deserializes its state into the scene, and pushes it back onto `undoStack`
- 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:

```cpp
// 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 `History` class in [`src/editor/undoRedo.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/editor/undoRedo.h) manages two stacks (`undoStack` and `redoStack`) with configurable limits via `maxHistorySize`.
- Transactions bracket modifications using `begin()`, `markChanged()`, and `end()` 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.