Calling Font.clear() frequently

I’m needing to call Font.clear() frequently, since aspects of my fonts may change often (specifically setPixelsPerUnit).

The manual notes that:
“Calling this frequently can result in wasted texture memory, as any previously rendered text will still keep a pointer to the old, previously-generated pages. As long as the previously rendered text remains around, the old pages will also remain around.”

How can I remove the pointer to the old pages?

You might be well off to keep multiple different copies of your font in different pixels per unit, rather than changing your one font back and forth. Alternatively, you could just always use the highest pixels per unit setting for all of your fonts.

If you really do want to change your font settings repeatedly, though, you should just make sure that each time you change the font settings, you don’t keep any old text around that was generated with the old font settings. You can call clear() on these old text objects if you want to force them out.

David

Is there a way to copy a font without having to re-load the file?

And TextNode.clear() doesn’t seem to work for me. Here is a test case; each time you switch (1 and 2 keys) the memory jumps.

from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.task import Task
import math

class TestMemory:
    
    def __init__(self):
        self.pixel2d = NodePath('pixel2d')
        self.pixel2d.reparentTo(render2d)
        self.pixel2d.setPos(-1,0,1)
        self.pixel2d.setScale(2.0/base.win.getXSize(), 1, -2.0/base.win.getYSize())
        
        self.node = None

        self.font = loader.loadFont("res/arial.ttf")
        self.font.setPixelsPerUnit(30)
        self.font.setScaleFactor(1)
        self.font.setTextureMargin(2)
        self.font.setMinfilter(Texture.FTNearest)
        self.font.setMagfilter(Texture.FTNearest)
        
        self.txtGen = None
        
        self.resize(15)
        
        base.accept('1', self.resize, [15])
        base.accept('2', self.resize, [35])
    
    def resize(self, size):
        #change font settings, delete old text
        if self.node is not None:
            self.node.removeNode()
            self.node = None
        if self.txtGen is not None:
            self.txtGen.clear()
            self.txtGen = None
        
        self.font.clear()
        self.font.setPixelsPerUnit(size)
        
        self.txtGen = TextNode("myText2")
        self.txtGen.setText("The quick brown fox.")
        self.txtGen.setFont(self.font)
        self.txtGen.setTextColor(1,1,1,1)
        self.txtGen.setAlign(TextNode.ALeft)
        
        self.node = NodePath(self.txtGen.generate())
        
        self.node.setScale(size, 1, -size)
        self.node.setPos(15, 0, 55)
        self.node.reparentTo(self.pixel2d)
        
        tw = self.font.getPageXSize()
        th = self.font.getPageYSize()
        self.node.setTexOffset(TextureStage.getDefault(), 0.4/tw, -0.4/th)
        
t = TestMemory()
run()

Hmm, you’re right; there’s no convenient way to make a copy of a font without reloading it. (And even to make a copy by reloading is clumsy: you have to call FontPool.releaseFont(‘filename’) between reloads, or you won’t get a copy.) I’ll see about putting a convenient makeCopy() method in for the future.

Also, you’re right about the leak. It looks like the old Texture objects aren’t getting released. Curious. I’ll have to investigate.

David

Thanks for the quick reply. :slight_smile:

Was this ever added? The make_copy() method? I need it at the moment because in order to have pixel perfect fonts I need to set different PPU for every size in use. Reloading it every time is not an option, I have pan-unicode fonts that take seconds to load (if you know any other way to support all languages at once please tell me).

A CJK (chinese, japanese, korean) font, btw, takes 3MB in hard disk, I can only imagine that it takes something like 20MB on RAM once deflated by truetype. Having to make different copies of this just to use different PPU sounds like it will end up wasting a lot of RAM. Is it really necessary to specify the PPU at the font level? I mean, isn’t PPU a setting that is used when freetype is about to render a particular textnode? If so, would it make sense to move it to TextNode? In order not to break anything we could even leave it where it’s at and add a SetOverridePPU() method to TextNode. Do you think this is feasible, David? Patch welcome?

Actually, loading a font from the FontPool twice seems to yield a copy while the font is not reloaded from disk:

>>> a=FontPool.loadFont("panda3d/models/cmss12.egg")
:text: Loading font /home/pro-rsoft/panda3d/models/cmss12.egg
>>> b=FontPool.loadFont("panda3d/models/cmss12.egg")
>>> a is b
False

Not 100% sure if “a is b” only checks if the Python pointers match or also the C++ pointers.

I run some tests. When I reload a font, it loads from a cache as you said so there’s no hard drive delay, and there’s no RAM used after the first font loaded, but the PPU of all the fonts is linked. When I change it in one of the font objects, it changes in all of them, so it seems like I found a bigger problem.

If instead of using the FontPool I load them with the DynamicTextFont constructor, then there’s no caching and it loads the font from disk everytime, and it’s in RAM every time, at 6MB per size, but then I can use different PPU’s.

So my problem still stands, need both of both worlds. Caching, no memory waste and different PPU’s.

Loading from FontPool actually returns the same object each time, so that’s just a misunderstanding, not really a bug. (The Python “is” operator only compares the Python wrappers here. You can check a.this == b.this if you want to check the underlying C++ objects.)

The DynamicTextFont wants to build up caches of the glyphs it has already rendered, and it is most convenient to do this on a per-font basis. It would be possible to extend this to support multiple different PPU renderings simultaneously, but it’s probably the wrong approach.

I think the right approach is to share the FT_Face pointer between multiple different FreetypeFont instances. If Freetype includes a mechanism to reference-count this automatically, that makes it easy; otherwise, we’ll have to move the FT_Face member into its own reference-counted structure, and replace FT_Face _face with PT(FreetypeFace) _face in FreetypeFont.

I think this would be a fairly straightforward change. Patches welcome. :slight_smile:

David

Ok, that sounds good and I’ll try, but does that mean that we want to change the FontPool behavior where it returns a copy now? That may break existing code that relies on this (maybe somebody is changing the PPU once instead of in all the copies). Are we OK with that?

If we instead want to leave the FontPool intact and just make it so that all fonts from the same source reuse the FT_Face then we’ll end up making another FontPool… DynamicFontPool?

So which one is it, break FontPool compatibility or make a new DynamicFontPool?

Or am I missing something? (Which wouldnt surprise me much)

EDIT: well, now that I think about it, we dont really need another pool, just a std::map somewhere relating filepaths to FT_face pointers, is this the way to do it?

Hmm, you’re right, this does get a little sticky.

FontPool kind of is a map of filename -> font, but it should continue to return the same Panda Font object for each filename. I was simply envisioning adding a DynamicFont.makeCopy() method, which would return a new DynamicFont that shares the same _face object internally.

If we really wanted to generalize this to return a shared _face object automatically, we’d need some new kind of pool that would be somewhere higher-level than the FontPool. This could either be a new DynamicFontPool, or an implicit table hidden within FreetypeFont; both are a little bit clumsy.

Do we need that level of abstraction, though? Is makeCopy() sufficient?

David

If we assume that a FT_face pointer points to the same data regardless of PPU, and you already confirmed that by omission, then yes, a makeCopy is perfect, I just didn’t get it the first time. Thanks.

I also found myself wishing for makeCopy() recently, since it seemed the only way to get two different styles (e.g. RMWireframe and then RMSolid) with the same font at the same time was to symlink to a second filename and have the same font loaded 2x. I tried FontPool.releaseFont(‘fonts/foo.ttf’) before beginning to set up the second TextNode with the same filename in a different font object (which from the docs would only be a pointer to the first) and it didn’t help this, but then Fontpool.releaseAllFonts() did the trick, however brutally… is there a better way to make this work?

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from pandac.PandaModules import TextFont, DynamicTextFont, TexGenAttrib, FontPool, TextNode


class sometext(ShowBase):
	def __init__(self):
		ShowBase.__init__(self)
#		base.disableMouse()
#		base.camera.setPos(0.5, -5, 0.5)
		base.setBackgroundColor(0.0, 0.0, 0.0)
		sysfont="/usr/share/fonts/TTF/luximri.ttf"

		self.font1 = loader.loadFont(sysfont)
		self.font1.setRenderMode(TextFont.RMWireframe)

		self.myText1 = OnscreenText(text='Should Be Wireframe',
			pos=(0, 0.2), scale=0.1, fg=(1,0,0,1), align=TextNode.ACenter, font=self.font1)

		FontPool.releaseFont(sysfont)

		self.font2 = loader.loadFont(sysfont)
		self.font2.setRenderMode(TextFont.RMSolid)

		self.myText2 = OnscreenText(text='Most Recently set to Solid',
			pos=(0, 0), scale=0.1, fg=(0,1,0,1), align=TextNode.ACenter, font=self.font2)

		FontPool.releaseAllFonts()

		self.font3 = loader.loadFont(sysfont)
		self.font3.setRenderMode(TextFont.RMWireframe)

		self.myText3 = OnscreenText(text='RMWireframe again, won\'t touch RMSolid nodes',
			pos=(0, -0.2), scale=0.1, fg=(0,1,0,1), align=TextNode.ACenter, font=self.font3)

t=sometext()
t.run()

Hmm, you’re right, there’s a minor bug in FontPool::release_font() that makes it require the full pathname at which the font was found, rather than the relative pathname you specified to load_font(). I’ll check in a fix right now.

I guess we don’t have a make_copy() yet, seems like it should be easy for me to add that one too.

For now, the symlink is probably your best bet.

David

That’s odd, I always first tried relative paths and only when hacking up that example did I enter the full system path, and it acted the same in both cases. Anyway, thanks for checking it.