1. Kiến trúc hệ thống nạp font
Hệ thống font của một menu mod GTA V dựa trên Dear ImGui. Khi người chơi chuyển ngôn ngữ, font_mgr phải thực hiện ba việc chính: xác định glyph cần thiết, tìm file font phù hợp, sau đó xây dựng lại font atlas và gửi texture lên GPU. Các bước này thường chạy trên luồng chính, gây ra hiện tượng giật khi gặp ký tự CJK hoặc ngôn ngữ có bộ glyph lớn.
Thành phần then chốt bao gồm:
- Language resolver: ánh xạ ngôn ngữ sang danh sách font ứng viên.
- Path resolver: tìm file font trên thư mục dự án, sau đó đến các đường dn hệ thống theo nền tảng.
- Atlas builder: sử dụng
ImFontAtlasđể rasterize glyph và tạo texture. - Cache manager: lưu trữ atlas đã xây dựng theo cặp
(ngôn ngữ, c chữ).
2. Các vấn đề thường gặp và cách khắc phục
2.1 Ký tự CJK hiển thị thành ô trống
Triệu chứng: tiếng Anh, số hiển thị bình thường, nhưng Hán, Nhật, Hàn thành ô vuông. Nguyên nhân là atlas chưa chứa glyph của khu vực Unicode tương ứng, hoặc font_mgr chọn nhầm font không có glyph đó.
Giải pháp là triển khai cơ chế tìm kiếm theo thứ tự ưu tiên: local font → system font → embedded font. Ví dụ dưới đây sử dụng tên lớp và biến khác với gốc nhưng vẫn giữ đúng luồng xử lý.
enum class ELanguage { LATIN, CJK, CYRILLIC, ARABIC };
class CFontManager {
public:
std::filesystem::path ResolveFont(ELanguage lang) const;
private:
std::unordered_map<ELanguage, std::vector<std::string>> m_fontCandidates = {
{ELanguage::CJK, {"msyh.ttc", "noto-sans-cjk.ttc", "simsun.ttc"}},
{ELanguage::CYRILLIC, {"arial.ttf", "segoeui.ttf"}},
{ELanguage::LATIN, {"segoeui.ttf", "arial.ttf", "roboto.ttf"}},
};
std::vector<std::filesystem::path> GetSearchPaths() const;
};
std::filesystem::path CFontManager::ResolveFont(ELanguage lang) const {
auto it = m_fontCandidates.find(lang);
if (it == m_fontCandidates.end())
return {};
// Ưu tiên thư mục fonts trong dự án
std::filesystem::path localDir = g_file_manager.GetProjectFolder("fonts");
for (const auto& name : it->second) {
auto p = localDir / name;
if (std::filesystem::exists(p)) return p;
}
// Tiếp theo là các đường dẫn hệ thống
for (const auto& name : it->second) {
for (const auto& dir : GetSearchPaths()) {
auto p = dir / name;
if (std::filesystem::exists(p)) return p;
// Thử cả file viết thường/hoa
std::string lower = name;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
auto p2 = dir / lower;
if (std::filesystem::exists(p2)) return p2;
}
}
return {};
}
2.2 Giật khi chuyển ngôn ngữ
Việc xây dựng atlas trong luồng chính có thể chiếm hàng trăm mili giây với font CJK. Ta chuyển tác vụ này sang worker thread, kết hợp với bộ đệm m_atlasCache để tái sử dụng kết quả.
class CFontManager {
public:
void SwitchLanguage(ELanguage lang);
void AsyncRebuild();
private:
std::unordered_map<ELanguage, std::unordered_map<float, ImFont*>> m_atlasCache;
std::future<void> m_buildTask;
std::atomic<bool> m_building{false};
std::mutex m_buildMutex;
};
void CFontManager::AsyncRebuild() {
if (m_building.exchange(true)) return;
m_buildTask = std::async(std::launch::async, [this]() {
std::lock_guard<std::mutex> lock(m_buildMutex);
RebuildAtlas();
m_building = false;
});
}
void CFontManager::SwitchLanguage(ELanguage lang) {
m_activeLang = lang;
// Nếu đã cache theo cỡ chữ hiện tại thì chỉ cần thay đi con trỏ font
auto it = m_atlasCache.find(lang);
if (it != m_atlasCache.end() && it->second.contains(m_fontSize)) {
ApplyCachedFont(it->second[m_fontSize]);
return;
}
// Không có cache thì rebuild bất đồng bộ
g_thread_pool->Submit([this]() { AsyncRebuild(); });
}
2.3 Cỡ chữ không đồng nhất giữa các ngôn ngữ
Font Latin và CJK có baseline, ascent, descent khác nhau. Ta chuẩn hóa qua ImFontConfig và căn chỉnh GlyphOffset, Oversample để layout ổn định.
ImFontConfig MakeFontConfig(float size, ELanguage lang) {
ImFontConfig cfg{};
cfg.FontDataOwnedByAtlas = false;
cfg.SizePixels = size;
cfg.OversampleH = 2;
cfg.OversampleV = 2;
cfg.PixelSnapH = true;
cfg.MergeMode = false;
// Một số font CJK bị lệch xuống, nâng nhẹ glyph
if (lang == ELanguage::CJK) {
cfg.GlyphOffset.y = -1.0f;
}
return cfg;
}
3. Tối ưu nâng cao
3.1 Subset glyph động
Thay vì nạp toàn bộ bảng chữ Hán (~20.000 glyph), ta chỉ nạp các glyph xuất hiện trong chui cần hiển thị. Điều này giảm đáng kể kích thước atlas và thời gian rasterize.
std::vector<ImWchar> BuildGlyphRanges(const std::string& text) {
std::set<ImWchar> unique;
for (unsigned char c : text) {
unique.insert(static_cast<ImWchar>(c));
}
// Thêm ASCII cơ bản
for (ImWchar c = 0x20; c <= 0x7E; ++c) {
unique.insert(c);
}
std::vector<ImWchar> ranges;
ranges.reserve(unique.size() * 2 + 1);
for (ImWchar c : unique) {
ranges.push_back(c);
ranges.push_back(c);
}
ranges.push_back(0);
return ranges;
}
ImFont* CFontManager::LoadSubsetFont(const char* filename, float size,
const std::string& sampleText) {
ImFontConfig cfg = MakeFontConfig(size, ELanguage::CJK);
cfg.MergeMode = true;
auto ranges = BuildGlyphRanges(sampleText);
return ImGui::GetIO().Fonts->AddFontFromFileTTF(
filename, size, &cfg, ranges.data());
}
3.2 Nạp trước thông minh theo khu vực
Dựa vào region game hoặc locale của người dùng, ta ưu tiên nạp trước các bộ font ph biến trong nền, tránh trễ khi mở giao diện.
enum class ERegion { GLOBAL, ASIA, MIDDLE_EAST, EASTERN_EUROPE };
void CFontManager::PreloadForRegion(ERegion region) {
std::vector<ELanguage> plan = {ELanguage::LATIN, ELanguage::CJK};
switch (region) {
case ERegion::ASIA:
plan.push_back(ELanguage::CJK);
break;
case ERegion::MIDDLE_EAST:
plan.push_back(ELanguage::ARABIC);
break;
case ERegion::EASTERN_EUROPE:
plan.push_back(ELanguage::CYRILLIC);
break;
default: break;
}
for (auto lang : plan) {
g_thread_pool->Submit([this, lang]() {
std::lock_guard<std::mutex> lock(m_buildMutex);
m_activeLang = lang;
RebuildAtlas();
});
}
}
3.3 Đo hiệu năng xây dựng atlas
Cần có bộ đo để biết tác động của tối ưu. Dưới đây là ví dụ benchmark với chu kỳ đi rebuild hoàn tất.
void BenchmarkAtlasBuild() {
using namespace std::chrono;
auto t0 = steady_clock::now();
std::vector<ELanguage> targets = {
ELanguage::LATIN, ELanguage::CJK, ELanguage::CYRILLIC
};
for (auto lang : targets) {
g_font_mgr->SwitchLanguage(lang);
while (g_font_mgr->IsBuilding()) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
auto ms = duration_cast<milliseconds>(steady_clock::now() - t0).count();
LOG(INFO) << "Atlas build benchmark: " << ms << " ms";
}
4. Tương thích đa nền tảng
4.1 Phân giải đường dẫn theo OS
Windows, Linux và macOS có thư mục font hệ thống khác nhau. Ta tách riêng phần này để dễ bảo trì.
std::vector<std::filesystem::path> CFontManager::GetSearchPaths() const {
std::vector<std::filesystem::path> paths;
#ifdef _WIN32
if (const char* windir = std::getenv("WINDIR")) {
paths.emplace_back(std::string(windir) + "\\Fonts");
}
if (const char* localApp = std::getenv("LOCALAPPDATA")) {
paths.emplace_back(std::string(localApp) + "\\Microsoft\\Windows\\Fonts");
}
#elif defined(__APPLE__)
paths.emplace_back("/System/Library/Fonts");
paths.emplace_back("/Library/Fonts");
if (const char* home = std::getenv("HOME")) {
paths.emplace_back(std::string(home) + "/Library/Fonts");
}
#else
paths.emplace_back("/usr/share/fonts");
paths.emplace_back("/usr/local/share/fonts");
if (const char* home = std::getenv("HOME")) {
paths.emplace_back(std::string(home) + "/.local/share/fonts");
paths.emplace_back(std::string(home) + "/.fonts");
}
#endif
return paths;
}
4.2 Font nhúng làm phương án cuối
Khi không tìm thấy font hệ thống, ta dùng font nhúng trong binary để đảm bảo giao diện vẫn khả dụng.
// fonts_embedded.hpp
extern const unsigned char g_fallbackCjk[];
extern const size_t g_fallbackCjkSize;
std::filesystem::path CFontManager::ExtractEmbeddedFont(ELanguage lang) const {
if (lang != ELanguage::CJK) return {};
auto temp = g_file_manager.GetTempFolder().get_path() / "fallback_cjk.ttf";
std::ofstream out(temp, std::ios::binary);
out.write(reinterpret_cast<const char*>(g_fallbackCjk),
static_cast<std::streamsize>(g_fallbackCjkSize));
return temp;
}
5. Quản lý cấu hình và gỡ lỗi
5.1 Tập trung cấu hình font
Dùng một cấu trúc duy nhất mô tả từng ngôn ngữ, giúp thêm/bớt ngôn ngữ dễ dàng.
struct FontProfile {
ELanguage lang;
std::vector<std::string> candidates;
float glyphOffsetY;
const ImWchar* (*rangeFn)();
};
const std::unordered_map<ELanguage, FontProfile> g_profiles = {
{ELanguage::LATIN, {
ELanguage::LATIN,
{"segoeui.ttf", "arial.ttf", "roboto.ttf"},
0.0f,
[](){ return ImGui::GetIO().Fonts->GetGlyphRangesDefault(); }
}},
{ELanguage::CJK, {
ELanguage::CJK,
{"msyh.ttc", "noto-sans-cjk.ttc", "simsun.ttc"},
-1.0f,
[](){ return ImGui::GetIO().Fonts->GetGlyphRangesChineseFull(); }
}},
// ... mở rộng thêm ngôn ngữ khác
};
5.2 Cửa sổ chẩn đoán font
Công cụ nội bộ giúp kiểm tra số font đang tải, kích thước texture, số glyph và hiển thị đoạn mẫu.
void DrawFontDiagnostics() {
if (!ImGui::Begin("Font Diagnostics")) {
ImGui::End();
return;
}
ImFontAtlas* atlas = ImGui::GetIO().Fonts;
ImGui::Text("Loaded fonts: %d", atlas->Fonts.Size);
ImGui::Text("Texture: %dx%d", atlas->TexWidth, atlas->TexHeight);
ImGui::Text("Estimated GPU memory: %.2f MB",
(atlas->TexWidth * atlas->TexHeight * 4) / (1024.0f * 1024.0f));
ImGui::Separator();
for (ImFont* font : atlas->Fonts) {
if (ImGui::TreeNode(font->ConfigData->Name)) {
ImGui::Text("Size: %.1fpx", font->FontSize);
ImGui::Text("Glyphs: %d", font->Glyphs.Size);
ImGui::Text("Ascent: %.1f, Descent: %.1f", font->Ascent, font->Descent);
ImGui::Text("Sample: 你好 Hello Привет");
ImGui::TreePop();
}
}
ImGui::End();
}