How the Editor Becomes a Frame

For ArmorPaint, the important runtime question is not simply how a window opens. It is how an application that owns editable project state turns input, material changes, and viewport choices into one synchronized frame. This chapter follows that lifecycle from kickstart through update callbacks, dirty state, render-path commands, and presentation so you can see where each transition is owned—and what would go wrong if the editor and GPU drifted apart.

The first chapter treated ArmorPaint as a connected dataflow. Here, time becomes the missing dimension. Project state does not become visible once and remain fixed. It changes as the user moves a cursor, edits a graph, switches a viewport, drags an asset, plays a timeline, or changes a setting. ArmorPaint’s loop exists to turn those changes into ordered work without making every subsystem independently redraw everything.

Startup establishes the owners of a frame

The process begins in an application kickstart path. ArmorPaint allocates or initializes its global project, context, configuration, and UI-related state. It starts the system layer, creates the initial scene, chooses the render-path resources, and then enters the application’s long-running loop. Plugins may be initialized along the way, but they participate in the same application lifecycle rather than defining a separate outer loop.

That order matters. The application needs a project and context before it can decide which object is paintable or which layer is active. It needs a scene and GPU resources before it can build a visible surface. It needs configuration before it can choose forward or deferred rendering, texture resolution, viewport behavior, or optional features. Initialization is therefore not a collection of independent constructors. It establishes the state that later transitions are allowed to read and change.

The base layer then installs the recurring update callback. At the engine level, the loop initializes threads and the display, invokes ArmorPaint’s kickstart function, runs the update callback, closes the UI frame when needed, and presents the GPU frame. This gives the application a clear ownership boundary: the system owns the outer cadence and presentation, while ArmorPaint owns the meaning of each update and the render commands that produce the frame.

A useful way to see the arrangement is as a series of handoffs:

sequenceDiagram participant S as System loop participant A as ArmorPaint startup participant U as Editor update participant C as Context and dirty state participant R as Render-path commands participant G as GPU present S->>A: Start threads, display, and application A->>C: Create project, configuration, scene, and active state A-->>S: Register update ownership S->>U: Run one update U->>C: Apply input, UI, timeline, camera, and material changes C->>R: Select work from dirty state and viewport mode R->>G: Draw, composite, and present the frame G-->>S: Complete the iteration

The diagram is deliberately less detailed than the source. Its point is ownership. Startup creates the conditions for a valid frame; update changes the conditions; render commands consume them; presentation closes the iteration.

The update callback owns more than input

It is tempting to imagine an update loop as “read the mouse, draw the scene.” ArmorPaint’s update work is broader because many kinds of state can affect the next image. It handles dropped assets and drag-and-drop consequences, updates the camera and compass, advances the timeline or player workspace, updates the 2D view and node editor, runs the main UI, and controls simulation behavior. When the current material contains a live script-driven node, fill layers may need to be refreshed even if the user has not made a conventional brush stroke.

This is a state-propagation problem. Suppose a user changes a material node. The visible mesh may need a new shader context. A fill layer using that material may need to be repainted or recomputed. A preview in the sidebar may need to be regenerated. The UI must redraw its thumbnail and perhaps the node canvas. If these actions happen in arbitrary order, the screen can show a mixture of old and new meanings. The update path gives ArmorPaint a place to coordinate them.

The same principle applies to a project file being opened or a mesh being replaced. Some work is scheduled for the next frame rather than completed at the instant the event is received. That delay is not necessarily a sign that the application has lost control. It can ensure that a file browser operation, GPU draw scope, or resource replacement has finished before dependent work begins. The source repeatedly uses next-frame or next-update scheduling around operations that change render targets, parse materials, resize layers, or initialize sculpt data.

Several pieces of state describe what must be redrawn. The context tracks separate forms of dirtiness for depth, paint, and ray-traced work. The names are implementation details, but the separation expresses a useful rule: a new brush position may require paint work; a changed mesh or camera may require depth work; a change to flattened ray-tracing inputs may require a different rebuild. Treating all changes as the same would be simpler to describe but more expensive and less precise to execute.

The frame counter also matters. Early frames can be used to complete startup tasks in stages: create previews, allocate undo resources, parse mesh and paint materials, and settle the initial dirty state. Staging this work avoids forcing every resource-heavy operation into one initialization moment. The resulting application can become interactive while some derived resources are still being prepared, provided later frames know which state remains dirty.

Consider what happens when the user changes the material immediately after startup. The editor does not need to rebuild every representation at once. It can finish the current update, let the existing draw scope close, schedule material parsing or preview work for a safe boundary, and mark the relevant render data for another pass. The next iteration then consumes the new result. This is a small but important distinction: “next frame” is not the same as “later, if convenient.” It is a controlled transition between resource ownership states.

The same ordering protects ordinary interaction. A camera move changes the view that geometry and paint tests depend on, so the depth-related work must be refreshed before a paint operation can trust it. A layer resize changes the dimensions and formats of textures that material and paint shaders sample, so dependent render targets must be recreated before those shaders are used again. A timeline or player update changes scene state even when no brush is active, so the renderer still needs a path to notice and consume that change. The loop is the place where these different causes become one ordered frame rather than competing redraw requests.

Rendering is a staged consumer of that state

Once the update work has established the current context, ArmorPaint enters a shared render-path lifecycle. The base render path prepares the camera and projection, handles split views when enabled, and coordinates the common order of depth, paint, scene, overlay, and cursor work. It then allows a selected render path to fill in the lighting and presentation behavior.

In a deferred path, the mesh is first drawn into a depth target and multiple G-buffer targets. A G-buffer is a set of intermediate images that stores surface information for later lighting rather than calculating the final color in the first geometry pass. ArmorPaint’s configured deferred path then performs optional screen-space ambient occlusion, deferred lighting, world rendering, compositor work, overlays, and temporal or supersample resolve. The sequence separates “what surface is here?” from “how should this surface look under the current lighting?”

The forward path shares the outer lifecycle but changes the middle. It renders the world and compositor around a forward scene pass rather than running the same deferred-light sequence. This is an important architectural distinction: the application does not need a separate editor loop for each renderer. The shared base path continues to own camera setup, paint timing, split-view handling, and presentation boundaries, while the selected command set changes how the surface is shaded.

The same logic allows a path-traced viewport mode to become another consumer of the project state. The evidence supports the selection and coordination of that mode, but not a claim that every target supports it or that it has a particular speed. The relevant design point is narrower: viewport choice changes which rendering work consumes the current state, while the surrounding application lifecycle remains recognizable.

This is why the render path cannot be understood as a passive display layer. It reads the active project and context, selects resources, may trigger material recompilation, and determines which derived targets must be refreshed. In a PBR application, the frame is an argument about the current state: the geometry, channels, masks, material behavior, environment, and camera must agree before the result is meaningful.

Dirty state is a contract, not a status light

A dirty flag is often treated as a simple “needs redraw” marker. In ArmorPaint, the separate dirty categories act more like contracts between state owners and render consumers. When the context says that depth is dirty, the base path must rebuild the geometric information that later paint and lighting operations read. When paint is dirty, the paint path may write channel targets or update linked layers. When ray-traced inputs are dirty, a path-traced consumer may need to flatten layers or rebuild its acceleration-related data before presenting a stable result.

The contract has two sides. Setting a dirty state too rarely risks stale output: the user changes a mesh or material, but the screen still reflects the old representation. Setting it too broadly can cause needless GPU work, rebuild resources that did not change, or make interactive editing feel slow. The source’s use of separate counters and delayed callbacks is therefore an attempt to preserve both correctness and responsiveness.

The boundary is also visible when a render mode changes. Switching between forward and deferred commands triggers material work because the shader context and its resource expectations can differ. Switching into path tracing can require different derived render state. A mode selector is consequently not just a display preference; it changes which derived representations are valid.

There are failure cases the lifecycle is designed to prevent. A resource can be replaced while a draw scope is still using it. A material can be parsed before the layer data it samples has been resized. A live preview can accidentally overwrite the committed layer. A frame can present before a required G-buffer or derived texture has been refreshed. Next-frame scheduling, render-path boundaries, and dirty-state distinctions all reduce the chance that these transitions are treated as one undifferentiated operation.

That does not make the system immune to bugs. The repository does not establish latency, frame-rate guarantees, or perfect synchronization on every backend. It does show where the application expects synchronization to be managed: at the boundary between context changes, derived resources, render commands, and presentation.

The frame is the first portability boundary

ArmorPaint’s lifecycle also explains why portability must be examined after, not before, the runtime model. The application can keep a common sequence—update state, prepare render work, draw the surface, present the result—while platform-specific code supplies the window, GPU resource, shader, and presentation details. The next question is how that common sequence crosses those platform boundaries without changing the meaning of a painting operation.

A live frame is therefore not the final achievement of the architecture. It is the first proof that the dataflow is coherent in time. The editor has changed state, the renderer has consumed the right version of that state, and the system has presented the result. To understand how ArmorPaint keeps that promise across Windows, Linux, Apple, Android, and the web, we must next examine the shared GPU and build layer—and the feature boundaries it makes visible.

References