Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ A few things you will likely need next:
writing our own made no sense (Brotli for WOFF2, zlib for WOFF1 and PNG, clap for the
CLI). [docs/deps.md](docs/deps.md) explains each one.
- `no_std` support. The default build uses `std`, but the shaping path runs under
`no_std` with `alloc`.
`no_std` with `alloc`. The target needs pointer-width atomics with compare-and-swap,
which every target with `alloc::sync::Arc` has (`thumbv6m-none-eabi` does not).
- Deterministic output. The same font, text, features, and direction produce the same
glyphs, byte for byte. That matters for replays, lockstep networking, and golden-file
tests.
Expand Down
13 changes: 12 additions & 1 deletion docs/STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ with, for example, `sigilbuzz-render = "0.10"` and `sigilbuzz-paint = "0.3"`.
The target is 1.0 in 2026, with a stable shaping API and a documented path for
renderers built on top of it.

## Supported targets

The core crate builds on stable Rust 1.81 or later, with or without `std`, on any
target that has pointer-width atomics with compare-and-swap
(`cfg(target_has_atomic = "ptr")`). A `Font` builds its shaping caches through atomic
compare-and-swap, and `OwnedFace` shares its bytes through `alloc::sync::Arc`, which
needs the same. Targets without them, such as `thumbv6m-none-eabi` (Cortex-M0 and
M0+), do not build; this was already true before 0.24.0, through `alloc::sync`.
`wasm32-unknown-unknown` and the usual desktop and mobile targets have them.

## Three tiers

### Tier 1: stable public API
Expand Down Expand Up @@ -92,7 +102,8 @@ signatures only change in a major version.
- Added in 0.24.0:
- `Font::with_variations`: bind a variable instance from user-space axis values, as
HarfBuzz's `hb_font_set_variations` does. `Font::coords` now returns a slice that
borrows the font, since such a font owns its coords.
borrows the font, since such a font owns its coords, and is no longer a `const fn`,
so it cannot be called in a const context. `Font::new` is still `const`.
- `Face::glyph_outlines` and `GlyphOutlines::{outline, draw}`: draw many glyphs at
one instance with the outline tables read once.

Expand Down
47 changes: 37 additions & 10 deletions fuzz/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Shared helpers for the sigilbuzz fuzz targets.
//!
//! The helpers never panic on their own, so any crash a target reports comes
//! from sigilbuzz itself.
//! from sigilbuzz itself. The one assertion, in [`shape_samples`], checks
//! sigilbuzz's own promise that a `Font`'s caches never change its output.

use sigilbuzz::{
shape, BidiParagraph, Buffer, BufferFlags, ClusterLevel, Direction, Face, Feature, Font,
Expand Down Expand Up @@ -101,25 +102,43 @@ pub fn axis_coords(face: &Face<'_>, knobs: &mut Knobs<'_>) -> Vec<f32> {
(0..axes.min(16) + extra).map(|_| knobs.coord()).collect()
}

/// A font of `face` at `size`, at `variations` (user-space values) when
/// given, else at the normalized `coords`.
fn bind<'a>(
face: Face<'a>,
size: f32,
coords: &'a [f32],
variations: Option<&[([u8; 4], f32)]>,
) -> Font<'a> {
let font = Font::new(face, size);
match variations {
Some(variations) => font.with_variations(variations),
None => font.with_coords(coords),
}
}

/// Shapes every sample text (plus `extra`) against `face` with the given
/// knobs choosing direction, features, size, and coordinates.
///
/// One font shapes every text, so from its second call on it shapes with
/// the caches it keeps. Each text is shaped with a fresh font too, which
/// builds none, and the two must agree glyph for glyph.
pub fn shape_samples(face: Face<'_>, knobs: &mut Knobs<'_>, extra: Option<&str>) {
let coords = axis_coords(&face, knobs);
let size = f32::from(knobs.byte()) + 1.0;
// Half the time the instance comes from user-space values.
let font = if knobs.byte() & 1 == 1 {
let variations: Vec<([u8; 4], f32)> = match face.fvar() {
let variations: Option<Vec<([u8; 4], f32)>> =
(knobs.byte() & 1 == 1).then(|| match face.fvar() {
Ok(Some(fvar)) => fvar
.axes()
.iter()
.map(|a| (a.tag, a.default_value + knobs.coord() * 500.0))
.collect(),
_ => Vec::new(),
};
Font::new(face, size).with_variations(&variations)
} else {
Font::new(face, size).with_coords(&coords)
};
});
let variations = variations.as_deref();
let font = bind(face.clone(), size, &coords, variations);
let fresh = || bind(face.clone(), size, &coords, variations);
let mut features = Vec::new();
for _ in 0..(knobs.byte() % 6) {
let i = usize::from(knobs.byte()) % FEATURE_TAGS.len();
Expand Down Expand Up @@ -147,7 +166,13 @@ pub fn shape_samples(face: Face<'_>, knobs: &mut Knobs<'_>, extra: Option<&str>)
// Bidi text shapes run by run, each run in its own
// direction, with `buffer` as the settings template.
let paragraph = BidiParagraph::new(text, None);
let _ = paragraph.shape(&font, &buffer, &features);
let warm = paragraph
.shape(&font, &buffer, &features)
.map(|run| run.glyphs);
let cold = paragraph
.shape(&fresh(), &buffer, &features)
.map(|run| run.glyphs);
assert_eq!(warm, cold, "a used Font shaped {text:?} unlike a fresh one");
buffer.set_text(text);
}
}
Expand All @@ -158,6 +183,8 @@ pub fn shape_samples(face: Face<'_>, knobs: &mut Knobs<'_>, extra: Option<&str>)
_ => Direction::Btt,
});
let _ = buffer.script_runs();
let _ = shape(&font, &buffer, &features);
let warm = shape(&font, &buffer, &features).map(|run| run.glyphs);
let cold = shape(&fresh(), &buffer, &features).map(|run| run.glyphs);
assert_eq!(warm, cold, "a used Font shaped {text:?} unlike a fresh one");
}
}
56 changes: 30 additions & 26 deletions src/font/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
//! [`Font::with_coords`](super::Font::with_coords) and
//! [`Font::with_size`](super::Font::with_size) keep it.
//! - [`InstanceCache`], which depends on the variation coordinates too:
//! the vertical origins and the advances that come from phantom
//! points, which cost an outline walk per glyph. `with_size` keeps it
//! and `with_coords` starts a new one.
//! the vertical origins and the varied advances, which cost an outline
//! walk (phantom points) or a walk of a variation store's regions
//! (`HVAR`, `VVAR`) per glyph. `with_size` keeps it and `with_coords`
//! starts a new one.
//!
//! A font's first shaping call builds none of this: a font shaped once,
//! which many callers build per run, would spend more building it than
Expand All @@ -31,11 +32,13 @@
//! output does not depend on what is cached.
//!
//! Memory is bounded per font. For each of GSUB and GPOS: one pointer
//! per lookup, 24 bytes per subtable of each lookup a run reached, at
//! most 16 resolved language systems of at most 1024 features and 16384
//! lookup indices each, and at most 64 stage plans of at most 4096
//! lookups each. For the glyph caches: at most [`GLYPH_CACHE_MAX`]
//! four-byte entries for each of the three, whatever the glyph count.
//! per lookup; 24 bytes per subtable of each lookup a run reached, for
//! at most twice as many subtables as the table has bytes plus 64 Ki
//! (lookup indices that name one lookup share its digests); at most 16
//! resolved language systems of at most 1024 features and 16384 lookup
//! indices each; and at most 64 stage plans of at most 4096 lookups
//! each. For the glyph caches: at most [`GLYPH_CACHE_MAX`] four-byte
//! entries for each of the three, whatever the glyph count.

use alloc::boxed::Box;
use alloc::sync::Arc;
Expand All @@ -44,7 +47,7 @@ use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};

use crate::sync::OnceBox;
use crate::tables::layout::accel::LayoutCache;
use crate::tables::layout::LayoutTable;
use crate::tables::layout::{LayoutTable, LookupList};

/// The caches of one [`Font`](super::Font).
pub(crate) struct FontCaches {
Expand Down Expand Up @@ -148,18 +151,18 @@ impl FaceCache {
}
}

/// What the font keeps for its GSUB, which has `lookup_count`
/// lookups in `table_len` bytes.
pub(crate) fn gsub(&self, lookup_count: u16, table_len: usize) -> &LayoutCache {
/// What the font keeps for its GSUB, whose LookupList is `lookups`,
/// in `table_len` bytes.
pub(crate) fn gsub(&self, lookups: &LookupList<'_>, table_len: usize) -> &LayoutCache {
self.gsub
.get_or_init(|| LayoutCache::new(LayoutTable::Gsub, lookup_count, table_len))
.get_or_init(|| LayoutCache::new(LayoutTable::Gsub, lookups, table_len))
}

/// What the font keeps for its GPOS, which has `lookup_count`
/// lookups in `table_len` bytes.
pub(crate) fn gpos(&self, lookup_count: u16, table_len: usize) -> &LayoutCache {
/// What the font keeps for its GPOS, whose LookupList is `lookups`,
/// in `table_len` bytes.
pub(crate) fn gpos(&self, lookups: &LookupList<'_>, table_len: usize) -> &LayoutCache {
self.gpos
.get_or_init(|| LayoutCache::new(LayoutTable::Gpos, lookup_count, table_len))
.get_or_init(|| LayoutCache::new(LayoutTable::Gpos, lookups, table_len))
}

fn heap_bytes(&self) -> usize {
Expand All @@ -173,7 +176,7 @@ impl FaceCache {
}

/// What a font keeps that depends on its coordinates too: per-glyph
/// values whose computation walks an outline.
/// values whose computation walks an outline or a variation store.
pub(crate) struct InstanceCache {
v_origins: OnceBox<GlyphCache>,
v_advances: OnceBox<GlyphCache>,
Expand All @@ -185,12 +188,13 @@ pub(crate) struct InstanceCache {
pub(crate) enum GlyphValue {
/// The y of the vertical origin.
VOrigin,
/// The vertical advance from varied phantom points, if they could
/// be computed.
VPhantomAdvance,
/// The horizontal advance from varied phantom points, if they
/// could be computed.
HPhantomAdvance,
/// The varied vertical advance: from `VVAR`, or from varied phantom
/// points, if they could be computed. A font has `VVAR` or not, so
/// one instance only ever stores one kind.
VAdvance,
/// The varied horizontal advance: from `HVAR`, or from varied
/// phantom points, if they could be computed.
HAdvance,
}

impl InstanceCache {
Expand All @@ -205,8 +209,8 @@ impl InstanceCache {
fn cache(&self, which: GlyphValue) -> &OnceBox<GlyphCache> {
match which {
GlyphValue::VOrigin => &self.v_origins,
GlyphValue::VPhantomAdvance => &self.v_advances,
GlyphValue::HPhantomAdvance => &self.h_advances,
GlyphValue::VAdvance => &self.v_advances,
GlyphValue::HAdvance => &self.h_advances,
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
//! The crate compiles with `--no-default-features` on stable Rust. The
//! default `std` feature adds filesystem helpers such as `Blob::from_path`.
//! Everything on the shaping path works without it.
//!
//! The target needs pointer-width atomics with compare-and-swap
//! (`cfg(target_has_atomic = "ptr")`): a [`Font`] builds its shaping
//! caches through them without a lock, and [`OwnedFace`] shares its bytes
//! through `alloc::sync::Arc`, which needs them too. Targets without
//! them, such as `thumbv6m-none-eabi`, do not build.

#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_op_in_unsafe_fn)]
Expand Down
Loading
Loading