How ArmorPaint Viewport Rendering Handles MSAA, Post-Processing, and Real-Time Brush Previews
ArmorPaint resolves multi-sample anti-aliasing on the GPU via gpu_resolve_msaa() in util/render.c, applies tone-mapping and FXAA post-processing, then composites real-time brush previews in viewport.c using immediate-mode draw calls to maintain low-latency cursor feedback.
ArmorPaint's viewport rendering system balances visual fidelity with interactive painting performance through a carefully orchestrated GPU pipeline. The implementation spans viewport.c, util/render.c, and util/brush.c to resolve MSAA, execute post-processing passes, and overlay dynamic brush cursor previews without CPU readbacks. This architecture ensures high-quality antialiasing while keeping brush feedback instantaneous.
MSAA Rendering and GPU-Side Resolve
ArmorPaint begins each frame by rasterizing geometry into a multi-sampled render target configured according to user quality settings.
Creating the Multi-Sampled Target
In render_path.c, the primary color attachment is created with GPU_TEXTURE_FORMAT_RGBA32 and the GPU_TEX_FLAG_MSAA flag. This enables hardware multi-sample antialiasing for all subsequent geometry passes, including deferred G-buffer fills and forward opaque or transparent draws.
The Resolve Operation
After scene rasterization completes, util_render.c invokes the Iron-engine helper gpu_resolve_msaa() to convert the multi-sample buffer into a single-sample texture stored in render_target->resolved. This operation executes entirely on the GPU without CPU readback, preserving performance by avoiding costly host-device transfers.
/* util/render.c – resolve MSAA and run post‑process passes */
gpu_resolve_msaa(rt->_image, rt->resolved); // ← MSAA resolve
draw_begin(rt->resolved, true, 0); // start post‑process chain
apply_tonemap(); // tone‑mapping, exposure, etc.
apply_fxaa(); // optional fast AA pass
draw_end(); // finish and store final image
As shown in the code, the resolve step immediately precedes the post-processing chain, operating on rt->_image and outputting to rt->resolved.
Post-Processing Pipeline
Following the MSAA resolve, util_render.c executes a series of full-screen passes to finalize the image before compositing.
Tone-Mapping and Exposure
The pipeline applies tone-mapping and exposure adjustments via apply_tonemap(), processing the resolved texture to map high dynamic range values into displayable color space.
FXAA Anti-Aliasing Pass
An optional fast approximate anti-aliasing (FXAA) pass is applied through apply_fxaa() to further refine edge quality. This runs after tone-mapping within the same draw_begin/draw_end block initiated on the resolved texture.
Real-Time Brush Preview System
The brush preview operates as an overlay drawn after the 3D scene post-processing completes, ensuring it appears instantly without requiring additional resolves.
Generating the Preview Texture in util/brush.c
When users modify brush parameters or move the cursor, util_brush.c constructs a small preview mesh representing the brush radius and hardness. This geometry is rendered into a transient GPU texture assigned to brush_preview using immediate-mode draw calls.
/* util/brush.c – update preview mesh */
brush_preview = gpu_create_texture(...);
draw_begin(brush_preview, false, 0);
draw_brush_shape(brush_radius, brush_hardness);
draw_end();
Compositing in viewport.c
The viewport.c module retrieves the resolved render target from the render path (identified as "last") and composites the brush preview on top using additive blending. This occurs each frame via draw_image calls that layer the base image followed by the brush overlay at cursor coordinates.
/* viewport.c – draw final image with brush overlay */
render_target_t *rt = any_map_get(render_path_render_targets, "last");
gpu_texture_t *final = rt->resolved; // already MSAA‑resolved
draw_begin(g_context->output, false, 0);
draw_image(final, 0, 0); // base image
draw_image(g_context->brush_preview, cursor_x, cursor_y); // brush overlay
draw_end();
By deferring brush compositing until after the MSAA resolve and post-processing, the system avoids sampling artifacts on the cursor while maintaining full interactivity.
Summary
ArmorPaint's viewport pipeline achieves high-quality rendering with responsive tool feedback through these key architectural decisions:
- GPU-side MSAA resolve: The
gpu_resolve_msaa()function inutil/render.cconverts multi-sample buffers to single-sample textures without CPU readback, preserving fill rate performance. - Sequential post-processing: Tone-mapping and FXAA operate on the resolved texture within the same render pass chain, ensuring consistent anti-aliasing across the final image.
- Deferred brush compositing: Real-time brush previews generated in
util/brush.care overlaid inviewport.cafter all 3D processing completes, preventing cursor lag and sampling artifacts. - Immediate-mode UI: The use of
draw_begin,draw_image, anddraw_endcalls enables efficient per-frame updates of brush position without reallocating render targets.
Frequently Asked Questions
Why is the brush preview drawn after the MSAA resolve?
Drawing the brush preview after gpu_resolve_msaa() ensures the cursor overlay appears sharp and undistorted by multi-sample reconstruction artifacts. Since the brush cursor is a UI element rather than scene geometry, it does not require MSAA and benefits from being composited as a single-sample overlay on the already-resolved final image, as implemented in viewport.c.
What post-processing effects does ArmorPaint apply in util_render.c?
According to the source code in util/render.c, the pipeline applies tone-mapping via apply_tonemap() for exposure correction and high dynamic range mapping, followed by an optional FXAA (Fast Approximate Anti-Aliasing) pass via apply_fxaa() performed on the resolved single-sample texture.
How does ArmorPaint maintain performance during the MSAA resolve?
ArmorPaint maintains interactive frame rates by executing the MSAA resolve entirely on the GPU using gpu_resolve_msaa(), which avoids CPU readback of the multi-sample buffer. This GPU-side operation preserves memory bandwidth and allows the subsequent post-processing and brush preview compositing to occur without stalling the rendering pipeline.
Where is the brush preview texture stored between frames?
The brush preview texture is maintained in the global context as g_context->brush_preview and is regenerated in util/brush.c whenever brush parameters change. This transient texture persists in GPU memory, allowing viewport.c to reference it directly during the final compositing pass without reconstruction each frame.
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 →