Skip to content

replace pre-baked bitmap fonts with a runtime glyph atlas - #559

Merged
nschimme merged 1 commit into
MUME:masterfrom
nschimme:sdf-font
Sep 16, 2026
Merged

nschimme merged 1 commit into
MUME:masterfrom
nschimme:sdf-font

Conversation

@nschimme

@nschimme nschimme commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

The map font was three pre-baked Cantarell BMFont atlases (18/27/36px), picked by device pixel ratio, limited to Latin-1. Replace them with a streaming glyph cache (src/font/GlyphAtlas) that rasterizes glyphs on demand from any installed QFont at the exact physical pixel size they are drawn at.

  • Text is shaped with QTextLayout, so kerning, ligatures and font fallback (color emoji, non-Latin scripts) come for free; callers now pass UTF-8.
  • Glyphs are rasterized with full hinting via QPainter::drawGlyphRun into a shelf-packed RGBA8 atlas; monochrome glyphs store coverage in alpha and color glyphs (detected per glyph) store straight RGBA in the same texture, discriminated by a per-vertex flag.
  • The atlas starts at 256^2 and doubles on demand up to min(2048, GL_MAX_TEXTURE_SIZE). Texture coordinates are atlas texels normalized in the vertex shader, so growing the texture in place never invalidates cached text meshes. Dirty regions are uploaded with glTexSubImage2D before a mesh is created or drawn; worker threads only touch the CPU-side image under a mutex.
  • The font is re-initialized (and text meshes rebuilt) whenever its spec goes stale: family/point size from the preferences, logical DPI, or device pixel ratio.
  • "Map font point size" is a real point size (default 11pt, matching the old Cantarell18 look); the embedded Cantarell-Regular.ttf remains the default family.

Summary by Sourcery

Replace pre-baked map font atlases with a configurable, DPI-aware runtime glyph atlas that supports modern Unicode text rendering.

New Features:

  • Replace the fixed bitmap font atlases with runtime font selection and on-demand glyph caching.
  • Support Unicode text shaping, font fallback, and color glyph rendering such as emoji.
  • Add configurable map font family and point size preferences.

Bug Fixes:

  • Reload map fonts and rebuild text meshes when font settings, logical DPI, or device pixel ratio changes.

Enhancements:

  • Stream glyph atlas updates efficiently while preserving existing text mesh coordinates as the atlas grows.

Build:

  • Add the runtime glyph atlas implementation to the build and remove the pre-baked bitmap font resources.

Tests:

  • Add coverage for point-size conversion, shaping, atlas contents, incremental uploads, growth, capacity handling, and Unicode fallback.

Chores:

  • Bundle the default Cantarell font for runtime rasterization.

@sourcery-ai

sourcery-ai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Replaces fixed Latin-1 Cantarell BMFont atlases with a runtime, UTF-8-aware glyph pipeline: QTextLayout shapes text, worker threads populate a synchronized shelf-packed glyph atlas, and the GL thread uploads incremental changes while shaders support both tinted monochrome and color glyphs. Font family, point size, DPI, and device pixel ratio are configurable inputs that trigger font and mesh rebuilding.

Sequence diagram for streaming glyph rasterization and upload

sequenceDiagram
    participant Worker as Worker thread
    participant Atlas as GlyphAtlas
    participant GL as GL thread
    participant Texture as Font texture
    participant Shader as Font shader

    Worker->>Atlas: shape(utf8)
    Atlas->>Atlas: QTextLayout shaping
    Atlas->>Atlas: rasterizeLocked(rawFont, glyphIndex)
    Atlas-->>Worker: ShapedText
    GL->>Atlas: takePendingUpload()
    Atlas-->>GL: AtlasUpload dirty region
    GL->>Texture: setData(region)
    GL->>Shader: createFontMesh(vertices)
    Shader->>Texture: sample normalized atlas texels
    Shader-->>GL: Render tinted or color glyphs
Loading

State diagram for font and glyph atlas lifecycle

stateDiagram-v2
    [*] --> Uninitialized
    Uninitialized --> Initialized: init()
    Initialized --> Streaming: shape(utf8)
    Streaming --> Streaming: new glyph rasterized
    Streaming --> TextureSynced: takePendingUpload()
    TextureSynced --> Streaming: shape(utf8)
    Initialized --> Stale: isStale()
    TextureSynced --> Stale: font spec or DPI changed
    Stale --> Uninitialized: cleanup()
    Stale --> Initialized: init() and rebuild meshes
    Streaming --> Full: atlas reaches max size
    Full --> Full: further glyphs omitted
Loading

File-Level Changes

Change Details Files
Introduces a thread-safe, on-demand glyph atlas that shapes UTF-8 with QTextLayout and rasterizes hinted glyphs into a dynamically growing RGBA texture.
  • Supports kerning, ligatures, fallback fonts, Unicode scripts, and color glyphs.
  • Packs glyphs into a shelf allocator with dirty-region uploads and texture growth up to the hardware limit.
  • Caches shaped strings and glyphs, handles atlas exhaustion, and provides synthetic background/underline texels.
  • Adds focused tests for sizing, shaping, pixels, uploads, growth, exhaustion, and Unicode fallback.
src/font/GlyphAtlas.cpp
src/font/GlyphAtlas.h
src/CMakeLists.txt
tests/CMakeLists.txt
tests/TestFont.cpp
Reworks OpenGL font rendering to consume runtime glyph data while preserving mesh validity as the atlas grows.
  • Replaces BMFont loading and Latin-1 lookup/kerning with shaped glyph placement and atlas texel coordinates.
  • Synchronizes atlas uploads on the GL thread and rebuilds the font and cached meshes when font or display metrics become stale.
  • Adds per-vertex color-glyph classification and shader support for tinted monochrome versus straight-alpha color glyphs.
  • Updates vertex attribute layouts and normalizes atlas coordinates in the vertex shader.
src/opengl/Font.cpp
src/opengl/Font.h
src/opengl/OpenGLTypes.h
src/opengl/legacy/FontMesh3d.h
src/resources/shaders/legacy/font/vert.glsl
src/resources/shaders/legacy/font/frag.glsl
Adds configurable map font family and point size settings with runtime preference integration.
  • Persists font family and clamps the point size to a safe range.
  • Adds a font selection dialog and reflects the active family and size in the graphics preferences UI.
  • Uses the embedded Cantarell TTF as the default application font and converts point size to physical pixels using DPI and device pixel ratio.
src/configuration/configuration.cpp
src/configuration/configuration.h
src/preferences/graphicspage.cpp
src/preferences/graphicspage.ui
src/resources/mmapper2.qrc
Migrates map and diagnostic text callers from Latin-1 conversion to UTF-8 and removes obsolete pre-baked font assets.
  • Passes UTF-8 labels, room names, infomarks, centered text, and performance indicators to the renderer.
  • Removes the 18px, 27px, and 36px Cantarell BMFont descriptors and atlases.
src/display/Characters.cpp
src/display/Connections.cpp
src/display/Infomarks.cpp
src/display/mapcanvas_gl.cpp
src/resources/fonts/Cantarell18.bmfc
src/resources/fonts/Cantarell18.fnt
src/resources/fonts/Cantarell27.bmfc
src/resources/fonts/Cantarell27.fnt
src/resources/fonts/Cantarell36.bmfc
src/resources/fonts/Cantarell36.fnt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/opengl/Font.cpp" line_range="335-339" />
<code_context>
+    return family.isEmpty() ? QStringLiteral("Cantarell") : family;
+}
+
+NODISCARD static float getLogicalDpi()
+{
+    if (const QScreen *const screen = QGuiApplication::primaryScreen()) {
+        return static_cast<float>(screen->logicalDotsPerInchY());
     }
+    return 96.f;
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** `getLogicalDpi()` always reads the primary screen's DPI rather than the screen displaying the map window, so moving the application to a monitor with a different logical DPI leaves glyphs rasterized at the wrong physical size.

**Triggers:** When the application window is moved to a secondary monitor with different logical DPI.

**Suggested fix:** Read the DPI from the map window or OpenGL surface's current screen and update the font when that screen changes.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/opengl/Font.cpp:339


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/opengl/Font.cpp Outdated
@nschimme
nschimme force-pushed the sdf-font branch 2 times, most recently from c33e3bc to 5fb7dad Compare September 16, 2026 02:57
sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Sep 16, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sourcery assessment

Approved.

sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Sep 16, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sourcery assessment

Approved.

sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Sep 16, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sourcery assessment

Approved.

@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.95575% with 198 lines in your changes missing coverage. Please review.
✅ Project coverage is 27.91%. Comparing base (6306cfd) to head (9bc156d).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/opengl/Font.cpp 0.00% 124 Missing ⚠️
src/preferences/graphicspage.cpp 0.00% 17 Missing ⚠️
src/display/mapcanvas_gl.cpp 0.00% 15 Missing ⚠️
src/global/window_utils.cpp 0.00% 8 Missing ⚠️
src/font/GlyphAtlas.cpp 96.74% 7 Missing ⚠️
src/preferences/clientpage.cpp 0.00% 6 Missing ⚠️
src/display/mapcanvas.cpp 0.00% 4 Missing ⚠️
src/display/MapCanvasWindow.h 0.00% 3 Missing ⚠️
src/display/mapcanvas.h 0.00% 3 Missing ⚠️
src/mainwindow/mainwindow.cpp 0.00% 3 Missing ⚠️
... and 7 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #559      +/-   ##
==========================================
+ Coverage   27.24%   27.91%   +0.66%     
==========================================
  Files         559      563       +4     
  Lines       45893    46169     +276     
  Branches     4879     4884       +5     
==========================================
+ Hits        12505    12889     +384     
+ Misses      33388    33280     -108     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The map font was three pre-baked Cantarell BMFont atlases (18/27/36px),
picked by device pixel ratio, limited to Latin-1. Replace them with a
streaming glyph cache (src/font/GlyphAtlas) that rasterizes glyphs on
demand from any installed QFont at the exact physical pixel size they are
drawn at.

- Text is shaped with QTextLayout, so kerning, ligatures and font fallback
  (color emoji, non-Latin scripts) come for free; callers now pass UTF-8.
- Glyphs are rasterized with full hinting via QPainter::drawGlyphRun into a
  shelf-packed RGBA8 atlas; monochrome glyphs store coverage in alpha and
  color glyphs (detected per glyph) store straight RGBA in the same texture,
  discriminated by a per-vertex flag.
- The atlas starts at 256^2 and doubles on demand up to
  min(2048, GL_MAX_TEXTURE_SIZE). Texture coordinates are atlas texels
  normalized in the vertex shader, so growing the texture in place never
  invalidates cached text meshes. Dirty regions are uploaded with
  glTexSubImage2D before a mesh is created or drawn; worker threads only
  touch the CPU-side image under a mutex.
- The font is re-initialized (and text meshes rebuilt) whenever its spec
  goes stale: family/point size from the preferences, logical DPI, or
  device pixel ratio.
- "Map font point size" is a real point size (default 11pt, matching the
  old Cantarell18 look); the embedded Cantarell-Regular.ttf remains the
  default family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nschimme nschimme changed the title Replace pre-baked bitmap fonts with a runtime glyph atlas replace pre-baked bitmap fonts with a runtime glyph atlas Sep 16, 2026
@nschimme
nschimme merged commit cca5ea3 into MUME:master Sep 16, 2026
21 checks passed
@nschimme
nschimme deleted the sdf-font branch September 16, 2026 03:59
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.

1 participant