One Painting System Across Many GPUs
ArmorPaint’s next architectural problem is portability: how can the same editable painting model reach Windows, Linux, Apple devices, Android, and the web without pretending those platforms have the same GPU or operating-system behavior? This chapter examines the boundary between ArmorPaint’s application logic and the base/Iron layer beneath it. The goal is practical: distinguish the shared contract from the target-specific implementation, then see why build tools and feature gates are part of the product rather than installation details.
The previous chapter followed one frame through startup, update, render commands, and presentation. That sequence can stay stable only if its lower layers provide stable meanings for operations such as “create a render target,” “bind a shader,” “draw,” or “present.” The platform layer can change how those operations are implemented, but it cannot change what an ArmorPaint paint operation means without breaking the dataflow above it.
The stable contract is smaller than a platform
ArmorPaint does not write its editor logic separately for every graphics API. The base project selects platform files for system services, windows, threads, networking, file dialogs, audio, and GPU work. The application project then adds ArmorPaint’s own source and assets on top. This arrangement lets a paint operation ask for shared concepts instead of directly asking Vulkan, Direct3D, Metal, or WebGPU for their private objects.
The shared GPU interface is deliberately made of ordinary rendering ideas. It defines textures, render targets, buffers, vertex structures, pipelines, shader creation, texture states, barriers, drawing, and presentation. It also exposes ray-tracing concepts where a target supports them. A render path can therefore describe a sequence in terms of a target image, a depth buffer, a pipeline, bound textures, and a draw. It does not need to know which API-specific image layout or resource transition makes that sequence legal.
That does not mean the shared interface is a universal GPU. It is a negotiated vocabulary. For example, the interface names a small set of texture formats and pipeline states. The Vulkan backend maps those formats to Vulkan formats and those states to image layouts. The Direct3D12 backend maps them to resource states and descriptor-backed resources. The public operation remains “move this texture from one use to another”; the backend owns the API-specific transition that makes the move safe.
This is the key boundary:
The diagram is a responsibility map, not a claim that every target has identical behavior. ArmorPaint owns the application model. Iron and the base layer provide common runtime services. The shared GPU contract defines what the render paths can request. Each backend owns the translation into a target API. The build and shader tools prepare the artifacts those backends need.
What the backends still have to decide
A shared call does not erase platform-specific decisions. A GPU backend must choose how a texture is represented, how it changes from a render target to a shader resource, how descriptors or bindings are allocated, how buffers reach the device, and how synchronization is enforced. It must also deal with the platform’s window and presentation rules. These details are precisely where a cross-platform renderer can fail if the abstraction hides too much.
A texture state illustrates the point. A render path may first write an image and later sample it in a shader. At the application level, that is a transition in use. Vulkan expresses the transition through image layouts and pipeline barriers. Direct3D12 expresses it through resource-state transitions. The two implementations are not interchangeable, even though the render path needs one portable concept. If the backend applies the wrong transition, the problem appears as a graphics error or stale data; the editor code that requested the texture need not know the API vocabulary to be affected by it.
The same boundary appears in shader resources. A pipeline needs a vertex layout, vertex and fragment shaders, depth behavior, blending, color attachments, and bound textures. The shared description can carry those requirements, but the backend must turn them into a valid pipeline for its API. Portability therefore moves complexity downward; it does not remove it. The reward is that the application’s material and paint logic can remain focused on surfaces instead of repeating graphics setup for every target.
There is a second limit: the interface exposes only what the application has chosen to standardize. If one target offers a feature that is not represented in the shared contract, the feature may require a specialized path, a conditional implementation, or a deliberate omission. That is why a platform abstraction should be judged by the product behavior it enables and the exceptions it requires, not by the number of backend files it contains.
This boundary also determines where a failure is diagnosed. If ArmorPaint asks for an intermediate texture and the wrong state transition is applied, the visible symptom may be a missing layer, stale lighting, or a validation error from the graphics API. The project model may be correct, and the render path may have issued the expected operation, yet the backend can still violate the API’s resource rules. Conversely, a backend can be correct while the application asks for a feature that the target configuration intentionally disabled. In both cases, the shared contract helps locate the question: is the meaning wrong, is the portable request incomplete, or is the target translation invalid?
The abstraction therefore has a maintenance consequence. A new renderer feature must be expressed at the right level. If it is added only to one backend, ArmorPaint may gain a private capability that the rest of the application cannot reason about consistently. If it is forced into the shared interface before its semantics are clear, every backend inherits a concept it may not be able to implement. The source’s conditional paths are evidence of this negotiation, not evidence that portability has been solved once and for all.
The build pipeline is part of the runtime design
Portability begins before the program starts. ArmorPaint’s project descriptions select source files, libraries, shaders, assets, defines, and target options. The amake tool processes those descriptions and can generate platform project files or build outputs. The build system is therefore not only a developer convenience. It decides which implementation crosses the boundary into a particular binary and which optional data is available at runtime.
Shader compilation makes this especially clear. ArmorPaint’s shader sources use the Kong language, while the target GPU expects a target-specific representation. The ashader tool wraps the Kong compiler and chooses an export route based on the target: Direct3D-oriented output on Windows, Metal source on Apple platforms, SPIR-V on the Vulkan path, and WGSL for WebGPU. The source description remains shared, but the artifact that the runtime loads changes.
That two-stage arrangement protects the editor from API-specific shader syntax. A material or render pass can be described in the project’s shader language and then converted into a form the target can consume. It also creates a clear failure boundary. A change can be correct at the application or Kong-source level and still fail during target compilation. Conversely, a successful compilation does not prove that every target gives the shader identical performance or precision.
The asset side follows the same logic. Common engine assets, ArmorPaint assets, export presets, locales, plugin assets, and ray-tracing resources are gathered by project descriptions. Builds may embed data, or leave it as external files. Embedding can simplify distribution and lookup; external data can make development or replacement easier. Neither choice is universally better. It changes the relationship between the executable and the files it expects to find.
Version information and target-specific data lists also come from this build layer. Those generated details help the running application locate or describe the data shipped with it. Seen this way, the build system is another transformation in ArmorPaint’s dataflow: source intent becomes a target-specific set of code and resources before the runtime can execute the frame lifecycle described earlier.
Feature gates are explicit product boundaries
The repository does not promise that every platform receives every feature. That is an important sign of an honest abstraction. The WASM configuration disables native file dialogs, compression, plugins, and ray tracing. Neural material nodes are registered only on desktop-class targets. The reason is not simply that the web is “less powerful” in the abstract. These features depend on particular system services, libraries, file access patterns, native code, or GPU capabilities that the selected target does not provide in the same way.
Mobile changes the product surface in other ways. The UI uses a narrower sidebar on Android and iOS, and the layer utility sets a lower maximum layer count for those targets than for desktop. At high texture resolutions, the application reduces the number of retained undo steps to control memory. These are not cosmetic differences. They change how much editable state the user can keep and how much history the application can afford to preserve.
The renderer also exposes resource limits that are independent of operating system branding. Each visible layer and mask can require texture bindings. When the mesh shader approaches the available texture-unit budget, ArmorPaint divides layer rendering into additional passes. That preserves the ability to process the layer stack, but it changes the amount of work required for a frame. The abstract model remains “render the visible layers”; the concrete execution may be one pass or several depending on resource pressure.
The feature flags therefore have two meanings. They are build instructions, because they decide which source and assets are compiled. They are also statements about the supported product surface. A disabled plugin path means more than a missing checkbox: it changes which import and extension boundaries exist on that target. A reduced undo budget changes how reversible editing behaves. A multi-pass layer path changes how the renderer pays for a rich project model.
Imagine carrying the same project between a desktop build and a web build. The project’s painting intent may still be recognizable, but the available route through the application can change: a native file dialog may not exist, a plugin importer may not be available, and a ray-traced viewport may have no corresponding consumer. Portability therefore means preserving the core model while acknowledging that some transitions have no safe implementation on a target. The honest result is not identical behavior everywhere; it is a visible boundary instead of a hidden failure.
The trade-off is translation, not magic
A shared application model gives ArmorPaint leverage. One project representation can feed several renderers. One family of shader sources can reach several target formats. One render-path lifecycle can coordinate different backend implementations. That is why portability belongs in the same architectural story as painting: the application is trying to keep meaning stable while representations change.
The cost is that every boundary needs a translation and a condition. Backend resource states must be correct. Shader artifacts must be generated for the target. Build descriptions must include the required assets. Feature flags must tell the truth about what the target can support. Resource limits may turn one conceptual layer stack into several physical passes. The application’s integrated design makes these relationships visible, but it also means a change can travel farther than one isolated subsystem.
The repository supports these mechanisms and configured boundaries. It does not establish that the APIs perform identically, that every device supports every enabled feature, or that a generated artifact has been built successfully in every possible configuration. Those questions require device testing and measurement. What the code does establish is the next part of ArmorPaint’s architecture: now that the platform can carry a frame, what exactly is the project state that all of those targets are carrying?