Changelog
1.1.7
Two spec-conformance fixes in the subsetter, both found while extracting it into a separate project and validating the output against the OpenType spec and fontTools rather than against daegun itself. Output bytes move for anything that rebuilds a font, so pinned subset fingerprints need re-recording. Nothing in the public API changed.
Fixed
head’s directory checksum was computed over the wrong bytes.build_ttfchecksummed the table as written, including whatevercheckSumAdjustmentthe incoming font happened to carry. The spec computes that entry with the field read as zero, since it is derived from the very checksum it would otherwise feed into. 26 of 27 test fonts came out with a wrong entry. Nothing caught it because the whole-file adjustment subtracted the stale value straight back out, so the file checksum validated and the one rule nobody was checking stayed broken. OTS runs over every font a browser downloads, and fontbakery checks the same field.- A subset could not address any codepoint above U+FFFF. Format 4 covers the BMP and nothing
else, and nothing emitted a format 12 subtable, so a supplementary-plane glyph was embedded and
named correctly in the glyph map while the cmap had no way back to it.
STIX2Mathwent from 5 of 16 requested codepoints addressable to all 16, and the plane 15 private-use test fonts from none to all of theirs. Fonts living entirely in the BMP emit nothing extra and their bytes are unchanged.
Changed
build_format4_cmapis nowbuild_unicode_cmap. It emits a format 12 subtable beside the format 4 one, with the(0, 4)and(3, 10)encoding records, whenever a mapping sits above the BMP. It is internal to the subsetter and absent from the C API.- The
subset fontsandinstanced fontsfingerprints moved. Theheadfix changes exactly two fields in every rebuilt font: its directory entry checksum, andcheckSumAdjustment, which shifts with it because the entry sits inside the header the file checksum covers. No other table and no glyph data changes.
Both fixes carry a test that was confirmed to fail without it, which the head bug in particular
earned: it had already survived one checksum rule that passed the whole time.
1.1.6
A fix to the fuzz harness, which is a development tool rather than part of the published crate. The library is byte-identical to 1.1.5 and there is nothing here to upgrade for.
Fixed
- The fuzz harness moved a field out of a shaped run, which stopped compiling once
Font::shapebegan handing back aShared<ShapedRun>. It sits outside the workspace, so the change that broke it never built it, and only the full gate caught it.
1.1.5
The measurement release. Testing the engine against real fonts and a real frame budget turned up seven correctness defects and two paths doing far more work than they needed to.
Fixed
- A contour started at the wrong point whenever its first point was off-curve. TrueType begins at point 0 if it is on-curve, otherwise at the last point if that one is, and only failing both at the midpoint. No rasterized comparison can catch this – a closed contour fills identically whichever vertex it starts from.
- CFF2 blending rounded every delta to a whole unit. Charstring coordinates are relative, so the error accumulated along the outline.
- CFF contours restated the closing segment that
closealready draws. - A font carrying both
glyfandCFFalways read CFF.sfntVersionis what breaks the tie. - Macintosh cmap subtables were all read as MacRoman, so a MacOS Turkish subtable resolved six
letters to
.notdef. - An axis tag shorter than four bytes matched nothing, because
fvarstores short tags space-padded.("wght", …)worked and("M1", …)was silently ignored. - Tai Tham was scored against the Universal Shaping Engine’s vowel ordering, which the script does not follow, so ordinary words came back marked as broken clusters. daegun now gives Tai Tham a cluster grammar of its own, scoped to that script so nothing else changes.
Performance
Two root causes account for most of it. Resolving an axis position built a BTreeMap and two
intermediate vectors to hold what is almost always a single axis; and the rasterizer’s overlap
resolution divided in its innermost loop.
| before | after | ||
|---|---|---|---|
Font::shape, cache hit |
285 ns, 13 allocations | 88 ns, 3 | −69% |
rasterize_glyph, first time |
25.7 µs, 31 allocations | 20.8 µs, 16 | −19% |
rasterize_glyph, cached |
73.4 ns | 63.5 ns | −13% |
advance_widths ×1 |
26.0 ns | 19.6 ns | −25% |
colr_v1_paint_graph_variable |
621.7 µs | 555.6 µs | −11% |
CPU raster pipeline, B at 16px |
4.999 µs | 4.543 µs | −9% |
Measured at the frame rather than the call, in a scrolling GPU app: re-laying-out a document of unchanged text fell from 0.0105 ms to 0.0048 ms a frame, and a page zoom from 12 to 96 pixels fell from 0.507 ms to 0.284 ms of rasterization a frame.
Bytes allocated per shape fell furthest of all, from 1,033 to 53.
Full figures in benchmark.md, which is re-measured for this release as the best of three runs.
Added
- Every cache budget is the caller’s. The glyph cache took a budget already; the curve,
outline, shape and variable-instance caches now do too, each with a
_statscall, and the cmap index takes an allowance. In Rust and in C alike. Those five were compile-time constants, which meant a CPU-only program carried the GPU curve cache, a Latin-only one carried a cmap index sized for CJK, and a fixed-weight one carried 64 MB of variable-instance headroom it never touched. daegun_vulkan_renderer_handles,daegun_d3d11_renderer_handlesanddaegun_d3d12_renderer_handles, the interop escape hatch that existed in Rust and had no C entry point.
Changed
Font::shapereturnsShared<ShapedRun>rather than an owned run. This is a breaking change.Sharedderefs toShapedRun, so field reads compile unchanged; moving a field out of the returned run now needs a.clone(). Three call sites needed that across daegun’s own tree.- The default glyph cache is 8 MB, up from 4. A full 12 to 96 pixel page zoom holds 6.9 MB of rasterized glyphs, so the old default evicted continuously through one. Measured: 8 MB removes that and 16 MB adds nothing.
- Resizing a cache evicts down to the new bound and keeps what still fits. It used to discard everything, so making a cache smaller cost you a warm one.
1.1.0
The refining release. Driving 1.0.0 from a real-time app turned up defects a static capture never would, and the capability that app needed most.
Fixed
- COLR v0 glyphs rendered nothing at all. Both entry points resolved color through the v1 paint graph, so a v0 font came back empty on the CPU and GPU paths alike.
- Two fonts in one
GpuBatchreturned each other’s outlines. The slot key carried no font identity. - COLR layers that defer to the caller’s text color rendered as nothing.
- Degenerate gradients flooded their shape with the last stop color instead of painting nothing.
- A draw with no instances was a validation error rather than a no-op.
- A borrowed Vulkan target leaked its image view and framebuffer.
- Metal was the one backend with no C entry point for adopting a device or borrowing a surface.
DAEGUN_SURFACE_RGBA8andDAEGUN_SURFACE_BGRA8sat inside the__APPLE__guard, so no caller on Windows or Linux had the constants their own calls take.- The C wrapper had never been compiled for Windows. It now type-checks and builds there.
Added
- Render into a surface you own, on Metal, Vulkan, D3D11 and D3D12.
from_deviceadopts the device you already made, and a target can wrap your swapchain image directly. No readback, no copy. See the GPU path. - The four backends are a documented module,
daegun::gpu, each one gated to the platform that has it. They were reachable before but hidden from the reference, so a Rust caller had no documentation for calls the C header described in full. - On Metal, daegun presents the drawable on the command buffer carrying the draw, so the queue orders the two and neither waits on the other.
- A caller-supplied text color for COLR layers that defer to it:
render_colr_glyph_with,gpu_color_glyph_withanddraw_glyph_with. See color. - A target’s byte order is yours to choose, RGBA8 or BGRA8, which is what it takes to match a swapchain without swizzling every frame.
- A target’s clear color is yours to choose, and
Nonekeeps what the target already holds, so several geometries layer into one surface rather than erasing each other. - One
GpuBatchnow holds any number of fonts, so a mixed-font page uploads once and draws once. Rgbais public at the crate root. It is in the signature of every_withcolor call, so it had to be documented beside them rather than reachable only through a hidden module.
Changed
GpuGlyphKeygains afontfield and#[non_exhaustive]. It is internal and#[doc(hidden)], constructed only inside daegun, and absent from the C API. This is why the release is 1.1.0 rather than 1.0.1.
No documented signature changed and nothing was removed, so 1.0.0 code compiles against 1.1.0 untouched.
1.0.0
2026-08-20
First release. A text engine in one crate: parsing, shaping, layout, rasterizing, subsetting and a
GPU path, with no dependencies and no std required.