feat: add immutable vertex access and rasterizer cell caching for text rendering#1
feat: add immutable vertex access and rasterizer cell caching for text rendering#1fschutt wants to merge 2 commits intolarsbrubaker:masterfrom
Conversation
- PathStorage::vertices() — immutable access to raw vertex slice - RasterizerScanlineAa::add_path_vertices() — add from &[VertexD] slice - RasterizerScanlineAa::add_path_vertices_transformed() — with affine transform - RasterizerCellsAa::add_cells_offset() — import pre-computed cells with offset - RasterizerScanlineAa::outline_cells() — extract cells for caching - RasterizerScanlineAa::add_cells_offset() — replay cached cells These enable glyph cell caching: rasterize a glyph outline once at (0,0), cache the cells, then replay at any position without re-running the path→cell conversion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
If this is merged, please release a new version so I can depend on it. Thanks for your work! |
|
Thank you for this contribution — the use case is real and well-motivated, and the cell caching concept is mathematically sound. This directly motivated us to implement the feature, so the PR had a concrete impact. We've implemented a cleaner version of the core idea in our own branch, so we're closing this PR rather than merging it as-is. Here's what we changed and why: What we implemented
What we did differentlyDropped let transform = TransAffine::new_translation(x, y);
let mut conv = ConvTransform::new(&mut path, transform);
ras.add_path(&mut conv, 0);Dropped Added tests — we require a correctness proof for any new API. Two tests were added:
Documented the clipping limitation — cells bypasses the Notes for your azul integrationThe API you'll want is exactly: // Cache once per (font, glyph, ppem, subpixel_slot):
ras.add_path(&mut glyph_path, 0);
let cells = ras.outline_cells();
cache.insert(key, cells);
// Replay N times per frame:
ras.add_cells_offset(&cached_cells, glyph_x as i32, glyph_y as i32);For the transform + no-clone case, use Thanks again for the well-written PR and real-world motivation. |
When rendering text with agg-rust in azul (my GUI framework), each text run (paragraph, label, button text) contains many glyphs that share the same font at the same pixel size. The letter "e" in 14px Helvetica produces the same rasterizer cells every time — only the screen position changes.
It's not possible to "just cache the rasterized output as a texture", since the different sub-pixel positioning can produce different visual output for the same glyph. So, I need to cache at the "SubpixelAa" level, before the glyph is actually rasterized (Note: here, sub-pixel positioning is quantized to 1/4 pixel increments (16 cache slots per glyph). This change is probably very noticable when scrolling text (redrawing most of the screen at 60 FPS).
Currently, rendering 50 glyphs in a text run requires running the full path -> cell pipeline 50 times: iterate vertices, compute edge crossings, build the cell array. For a 1000-character page with ~40 unique glyphs repeated ~25 times each, that's ~960 redundant rasterizations per frame.
Solution
This PR adds 6 small API additions that enable glyph cell caching — rasterize each unique glyph outline once, cache the cells, then replay them at any position:
New APIs
PathStorage::vertices() -> &[VertexD]: Immutable access to the raw vertex slice. Avoids requiring &mut self through the VertexSource trait when the path is already built and won't change.RasterizerScanlineAa::add_path_vertices(vertices: &[VertexD]): Add vertices from an immutable slice, bypassing the mutable VertexSource iterator protocol. Useful when vertices are pre-computed or shared.RasterizerScanlineAa::add_path_vertices_transformed(vertices: &[VertexD], transform: &TransAffine): Same as above but applies an affine transform to each vertex on-the-fly. This avoids cloning PathStorage just to apply a translation or scale before rasterizing.RasterizerScanlineAa::outline_cells() -> Vec<CellAa>- Snapshot the rasterizer's current cell array for external caching. Call after add_path but before render_scanlines. Flushes any pending cell state first.RasterizerScanlineAa::add_cells_offset(cells: &[CellAa], dx: i32, dy: i32): Import pre-computed cells with a pixel offset, bypassing the entire path→cell conversion. Each cell's (x, y) is shifted by (dx, dy) before insertion.RasterizerCellsAa::add_cells_offset(src: &[CellAa], dx: i32, dy: i32)(internal, called by the above): The underlying cell-level import with bounds tracking.Usage in
azulBefore (re-rasterize every glyph every frame)
After (rasterize once, replay from cache)
This is used in glyph-cache.rs - see cpurender.rs for the usage of these new APIs. The glyph caching was introduced in commit fschutt/azul@767f603
For typical body text (1000 characters, ~40 unique glyphs at same size):
The cell import (add_cells_offset) is essentially a Vec::reserve + loop with 4 additions per cell — orders of magnitude cheaper than edge-crossing computation.
Backward Compatibility
All additions are new pub methods on existing structs, no existing APIs are modified, removed, or have changed signatures. This change does not break any existing APIs, only adds some code, so that users of this crate can implement glyph caching caching efficiently.