ArmorPaint ui_nodes.c Internal Architecture: How the Node Graph Drives Material Evaluation

The ui_nodes.c and util_nodes.c files in ArmorPaint implement a model-view-controller pattern where util_nodes.c manages the node graph data model and material recompilation triggers, while the UI layer captures input and delegates to material parsing functions that traverse the graph to generate shader code.

ArmorPaint's node-based material system relies on a tight integration between the visual node editor and the real-time material evaluation pipeline. The internal architecture spans across util_nodes.c—which serves as the core backend referred to as uinodes in the source—and ui_nodes.c, which handles UI event delegation. Together, these components manage the node graph state, track changes for undo/redo, and dispatch shader recompilation when the graph topology changes.

Core Architecture Components

The node editor's backend in util_nodes.c organizes functionality into three primary domains: graph storage, node grouping, and change management.

The Canvas and Graph Model

At the heart of the system lies the canvas structure that persists the node graph for materials and brushes. The ui_node_canvas_t structure contains dynamic arrays of ui_node_t (nodes) and ui_node_link_t (connections), while ui_nodes_t tracks editor state including selection, the undo stack, and preview flags.

Key accessor functions retrieve the active context:

  • ui_nodes_get_canvas() – Returns the active canvas for the current material, brush, or nested group.
  • ui_nodes_get_nodes() – Retrieves the active ui_nodes_t instance based on the current canvas type.
/* Example: Accessing the active canvas to add a new node */
ui_node_t *new_node = ui_nodes_make_node(template_node, ui_nodes_get_nodes(), ui_nodes_get_canvas(true));
any_array_push(ui_nodes_get_nodes()->nodes, new_node);

Node Grouping and Nested Sub-Graphs

ArmorPaint supports hierarchical node groups through the node_group_t structure, which owns an isolated ui_node_canvas_t for its sub-graph. The system maintains a ui_nodes_group_stack to track nested group editing contexts.

Critical validation functions ensure graph integrity:

  • ui_nodes_make_group_node() – Constructs a GROUP node from a material group definition.
  • ui_nodes_traverse_group() – Recursively discovers all nested groups within a canvas.
  • ui_nodes_can_place_group() – Prevents recursive group insertion that would create cycles.

The Material Evaluation Pipeline

The connection between the visual graph and the rendered material follows a deferred recompilation pattern. Changes are flagged immediately but processed during the engine's main loop to batch updates and prevent redundant shader regeneration.

Change Detection and Recompilation Flags

When a user modifies the graph—adding nodes, changing connections, or adjusting parameters—the UI layer calls ui_nodes_canvas_changed(). This sets the ui_nodes_recompile_mat boolean flag, signaling that the material requires reprocessing. A secondary flag, ui_nodes_recompile_mat_final, triggers after compilation to update dependent layers like fill or path layers.

/* Example: Flagging material for recompilation after edit */
any_array_push(ui_nodes_get_canvas(true)->nodes, new_node);
ui_nodes_canvas_changed();   // Sets ui_nodes_recompile_mat = true

Shader Code Generation

The ui_nodes_recompile() function serves as the central dispatcher. When invoked each frame, it checks the recompilation flag and initiates the parsing phase:

  1. Switches the active context (g_context->material) to the currently edited material.
  2. Invokes material-specific parsing functions such as make_material_parse_brush() or make_material_parse_paint_material().
  3. These functions traverse the node graph via ui_nodes_get_nodes() to generate shader code.

This parsing occurs in the material-parsing module (typically util_material.c or similar), which translates the visual graph into executable GPU shaders.

Preview Texture Updates

After shader compilation, the system generates visual feedback through two pathways:

  • Material Previews: util_render_make_material_preview() renders the full material to the viewport.
  • Node Previews: Individual node thumbnails are updated via util_render_make_node_preview(), which renders isolated outputs for each node.

Node-specific previews are cached in g_context->node_preview_map, mapping node IDs to their texture resources.

/* Example: Generating a preview for a selected node */
ui_node_t *selected = ui_get_node(active_canvas->nodes, selected_id);
ui_nodes_make_node_preview(selected);   // Updates cached preview texture

Integration Flow: From User Input to Rendered Preview

The complete data flow from interaction to evaluation follows these stages:

  1. User Interaction – The UI layer in ui_nodes.c detects input and calls util_nodes.c functions to modify the active canvas (g_context->material->canvas or g_context->brush->canvas).

  2. State Recording – ui_nodes_push_undo() records the current canvas state onto the undo stack before modifications occur.

    /* Example: Recording state for undo */
    ui_node_canvas_t *last = NULL;
    ui_nodes_push_undo(last);
  3. Recompilation Trigger – ui_nodes_canvas_changed() sets ui_nodes_recompile_mat = true.

  4. Shader Rebuild – The main loop invokes ui_nodes_recompile(), which calls the appropriate make_material_parse_* function to traverse the graph and generate shader code.

  5. Render Update – Post-compilation, preview textures refresh through util_render_make_node_preview() and viewport updates.

Summary

  • Architecture Pattern: util_nodes.c implements a model-view-controller structure where ui_node_canvas_t stores the graph model, preview utilities handle the view, and recompilation flags manage the controller logic.
  • File Responsibilities: util_nodes.c contains the core data structures and evaluation logic, while ui_nodes.c forwards UI events to the backend.
  • Key Structures: The system centers on ui_node_canvas_t (graph storage), ui_nodes_t (editor state), and node_group_t (hierarchical grouping).
  • Evaluation Trigger: The ui_nodes_recompile_mat flag bridges user edits to shader generation, parsed by make_material_parse_* functions that traverse the active canvas.
  • Preview System: Node previews map to GPU textures via g_context->node_preview_map, updated through util_render_make_node_preview().

Frequently Asked Questions

What is the difference between ui_nodes.c and util_nodes.c in ArmorPaint?

util_nodes.c (referred to as uinodes internally) contains the core node graph implementation including data structures (ui_node_canvas_t), grouping logic, and material recompilation dispatch. ui_nodes.c serves as the UI event layer that captures user input and delegates actions to the functions defined in util_nodes.c.

How does ArmorPaint prevent recursive node group insertion?

The system validates group placement through ui_nodes_can_place_group(), which checks the ui_nodes_group_stack and existing node hierarchy to detect potential cycles before allowing a group to be nested within itself or its descendants.

What triggers a material recompilation in the node editor?

Any modification to the graph topology or node parameters calls ui_nodes_canvas_changed(), which sets the ui_nodes_recompile_mat flag to true. The engine checks this flag each frame via ui_nodes_recompile() to determine if the material parser needs to regenerate shader code.

How are node preview textures generated and stored?

Individual node previews are created through ui_nodes_make_node_preview(), which renders the node's output to a texture cached in g_context->node_preview_map using the node's ID as the key. These textures are updated when the node graph recompiles or when specific nodes are selected for inspection.

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 →