How Custom Shaders Are Compiled Using ashader.c and Registered with Iron’s Runtime Shader System in ArmorPaint
ArmorPaint compiles custom material shaders by first generating Kong source code from node graphs, converting that source to platform-specific binaries via ashader.c, and finally registering the compiled modules with Iron’s GPU layer through iron_gpu.c.
ArmorPaint enables artists to author material shaders using the high-level Kong language (.kong files). When a material is saved, the engine executes a four-stage pipeline that bridges the visual node editor to the native GPU runtime. This article explains how the ashader.c compiler transforms Kong scripts into executable shader binaries and how Iron’s runtime system loads and binds these assets.
The Kong-to-GPU Pipeline Overview
The compilation workflow traverses four distinct layers of the ArmorPaint codebase:
node_shader.cconstructs a temporary Kong script containing vertex and fragment functions, constants, and texture declarations.ashader.cparses the Kong AST and emits platform-specific code (WGSL, HLSL, Metal) or binary blobs (SPIR-V, D3D11 bytecode).iron_gpu.cloads the compiled files, creates native GPU objects (e.g.,VkShaderModule,ID3D11VertexShader), and caches them.- Shader Context Binding attaches the loaded modules to a
shader_context_tstructure for pipeline creation.
Step 1: Generating Kong Source with node_shader.c
Before compilation can begin, the material’s node graph must be serialized into the Kong shading language.
Building the Material Definition
In paint/sources/node_shader.c, the function node_shader_get() iterates over the material’s nodes to build a complete Kong script. It constructs input/output structs, samples textures, and emits the kong_vert and kong_frag functions that define the material’s behavior.
// Build a Kong script containing the final vertex and fragment code.
// The script is later written to <material>_<name>.kong and handed to ashader().
char *node_shader_get(node_shader_t *raw) {
// … build input/output structs, constants, samplers …
string_buffer_append(sb, "fun kong_vert(...){ … }\n");
string_buffer_append(sb, "fun kong_frag(...){ … }\n");
// … create the pipe structure that ties the two functions together …
return string_buffer_get(&out); // returns the full Kong source string
}
The resulting .kong file is saved to disk by the editor and serves as the input for the amake build tool.
Step 2: Compiling Kong to Platform-Specific Shaders with ashader.c
The ashader.c compiler in base/tools/amake/ashader.c is responsible for transforming the intermediate Kong representation into executable GPU code.
The ashader() Entry Point
The ashader() function acts as the primary interface for the build pipeline. It accepts the target shader language, the source Kong file path, and the output base name.
int ashader(char *shader_lang, char *from, char *to) {
// `from` = path to the generated *.kong file
// `to` = base name for the output files (e.g. "…/myMat_myPass")
// `shader_lang` = "wgsl", "spirv", "hlsl", "metal"
kong_compile(shader_lang, from, to);
return 0;
}
Backend-Specific Code Generation
Inside kong_compile(), the compiler performs lexical analysis, parses the Kong source into an AST, and resolves types. It then dispatches to backend-specific exporters:
- WGSL –
wgsl_export2()writes*.vert.wgsland*.frag.wgslfor WebGPU targets. - Direct3D 11 –
hlsl_export2()generates HLSL source strings, which are then compiled withD3DCompileto produce binary files*.vert.d3d11and*.frag.d3d11. - Metal –
metal_export()creates Metal Shading Language source files (*.vert.metaland*.frag.metal). - SPIR-V –
spirv_export2()emits binary SPIR-V files (*.vert.spirvand*.frag.spirv) for Vulkan, Linux, and Android builds.
The compiled artifacts are written to disk using the output buffer to_ (see lines 190-210 of ashader.c), producing pairs of vertex and fragment shader files for the target platform.
Step 3: Loading and Registering Shaders in Iron’s Runtime (iron_gpu.c)
Once the binaries exist on disk, Iron’s GPU abstraction layer takes over to make them available to the rendering pipeline.
Creating Native GPU Objects
In base/sources/iron_gpu.c, the shader_load() function reads the compiled shader files and instantiates the appropriate native objects based on the current graphics API.
// Load a compiled shader file and create a GPU-native module.
static shader_t *shader_load(const char *path, api_kind api) {
if (api == API_VULKAN) {
// read SPIR-V binary …
vkCreateShaderModule(device, &createInfo, NULL, &module);
} else if (api == API_DIRECT3D11) {
// read D3D bytecode …
device->CreateVertexShader(...);
} else if (api == API_METAL) {
// compile Metal source into a MTLFunction …
}
// Store the object in the engine’s shader cache.
shader_cache_add(path, module);
return module;
}
The Shader Context and Pipeline Binding
When a material is instantiated, Iron populates a shader_context_t structure (defined in iron_gpu.c) with the paths to the compiled vertex and fragment shaders. Before the first draw call, material_prepare() invokes shader_load() for both stages, caches the returned handles in the context’s vertex_shader and fragment_shader fields, and creates the graphics pipeline.
// Example for a Vulkan pipeline:
shader_t *vs = shader_load("myMat_myPass.vert.spirv", API_VULKAN);
shader_t *fs = shader_load("myMat_myPass.frag.spirv", API_VULKAN);
pipeline_create(vs, fs, ...);
During the render loop, Iron retrieves these cached objects from the shader_context_t and binds them to the command buffer.
Practical Implementation Example
Implementing a custom shader workflow requires interaction with the editor, the build system, and the runtime API.
Creating a Material in the Editor
First, define the material structure and node graph programmatically:
// 1. Create a new material and obtain its context.
material_t *mat = material_create("MyMaterial");
// 2. Add a custom Kong shader (the editor writes this to a .kong file).
node_shader_context_add_elem(mat->shader_ctx, "myCustomVert", "float4");
node_shader_add_constant(mat->shader_ctx, "myColor: float4", NULL);
node_shader_add_texture(mat->shader_ctx, "albedoTex", NULL);
Triggering Compilation via amake
Use the amake build tool to invoke ashader.c and generate the platform binaries:
# Inside the repository root:
./amake -c myMat_myPass.kong -t wasm # or -t windows, -t macos, -t linux
# The tool calls ashader.c → produces:
# myMat_myPass.vert.spirv
# myMat_myPass.frag.spirv
Runtime Loading and Rendering
At runtime, Iron automatically loads the compiled shaders when the material is first used:
// Inside the rendering loop (simplified):
if (!material_is_ready(mat)) {
// Iron loads the compiled shaders and creates the pipeline.
material_prepare(mat); // calls shader_load() for the .vert/.frag files
}
renderer_draw(mesh, mat);
Summary
node_shader.cgenerates Kong source files from material node graphs, defining vertex and fragment entry points.ashader.ccompiles Kong scripts to platform-specific formats (WGSL, SPIR-V, HLSL, Metal) via theashader()andkong_compile()functions.iron_gpu.cloads the compiled binaries into native GPU objects (e.g.,VkShaderModule) and manages them throughshader_load().shader_context_tstores references to the compiled shaders, allowing Iron to bind them to the rendering pipeline when the material is drawn.
Frequently Asked Questions
What is the Kong shading language in ArmorPaint?
Kong is a high-level shading language used by ArmorPaint to author material shaders. It provides a syntax for defining vertex and fragment functions, uniforms, and texture samplers. The node_shader.c module generates Kong source code from the visual node graph editor, which is then compiled to platform-specific GPU languages by ashader.c.
How does ashader.c support multiple graphics APIs?
The kong_compile() function in ashader.c dispatches to backend-specific exporters based on the target parameter. For WebGPU it calls wgsl_export2(), for Vulkan it calls spirv_export2(), for Direct3D it calls hlsl_export2() followed by D3DCompile, and for Metal it invokes metal_export(). This architecture allows a single Kong source file to target Windows, macOS, Linux, and web platforms.
Where are compiled shaders cached in Iron?
Compiled shader objects are cached in Iron’s internal shader cache within iron_gpu.c. When shader_load() creates a native GPU module (such as a VkShaderModule or ID3D11VertexShader), it immediately calls shader_cache_add() to store the handle. Subsequent requests for the same file path retrieve the cached object instead of reloading it from disk, minimizing GPU upload overhead.
What triggers shader compilation in the ArmorPaint workflow?
Shader compilation is triggered when a material is saved in the editor, invoking the amake build tool. The editor writes the generated Kong file to disk, then amake calls the ashader() function in ashader.c to produce the binary artifacts. At runtime, iron_gpu.c loads these binaries automatically when material_prepare() is called prior to the first draw command.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →