I do see one genuinely “science-y” place where you can get a non‑incremental win: add a Signed Distance Field / Multi‑Channel SDF (MSDF) glyph atlas renderer as a new backend.
Everything else in typf already looks very systematically optimized: multi‑stage pipeline, multi‑backend, multi‑level caching, performance bench/regression infra, fuzzing, security posture, SIMD, linra single‑pass paths, etc.
Below is a focused plan for that one breakthrough‑level improvement.
- Raster backends (Opixa/Skia/Zeno/CoreGraphics) rasterize paths per size; cost scales roughly with pixel count. Benchmarks show super‑linear slow‑down at large sizes (e.g. ~13× slower at 128px vs 16px where ideal is ~8× area scaling), and render time dominates for big text.
- You already aim for GPU acceleration (Skia GPU, future batch renderers) but still in a “classic path rasterization → bitmap” model.
- The WebAssembly example currently sends full pixel buffers to JS, which is costly as sizes grow.
Signed Distance Field (SDF) and especially Multi‑Channel SDF (MSDF) rendering converts each glyph outline into a small distance‑field texture; at runtime you just draw textured quads and evaluate a cheap shader:
- Raster cost mostly independent of output size: you draw the same 32×32 / 64×64 glyph texture whether final text is 12px or 256px; quality stays crisp thanks to distance evaluation. (steamcdn-a.akamaihd.net)
- Massive reuse: same glyph atlas works across many strings and frames; you amortize the expensive part over all uses. (GitHub)
- Widely proven in games/UI engines (Valve’s original SDF paper, msdfgen, UE5’s SDF UI text, lots of WebGL examples). (steamcdn-a.akamaihd.net)
For typf specifically:
- It fits naturally as another renderer backend (
typf-render-msdf), like Opixa/Skia/Zeno. - It attacks a real pain point: large sizes, web/GUI rendering, WASM, where you care more about speed + crispness than pixel‑perfect CoreGraphics matching.
- It’s orthogonal to your existing linra and opixa optimization roadmap; you don’t have to rip out anything.
Net: for UI/WASM/interactive and “lots of repeated text” workloads, this can easily be a multi‑x speedup at large sizes, not just a 10–20% tweak, while also enabling a whole new quality/performance trade‑off space.
Add a new backend crate:
backends/typf-render-msdf/- Expose a feature flag
render-msdfin the workspace andcrates/typf. - Implement the existing
Renderertrait fromtypf-core(“glyphs → pixels/vectors”) and register it alongside Opixa/Skia/Zeno.
The backend is responsible for:
-
Maintaining glyph MSDF atlases per (font, size‑band, options) tuple.
-
Turning a
ShapingResultinto quads with appropriate texture coordinates. -
Rasterizing via either:
- CPU (reference path; handy for CLI, tests, non‑GPU environments).
- GPU (wgpu/OpenGL/Metal/Vulkan via a thin abstraction) for the real performance win.
Prefer MSDF over single‑channel SDF:
- Classic single‑channel SDFs (Valve) are great but can blur corners at extreme zooms. (steamcdn-a.akamaihd.net)
- MSDF encodes distances to different edges in RGB; properly decoded via median you keep sharp corners across a wider size range. (GitHub)
Use banded atlases, e.g.:
- Band per “nominal pixel size range”: 8–24px, 24–64px, 64–160px.
- Each band has its own atlas resolution (e.g. 32×32, 48×48, 64×64 per glyph) to balance quality & memory.
This matches real‑world usage: UI text is usually in a fairly narrow size band, so a small number of atlases covers almost all usage.
Goal: prove this fits typf’s architecture, integrate with fonts & shaping, and validate quality.
Create a new internal crate or module, e.g. crates/typf-msdf:
-
Types:
MsdfGlyphKey { font_id: FontId, glyph_id: u32, band: SizeBand, bold: bool, italic: bool }MsdfGlyph { atlas_rect: Rect, advance: f32, bearing: (f32,f32) }MsdfAtlas { texture: Vec<u8>, width: u32, height: u32, glyphs: HashMap<MsdfGlyphKey, MsdfGlyph> }
-
Builder:
-
MsdfAtlasBuilder::new(font: &FontRef, band: SizeBand). -
Enumerate glyphs you care about (you already have shaping results and test font fonts, plus you can seed with ASCII ranges, etc.).
-
For each glyph:
- Get outline via
FontRef/ skrifa glyph outlines. - Generate MSDF bitmap for that outline.
- Pack into an atlas (simple skyline or shelf packer is enough; use a tight margin for filtering).
- Get outline via
-
-
MSDF generation algorithm:
-
Base it on Viktor Chlumský’s msdfgen approach (edge coloring + multi‑channel distance evaluation). (GitHub)
-
Port the core math into Rust, not necessarily full binding to msdfgen C++:
- Edge classification (lines, quadratic/cubic Beziers).
- Edge coloring to avoid artifacts.
- Distance sampling with correct pixel footprint.
-
Use
f32output distances remapped to[0, 255]per channel as in standard MSDF.
-
You can validate the generator in isolation by dumping atlas PNGs and visually verifying shapes.
In typf-render-msdf, implement a CPU fallback renderer:
-
For each glyph in
ShapingResult:-
Look up or lazily generate the MSDF glyph in the atlas (via
MsdfAtlasManager). -
Compute the quad bounds in the target bitmap (respecting
RenderParams.padding, baseline, etc.). -
For every pixel of the quad:
- Sample MSDF texture (bilinear).
- Reconstruct distance
d(using the standard median of RGB for MSDF). - Convert to coverage via a smoothstep around the 0‑isocontour.
- Blend with background as usual (respecting AA and alpha pre‑mult rules).
-
This is slower than GPU but gives you:
- A golden reference to test shader correctness.
- A purely‑Rust “minimal” path that still fits your minimal feature set story.
Use your existing visual_diff + PSNR/MSE tooling to compare MSDF vs Opixa/Skia/CoreGraphics across a grid of sizes & texts.
-
Add a new column “MSDF” to
compare_quality.py/visual_diff.py. -
For each combination (Latin, Arabic, variable fonts, etc., 12–200px):
-
Compute PSNR vs CoreGraphics.
-
Verify:
- No obvious aliasing at small sizes.
- No wobbliness at large sizes.
- Corners preserved where MSDF should help.
-
Exit criteria for Phase 1:
- CPU MSDF backend passes tests and has acceptable PSNR at normal UI sizes (e.g. > 25–30 dB vs CoreGraphics).
- Rendering semantics (metrics, layout) match existing backends.
Goal: turn MSDF into an actually much faster backend for real workloads.
You already have GPU ambitions (Skia GPU comment; WASM demo), but not a dedicated typf GPU path.
Pick a GPU abstraction:
- EITHER:
wgpu(modern, cross‑platform, good for Rust/WASM). - OR: expose a small trait
GpuContextthat the host app supplies, and provide awgpureference implementation.
In typf-render-msdf:
-
Add optional
gpufeature:render-msdf-gpu = ["dep:wgpu"].
-
Implement
Rendererto:- Upload MSDF atlas as a 2D texture.
- Stream per‑glyph instance data (position, uv rect, color).
- Dispatch a single draw call (or a few) per text.
Shader:
-
Vertex: transforms per‑glyph quad; passes UVs.
-
Fragment:
- Sample MSDF texture.
- Reconstruct signed distance.
- Apply smoothstep threshold; optionally support bold, outline, glow via distance offsets, as seen in MSDF tutorials and game engines. (GitHub)
Hooks into typf:
-
Extend
RenderMode/RenderOutputto allow “GPU frame handle” or “texture ID” for GPU‑only pipelines, while keeping a path to still read back to a CPU bitmap for export (PNG, etc.). -
Extend backend selection logic to treat MSDF GPU as a candidate when:
- Output is primarily on‑screen (not print).
- Target platform has GPU.
- User requests “fast UI text” or an explicit
RendererBackend::Msdf.
Leverage existing caching philosophy: you already have shaping & glyph caches, plus multi‑level caching for shaping/export.
Implement:
-
MsdfAtlasManagerwith:- Global
HashMap<(FontId, SizeBand), MsdfAtlas>with LRU eviction. - Reference counts per prepared text or per pipeline.
- Global
-
API:
fn ensure_glyph(&mut self, key: MsdfGlyphKey) -> MsdfGlyphReffn trim_to_memory(&mut self, max_bytes: usize)
Config (via CacheConfig or new MsdfConfig):
max_msdf_memory_mbmax_glyphs_per_atlasmax_atlases_per_font
This integrates nicely with existing CacheConfig / PipelineBuilder patterns.
Extend typf-tester:
-
Add “MSDF CPU” and “MSDF GPU” to the bench_scaling and bench_rendering suites.
-
Specifically test:
-
Scaling from 12px → 24px → 48px → 96px → 192px:
- Compare ns/op to Opixa / Skia / Zeno.
- Expect MSDF GPU curve to be much flatter vs size.
-
Longer texts (paragraphs, pages) where atlas reuse is significant.
-
Target: demonstrate near-constant time per glyph across size bands and big wins at large sizes.
Expose MSDF renderers in:
-
Rust:
RendererBackend::MsdfCpu,RendererBackend::MsdfGpu(or a singleMsdfwith CPU fallback when GPU not present).
-
CLI:
--renderer msdf(auto chooses GPU vs CPU).
-
Python (
typfpy):- Expose renderer option
"msdf"and/or aTypfMsdfhelper similar to Linra wrappers.
- Expose renderer option
Documentation:
- Update backend matrix docs with MSDF row explaining trade‑offs: quality vs scaling vs platform support.
Your WASM demo currently dumps raw pixel buffers and blits them via putImageData, which is bandwidth‑heavy.
With MSDF you can:
- Option A: keep the glyph atlas in WASM memory and have JS/WebGL/ WebGPU draw it using a tiny shader & instancing.
- Option B: include a minimal
wgpuWebGPU path directly in the WASM build.
Changes:
- Export an additional JS binding that returns glyph quads + atlas texture instead of per‑frame bitmaps (e.g.
render_text_msdf()returning JSON of quads plus a handle to atlas bytes). - Frontend code uses WebGL or WebGPU to draw the text with standard MSDF shader (lots of examples in the wild). (GitHub)
Result: drastically reduced per‑frame memory copies in the browser, and you can animate text transforms essentially for free.
SDF/MSDF brings a few new risks:
- Numerical robustness: distance evaluation near edges, color bleeding.
- Shader correctness: NaN/Inf, out‑of‑range texture coordinates.
- Resource exhaustion: unbounded atlas growth on adversarial input.
Mitigations:
-
Property‑based tests for MSDF generator:
- Random glyph outlines → check distance field is monotone wrt true signed distance at sample points. (A few analytic checks, not formal proof.)
-
Fuzz tests on atlas packing + rendering: feed random render sequences, ensure no panics, no invalid indices, no overflow (you already have fuzz infra; just add a target).
-
Enforce strict config limits for atlas memory; treat OOM as a controlled failure (
RenderError::ResourceLimitExceeded).
These fold into your existing security posture (resource exhaustion defenses, fuzzing, Miri on unsafe code).
Roughly ordered; many can be parallelized.
-
Scoping & design
- Decide on
wgpuvs “host‑supplied GPU context” for MSDF GPU. - Specify size bands & default atlas resolutions.
- Decide on
-
Crates & features
- Create
backends/typf-render-msdfcrate withrender-msdffeature. - Optionally create
crates/typf-msdffor shared math/types. - Wire
render-msdffeature into workspace metadata and maintypfcrate.
- Create
-
CPU MSDF core
- Implement outline extraction for glyphs (using existing
FontRef/skrifa plumbing). - Implement MSDF edge coloring & distance sampling based on msdfgen algorithm. (GitHub)
- Implement atlas packer and
MsdfAtlasManagerwith LRU & memory limits. - Implement CPU raster path using MSDF textures and verify correctness vs reference renderer (Opixa/Skia/CoreGraphics).
- Implement outline extraction for glyphs (using existing
-
Visual quality tooling
- Extend
typf-tester/visual_diff.pyandcompare_quality.pyto add MSDF combinations. - Define acceptance thresholds for PSNR/MSE vs CoreGraphics.
- Extend
-
GPU renderer
- Implement GPU texture upload, instance buffers, and MSDF shader (vertex + fragment).
- Implement
Rendererfor MSDF GPU backend, including CPU readback for export when needed. - Integrate with pipeline builder and auto‑backend selection (e.g. prefer MSDF GPU for large UI text).
-
Integration & frontends
- Add
msdfrenderer to CLI & Rust API enums. - Add
msdfoption to Python bindings, including tests inbindings/python/tests. - Add WASM demo using MSDF (WebGL/WebGPU) instead of pure CPU pixels.
- Add
-
Perf & safety
- Add criterion benchmarks for MSDF CPU & GPU in
pipeline_bench.rsand/or new benches. - Add fuzz targets for MSDF generation and atlas management.
- Run Miri on any unsafe SIMD code in SDF sampling.
- Add criterion benchmarks for MSDF CPU & GPU in
So my verdict is:
-
Most of typf is already in “A‑grade, well‑optimized, well‑architected” territory; most conventional performance and safety tricks are already in play or explicitly planned.
-
The one area with genuine breakthrough‑style upside is adding an MSDF/SDF glyph atlas renderer as a first‑class backend:
- Shifts scaling for large sizes and repeated text from “expensive path raster every time” to “cheap quad draw over a reusable atlas”.
- Plays extremely well with your WASM story and any GPU‑rich environments.
- Has a clear integration path with your existing traits, caching, and testing infrastructure.
If you’d like, I can next help you sketch exact Rust API shapes (traits/enums/feature flags) for typf-render-msdf and even pseudo‑code for the MSDF generator itself.
I do see one genuinely “different game” idea that isn’t in your current REVIEW/PLAN docs: introducing a distance‑field (SDF/MSDF) based renderer (optionally GPU‑accelerated) as a new backend, using your existing vector/SVG infrastructure.
Everything else in the codebase looks solid and already has an incremental optimization roadmap (opixa SoA/GET+AET, caches, profiling, etc.). So I’ll focus on this one big lever.
-
Pipeline is: Shaper → Renderer → Exporter via traits (
Shaper,Renderer,Exporter,RenderOutput). -
Opixa is a high‑quality CPU rasterizer with SIMD & planned scan‑conversion improvements; Skia/Zeno exist for color fonts; SVG export is already very fast and high quality.
-
Benchmarks show:
- HarfBuzz shaping meets targets.
- Rendering is good at 16px, but scales super‑linearly at 128px (~13x slower vs 16px).
- SVG export is ~23× faster than PNG bitmap export on average.
You already optimized how to rasterize glyph outlines on the CPU (opixa TODO: SoA, GET/AET, SIMD, zero‑alloc scan conversion). The remaining step‑change is to change what we rasterize: not raw coverage bitmaps per size, but a resolution‑independent distance representation reused across sizes and draws.
Signed distance fields (SDF) and multi‑channel SDF (MSDF):
-
Store, in a low‑resolution texture, the distance to the nearest edge of each glyph, possibly with multiple channels to distinguish corners. (steamcdn-a.akamaihd.net)
-
At render time, a tiny shader or CPU kernel reconstructs crisp edges at arbitrary sizes, with subpixel AA, from this single precomputed field.
-
In practice, this gives:
- Near constant cost per glyph across font sizes (vs O(size²) coverage rasterization).
- One glyph SDF reused across many renders (repeated text, UI elements, multiple resolutions, zoom, animations).
- A natural fit for GPU acceleration: render whole text with a few draw calls from one atlas. (steamcdn-a.akamaihd.net)
Typf already:
- Has reliable outlines via
typf-fontdb+skrifa. - Exports SVG vectors and measures bitmap quality via PSNR and visual diffs.
So Typf is almost perfectly positioned to add one new backend that uses the existing outline/VG pipeline and testing infra, but radically changes the cost profile of “raster text at arbitrary size”.
Add a new backend:
-
Crate:
backends/typf-render-sdf -
Implements
Rendererbut internally:- Uses outlines (via
FontRef→typf-fontdb/skrifa) to build a glyph SDF/MSDF atlas. - On each render, draws glyph quads sampling the atlas (CPU or GPU).
- Returns a normal
RenderOutput::Bitmap, so the rest of the pipeline, CLI, and Python bindings stay unchanged.
- Uses outlines (via
This is deliberately orthogonal to opixa: opixa remains your precise CPU rasterizer; SDF is an optional backend for “fast at large sizes / dynamic UI / GPU‑friendly”.
Goal: prove quality & performance within current pipeline, with no public API change.
-
Add new workspace member:
# Cargo.toml (workspace) members = [ # existing... "backends/typf-render-sdf", ] [workspace.metadata.features] render-sdf = []
-
New crate
backends/typf-render-sdf/Cargo.toml:[package] name = "typf-render-sdf" version.workspace = true edition.workspace = true license.workspace = true [dependencies] typf-core = { workspace = true } typf-fontdb = { workspace = true } skrifa = { workspace = true } log = { workspace = true } thiserror = { workspace = true } [features] default = [] simd = []
-
In
typf-coreadd a feature alias if you want:render-sdf = []similarly to opixa/skia/zeno.
Inside typf-render-sdf/src/lib.rs:
-
Define SDF glyph primitives:
pub struct SdfGlyph { pub glyph_id: u32, pub size_em: f32, // SDF texture resolution in EM units pub width: u16, pub height: u16, pub bearing_x: f32, pub bearing_y: f32, pub advance: f32, pub data: Vec<f32>, // single-channel SDF in [−range, +range] pub range: f32, // max distance encoded, in pixels at size_em } pub struct SdfAtlas { pub texture_width: u32, pub texture_height: u32, // packing placement: glyph_id → (u0,v0,u1,v1) pub uv_map: HashMap<(u32, SdfStyleKey), SdfUvRect>, pub pixels: Vec<f32>, // or Vec<u8> if quantized }
where
SdfStyleKeyencodes color, weight, optical size variants if needed. -
Don’t change
RenderOutputyet; SDF stays an internal representation that is re‑rasterized into an RGBA bitmap at the final requested size.
Implement a simple, robust SDF generator for glyph outlines:
-
Use
typf-fontdb/FontRefto obtain glyph outline geometry forglyph_id:- Either extend
FontRefwith anoutline(glyph_id) -> Pathmethod, or call intotypf-fontdbdirectly (it already usesskrifafor outlines in SVG export).
- Either extend
-
Rasterize SDF for a fixed reference size (e.g. 64×64 or 96×96 per EM) using a brute‑force but well‑vectorized algorithm:
- For each texel center, compute signed distance to nearest segment in the glyph path (line/cubic/quadratic).
- Use a bounding volume hierarchy or per‑scanline edge buckets if needed.
-
Normalize distances into a float range
[−1, +1]or[0,1]where 0.5 is the contour; store asf32, but add an option to quantize tou8later.
For quality, follow the approach in Green’s Valve paper and MSDF literature, but start with regular SDF (simpler) and add MSDF later if you see corner artifacts. (steamcdn-a.akamaihd.net)
Actionable tasks:
-
Add a small
sdf.rsmodule with:fn sdf_for_outline(outline: &GlyphOutline, size_em: f32, range: f32) -> SdfGlyph- Unit tests for simple shapes (rectangles, circles, big “O”) checking known distance values.
To integrate with Typf today, add a CPU path:
-
For each output glyph at requested font size
S:-
Compute scale factor
k = S / size_em. -
For each destination pixel:
-
Find corresponding (u,v) in atlas for that glyph.
-
Sample SDF (bilinear).
-
Convert distance
dto coverageαusing a smoothstep adjusted by pixel footprint:fn coverage_from_distance(d: f32, range: f32, px: f32) -> f32 { // map to [0,1] with smooth edge; px ≈ 0.5–1.0 let x = d / (px * range); (0.5 - x).clamp(0.0, 1.0) // refine with smoothstep later }
-
-
-
Composite into RGBA bitmap as you already do in raster renderers (foreground/background, padding).
This gives a pure Rust, cacheable, resolution‑independent glyph representation with no public API changes yet.
Goal: make SDF a first‑class backend, wire it into your existing caching, testing, and tools.
Docs already say:
Renderers can cache rasterized glyphs: key: font ID + glyph ID + size + style.
For SDF we want two levels:
- SDF cache: key = (font_id, glyph_id, style) →
SdfGlyphat reference size. - Bitmap cache (optional): key = (font_id, glyph_id, size, style) → RGBA; can be smaller or disabled if memory‑sensitive.
Actionables:
- In
typf-render-sdf, implement an internalSdfGlyphCachethat storesArc<SdfGlyph>with LRU eviction (reuselrucrate like core caches do). - Optionally derive a
BitmapGlyphCachefor frequently repeated sizes/UI text.
In typf-render-sdf/src/lib.rs:
pub struct SdfRenderer {
font_db: Arc<FontDatabase>,
sdf_cache: SdfGlyphCache,
// maybe a bitmap cache
}
impl Renderer for SdfRenderer {
fn render(
&self,
shaped: &ShapingResult,
font: Arc<dyn FontRef>,
params: &RenderParams,
) -> Result<RenderOutput> {
// 1. For each glyph in shaped, fetch SdfGlyph from cache or generate.
// 2. Layout quads in destination bitmap.
// 3. Run CPU sampling kernel to fill pixels.
// 4. Return RenderOutput::Bitmap { .. } as usual.
}
}No change to RenderOutput or the core traits is required.
-
In
typf-cli:- Add a
render-sdffeature and a CLI enum variant"sdf"for the renderer selection. - Map
--renderer sdftoSdfRendererin the backend factory.
- Add a
-
In Python bindings (
bindings/python):- Add
"sdf"to the list returned bylist_backends(). - Optionally expose a
sdf=True/Falseflag in a future version if you want to hide the exact backend name.
- Add
Use your existing typf-tester infrastructure:
-
Add SDF to the backend matrix in
typfme.py:- Generate PNG outputs for SDF at 16px, 48px, 128px.
-
Use
compare_quality.pyandvisual_diff.pyto compare SDF vs CoreGraphics/opixa/Skia:- Target PSNR > 30 dB for “excellent” quality where possible.
-
Add SDF to
bench_svg.pyor create abench_sdf.pyto measure:- ns/op at different sizes; ideally show flatter scaling vs opixa for 16px → 128px.
Once CPU SDF is proven, make it the basis for a GPU‑accelerated path.
Multi‑channel signed distance fields (MSDF) encode distance to multiple edges in RGB channels and substantially improve sharp corners and small text quality. (GitHub)
Actionables:
-
Extend
SdfGlyphto optionally hold 3 channels. -
Implement an MSDF generator based on msdfgen’s algorithm (or port from their pseudocode), referencing:
- Chlumsky’s msdfgen docs. (GitHub)
-
Add a
mode: SdfMode { Sdf, Msdf }parameter toSdfRendererconfiguration.
Add backends/typf-render-sdf-gpu:
[dependencies]
typf-core = { workspace = true }
typf-render-sdf = { workspace = true }
wgpu = "0.19"
pollster = "0.3"Design:
-
Shared
SdfAtlastype with CPU crate (maybe factored into a smallsdf-typescrate). -
GPU renderer pipeline:
- Upload atlas to a
wgpu::Texture2D. - Build a vertex buffer of glyph quads (position + UV) for each text block.
- Simple WGSL fragment shader sampling SDF/MSDF and applying smoothstep to compute alpha.
- Upload atlas to a
A minimal WGSL fragment shader (conceptually, not verbatim):
@group(0) @binding(0)
var sdf_tex: texture_2d<f32>;
@group(0) @binding(1)
var samp: sampler;
@fragment
fn fs_main(fs_in: FsInput) -> @location(0) vec4<f32> {
let d = textureSample(sdf_tex, samp, fs_in.uv).r;
let alpha = smoothstep(0.5 - edge, 0.5 + edge, d);
return vec4<f32>(text_color.rgb, alpha);
}This is standard practice in game engines and GPU text renderers. (steamcdn-a.akamaihd.net)
Integration with Typf:
-
The GPU renderer still returns
RenderOutput::Bitmapby:- Rendering into an offscreen
wgputexture. - Reading back pixels into a
Vec<u8>(for server‑side PNG/PDF export).
- Rendering into an offscreen
-
For interactive GUI/WASM scenarios, you can later expose a “draw into existing surface” mode without reading back.
-
Add a workspace feature
render-sdf-gpuand optional dependency onwgpuin the maintypfcrate, gated to non‑WASM or WASM as desired. -
In
typf-cli, add--renderer sdf-gpu(behind feature). -
In bindings:
- Python: maybe
renderer="sdf-gpu"on platforms supporting GPU. - WASM: integrate with your existing
build-wasm.shand example HTML; instead ofputImageData, use WebGPU/Canvas2D to draw directly.
- Python: maybe
-
For small sizes / print‑quality output:
-
Keep opixa/CoreGraphics/Skia as “truth”.
-
Use SDF/MSDF with a quality floor:
- If PSNR vs CoreGraphics < threshold at requested size, fall back automatically.
-
-
Expose this as
RenderParamsknob (e.g.render_mode: HighQuality | FastSdf).
Use the infrastructure you already have: fuzzing, property tests, quality analysis.
-
Property tests:
- For random glyphs and sizes, ensure bounds are correct and no panics occur in SDF generator or renderer.
-
Pixel diffs:
- Extend
visual_diff.pyto include SDF vs CoreGraphics/Skia. - Track PSNR over time to avoid regressions.
- Extend
-
Performance benchmarks:
- Add criterion benches for SDF CPU & GPU (per glyph and per text block).
- Compare scaling 16px → 128px vs opixa; aim for much flatter slope, e.g., ~O(1) in SDF vs O(size²) in coverage raster.
SDF/MSDF introduces new numeric and memory behavior:
-
Guard against:
- Overflows in distance calculations (clamp distance to a sane max).
- Degenerate outlines (zero‑area, self‑intersecting paths).
-
Add targeted fuzzing:
- Fuzz
sdf_for_outlinewith random but realistic outlines produced by mutating real glyphs fromtest-fonts/benchmark-fonts.
- Fuzz
Because Typf is already Rust + fuzzed, SDF mainly needs this extra numeric robustness to be on par with existing backends.
If you want to push even further (these are more incremental than the SDF idea):
-
Adaptive renderer selection using live metrics
-
You already have a
choose_optimal_backendstub and aProductionMonitorwith perf regression detection. -
Use SDF as another candidate in the backend pool and let real‑time metrics (font type, size, text complexity) drive backend selection:
- For very large sizes or repeated text → prefer SDF.
- For small sizes/print → prefer opixa/CoreGraphics.
-
-
Formalizing core pipeline invariants
- The six‑stage pipeline (
Input → Unicode → Font Selection → Shaping → Rendering → Export) has clean, composable types. - You can encode invariants (no negative advances, monotonic glyph positions per run, etc.) and assert them in debug builds across all renderers, including SDF.
- The six‑stage pipeline (
Those are nice, but SDF/MSDF is the real “physics upgrade”.
-
The existing codebase and plans are strong: good architecture, caching, profiling, and a detailed opixa optimization spec already exist.
-
The one “brilliant science” lever that really changes the performance story is:
- Introduce a signed‑distance‑field / multi‑channel SDF renderer, with optional GPU acceleration, as a new backend (
typf-render-sdf) built on your existing outline + SVG infrastructure.
- Introduce a signed‑distance‑field / multi‑channel SDF renderer, with optional GPU acceleration, as a new backend (
-
It can make large‑size and dynamic text rendering much faster and more scalable, while staying compatible with your current API and quality tooling.
If you’d like, I can next sketch concrete Rust module layouts / function signatures for typf-render-sdf and a minimal CPU SDF implementation to bootstrap Phase A.
The typf codebase is a well-engineered, modular Rust library for text rendering. It demonstrates a robust architecture with clear separation of concerns (Shaping, Rendering, Export) and swappable backends.
While the existing implementation is solid, there are significant opportunities for "breakthrough" advancements by applying modern research in parallel computing, caching algorithms, and automated testing methodologies. These advancements can dramatically improve performance, scalability, and correctness.
Below is a detailed, extensive, and actionable plan to realize these breakthroughs.
This plan addresses four key tracks to elevate the typf architecture.
The pure-Rust typf-render-opixa renderer has significant untapped potential. By optimizing for modern CPU architectures, we can achieve substantial performance gains (5x-10x). This plan expands upon the strategy noted in backends/typf-render-opixa/TODO.md.
Phase 1.1: Algorithmic Efficiency and Memory Layout
-
Zero-Allocation Scan Conversion:
- Action: Refactor
scan_converter.rsto eliminate heap allocations in the hot path. - Technique: Use an arena allocator (e.g.,
bumpalo) for temporary edge data during a glyph's lifetime. Pre-allocate buffers for the Active Edge Table (AET).
- Action: Refactor
-
Optimized Edge Table Management (GET/AET):
- Rationale: Reduce the complexity of edge sorting from O(N log N) per scanline to O(N).
- Action: Implement a Global Edge Table (GET) using a bucket sort in
edge.rs. Optimize the AET inscan_converter.rsto use incremental updates (x += slope) instead of re-sorting.
-
Structure-of-Arrays (SoA) Layout:
- Rationale: Improve spatial locality for better cache utilization and enable effective SIMD vectorization.
- Action: Refactor
edge.rsfromVec<Edge>(AoS) to SoA:struct Edges { x_coords: Vec<F26Dot6>, slopes: Vec<F26Dot6>, y_max: Vec<i32>, }
-
Adaptive Curve Flattening:
- Action: Optimize
curves.rsto minimize the number of line segments generated by implementing adaptive flattening based on a scale-dependent tolerance.
- Action: Optimize
Phase 1.2: Advanced Parallelism
-
SIMD Scanline Processing:
- Action: Implement
simd.rsusing Rust's portablestd::simd. - Technique: Leverage the SoA layout to process multiple pixels simultaneously during coverage calculation (anti-aliasing) and blending.
- Action: Implement
-
Tile-Based Parallel Rendering:
- Rationale: Enable intra-glyph parallelism for large or complex glyphs and improve cache locality.
- Action: Implement tile-based rendering in
rasterizer.rs. Divide the glyph bounding box into small tiles (e.g., 32x32) and distribute them as work units usingrayon.
The most significant performance leap involves harnessing the massively parallel architecture of the GPU. Integrating an existing state-of-the-art Rust engine is the most strategic approach.
Goal: Introduce a GPU backend offering potential 10x-100x performance improvements.
Technology Choice: Vello 1. A modern, compute-shader-based 2D renderer built on wgpu.
Phase 2.1: Integration Strategy
- Create
typf-render-vello: Initialize the new backend crate. - Implement
RendererTrait: Implementtypf_core::Renderer. - Glyph Encoding: Convert
skrifaoutlines into Vello's path format (vello::kurbo::Path). - Scene Construction: Iterate through
ShapingResult, apply transformations, and construct avello::Scene. - Rendering Dispatch and Readback: Initialize the
wgpucontext and Vello renderer. Dispatch the scene. Implement efficient readback (using mapped buffers) to transfer the GPU texture back to the CPU asRenderOutput::Bitmap.
Phase 2.2: Optimization
- GPU Glyph Atlasing: Implement GPU-side caching of glyphs using Vello's mechanisms to minimize CPU-GPU transfer overhead.
- Advanced Color Fonts: Leverage Vello's capabilities to handle complex COLRv1 features (gradients, compositing) directly on the GPU.
The current caching implementation (typf-core/src/cache.rs) relies on LRU, which is suboptimal under high concurrency due to lock contention and eviction behavior.
Goal: Improve cache hit rates and scalability in multi-threaded environments.
Technology Choice: Moka 2, implementing the W-TinyLFU algorithm 3.
Phase 3.1: Migration to Moka
- Rationale: W-TinyLFU generally outperforms LRU by using a frequency sketch to retain popular items and resist cache pollution.
- Action: Replace the custom L1/L2 structure and
LruCacheintypf-core/src/shaping_cache.rsandglyph_cache.rswithmoka::sync::Cache. - Weighted Capacity: For glyph bitmap caches, use Moka's weighted capacity to limit total memory usage (bytes) rather than just entry count.
typf has multiple backends implementing the same abstractions (e.g., HarfBuzz vs. CoreText; Opixa vs. Skia). This architecture can be leveraged for automated correctness verification.
Goal: Automatically detect discrepancies and regressions between backends using Differential Testing.
Phase 4.1: Differential Harness Implementation
- Create
typf-diff-testerCrate: A dedicated utility for this purpose. - Implement
compare_shaping_results: Compare twoShapingResultstructs. This must allow for minor, acceptable floating-point differences (epsilon) in positioning, as different engines vary slightly. - Implement
compare_render_outputs: Compare twoRenderOutput::Bitmapstructs using a perceptual image comparison library (e.g.,dssim-core) to account for minor variations in anti-aliasing. - Implement Fuzz Targets:
fuzz_target_diff_shape.rs: Generate random inputs, run them through two different shapers, and compare results. Panic on significant discrepancies.fuzz_target_diff_render.rs: Shape once, render through two different renderers, and compare visually. Panic if the difference exceeds the threshold.
This framework ensures consistency and robustness across the diverse backend ecosystem.
References:
1 Vello Project: https://github.com/linebender/vello 2 Moka Cache Library: https://github.com/moka-rs/moka 3 D. Berger, et al. "TinyLFU: A Highly Efficient Cache Admission Policy." arXiv:1512.00727.Based on my analysis of the typf codebase — a Rust typography library with a six-stage pipeline (Input → Unicode → Font → Shaping → Rendering → Export) — here's my assessment:
The architecture is well-designed with clean trait abstractions (Shaper, Renderer, Exporter), proper modular separation, and an existing comprehensive optimization plan in backends/typf-render-opixa/TODO.md.1
The TODO.md already documents state-of-the-art improvements for the Opixa rasterizer:
- Phase 1: Zero-allocation scan conversion, LTO/codegen-units=1 build config
- Phase 2: Bucket-sorted Global Edge Table (GET), Structure-of-Arrays (SoA) layout, adaptive curve flattening
- Phase 3: SIMD scanline processing, tile-based parallelism for large glyphs
- Phase 4: Multi-level caching (glyph bitmap + outline cache)
These cover the established best practices for scanline rasterization.1
Signed Distance Field (SDF) / Multi-channel SDF (MSDF) rendering is not mentioned in the TODO.md and represents a fundamentally different approach that could add significant value as an alternative rendering backend:
| Aspect | Traditional Scanline | SDF/MSDF |
|---|---|---|
| Scaling | Re-rasterize at each size | Single texture, infinite scaling |
| GPU acceleration | Complex | Trivial (texture sampling) |
| Effects (outline, glow) | Expensive post-process | Single shader uniform |
| Memory per glyph | O(resolution²) | O(fixed SDF resolution²) |
However, implementing SDF is a parallel track, not a replacement — useful for UI/game contexts but not for print-quality typography where the current approach excels.
Execute the existing TODO.md plan in typf-render-opixa — it's already comprehensive and represents the right optimizations for your rasterizer. The codebase architecture supports these improvements cleanly through the existing module structure (simd.rs, parallel.rs, scan_converter.rs, edge.rs).1