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

Calling sf::Text::setOutlineThickness() in a loop allocates a lot of memory (0.5 GB) #1823

Open
gravitycontained opened this issue Sep 12, 2021 · 10 comments

Comments

@gravitycontained
Copy link
Contributor

gravitycontained commented Sep 12, 2021

I had a lot more memory consumption than I should have had recently, so I went through every step and figured that calling sf::Text::setOutlineThickness() (therefore sf::Text::ensureGeometryUpdate()) in a loop with different values allocates a lot of memory (process memory reached about 550 megabytes after 10 seconds and stays there):

memory

Environment is Windows 10, Visual Studio 2019 (16.9.6).
SFML version is 2.5.1 (Visual C++ 15 (2017) - 64-bit).
Build settings are Debug, x64, /std:c++latest.

Here is the visual studio project folder using the latest version on the sfml-dev.org side SFMLBug.zip.
and here's the project using a CMake build of the latest commit: SFMLBug_commit_eeeda74.zip

This example should reproduce the behavior. Any font should work here (as I tried a few), here I used Arial font.

#include <SFML/Graphics.hpp>

int main() {
	sf::RenderWindow window(sf::VideoMode(1280u, 720u), "SFML");

	sf::Font font;
	font.loadFromFile("arial.ttf");
	sf::Text text;
	text.setFont(font);
	text.setCharacterSize(60);
	text.setString("hello world!");
	text.setOutlineColor(sf::Color::White);

	sf::Clock time;
	float value = 0.0;
	while (window.isOpen()) {
		sf::Event event;
		while (window.pollEvent(event)) {
			if (event.type == sf::Event::Closed) window.close();
		}

		text.setOutlineThickness(value);
		value += 0.01;
		if (value > 10.0) {
			value = 0;
		}

		window.clear();
		window.draw(text);
		window.display();
	}
}

I do realize that calling setOutlineThickness this way is probably wrong as it's quite an expensive maneuver, however I don't expect there to be so much allocations. There should probably be a warning on the comment to setOutlineThickness if this memory problem is not to be solved.

Thanks!

@gravitycontained gravitycontained changed the title calling text::setOutlineThickness in a loop allocates a lot of memory (0.5 gigabyte) calling sf::Text::setOutlineThickness in a loop allocates a lot of memory (0.5 gigabyte) Sep 12, 2021
@Bromeon
Copy link
Member

Bromeon commented Sep 12, 2021

I tried to reproduce it with VS 2019, latest SFML master (commit eeeda74), Windows x64 Debug mode.

Could not see the behavior your experienced; memory stayed around ~36 MB and maybe 2-3 MB less with the setOutlineThickness() call commented out.

Did you set up a fresh project for this? Any other special configuration?

@gravitycontained
Copy link
Contributor Author

gravitycontained commented Sep 12, 2021

Yes I made a completely fresh setup. I downloaded latest 2.5.1 (Visual C++ 15 (2017) - 64-bit version, linked and set include path, added the dlls and added arial.ttf, I added a Project.zip in the post.
EDIT: I just realized I didn't use the latest commit, I simply used the latest version on the sfml-dev.org site. let me try out the last commit.
EDIT: i build this commit and still got the same behavior. also included the visual studio project in the post.

@eXpl0it3r
Copy link
Member

Changing the outline thickness, as well as changing any other visual properties, will trigger a recalculation of the geometry, which means that every glyph needs to be repositioned etc. This involves some std::vector clearing and appending.

The clear & append call shouldn't cause additional allocations, but I guess that's largely dependent on the STL implementation.
Since we're not doing anything magical with memory here, my guess is that this might just be some lazy deallocation or similar going on.

Have you tried this in release mode?

@Bromeon
Copy link
Member

Bromeon commented Sep 12, 2021

And out of interest, does it behave like this even if you don't set any text?

Asking because we have

 // No text: nothing to draw
if (m_string.isEmpty())
    return;

in the middle Text::ensureGeometryUpdate(), and that can bisect the potential cause 🙂
(not much of interest happens before, so I would guess the problem disappears)

@gravitycontained
Copy link
Contributor Author

gravitycontained commented Sep 13, 2021

Leaving the text empty will not cause the behavior.
Release mode has the exact same memory usage as in Debug mode.

I found In Text.cpp removing line 501 const Glyph& glyph = m_font->getGlyph(curChar, m_characterSize, isBold, m_outlineThickness); and the lines that follow, does indeed remove the memory issue.

@vittorioromeo
Copy link
Member

Unless I am misunderstanding the code, the culprit seems to be Font::getGlyph, which does this:

const Glyph& Font::getGlyph(Uint32 codePoint, unsigned int characterSize, bool bold, float outlineThickness) const
{
    // Get the page corresponding to the character size
    GlyphTable& glyphs = m_pages[characterSize].glyphs;

    // Build the key by combining the glyph index (based on code point), bold flag, and outline thickness
    Uint64 key = combine(outlineThickness, bold, FT_Get_Char_Index(static_cast<FT_Face>(m_face), codePoint));

    // Search the glyph into the cache
    if (auto it = glyphs.find(key); it != glyphs.end())
    {
        // Found: just return it
        return it->second;
    }
    else
    {
        // Not found: we have to load it
        Glyph glyph = loadGlyph(codePoint, characterSize, bold, outlineThickness);
        return glyphs.emplace(key, glyph).first->second;
    }
}

The key is dependent of outlineThickness, which varies on every iteration of your loop. Therefore we never find the glyph, and we have to load a new one with loadGlyph, then emplace it inside glyphs (memory allocation).

Why does the glyph depend on outlineThickness?

@Bromeon
Copy link
Member

Bromeon commented Dec 9, 2021

Why does the glyph depend on outlineThickness?

It's this code in loadGlyph():

if (outlineThickness != 0)
{
auto stroker = static_cast<FT_Stroker>(m_stroker);
FT_Stroker_Set(stroker, static_cast<FT_Fixed>(outlineThickness * static_cast<float>(1 << 6)), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
FT_Glyph_Stroke(&glyphDesc, stroker, true);
}

FT_Stroker is responsible of drawing the "stroke" (outline) onto the glyph.


So the cause of this issue is that for different outline thicknesses (and characters, character sizes, bold styles), separate glyphs are allocated. This is normally expected, because the user will use those glyphs to display text, and wants them to remain cached for fast rendering.

Now, the problem is that we never deallocate them. So cycling through many possible combinations of glyphs will cause lots of allocations, and memory that stays reserved. And the allocation pattern depends massively on the application:

  1. Typical applications will use a handful of fonts with a handful of styles, so it's never a problem.
  2. Others may use sf::Text heavily for graphical effects, needing many glyphs only for a a short time. This might be the case for some flashy games.
  3. Yet other applications may be long-running, and over the course of their uptime request many different glyph patterns, and sometimes even render a huge number simultaneously. Text editors or complex UIs could fall into this category.

So unfortunately there's no easy way to garbage-collect glyphs. A possible strategy might be a LRU cache with a fixed size, but that will kill performance for applications that need more than the cache size simultaneously. Maybe exposing a tweakable strategy in the sf::Font API would be an approach, but it's very hard to find a good compromise between customizability and simplicity.

@DanielRabl could you tell us more about the project that made you notice this in the first place? (I.e. not the reproducible example)

@gravitycontained
Copy link
Contributor Author

I wanted to make a animation, where text is highlighted by a big outline thickness that vanishes quickly. Initially I reduced the outline thickness, after seeing the memory consumption I now instead just reduce the alpha value. It does make sense to me now why this memory is not cleared, but like I said therefore it's maybe worth warning about this behavior in the description of setOutlineThickness.

@Bromeon
Copy link
Member

Bromeon commented Dec 15, 2021

I wanted to make a animation, where text is highlighted by a big outline thickness that vanishes quickly. Initially I reduced the outline thickness, [...]

So you started with a wide outline, and continuously made it narrower?

Can you tell us roughly how many steps those were?

[...] it's maybe worth warning about this behavior in the description of setOutlineThickness.

Yes, that's the least. Even better would be if we could enable workflows like yours 🙂

@gravitycontained
Copy link
Contributor Author

gravitycontained commented Dec 15, 2021

Can you tell us roughly how many steps those were?

It was starting with a outline thickness of 2, and when the animation starts, it interpolates the value down to 0 every frame (144 frames / second) in the span of 1 second. For interpolation I used a curve slope (return std::pow(1 - std::pow(1 - progress, slope), 1.0 / slope);) where slope was 1.5 and progress is the time progress in range 0 to 1. But I believe it doesn't make a difference if it's a curve or if it's linear.

@eXpl0it3r eXpl0it3r added bug and removed feature labels Jan 24, 2023
@eXpl0it3r eXpl0it3r changed the title calling sf::Text::setOutlineThickness in a loop allocates a lot of memory (0.5 gigabyte) Calling sf::Text::setOutlineThickness() in a loop allocates a lot of memory (0.5 GB) May 14, 2024
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

5 participants