feat: Added Subpixel Font rendering (#2092)

Proof of concept for implementing subpixel processing in ImGui. This is
work in progress, and it is bound to have problems.

What it does:
1) Uses freetype own subpixel processing implementation to build a
32-bit color atlas for the default font only (no icons, no unifont) . 2)
Avoids pixel perfect font conversion when possible. 3) Self contained,
no ImGui source code changes.
4) Results in much improved legibility of fonts rendered on low dpi LCD
screens that use horizontal RGB pixel layouts (no BRG or OLED or CRT if
they even exist anymore)

What it doesn't:
1) Fancy class based interface. The code is barely the minimum needed to
show it can work. 2) Dual source color blending. That needs to be
implemented in shader code, so it needs to change ImGui source code
although minimally. This will result in some characters appearing dimmer
than others. Easily fixed with small fragment and vertex shaders. 3)
subpixel positioning. If characters are very thin they will look
colored, or they can be moved to improve legibility. 4) deal with
detection of fringe cases including rare pixel layouts, non LCD screens,
Mac-OS not handling subpixel rendering and any other deviation from the
standard LCD. 5) tries to be efficient in speed or memory use. Font
Atlases will be 4 times the size they were before, but there are no
noticeable delays in font loading in the examples I have tried.

Any comments and code improvements are welcome.

---------

Co-authored-by: Nik <werwolv98@gmail.com>
This commit is contained in:
paxcut
2025-05-11 06:36:32 -07:00
committed by GitHub
parent 8cd961596e
commit 5c4cf7379f
19 changed files with 510 additions and 94 deletions

View File

@@ -3,6 +3,10 @@
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_freetype.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_LCD_FILTER_H
#include FT_BITMAP_H
#include <memory>
#include <list>
@@ -24,6 +28,34 @@ namespace hex::fonts {
return m_font->Descent;
}
float calculateFontDescend(FT_Library ft, float fontSize) const {
if (ft == nullptr) {
log::fatal("FreeType not initialized");
return 0.0f;
}
FT_Face face;
if (FT_New_Memory_Face(ft, reinterpret_cast<const FT_Byte *>(m_font->ConfigData->FontData), m_font->ConfigData->FontDataSize, 0, &face) != 0) {
log::fatal("Failed to load face");
return 0.0f;
}
// Calculate the expected font size
auto size = fontSize;
if (m_font->FontSize > 0.0F)
size = m_font->FontSize * std::max(1.0F, std::floor(ImHexApi::System::getGlobalScale()));
else
size = std::max(1.0F, std::floor(size / ImHexApi::Fonts::DefaultFontSize)) * ImHexApi::Fonts::DefaultFontSize;
if (FT_Set_Pixel_Sizes(face, size, size) != 0) {
log::fatal("Failed to set pixel size");
return 0.0f;
}
return face->size->metrics.descender / 64.0F;
}
ImFont* getFont() { return m_font; }
private:
@@ -109,6 +141,10 @@ namespace hex::fonts {
Font addFontFromMemory(const std::vector<u8> &fontData, float fontSize, bool scalable, ImVec2 offset, const ImVector<ImWchar> &glyphRange = {}) {
auto &storedFontData = m_fontData.emplace_back(fontData);
if (storedFontData.empty()) {
log::fatal("Failed to load font data");
return Font();
}
auto &config = m_fontConfigs.emplace_back(m_defaultConfig);
config.FontDataOwnedByAtlas = false;
@@ -152,7 +188,7 @@ namespace hex::fonts {
void setAntiAliasing(bool enabled) {
if (enabled)
m_defaultConfig.FontBuilderFlags &= ~ImGuiFreeTypeBuilderFlags_Monochrome | ImGuiFreeTypeBuilderFlags_MonoHinting;
m_defaultConfig.FontBuilderFlags &= ~(ImGuiFreeTypeBuilderFlags_Monochrome | ImGuiFreeTypeBuilderFlags_MonoHinting);
else
m_defaultConfig.FontBuilderFlags |= ImGuiFreeTypeBuilderFlags_Monochrome | ImGuiFreeTypeBuilderFlags_MonoHinting;
}
@@ -197,9 +233,16 @@ namespace hex::fonts {
return m_fontAtlas;
}
float calculateFontDescend(const ImHexApi::Fonts::Font &font, float fontSize) const {
auto atlas = std::make_unique<ImFontAtlas>();
auto cfg = m_defaultConfig;
float calculateFontDescend( FT_Library ft, const ImHexApi::Fonts::Font &font, float fontSize) const {
if (ft == nullptr) {
log::fatal("FreeType not initialized");
return 0.0f;
}
FT_Face face;
if (FT_New_Memory_Face(ft, reinterpret_cast<const FT_Byte *>(font.fontData.data()), font.fontData.size(), 0, &face) != 0) {
log::fatal("Failed to load face");
return 0.0f;
}
// Calculate the expected font size
auto size = fontSize;
@@ -208,28 +251,20 @@ namespace hex::fonts {
else
size = std::max(1.0F, std::floor(size / ImHexApi::Fonts::DefaultFontSize)) * ImHexApi::Fonts::DefaultFontSize;
cfg.MergeMode = false;
cfg.SizePixels = size;
cfg.FontDataOwnedByAtlas = false;
// Construct a range that only contains the first glyph of the font
ImVector<ImWchar> queryRange;
{
auto firstGlyph = font.glyphRanges.empty() ? m_glyphRange.front() : font.glyphRanges.front().begin;
queryRange.push_back(firstGlyph);
queryRange.push_back(firstGlyph);
if (FT_Set_Pixel_Sizes(face, size, size) != 0) {
log::fatal("Failed to set pixel size");
return false;
}
queryRange.push_back(0x00);
// Build the font atlas with the query range
auto newFont = atlas->AddFontFromMemoryTTF(const_cast<u8 *>(font.fontData.data()), int(font.fontData.size()), 0, &cfg, queryRange.Data);
atlas->Build();
return newFont->Descent;
return face->size->metrics.descender / 64.0F;
}
void reset() {
m_fontData.clear();
m_glyphRange.clear();
m_fontSizes.clear();
m_fontConfigs.clear();
m_fontAtlas->Clear();
m_defaultConfig.MergeMode = false;
}
@@ -255,5 +290,4 @@ namespace hex::fonts {
std::list<std::vector<u8>> m_fontData;
};
}

View File

@@ -1,8 +1,17 @@
#pragma once
#include <hex/api/imhex_api.hpp>
#include <hex/api/content_registry.hpp>
namespace hex::fonts {
class AntialiasPicker : public ContentRegistry::Settings::Widgets::DropDown {
public:
AntialiasPicker() : DropDown(
std::vector<UnlocalizedString>({"hex.fonts.setting.font.antialias_none", "hex.fonts.setting.font.antialias_grayscale", "hex.fonts.setting.font.antialias_subpixel"}),
std::vector<nlohmann::json>({"none" , "grayscale" , "subpixel"}),
nlohmann::json("subpixel")
){}
};
class FontFilePicker : public ContentRegistry::Settings::Widgets::FilePicker {
public:
@@ -31,7 +40,7 @@ namespace hex::fonts {
class FontSelector : public ContentRegistry::Settings::Widgets::Widget {
public:
FontSelector() : m_fontSize(16, 2, 100), m_bold(false), m_italic(false), m_antiAliased(true) { }
FontSelector() : m_fontSize(ImHexApi::Fonts::pointsToPixels(10), 2, 100), m_antiAliased(), m_bold(false), m_italic(false) { }
bool draw(const std::string &name) override;
@@ -43,7 +52,7 @@ namespace hex::fonts {
[[nodiscard]] float getFontSize() const;
[[nodiscard]] bool isBold() const;
[[nodiscard]] bool isItalic() const;
[[nodiscard]] bool isAntiAliased() const;
[[nodiscard]] const std::string antiAliasingType() const;
private:
bool drawPopup();
@@ -51,7 +60,8 @@ namespace hex::fonts {
private:
FontFilePicker m_fontFilePicker;
SliderPoints m_fontSize;
ContentRegistry::Settings::Widgets::Checkbox m_bold, m_italic, m_antiAliased;
AntialiasPicker m_antiAliased;
ContentRegistry::Settings::Widgets::Checkbox m_bold, m_italic;
bool m_applyEnabled = false;
};