Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A way to speed up font loading #6169

Open
Chaojimengnan opened this issue Feb 16, 2023 · 1 comment
Open

A way to speed up font loading #6169

Chaojimengnan opened this issue Feb 16, 2023 · 1 comment

Comments

@Chaojimengnan
Copy link

Chaojimengnan commented Feb 16, 2023

Dear ImGui 1.89.2 WIP (18914)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=199711
define: _WIN32
define: _WIN64
define: _MSC_VER=1933
define: _MSVC_LANG=202002
--------------------------------
io.BackendPlatformName: imgui_impl_glfw
io.BackendRendererName: imgui_impl_opengl3
io.ConfigFlags: 0x00000000
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x0000000E
 HasMouseCursors
 HasSetMousePos
 RendererHasVtxOffset
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 4096,2904
io.DisplaySize: 1280.00,720.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00

Version/Branch of Dear ImGui:

Version: 1.89.2
Branch: master

Back-end/Renderer/Compiler/OS

Back-ends: imgui_impl_opengl3.cpp + imgui_impl_glfw.cpp
Compiler: MSVC
Operating System: windows11

My Issue/Question:

This may be useful for some people.

In my case, I need to load the entire unicode plain 0 glyph, and since imgui re-rasterises the glyphs each time it runs, it takes quite a while to start it up each time.

In most cases, we don't need to change the font, so why can't we reuse the previously generated textures and UV coordinates?

After much trial and error, I finally found what seems to work to save and restore the font atlas properly.

struct font_atlas_cache
{
    ImVec2 TexUvWhitePixel;
    int out_width;
    int out_height;
    std::unique_ptr<unsigned char[]> image_data;
    std::vector<ImFontGlyph> glyphs;
    ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1];
};

// TexUvWhitePixel
// TexUvLines
// n (size of glyphs)
// n * ImFontGlyph...
// out_width
// out_height
// TexDataAlpha8
void save_font_atlas(std::string_view path, bool override_if_exists = false, ImFontAtlas* font_atlas = nullptr)
{
    if (font_atlas == nullptr)
        font_atlas = ImGui::GetIO().Fonts;

    if (std::filesystem::exists(path) && !override_if_exists)
        throw std::runtime_error("Target already exists");

    std::ofstream file(path.data(), std::ios::out | std::ios::binary);

    file.write(reinterpret_cast<const char*>(&font_atlas->TexUvWhitePixel), sizeof(font_atlas->TexUvWhitePixel));
    file.write(reinterpret_cast<const char*>(font_atlas->TexUvLines), sizeof(font_atlas->TexUvLines));

    auto size = font_atlas->Fonts[0]->Glyphs.size();
    file.write(reinterpret_cast<const char*>(&size), sizeof(int));
    file.write(reinterpret_cast<const char*>(font_atlas->Fonts[0]->Glyphs.Data), sizeof(ImFontGlyph) * size);

    unsigned char* out_pixels {};
    int out_width {};
    int out_height {};
    font_atlas->GetTexDataAsAlpha8(&out_pixels, &out_width, &out_height);
    file.write(reinterpret_cast<const char*>(&out_width), sizeof(int));
    file.write(reinterpret_cast<const char*>(&out_height), sizeof(int));
    file.write(reinterpret_cast<const char*>(out_pixels), out_width * out_height);
}

font_atlas_cache load_font_atlas_cache(std::string_view path)
{
    if (!std::filesystem::exists(path))
        throw std::runtime_error("Target doesn't exists");

    std::ifstream file(path.data(), std::ios::in | std::ios::binary);

    font_atlas_cache cache {};

    file.read(reinterpret_cast<char*>(&cache.TexUvWhitePixel), sizeof(cache.TexUvWhitePixel));
    file.read(reinterpret_cast<char*>(cache.TexUvLines), sizeof(cache.TexUvLines));

    int glyph_size {};
    file.read(reinterpret_cast<char*>(&glyph_size), sizeof(int));
    cache.glyphs.resize(glyph_size);
    cache.glyphs.shrink_to_fit();
    file.read(reinterpret_cast<char*>(cache.glyphs.data()), sizeof(ImFontGlyph) * glyph_size);

    file.read(reinterpret_cast<char*>(&cache.out_width), sizeof(int));
    file.read(reinterpret_cast<char*>(&cache.out_height), sizeof(int));
    
    cache.image_data = std::make_unique<unsigned char[]>(cache.out_width * cache.out_height);
    file.read(reinterpret_cast<char*>(cache.image_data.get()), cache.out_width * cache.out_height);

    return cache;
}

void restore_font_atlas(font_atlas_cache& cache, ImFontAtlas* font_atlas = nullptr)
{
    if (font_atlas == nullptr)
        font_atlas = ImGui::GetIO().Fonts;

    ImFontConfig dummy_config {};
    dummy_config.FontData = new unsigned char;
    dummy_config.FontDataSize = 1;
    dummy_config.SizePixels = 1;
    auto* font = font_atlas->AddFont(&dummy_config);
    font->FontSize = 16.0F;
    font->ConfigData = &dummy_config;
    font->ConfigDataCount = 1;
    font->ContainerAtlas = font_atlas;

    font_atlas->ClearTexData();
    font_atlas->TexPixelsAlpha8 = cache.image_data.release();
    font_atlas->TexWidth = cache.out_width;
    font_atlas->TexHeight = cache.out_height;
    font_atlas->TexUvWhitePixel = cache.TexUvWhitePixel;
    std::memcpy(font_atlas->TexUvLines, cache.TexUvLines, sizeof(cache.TexUvLines));

    for (auto&& glyph :cache.glyphs) {
        font->AddGlyph(&dummy_config, glyph.Codepoint, glyph.X0, glyph.Y0, glyph.X1, glyph.Y1,
            glyph.U0, glyph.V0, glyph.U1, glyph.V1, glyph.AdvanceX);
        font->SetGlyphVisible(glyph.Codepoint, glyph.Visible);
    }

    font->BuildLookupTable();
    font_atlas->TexReady = true;
}

Standalone, minimal, complete and verifiable example: (see #2261)

Save font atlas cache

ImFontConfig config;
config.OversampleH = 1;
config.OversampleV = 1;
config.SizePixels = 16;
config.RasterizerMultiply = 2.F;
io.Fonts->Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight;

const ImWchar range[] = {
    0x20, 0xFFFF,
    0x0
};

io.Fonts->AddFontFromFileTTF("./unifont-15.0.01.ttf", 0, &config, range);
io.Fonts->Build();
save_font_atlas("./unicode.imfont", true);

Load font atlas cache(No need reload ttf)

// save_font_atlas("./unicode.imfont", true);
font_atlas_cache cache = load_font_atlas_cache("./unicode.imfont");
restore_font_atlas(cache);

By testing, it takes 1.7s to load TTF and build, whereas loading the cache directly reduces it to 0.3s, which is a massive improvement!

But I don't know if restore_font_atlas is correct, at least from my computer it works perfectly

@ocornut
Copy link
Owner

ocornut commented Feb 16, 2023

Some extra links/ref for code tackling similar things.
#2093 #2127 #1948 #1853 #315

For now even thought I appreciate the interested/need and sharing of code snippets, my priority will be to re-overhaul the system to not need those ranges. First step being #3761 for 1.90.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants