From a6727b961728324adee76e07d1a517cc3900096b Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 31 Aug 2026 12:36:52 +0200 Subject: [PATCH 1/6] FTFont: For non-scalable (Bitmap) fonts pick the closest smaller one. Fix #3427 --- src/font.cpp | 117 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/src/font.cpp b/src/font.cpp index f968dbad5c..427efccf9e 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -17,8 +17,8 @@ // Headers #include +#include #include -#include #include #include @@ -45,8 +45,8 @@ #endif #include -#include "bitmapfont.h" +#include "bitmapfont.h" #include "filefinder.h" #include "output.h" #include "font.h" @@ -149,6 +149,25 @@ namespace { #ifdef HAVE_FREETYPE FT_Library library = nullptr; + static bool IsBetterFontSize(int candidate, int current_best, int target_size) { + // When the font is not scalable this helper function is used to find + // the closest smaller font height. An exception is when their is no + // smaller in which cases it picks the minimum. + if (current_best < 0) { + return true; + } + bool cand_fits = (candidate <= target_size); + bool best_fits = (current_best <= target_size); + + if (cand_fits != best_fits) { + return cand_fits; + } + if (cand_fits) { + return candidate > current_best; + } + return candidate < current_best; + } + struct FTFont final : public Font { FTFont(Filesystem_Stream::InputStream is, int size, bool bold, bool italic); ~FTFont() override; @@ -304,6 +323,55 @@ FTFont::FTFont(Filesystem_Stream::InputStream is, int size, bool bold, bool ital return; } + if (face->num_faces > 1) { + // When there are multiple faces and the font is not scalable try to load the face where the size is closest + // (required for BitmapFonts) + if (!FT_HAS_COLOR(face) && !FT_IS_SCALABLE(face)) { + auto num_faces = face->num_faces; + FT_Done_Face(face); + face = nullptr; + + // Find a predefined height that is as close as possible to the requested height + int best_height = -1; + + // Stores the best match + FT_Face best_face = nullptr; + + for (FT_Long i = 0; i < num_faces; ++i) { + if (FT_New_Memory_Face(library, ft_buffer.data(), ft_buffer.size(), i, &face) != 0) { + continue; + } + + for (int j = 0; j < face->num_fixed_sizes; ++j) { + int face_height = face->available_sizes[j].height; + + if (IsBetterFontSize(face_height, best_height, size)) { + best_height = face_height; + + // Throw away previous best_face + if (best_face && face != best_face) { + FT_Done_Face(best_face); + } + + best_face = face; + } + } + + // If this face is not the new best throw it away + if (face != best_face) { + FT_Done_Face(face); + face = nullptr; + } + } + + face = best_face; + + if (face == nullptr) { + return; + } + } + } + if (face->num_charmaps > 0) { // Force unicode charmap if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) { @@ -581,9 +649,42 @@ void FTFont::vApplyStyle(const Style& style) { } void FTFont::SetSize(int height, bool create) { + // FreeType only supports exact bitmap strike matching, it never picks the + // closest strike on its own. For bitmap fonts we therefore select the + // strike whose height is closest to the requested height ourselves. + auto select_closest = [&]() { + FT_Error err; + if (face->num_fixed_sizes > 0) { + int best = 0; + int best_height = -1; + for (int j = 0; j < face->num_fixed_sizes; ++j) { + int face_height = static_cast(face->available_sizes[j].height); + if (IsBetterFontSize(face_height, best_height, height)) { + best_height = face_height; + best = j; + } + } + err = FT_Select_Size(face, best); + } else { + err = FT_Set_Pixel_Sizes(face, 0, height); + } + + if (err != FT_Err_Ok) { + Output::Debug("Couldn't select size {} for font {}", height, ToString(GetName())); + } + }; + if (FT_HAS_COLOR(face)) { - // FIXME: Find the best size - FT_Select_Size(face, 0); + if (FT_HAS_FIXED_SIZES(face)) { + // Color bitmap font (CBDT/CBLC): use the strike closest to the requested size + select_closest(); + } else { + // Scalable color font (COLR/CPAL): scale like a normal outline font + auto err = FT_Set_Pixel_Sizes(face, 0, height); + if (err != FT_Err_Ok) { + Output::Debug("Couldn't set pixel size {} for font {}", height, ToString(GetName())); + } + } } else if (FT_IS_SCALABLE(face)) { // Calculate the pt size from px auto table_os2 = static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_os2)); @@ -605,9 +706,13 @@ void FTFont::SetSize(int height, bool create) { height = std::max(1, pt); } - FT_Set_Pixel_Sizes(face, 0, height); + auto err = FT_Set_Pixel_Sizes(face, 0, height); + if (err != FT_Err_Ok) { + Output::Debug("Couldn't set pixel size {} for font {}", height, ToString(GetName())); + } } else { - FT_Set_Pixel_Sizes(face, 0, face->available_sizes->height); + // Non-scalable bitmap font (FON, BDF, etc.): use the closest to the requested size + select_closest(); } #ifdef HAVE_HARFBUZZ From 61804ac373490e4d61ec7f90091b60eae08a8acb Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 31 Aug 2026 15:35:40 +0200 Subject: [PATCH 2/6] FTFont: Disable Auto-Hinting to match RPG_RT behaviour Harfbuzz: Call set_load_flags to synchronize the flags. Without this flag HB shapes witout considering the bitmap glyphs. This mismatches with FreeType and results in bad rendering. These two changes match now pretty close to Maniac Patch on Windows (and are pretty far away on Linux in Wine now) --- src/font.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/font.cpp b/src/font.cpp index 427efccf9e..62099e3596 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -466,10 +466,10 @@ Rect FTFont::vGetSize(char32_t glyph) const { // When it is a color font check if the glyph is a color glyph // If it is not then reload the glyph monochrome if (face->glyph->bitmap.pixel_mode != FT_PIXEL_MODE_BGRA) { - load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO); + load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT); } } else { - if (!load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO)) { + if (!load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT)) { if (fallback_font) { return fallback_font->vGetSize(glyph); } else { @@ -537,10 +537,10 @@ Font::GlyphRet FTFont::vRenderShaped(char32_t glyph) const { // If it is not then rerender the glyph monochrome // FIXME: This is inefficient if (face->glyph->bitmap.pixel_mode != FT_PIXEL_MODE_BGRA) { - render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO, FT_RENDER_MODE_MONO); + render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT, FT_RENDER_MODE_MONO); } } else { - if (!render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO, FT_RENDER_MODE_MONO)) { + if (!render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT, FT_RENDER_MODE_MONO)) { if (fallback_font) { return fallback_font->vRender(glyph); } else { @@ -723,6 +723,8 @@ void FTFont::SetSize(int height, bool create) { hb_font_destroy(hb_font); } hb_font = hb_ft_font_create_referenced(face); + // Match load flags so the shaping picks the Bitmap Font + hb_ft_font_set_load_flags(hb_font, FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT); hb_ft_font_set_funcs(hb_font); #endif From 9055e9e553dc250a4468a128fcd3082f82ccb910 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 31 Aug 2026 14:35:25 +0200 Subject: [PATCH 3/6] FTFont: Change rounding behaviour to match default rounding mode of TTF TrueType default is "Round to Grid" which matches std::round --- src/font.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/font.cpp b/src/font.cpp index 62099e3596..ac60fd5dd5 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -481,8 +481,10 @@ Rect FTFont::vGetSize(char32_t glyph) const { FT_GlyphSlot slot = face->glyph; Point advance; - advance.x = Utils::RoundTo(slot->advance.x / 64.0); - advance.y = Utils::RoundTo(slot->advance.y / 64.0); + // TrueType default rounding is "Round to Grid" (RTG) which matches the + // behaviour of std::round + advance.x = std::round(slot->advance.x / 64.0); + advance.y = std::round(slot->advance.y / 64.0); if (EP_UNLIKELY(rm2000_workaround)) { advance.x = 6; @@ -581,8 +583,8 @@ Font::GlyphRet FTFont::vRenderShaped(char32_t glyph) const { Point advance; Point offset; - advance.x = Utils::RoundTo(slot->advance.x / 64.0); - advance.y = Utils::RoundTo(slot->advance.y / 64.0); + advance.x = std::round(slot->advance.x / 64.0); + advance.y = std::round(slot->advance.y / 64.0); offset.x = slot->bitmap_left; offset.y = slot->bitmap_top - baseline_offset; @@ -628,10 +630,10 @@ std::vector FTFont::vShape(std::u32string_view txt) const { advance.y = s.height; ret.push_back({txt[info.cluster], advance, offset, true}); } else { - advance.x = Utils::RoundTo(pos.x_advance / 64.0); - advance.y = Utils::RoundTo(pos.y_advance / 64.0); - offset.x = Utils::RoundTo(pos.x_offset / 64.0); - offset.y = Utils::RoundTo(pos.y_offset / 64.0); + advance.x = std::round(pos.x_advance / 64.0); + advance.y = std::round(pos.y_advance / 64.0); + offset.x = std::round(pos.x_offset / 64.0); + offset.y = std::round(pos.y_offset / 64.0); ret.push_back({static_cast(info.codepoint), advance, offset, false}); } } From 6f56a675a3a1f4b7c528624759f582f5d3f1e943 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 31 Aug 2026 16:18:38 +0200 Subject: [PATCH 4/6] FTFont: Correctly determine the baseline --- src/font.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/font.cpp b/src/font.cpp index ac60fd5dd5..600ac8045f 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -676,6 +676,10 @@ void FTFont::SetSize(int height, bool create) { } }; + // Ascender (in font units) used for the size normalization. Used to + // compute the baseline so it matches the Windows GDI metrics. + int baseline_units = 0; + if (FT_HAS_COLOR(face)) { if (FT_HAS_FIXED_SIZES(face)) { // Color bitmap font (CBDT/CBLC): use the strike closest to the requested size @@ -696,8 +700,10 @@ void FTFont::SetSize(int height, bool create) { int units; if (table_os2->usWinAscent + table_os2->usWinDescent == 0) { units = table_hori->Ascender - table_hori->Descender; + baseline_units = table_hori->Ascender; } else { units = table_os2->usWinAscent + table_os2->usWinDescent; + baseline_units = table_os2->usWinAscent; } int pt = FT_MulDiv(face->units_per_EM, height, units); @@ -730,9 +736,16 @@ void FTFont::SetSize(int height, bool create) { hb_ft_font_set_funcs(hb_font); #endif - baseline_offset = static_cast(FT_MulFix(face->ascender, face->size->metrics.y_scale) / 64); + // For scalable fonts the baseline picked matching the Windows GDI metric + // For bitmap fonts (FON, BDF, etc.) use ascender from the metrics. + if (baseline_units > 0) { + baseline_offset = static_cast(FT_MulFix(baseline_units, face->size->metrics.y_scale) / 64); + } else { + baseline_offset = static_cast(face->size->metrics.ascender / 64); + } + if (baseline_offset == 0) { - // FIXME: Becomes 0 for FON files. How is the baseline calculated for them? + // Fallback when metrics are broken baseline_offset = static_cast(height * (10.0 / 12.0)); } } From f6e578c81212a560994995526811af0b27208aa7 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 31 Aug 2026 16:45:54 +0200 Subject: [PATCH 5/6] Origin: Truncate the result to match positioning of Maniacs --- src/game_pictures.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index fd9df44ac5..243957fb12 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -454,6 +454,9 @@ void Game_Pictures::Picture::ApplyOrigin(bool is_move) { break; } + x = std::floor(x); + y = std::floor(y); + if (!is_move) { data.current_x = x; data.current_y = y; From e727a68d79a86436313a092d868b7b08fe3778c3 Mon Sep 17 00:00:00 2001 From: Carsten Teibes Date: Sun, 20 Sep 2026 19:00:54 +0200 Subject: [PATCH 6/6] Fix wording --- src/font.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/font.cpp b/src/font.cpp index 600ac8045f..b6efb0da6b 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -150,9 +150,11 @@ namespace { FT_Library library = nullptr; static bool IsBetterFontSize(int candidate, int current_best, int target_size) { - // When the font is not scalable this helper function is used to find - // the closest smaller font height. An exception is when their is no - // smaller in which cases it picks the minimum. + // When the font is not scalable, this helper function is used to find + + // the closest smaller font height. When there is no smaller height, + + // it picks the minimum. if (current_best < 0) { return true; }