Fix text kerning, HiDPI glyph padding, and invisible-glyph rasterization#511
Merged
Conversation
advance_to_next_glyph only advanced by xadvance, so kerning never affected glyph positions, text measurement, or word-wrap -- and cf_font_get_kern looked its table up by codepoint even though the table was keyed by glyph index, so it silently returned 0 for almost every font. Fix both: kern is applied to the cursor (not just a per-glyph render nudge) so it accumulates and is reflected in cf_text_size/wrap, and cf_font_get_kern now tries stb's GPOS-aware codepoint API first and falls back to the glyph-index-keyed legacy kern table when GPOS reports nothing (as happens with this project's bundled Calibri, whose GPOS layout stb's minimal parser can't read but whose legacy kern table has real data). Claude-Session: https://claude.ai/code/session_011V2maY4DG1DHscsMEHy6Bf
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes text layout kerning so kerning affects the layout cursor (measurement and wrapping) rather than a per-glyph render-only offset, and corrects kerning data lookup by using stb’s codepoint kerning API with a fallback to the legacy kern table keyed by glyph index.
Changes:
- Update
cf_font_get_kernto preferstbtt_GetCodepointKernAdvance(GPOS) and fall back to the legacy kerning map using glyph indices. - Update
s_draw_textto accumulate kerning into the horizontal cursor so layout/measurement matches rendering. - Update
s_find_end_of_lineto include kerning in wrap width accounting.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+2030
to
+2033
| // stb can't read, yet still carry real data in the legacy `kern` table. Since | ||
| // stb only consults `kern` when there's no GPOS table at all, fall back to our | ||
| // own glyph-index lookup into `kerning` (built in cf_make_font_from_data) when | ||
| // the GPOS path comes up empty. |
Comment on lines
+1793
to
+1795
| // Build the legacy kerning table, keyed by glyph index (as stb provides it). Used | ||
| // as a fallback in cf_font_get_kern for fonts whose GPOS table isn't in a layout | ||
| // stb's minimal parser understands, even though real kern data still exists here. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Two more text-rendering bugs from the same review that turned up the kerning issue: - The glyph quad's border padding was a flat 1 logical unit, meant to cover spritebatch's 1-pixel atlas border. That border is 1 *device* pixel, and glyph bitmaps rasterize at pixel_scale resolution while q0/q1 are logical units -- so on HiDPI the pad overshot the actual border, stretching every glyph quad slightly past its real coverage. Divide by pixel_scale to match. - `glyph->visible |= w > 0 && h > 0` always evaluated true (w/h include border padding, which is never zero), silently overriding stbtt_IsGlyphEmpty's correct verdict. Every space character ended up rasterized, given an atlas slot, and pushed as a blank draw quad each time it appeared. Beyond fixing the flag, invisible glyphs now skip s_render's rasterization and atlas registration entirely -- layout metrics (xadvance, q0/q1) are still computed since those are needed regardless of visibility. A new `rendered` flag on CF_Glyph replaces the old `image_id`-based cache-hit check, since invisible glyphs never get an image_id. Verified via a debug print in samples/font_debug.cpp (reverted after): a space glyph now reports visible=0 rendered=1 image_id=0 xadvance=5.88 -- no rasterization or atlas slot, but layout spacing is unaffected. Claude-Session: https://claude.ai/code/session_011V2maY4DG1DHscsMEHy6Bf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A review of
src/cute_draw.cpp's text rendering path turned up three independent bugs. All are fixed here.1. Kerning was not applied to the text layout cursor
advance_to_next_glyphins_draw_textonly advancedxbyglyph->xadvance. The only place kerning was used was a local offset added to a single glyph's quad in the render block, which:if (render || markups), so the measurement pass (cf_text_size, word-wrap) never saw it, making returned sizes and wrap points disagree with what was actually drawn.cf_font_get_kernalso looked up the wrong key, so it usually returned 0 outright: the kerning table is built fromstbtt_GetKerningTable, keyed by glyph index, but the lookup queried it with raw codepoints.Fixing the lookup with stb's
stbtt_GetCodepointKernAdvance(which converts codepoint -> glyph index and also reads GPOS) still returned 0 for classic pairs like "AV"/"To" on this project's bundled Calibri. A standalone probe against the embedded font data showed why: Calibri has a populated legacykerntable (26,706 entries, real values e.g. "To" = -182 units) and aGPOStable, but stb's minimal GPOS parser doesn't support this font's GPOS lookup layout, and stb only consults the legacykerntable when there's no GPOS table at all. The fix now tries GPOS first and falls back to the (correctly glyph-index-keyed) legacy table when GPOS reports nothing.s_find_end_of_line(word-wrap) also now folds kerning into its width accounting, and the cursor correctly skips kerning for the first glyph of the string (cp_prevstarts at 0 -- thanks for catching that in the follow-up commit).2. HiDPI glyph padding was off by
pixel_scaleThe glyph quad's border pad was a flat
V2(1,1)logical unit, meant to cover spritebatch's 1-pixel atlas border. That border is 1 device pixel, while glyph bitmaps rasterize atpixel_scaleresolution andq0/q1are logical units -- so on Retina displays the pad overshot the border, stretching every glyph quad slightly past its actual coverage (worse for smaller glyphs). Fixed by dividing the pad byapp->pixel_scale, matching howq0/q1/xadvanceare already scaled elsewhere in the same function.3. Every space character was rasterized, given an atlas slot, and drawn as a blank quad
glyph->visible |= w > 0 && h > 0always evaluated true (w/hinclude border padding, which is never zero), silently overridingstbtt_IsGlyphEmpty's correct verdict. Every invisible glyph (spaces, etc.) ended up rasterized, cached into the atlas, and pushed as a blank draw quad on every occurrence. Beyond fixing the flag, invisible glyphs now skips_render's rasterization and atlas registration entirely -- layout metrics (xadvance,q0/q1) are still computed since those are needed regardless of visibility. A newrenderedflag onCF_Glyphreplaces the oldimage_id-based cache-hit check incf_font_get_glyph, since invisible glyphs never get animage_id.Changes (
src/cute_draw.cpp,src/internal/cute_font_internal.h)cf_font_get_kern: trystbtt_GetCodepointKernAdvancefirst, fall back to a glyph-index lookup into the legacykerntable.s_draw_text: accumulate kern into thexcursor before positioning each glyph (horizontal layout only, skipped right after a line break or for the first glyph), unconditionally so measurement stays consistent with rendering. Removed the now-redundant per-glyph kern offset on the quad.s_find_end_of_line: word-wrap width accounting now includes kerning too.app->pixel_scaleto match the device-pixel atlas border.s_render/cf_font_get_glyph: invisible glyphs skip rasterization and atlas registration;CF_Glyph::renderedtracks "already processed" instead ofimage_id.Test plan
cmake --build build-- clean, no new warnings./build/tests-- 173/173 passingsamples/font_debug.cppagainst the bundled Calibri: kerning readout for "To" went from0.00to-2.31at font size 26; before/after screenshot crop confirms theoglyph is now pulled tight under theTvisible=0 rendered=1 image_id=0 xadvance=5.88-- no rasterization or atlas slot allocated, but layout spacing is unaffectedhttps://claude.ai/code/session_011V2maY4DG1DHscsMEHy6Bf