How ArmorPaint Manages Undo/Redo Functionality: Circular Buffer Implementation Guide

ArmorPaint implements undo/redo using a circular buffer of undo layers that store snapshots of the active texture or mask after each edit, with the core logic centralized in paint/sources/history.c.

ArmorPaint is an open-source 3D texture painting application built on the Armory engine. Understanding how ArmorPaint manages undo/redo functionality reveals a memory-efficient circular buffer architecture that balances performance with user-configurable history depth, allowing artists to revert complex brush strokes, fill operations, and mask edits without excessive memory overhead.

Core Architecture of the Undo System

The undo system relies on a fixed-size circular buffer that overwrites old states once the configured limit is reached. This approach prevents unbounded memory growth during long painting sessions while maintaining instant access to recent history states.

Circular Buffer Structure

At the heart of the system lies history_undo_layers, an any_array that holds slot_layer_t objects representing individual undo steps. Each slot stores a complete snapshot of either a paint layer or mask at a specific point in time. The buffer size is governed by g_config->undo_steps, a user-configurable setting typically defaulting between 1 and 4 steps to manage GPU memory usage.

The circular behavior is managed through history_undo_i, an integer index that points to the next slot to be overwritten. When the buffer fills, this index wraps around to zero, overwriting the oldest state while preserving the most recent history.

Index and Counter Management

Two critical counters track the system state:

  • history_undos: Tracks how many undo operations are currently available
  • history_redos: Tracks how many redo operations are available after an undo

These counters enable and disable the UI buttons dynamically and prevent invalid operations when reaching buffer boundaries.

Capturing Edit States

Every paint operation—whether brush strokes, fill tools, or mask modifications—triggers the history capture mechanism through a standardized workflow.

The history_copy_to_undo() Function

After any editing operation completes, the code sets history_push_undo = true, triggering history_copy_to_undo() in paint/sources/history.c. This function serializes the current layer state into the circular buffer at the position indicated by history_undo_i.

// Called from any tool after the operation finishes
if (history_push_undo) {
    // Store a snapshot of the current layer (or mask)
    history_copy_to_undo(g_context->layer->id, history_undo_i,
                         slot_layer_is_mask(g_context->layer));
}

The function increments history_undo_i using modulo arithmetic for wrap-around: history_undo_i = (history_undo_i + 1) % g_config->undo_steps. It also increments history_undos (capped at undo_steps) and resets history_redos to zero, indicating that any new edit invalidates previous redo history.

Executing Undo and Redo Operations

The system treats undo and redo as directional movements through the same circular buffer, utilizing the index arithmetic to navigate between historical states.

The history_undo() Function

Located in paint/sources/history.c, this function handles both undo and redo logic by manipulating the circular index and restoring texture data from history_undo_layers.

void history_undo() {
    if (history_undos > 0) {
        // Move index back to the previous slot with wrap-around
        history_undo_i = history_undo_i - 1 < 0
            ? g_config->undo_steps - 1
            : history_undo_i - 1;

        // Retrieve the stored layer and apply it to the active texture
        slot_layer_t *lay = history_undo_slot(history_undo_i);
        sys_notify_on_next_frame(&history_undo_delete_layer_group, NULL);
        // ... (mask handling, gizmo updates, etc.)

        history_undos--;
        history_redos++;
    }
}

The function first validates that undos are available, then decrements the circular index (with bounds checking), retrieves the stored layer snapshot via history_undo_slot(), and copies the texture data back onto the active render target.

How Redo Works

Unlike many applications that implement separate undo and redo stacks, ArmorPaint uses a bidirectional approach. Redo functionality is not a separate function; instead, it utilizes the same history_undo() mechanism by moving forward through the buffer when history_redos > 0.

When the user triggers an undo, the system increments history_redos and decrements history_undos. A subsequent undo call while history_redos > 0 effectively functions as a redo, navigating back to newer states in the circular buffer. The UI enables the Redo button based on the history_redos counter value.

User Interface and Input Integration

The undo system integrates deeply with the input handling and menu systems to provide immediate feedback and accessibility.

Shortcut Handling

The util_shortcut_undo_redo() function in paint/sources/util/util_shortcut.c captures input events and routes them to the history system:

void util_shortcut_undo_redo() {
    bool undo_pressed = keymap_shortcut(any_map_get(g_keymap, "edit_undo"),
                                        SHORTCUT_TYPE_STARTED);
    // Two-finger tap also triggers undo
    if (mouse_released("right") && sys_time() - util_shortcut_undo_tap_time < 0.1) {
        undo_pressed = true;
    }
    if (undo_pressed) {
        history_undo();   // Executes the undo logic
    }
}

This handler supports both keyboard shortcuts mapped to "edit_undo" and touch gestures (two-finger tap), making the functionality accessible across desktop and tablet interfaces.

The paint/sources/ui/ui_menubar.c file manages the visual state of undo/redo buttons, dynamically constructing labels that show the specific layer name being reverted:

any_map_t *vars_undo = any_map_create();
any_map_set(vars_undo, "step",
    string_copy(history_steps->buffer[history_steps->length - 1 -
        history_redos]->name));
if (ui_menu_button(vtr("Undo {step}", vars_undo), any_map_get(g_keymap,
    "edit_undo"), ICON_UNDO)) {
    history_undo();
}
map_free(vars_undo);

The buttons enable or disable automatically based on the history_undos and history_redos counters, preventing user interaction when no history states are available in the respective direction.

Configuration and Render Target Management

Configurable Undo Steps

Users control history depth through Preferences → Undo Steps, implemented in paint/sources/ui/box_preferences.c. The g_config->undo_steps variable determines the array size of history_undo_layers. When users reduce the step count, the system automatically pops excess slots from the array to free GPU memory.

Render Target Architecture

All undo operations manipulate dedicated render textures created in paint/sources/render/render_path_paint.c. The system maintains parallel undo textures for different material channels:

  • texpaint_undo (base color/id)
  • texpaint_nor_undo (normal maps)
  • texpaint_pack_undo (ORM packing)
  • texpaint_sculpt_undo (sculpting data)

These textures are swapped in and out of the active framebuffer during history_undo() calls, ensuring that undo operations restore the complete material state rather than just color information.

Summary

  • ArmorPaint uses a circular buffer (history_undo_layers) of fixed size determined by g_config->undo_steps to store texture snapshots, preventing unlimited memory growth.
  • The history_undo() function in paint/sources/history.c handles both undo and redo by navigating bidirectionally through the buffer using the history_undo_i index.
  • State capture occurs via history_copy_to_undo(), triggered after every paint operation by the history_push_undo flag.
  • Input integration spans util_shortcut.c for keyboard/touch handling and ui_menubar.c for dynamic button states that display the target layer name.
  • GPU resources are managed through dedicated render targets (texpaint_undo*) that store complete material channel data for each history step.

Frequently Asked Questions

How many undo steps can ArmorPaint store?

ArmorPaint stores between 1 and 4 undo steps by default, depending on the g_config->undo_steps configuration set in Preferences. Each step represents a complete snapshot of the active texture layer or mask. Users can adjust this limit in box_preferences.c, though higher values consume additional GPU memory proportional to texture resolution and layer complexity.

Why does ArmorPaint use a circular buffer instead of a linear stack?

The circular buffer design in history.c prevents memory exhaustion during extended painting sessions. By overwriting the oldest state when history_undo_i wraps around, the system maintains constant memory usage regardless of session duration. This approach is critical for 4K/8K texture painting where each undo layer may consume hundreds of megabytes of VRAM.

Can developers trigger undo programmatically from custom tools?

Yes. Custom tools can invoke history_copy_to_undo() immediately after modifying a layer by setting history_push_undo = true and calling the function with the current layer ID and mask status. To trigger an undo operation programmatically, simply call history_undo() from paint/sources/history.c, which will handle the buffer navigation and texture restoration automatically.

Does the undo system support layer masks separately from paint layers?

Yes. The history_copy_to_undo() function accepts a boolean parameter (slot_layer_is_mask(g_context->layer)) that determines whether the snapshot captures paint data or mask data. The system stores masks in the same history_undo_layers array but handles them as distinct slot_layer_t types, allowing independent undo of mask edits without affecting underlying color information.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →