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

> Discover how ArmorPaint uses a circular buffer to manage undo redo functionality. Learn about the core logic in the history.c file for seamless texture editing.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: how-to-guide
- Published: 2026-09-13

---

**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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/history.c). This function serializes the current layer state into the circular buffer at the position indicated by `history_undo_i`.

```c
// 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`](https://github.com/armory3d/armorpaint/blob/main/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`.

```c
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`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/util/util_shortcut.c) captures input events and routes them to the history system:

```c
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.

### Menu Bar Integration

The [`paint/sources/ui/ui_menubar.c`](https://github.com/armory3d/armorpaint/blob/main/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:

```c
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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/util_shortcut.c) for keyboard/touch handling and [`ui_menubar.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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.