SDF Text Rendering¶
Pulp renders text through a Signed Distance Field (SDF) pipeline so a single atlas serves every font size with crisp edges. This page documents the current state and the phased path to production quality.
Pipeline¶
SdfAtlas(core/canvas/include/pulp/canvas/sdf_atlas.hpp) builds the atlas. Real glyph metrics (bearing, advance) come fromSkFontso layout matches the rasterized glyph pixels.- Felzenszwalb & Huttenlocher (2004) two-pass distance transform produces
the signed distance per texel, mapped to
uint8with128 = edge. - Padding around each glyph defines the SDF spread radius.
Variants¶
| Variant | Channels | Status | Strength |
|---|---|---|---|
| SDF | 1 (A8) | working | simple, cheap, small atlas |
| MSDF | 3 (RGB) | planned | sharp corners, crisp thin strokes |
| PSDF | 1 | planned | cheaper-to-generate alternative for simple geometry |
Multi-channel SDF (Chlumsky 2015) encodes three distance signals and
recovers the true edge via median(R, G, B) in the shader. This keeps
corners sharp where single-channel SDF rounds them off.
Sampling shader¶
SkSL smoothstep sampler — single-channel:
// See core/canvas/shaders/sdf_text.sksl
half4 main(float2 coord) {
half d = sample(atlas, coord).r;
half aa = fwidth(d);
half a = smoothstep(0.5 - aa, 0.5 + aa, d);
return color * a;
}
MSDF adds median(r, g, b):
Subpixel positioning¶
fwidth(d) in the sampler shader gives a pixel-accurate AA width at
any zoom, so glyph quads can be placed at fractional pixel positions
without introducing distance-field aliasing. For animated UIs that
prefer stable edges to minimal sample error, use the snapping helper
in <pulp/canvas/sdf_text.hpp>:
#include <pulp/canvas/sdf_text.hpp>
using pulp::canvas::SdfPenSnap;
using pulp::canvas::snap_pen_x;
float pen_x = snap_pen_x(fractional_x, SdfPenSnap::Nearest);
SdfPenSnap values:
- Free — pass-through; smoothest animation.
- Nearest — round to whole pixels; crisp at rest.
- SubpixelThird — round x to 1/3 px (LCD subpixel stripe); y to
whole pixels.
The sampler shader is unchanged regardless of policy.
Effects¶
A reusable effects layer — glow, shadow, outline, bevel — is
exposed via SdfEffectParams in <pulp/canvas/sdf_effects.hpp> and
backed by the sdf_text_effects.sksl shader. Design-token presets
(preset_subtle_shadow(), preset_outline(), preset_glow(),
preset_pressed_bevel()) compose onto any SDF or MSDF atlas without
extra geometry — outline and glow are shader-space ring sweeps and
bevel is a dFdx/dFdy light dot product. See
examples/sdf-effects-demo/ for a runnable showcase of the four
presets plus a plain baseline.
Runtime atlas management¶
SdfAtlasCache (in <pulp/canvas/sdf_atlas_cache.hpp>) lets UIs share
a single atlas across every fill_text_sdf call-site with per-glyph
dirty-rect upload hints and frame-based LRU eviction:
SdfAtlasCache cache;
cache.initialize(font, seed_chars);
cache.ensure(U'☃'); // dynamic growth: rebuild atlas if missing
cache.touch(U'A'); // record recency for LRU
cache.next_frame(); // call once per rendered frame
cache.evict_older_than(600); // drop glyphs unused for 10 seconds at 60fps
For procedural UI that needs SDFs beyond the font atlas (vector icons,
generated glyphs), <pulp/canvas/path_to_sdf.hpp> runs the same
Felzenszwalb-Huttenlocher EDT on a caller-supplied binary mask and
emits the 128 == edge field the SDF samplers expect.
Related¶
planning/next-features-plan.md§ Feature 4 — full phase planexamples/sdf-text-demo/— SDF vs MSDF comparisonexamples/sdf-effects-demo/— effects showcase across presetsdocs/reference/modules.md— module index