Skip to content

Fix text kerning, HiDPI glyph padding, and invisible-glyph rasterization#511

Merged
RandyGaul merged 3 commits into
RandyGaul:masterfrom
pusewicz:fix/text-kerning
Jul 6, 2026
Merged

Fix text kerning, HiDPI glyph padding, and invisible-glyph rasterization#511
RandyGaul merged 3 commits into
RandyGaul:masterfrom
pusewicz:fix/text-kerning

Conversation

@pusewicz

@pusewicz pusewicz commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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_glyph in s_draw_text only advanced x by glyph->xadvance. The only place kerning was used was a local offset added to a single glyph's quad in the render block, which:

  • Didn't accumulate — every glyph past the first carried none of the preceding kern adjustments.
  • Was gated behind 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_kern also looked up the wrong key, so it usually returned 0 outright: the kerning table is built from stbtt_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 legacy kern table (26,706 entries, real values e.g. "To" = -182 units) and a GPOS table, but stb's minimal GPOS parser doesn't support this font's GPOS lookup layout, and stb only consults the legacy kern table 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_prev starts at 0 -- thanks for catching that in the follow-up commit).

2. HiDPI glyph padding was off by pixel_scale

The 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 at pixel_scale resolution and q0/q1 are 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 by app->pixel_scale, matching how q0/q1/xadvance are 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 > 0 always evaluated true (w/h include border padding, which is never zero), silently overriding stbtt_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 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 in cf_font_get_glyph, since invisible glyphs never get an image_id.

Changes (src/cute_draw.cpp, src/internal/cute_font_internal.h)

  • cf_font_get_kern: try stbtt_GetCodepointKernAdvance first, fall back to a glyph-index lookup into the legacy kern table.
  • s_draw_text: accumulate kern into the x cursor 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.
  • Glyph quad padding now divides by app->pixel_scale to match the device-pixel atlas border.
  • s_render/cf_font_get_glyph: invisible glyphs skip rasterization and atlas registration; CF_Glyph::rendered tracks "already processed" instead of image_id.

Test plan

  • cmake --build build -- clean, no new warnings
  • ./build/tests -- 173/173 passing
  • Visual check via samples/font_debug.cpp against the bundled Calibri: kerning readout for "To" went from 0.00 to -2.31 at font size 26; before/after screenshot crop confirms the o glyph is now pulled tight under the T
  • Debug print confirmed space-glyph behavior directly: visible=0 rendered=1 image_id=0 xadvance=5.88 -- no rasterization or atlas slot allocated, but layout spacing is unaffected
image

https://claude.ai/code/session_011V2maY4DG1DHscsMEHy6Bf

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
Copilot AI review requested due to automatic review settings July 6, 2026 21:29
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_kern to prefer stbtt_GetCodepointKernAdvance (GPOS) and fall back to the legacy kerning map using glyph indices.
  • Update s_draw_text to accumulate kerning into the horizontal cursor so layout/measurement matches rendering.
  • Update s_find_end_of_line to include kerning in wrap width accounting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cute_draw.cpp
Comment thread src/cute_draw.cpp
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 thread src/cute_draw.cpp
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.
pusewicz and others added 2 commits July 6, 2026 23:35
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
@pusewicz pusewicz changed the title Apply kerning to text layout cursor Fix text kerning, HiDPI glyph padding, and invisible-glyph rasterization Jul 6, 2026
@RandyGaul RandyGaul merged commit 950b72f into RandyGaul:master Jul 6, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants