render, shaping: let a caller pin the font files a frame is drawn with #466

Merged
natsukium merged 1 commit from render-pinned-fonts into main 2026-09-26 00:45:07 +09:00
Owner

Closes #441 (the pinned-fonts half; the offscreen target and readback landed in #465).

Why

Font selection always scanned the system, so a recording or screenshot rendered with different faces on two machines and a golden test could not hold its pixels. felis-reel needs to name the exact font files.

What

  • RendererConfig::font_files: when non-empty, every face lookup (primary, bold/italic styles, fallback walk) runs auto_discover's discovery over these files instead of the system scan. It is read by the warm-up thread, the constructors and reload_font alike, so a reload cannot bring system faces back, and font features stay the config's as in a window.
  • FontStack::discover_in_files: reads each file into memory (not mmap: the mapping's safety rests on immutable system font roots, and a caller's file can be truncated underneath it) and rejects a file with no face, which fontdb otherwise accepts silently.
  • felis-client passes an empty list; its behavior is unchanged.
  • docs/explanation/rendering/text-shaping.md: "Font fallback" and "Font loading" describe the pinned path.

A prebuilt FontStack handed to the constructor (the issue's proposal 3) was rejected: start_fonts, reload_font and the config's feature list could each reintroduce faces or features the stack was not built with.

Verification

  • just check green.
  • New tests: discovery over the Monaspace Neon test files ignores a system fallback; a missing file and a non-font file each report FontPath; a warm-up stack is discarded when font_files changes.
  • pi review (sol): round 1 blocked the prebuilt-stack design; round 2 on this design passed with no findings.
Closes #441 (the pinned-fonts half; the offscreen target and readback landed in #465). ## Why Font selection always scanned the system, so a recording or screenshot rendered with different faces on two machines and a golden test could not hold its pixels. felis-reel needs to name the exact font files. ## What - `RendererConfig::font_files`: when non-empty, every face lookup (primary, bold/italic styles, fallback walk) runs `auto_discover`'s discovery over these files instead of the system scan. It is read by the warm-up thread, the constructors and `reload_font` alike, so a reload cannot bring system faces back, and font features stay the config's as in a window. - `FontStack::discover_in_files`: reads each file into memory (not mmap: the mapping's safety rests on immutable system font roots, and a caller's file can be truncated underneath it) and rejects a file with no face, which fontdb otherwise accepts silently. - felis-client passes an empty list; its behavior is unchanged. - `docs/explanation/rendering/text-shaping.md`: "Font fallback" and "Font loading" describe the pinned path. A prebuilt `FontStack` handed to the constructor (the issue's proposal 3) was rejected: `start_fonts`, `reload_font` and the config's feature list could each reintroduce faces or features the stack was not built with. ## Verification - `just check` green. - New tests: discovery over the Monaspace Neon test files ignores a system fallback; a missing file and a non-font file each report `FontPath`; a warm-up stack is discarded when `font_files` changes. - pi review (sol): round 1 blocked the prebuilt-stack design; round 2 on this design passed with no findings.
render, shaping: let a caller pin the font files a frame is drawn with
All checks were successful
darwin / detect relevant changes (pull_request) Successful in 14s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 19s
windows / windows docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 41s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 55s
darwin / publish felis (aarch64-darwin) (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m52s
pr / wire schema is compatible with the base (pull_request) Successful in 15s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 25s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 13s
windows / cargo nextest (Windows) (pull_request) Successful in 4m5s
windows / frontend smoke (Windows) (pull_request) Successful in 33s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
e44360a92d
Font selection always scanned the system, so the same recording or
screenshot rendered with different faces on two machines, and a golden
test could not hold its pixels. A windowless consumer needs to choose
the exact font files.

`RendererConfig::font_files` replaces the system scan with the given
files for every lookup: the discovery `auto_discover` runs, styled faces
and fallback walk included, runs over them instead. It is part of the
config rather than a prebuilt stack handed to the constructor, so the
warm-up thread, the initial build and `reload_font` all resolve from the
same source, and the font features stay the config's as they do for a
window. A single-face `FontStack::new` was not enough: it draws bold,
italic and fallback glyphs differently from a window using the same
fonts.

The files are read into memory instead of mapped. The mmap path relies
on system font roots being immutable, and a caller's file can be
truncated under the mapping, which faults the process. A file with no
face in it is an error: fontdb accepts one silently, which would let a
typo fall through to a different face.

Assisted-by: Claude Code
natsukium deleted branch render-pinned-fonts 2026-09-26 00:45:07 +09:00
Commenting is not possible because the repository is archived.
No description provided.