shaping: round the cell width to the nearest pixel #411

Merged
natsukium merged 1 commit from shaping/round-cell-width into main 2026-09-25 03:01:08 +09:00
Owner

What

CellMetrics::from_scaled rounds the cell width to the nearest pixel instead of ceiling it (the .max(1.0) floor stays, so a sub-half-pixel advance still yields a 1 px cell). Height keeps its separate ceil of ascent and descent. docs/explanation/rendering/text-shaping.md "Atlas integration" is rewritten with what kitty actually does, why rounding is safe, the rejected alternatives, why height keeps its rule, and a "Revisit if" trigger. CHANGELOG gains a "Changed" entry, since a window of a given size now holds more columns at the same font.size_px.

Why

The ceil was justified by "kitty ceils the same advance". That holds only on macOS, where kitty ceils CoreText's unhinted advance. On Linux kitty ceils a FreeType advance that hinting has already rounded to a whole pixel, so its effective rule there is round. felis therefore drew 12 px cells where kitty, alacritty and foot draw 11 (issue diagnosis comment has the source walk).

Measured

advance felis before felis after peers
Linux, DejaVu Sans Mono 14 pt (18.67 px) 11.2383 px (swash; FreeType hinted 11.0) 12 11 kitty, alacritty, foot 11
macOS, Menlo 14 pt (72 dpi) 8.4287 px (CoreText, measured on an M4 Max) 9 8 ghostty, alacritty 8; kitty 9

Menlo on the Mac via CoreText: advance 8.4287, ascent 12.9951, descent 3.3018, leading 0, matching the DejaVu-derived numbers in the diagnosis.

Spill with the new width, measured on felis's own Linux (hinted) raster at 18.67 px in an 11 px cell: ASCII ink reaches at most one column into the neighbor (_ 44% coverage, # 41%, W 36%); nothing reaches further or left of the cell. Backgrounds paint before any glyph and glyph quads are not clipped to the cell, so the spill is drawn over the neighbor.

Height is unchanged (23 px on Linux, matching every peer; 17 px for Menlo on macOS where peers get 16): rounding the descent would reach 16 on macOS but drop Linux to 22.

just check: fmt, clippy, portable, prose, skills, docs-links, deny, proto-compat and release-check pass; nextest 3542/3543 on the full run, the one failure (os_clipboard::tests::write_primary_program_lands_when_program_writes_allowed, a live PRIMARY selection test) and two earlier load flakes in felis-cli / felis-daemon pass in isolation and do not touch shaping.

Closes #404

## What `CellMetrics::from_scaled` rounds the cell width to the nearest pixel instead of ceiling it (the `.max(1.0)` floor stays, so a sub-half-pixel advance still yields a 1 px cell). Height keeps its separate ceil of ascent and descent. `docs/explanation/rendering/text-shaping.md` "Atlas integration" is rewritten with what kitty actually does, why rounding is safe, the rejected alternatives, why height keeps its rule, and a "Revisit if" trigger. CHANGELOG gains a "Changed" entry, since a window of a given size now holds more columns at the same `font.size_px`. ## Why The ceil was justified by "kitty ceils the same advance". That holds only on macOS, where kitty ceils CoreText's unhinted advance. On Linux kitty ceils a FreeType advance that hinting has already rounded to a whole pixel, so its effective rule there is round. felis therefore drew 12 px cells where kitty, alacritty and foot draw 11 (issue diagnosis comment has the source walk). ## Measured | | advance | felis before | felis after | peers | |---|---|---|---|---| | Linux, DejaVu Sans Mono 14 pt (18.67 px) | 11.2383 px (swash; FreeType hinted 11.0) | 12 | 11 | kitty, alacritty, foot 11 | | macOS, Menlo 14 pt (72 dpi) | 8.4287 px (CoreText, measured on an M4 Max) | 9 | 8 | ghostty, alacritty 8; kitty 9 | Menlo on the Mac via CoreText: advance 8.4287, ascent 12.9951, descent 3.3018, leading 0, matching the DejaVu-derived numbers in the diagnosis. Spill with the new width, measured on felis's own Linux (hinted) raster at 18.67 px in an 11 px cell: ASCII ink reaches at most one column into the neighbor (`_` 44% coverage, `#` 41%, `W` 36%); nothing reaches further or left of the cell. Backgrounds paint before any glyph and glyph quads are not clipped to the cell, so the spill is drawn over the neighbor. Height is unchanged (23 px on Linux, matching every peer; 17 px for Menlo on macOS where peers get 16): rounding the descent would reach 16 on macOS but drop Linux to 22. `just check`: fmt, clippy, portable, prose, skills, docs-links, deny, proto-compat and release-check pass; nextest 3542/3543 on the full run, the one failure (`os_clipboard::tests::write_primary_program_lands_when_program_writes_allowed`, a live PRIMARY selection test) and two earlier load flakes in `felis-cli` / `felis-daemon` pass in isolation and do not touch shaping. Closes #404
shaping: round the cell width to the nearest pixel
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / publish felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / darwin docs-only no-op (pull_request) Has been cancelled
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
bench / Criterion regression gate (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
73c81c2627
Ceiling the `M` advance was justified as matching kitty, but that holds
only on macOS. On Linux kitty ceils an advance FreeType has already
hinted to a whole pixel, so DejaVu Sans Mono at 14 pt gets 11 px cells
in kitty, alacritty and foot while felis got 12, painting 9% more
pixels per row and fitting fewer columns in the same window. Rounding
gives 11 there and 8 for Menlo at 14 pt on macOS (CoreText advance
8.4287 px, measured on the Mac), matching ghostty and alacritty; only
kitty's macOS path, which ceils CoreText's unhinted advance, stays at 9.

Glyph quads are never clipped to the cell and every background paints
before any glyph, so the ink a rounded-down advance pushes into the
neighbor (under half a pixel unhinted; one column of up to 44% coverage
for DejaVu's hinted `_`, `#` and `W` on Linux) is drawn, not lost. The height
keeps its separate ceil of ascent and descent: rounding the descent
would give macOS peers' 16 px for Menlo but drop DejaVu on Linux to
22 px, one row short of every Linux peer.

Assisted-by: Claude Code
natsukium force-pushed shaping/round-cell-width from 73c81c2627
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / publish felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / darwin docs-only no-op (pull_request) Has been cancelled
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
bench / Criterion regression gate (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
to 1b7c652df0
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m5s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m48s
pr / wire schema is compatible with the base (pull_request) Successful in 12s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m22s
windows / detect relevant changes (pull_request) Successful in 4s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 12s
bench / Criterion regression gate (pull_request) Successful in 2m11s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 53s
darwin / publish felis (aarch64-darwin) (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 35s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 4m43s
windows / frontend smoke (Windows) (pull_request) Successful in 1m31s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
2026-09-25 02:44:41 +09:00
Compare
natsukium deleted branch shaping/round-cell-width 2026-09-25 03:01:08 +09:00
Commenting is not possible because the repository is archived.
No description provided.