shaping: felis's cell is a pixel larger than kitty's at the same pinned size #404
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#404
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
At the pinned font and size the harness measured felis's cell as larger than every peer's (
docs/reference/benchmarks.md"Limits of this run"):At 106x35 cells felis paints 8-21% more pixels, which also puts it at a disadvantage in every throughput and latency suite.
CellMetrics::from_scaled(crates/felis-shaping/src/lib.rs) ceils the width, anddocs/explanation/rendering/text-shaping.mdjustifies it partly with "kitty ceils the same advance". The Linux figures contradict that premise: kitty gets 11 px where felis gets 12. Either the pt-to-px conversion differs, or kitty's rounding is not what the doc says.Decide:
text-shaping.mdand keep the ceil, or change the rounding and record why.Done when the doc's premise matches what kitty does, and the choice is recorded in the owning explanation doc.
Diagnosis: where felis's cell width and kitty's diverge
Short answer: the pt→px conversion is the same in both. The difference is hinting. On Linux, kitty's FreeType path ceils an advance that FreeType has already rounded to a whole pixel, so kitty's ceil does nothing there. The doc's premise holds for kitty's CoreText path and fails for its FreeType path.
Sources: kitty
4498c0028, crossfontmaster, ghostty4ae9f1a2d. The numbers come from a FreeType 2.13 probe (FT_Set_Char_SizeplusFT_Load_Glyphunder each load flag) and a swash probe ofFont::cell_metricsin a throwaway worktree. Both ran onDejaVuSansMono.ttf2.37 from nixpkgs. These are deterministic computations, so host load does not affect them.1. pt → px: identical
dpi_from_scale(kitty/glfw.c:1491) uses 96 × scale on Linux and 72 × scale on macOS.FT_Set_Char_Size(face, 0, ceil(pt*64), dpi, dpi)(kitty/freetype.c:184,228).tools/bench/field.py:150givespt * 96/72on Linux andpton macOS.head.flagsbit 3 (integer ppem) is set, so FreeType reportsx_ppem = 19.x_scalestill corresponds to 18.67 px, and the metrics below use that scale.2. Which advance, and how it is rounded
crates/felis-shaping/src/lib.rs:182-191,:692)Mceilkitty/freetype.c:546-552)FT_Load_Glyphwith the fontconfig hint style (get_load_flags,:129) →glyph->metrics.horiAdvanceceilfof an already pixel-rounded valuekitty/core_text.m:671)CTFontGetAdvancesForGlyphs, unhintedceilfsrc/ft/mod.rs:423)0horiAdvancefloor(display/mod.rs:1612)src/darwin/mod.rs:327)0floorsrc/font/Metrics.zig:239-265)@round, with a comment explaining why ceil was rejectedFreeType probe at 18.667 px (this host's fontconfig:
hinting=True hintstyle=1(slight), so kitty passesFT_LOAD_TARGET_LIGHT):felis/swash at 18.667 px: advance 11.2383.
ceilgives 12, and kitty'sceilf(11.0)gives 11. Even at slight hinting, FreeType rounds the advance to the nearest pixel (11.24 → 11), so kitty's ceil acts on an integer. Withhinting=falsein fontconfig, kitty would also get 12.macOS, Menlo 14 px: CoreText does not hint advances. kitty gets
ceilf(8.43) = 9, the same as felis's 9, and alacritty (floor) and ghostty (round) get 8. The raw values are swash at 14 px: advance 8.4287, ascent 12.9951, descent 3.3018, leading 0. They were computed from DejaVu Sans Mono, which shares Menlo's metrics, and they reproduce the measured felis 9x17 exactly. Verifying them against Menlo itself on the Mac remains open.Verdict on
docs/explanation/rendering/text-shaping.md:222-225: "kitty ceils the same advance (its CoreText and FreeType backends bothceilper-glyph advances)" describes the code correctly but gets the effect wrong. On FreeType, kitty ceils a hinted advance that is already whole, so its effective rule isroundat the fontconfig default. The claim that ceiling "keeps felis and kitty on the same column count" holds on macOS only.3. Height (Linux 23 = 23, macOS 17 vs 16)
ceil(17.327) + ceil(4.402) = 18 + 5 = 23. kittyceil(height*y_scale) = ceil(21.73) = 22(freetype.c:167), which the_bitmap check (:169-176) then bumps to 23. alacrittymax(size_metrics.height = 22, asc − desc = 18 + 5)= 23. foot uses fcft's per-metric ceil, also 23. Every terminal agrees.ceil(12.995) + ceil(3.302) = 13 + 4 = 17. alacritty rounds each part:13 + 3 + 0 = 16(darwin/mod.rs:329-332). ghostty rounds the sum:round(16.297) = 16. kitty gets 16 fromceilfof the CTFramesetter line pitch (core_text.m:704), so CoreText must lay lines out 16.0 px apart. The extra row comes from ceiling a 3.30 px descent.4. Proposed change
Width: switch
CellMetrics::from_scaledfromwidth.ceil()towidth.round()(ghostty's rule). The results would be:roundClipping: rounding does not reintroduce the clipping the doc warns about, because felis draws glyph quads at bitmap size and never clips them to the cell. Instances carry per-glyph
left/topoffsets, and negative bearings already slice back across cells (crates/felis-render-wgpu/src/instances.rs). DejaVu's ASCII ink at 18.667 px spans [0.000 ('W'), 11.234 ('R')]. In an 11 px cell the widest glyphs therefore reach at most 0.23 px of antialiased coverage into the neighbor cell, and no ink is lost. kitty on Linux accepts the same overlap: slight hinting rounds the advance but leaves the outline unhinted horizontally. ghostty accepts it explicitly (Metrics.zig:239-248). The doc's real concern is that "glyphs touch", which is the costroundaccepts. Thefrom_scaleddoc comment and the7.35 → 8test (lib.rs:1233) flip to 7.A kitty-exact alternative would be "round on Linux, ceil on macOS". felis rasterizes with swash on both platforms, so that split would mirror a rasterizer difference felis does not have. I recommend one rule.
Height: keep the separate ceil, and record in
text-shaping.mdthat macOS peers get 16 for Menlo 14 because CoreText rounds a 3.30 px descent down, which a single cross-platform rule cannot match without losing Linux parity.Size: about 3 lines of code plus 2 test assertions, a rewrite of the
text-shaping.md"Atlas integration" width paragraph (≈8 lines), and a CHANGELOG entry, since the default column count changes for the samefont_size. Thetools/bench/field.pygrid probe re-derives geometry from felis, so the bench field follows automatically.Confidence: high for Linux (every number measured on this host). For macOS it is medium, because Menlo's metrics are inferred from DejaVu (they reproduce the measured 9x17) and not read from the Mac.