sammycage / lunasvg

lunasvg is a standalone SVG rendering library in C++
MIT License
818 stars 115 forks source link

Performance issue when used from ImGui as a SVG font loader #150

Open pthom opened 6 months ago

pthom commented 6 months ago

Hello,

I saw some performance issues with ImGui when it uses LunaSvg in order to load some Svg fonts:

It can load some fonts with a very good performance. However, with some others, the performance drops and it takes about 2 seconds per glyph to load.

In order to facilitate the analysis I prepared a repro repository, here: https://github.com/pthom/lunasvg_perf_issue

    {
        // will load 1408 colored glyphs, with lunasvg. Fast!
        std::string fontFile = ThisDir() + "/fonts/noto-untouchedsvg.ttf";
        gEmojiFont  = ImGui::GetIO().Fonts->AddFontFromFileTTF(fontFile.c_str(), 30.0f, &cfg, ranges);
    }
    {
        // will load 1428 glyphs, with lunasvg. Slow. About 2 seconds per glyph
        std::string fontFile = ThisDir() + "/fonts/NotoColorEmoji-Regular.ttf";
        gEmojiFont  = ImGui::GetIO().Fonts->AddFontFromFileTTF(fontFile.c_str(), 30.0f, &cfg, ranges);
    }

As explained is the repro repository, a quick analysis with a profiler shows that time seems to be spent in std::map::find + std::vector::emplace_back.

PS: Happy new year! I'm sorry to bother you on Jan 2nd!

sammycage commented 6 months ago

@pthom After conducting a thorough investigation into the issue, I discovered that a large SVG file (approximately 12MB) from https://github.com/cppfw/svgren/blob/master/tests/unit/samples_data/back.svg takes about 2.5 seconds to load and render using LunaSvg alone. Interestingly, it loads faster than smaller files like glyphs in the repro profile analysis. This suggests that the problem might be associated with FreeType, ImGui, or other components in the reproduction process. Your ongoing collaboration is crucial for improving this library. Thank you for your support!

PS: Happy new year! I'm sorry to bother you on Jan 2nd!

Thanks for the New Year wishes! No bother at all, because clearly, dealing with GitHub issues is the best way to kick off the year. 😜

pthom commented 6 months ago

@sammycage :

You are probably right, there is something sniffy in the various components chain (ImGui => FreeType => LunaSvg).

Here is how I came to this conclusion:

I saw that for example, the glyph number 18 is extremely slow to load.

  1. I put a breakpoint inside ImFontAtlasBuildWithFreeTypeEx(ImGui), when glyph_i==18, just before it calls src_tmp.Font.LoadGlyph()
  2. This loop will call ImGuiLunasvgPortPresetSlot, which will call lunasvg::Document::loadFromData (a few lines into this function)
  3. Just before the call to lunasvg::Document::loadFromData, I wrote the SVG document with this code:
        // Write document->svg_document to a file for debugging
        std::ofstream file("svg_document.svg");
        file.write((const char*)document->svg_document, document->svg_document_length);

And then I looked at the saved svg document for the glyph number 18. And then, surprise, surprise:

image
pthom commented 6 months ago

@ocornut: what do you think? Is is related to Freetype? Or maybe to ImGui?

pthom commented 6 months ago

I have a suspect inside Freetype, namely ttsvg.c.

More info:

If we look at the evolution of the svg document sizes per glyph, we have:

glyph_i: 0 / 1427
glyph_i: 1 / 1427 document->svg_document_length: 1650
glyph_i: 2 / 1427 document->svg_document_length: 1060
glyph_i: 3 / 1427 document->svg_document_length: 686
glyph_i: 4 / 1427 document->svg_document_length: 534
glyph_i: 5 / 1427 document->svg_document_length: 837
glyph_i: 6 / 1427 document->svg_document_length: 1087
glyph_i: 7 / 1427 document->svg_document_length: 917
glyph_i: 8 / 1427 document->svg_document_length: 922
glyph_i: 9 / 1427 document->svg_document_length: 908
glyph_i: 10 / 1427 document->svg_document_length: 612
glyph_i: 11 / 1427 document->svg_document_length: 941
glyph_i: 12 / 1427 document->svg_document_length: 884
glyph_i: 13 / 1427 document->svg_document_length: 7870
glyph_i: 14 / 1427 document->svg_document_length: 7870
glyph_i: 15 / 1427
glyph_i: 16 / 1427 document->svg_document_length: 14516311
glyph_i: 17 / 1427 document->svg_document_length: 14516311
glyph_i: 18 / 1427 document->svg_document_length: 14516311
glyph_i: 19 / 1427 document->svg_document_length: 4774
glyph_i: 20 / 1427 document->svg_document_length: 14516311
glyph_i: 21 / 1427 document->svg_document_length: 14516311
glyph_i: 22 / 1427 document->svg_document_length: 14516311
glyph_i: 23 / 1427 document->svg_document_length: 14516311
glyph_i: 24 / 1427 document->svg_document_length: 14516311
glyph_i: 25 / 1427 document->svg_document_length: 14516311
glyph_i: 26 / 1427 document->svg_document_length: 14516311
glyph_i: 27 / 1427 document->svg_document_length: 14516311
glyph_i: 28 / 1427 document->svg_document_length: 14516311
glyph_i: 29 / 1427 document->svg_document_length: 14516311
glyph_i: 30 / 1427 document->svg_document_length: 6554
glyph_i: 31 / 1427 document->svg_document_length: 8158
glyph_i: 32 / 1427 document->svg_document_length: 14516311
glyph_i: 33 / 1427 document->svg_document_length: 14516311
...

So, I investigated what happens when glyph_i=16, and somewhere inside the long callstack, we have:

external/freetype/src/sfnt/ttsvg.c, line 283:

  FT_LOCAL_DEF( FT_Error )
  tt_face_load_svg_doc( FT_GlyphSlot  glyph,
                        FT_UInt       glyph_index )
  {
    FT_Error   error  = FT_Err_Ok;
    TT_Face    face   = (TT_Face)glyph->face;
    FT_Memory  memory = face->root.memory;
    Svg*       svg    = (Svg*)face->svg;

    FT_Byte*  doc_list;
    FT_ULong  doc_limit;

    FT_Byte*   doc;
    FT_ULong   doc_offset;
    FT_ULong   doc_length;
    FT_UShort  doc_start_glyph_id;
    FT_UShort  doc_end_glyph_id;

    FT_SVG_Document  svg_document = (FT_SVG_Document)glyph->other;

    FT_ASSERT( !( svg == NULL ) );

    doc_list = svg->svg_doc_list;

    error = find_doc( doc_list + 2, svg->num_entries, glyph_index,
                                    &doc_offset, &doc_length,
                                    &doc_start_glyph_id, &doc_end_glyph_id );

And error = find_doc(...) did set these values:

svg = {Svg *} 0x6000004171c0
 version = {FT_UShort} 0
 num_entries = {FT_UShort} 674
glyph_index = {FT_UInt} 6
doc_offset = {FT_ULong} 9365
doc_length = {FT_ULong} 14516311           // ARGH.... 14.5MB
doc_start_glyph_id = {FT_UShort} 4.         // Glyph number 4...
doc_end_glyph_id = {FT_UShort} 2808.    // ... to 2808... Exactly what I saw inside Inkscape

I'm using Freetype VER-2-13-2 (i.e. the latest tag)

pthom commented 6 months ago

And finally, there is a probably related issue for freetype about the same font (NotoColorEmoji):

Freetype does not support 'COLR' v1 tables (whatever that means), but as it appears, it still tries to load fonts with such a format, instead of giving an error.

See https://gitlab.freedesktop.org/freetype/freetype-demos/-/issues/35

Werner Lemberg @wl · 6 months ago Owner

The new NotoColorEmoji.ttf font comes with a 'COLR' v1 table. While FreeType provides routines to read and parse fonts with 'COLR' v1 tables, it has no possibility to actually render them. It is rather simple to provide a simple default rendering for 'COLR' v0, so FreeType has it. However, the version 1 format is far too involved to be handled within FreeType – it is almost as complex as SVG.

If someone provides a library for rendering 'COLR' v1, FreeType can provide hooks in a similar way to SVG. Until then, it is unfortunately not possible to get something better.

pthom commented 6 months ago

I posted the issue to Freetype: https://gitlab.freedesktop.org/freetype/freetype/-/issues/1265

sammycage commented 6 months ago

@pthom Well done 👍

pthom commented 6 months ago

Summary of the current situation:

23M   NotoColorEmoji-Regular.ttf.      // probable issue with Freetype (colored, probably COLR v1)
10M   OpenMoji-color-colr0_svg.ttf     // probable issue with Freetype (colored, probably COLR v0)

858K  NotoEmoji-Regular.ttf            // works with Freetype (not colored)
14M   TwitterColorEmoji-SVGinOT.ttf    // works with Freetype (colored)
39M   noto-untouchedsvg.ttf            // works with Freetype (colored)
660K  seguiemj.ttf                     // works with Freetype
pthom commented 6 months ago

I can confirm the issue is in Freetype only, since I created a repro that depends only on it. See https://gitlab.freedesktop.org/freetype/freetype/-/issues/1265#note_2226979

pthom commented 6 months ago

If a deep dive into FreeType internals interests you, you can look at https://gitlab.freedesktop.org/freetype/freetype/-/issues/1265#note_2228459