Any tricks for optimizing text drawing?

Right now the slowest thing in my game is trying to draw multiple blocks of text. It seems particularly slow to drawTextInRect with a string longer than a couple hundred characters that breaks into 4 or 5 lines nearly the width of the screen in the included pedallica font.

I have a simulated "email" system in a gridview that looks like this:

project-bottle-crystal-cube-text

This scene drops my framerate sometimes down below 10 fps on the hardware, just sitting still on this screen. I know I'm drawing multiple textboxes (it's supposed to only draw boxes that are within view of the screen), but even 3 simultaneous drawTextInRect calls is really slow.

But I can also produce a huge FPS drop if I render a single really long string outside of a gridview, just sitting in a text box like this:

Screen Shot 2020-05-09 at 11.34.46 PM

If this string is long enough to exceed the bounds of the box so that it gets truncated, FPS drops from 30 to 9. Just sitting still, no other calculations, nothing. Shorter strings in this textbox are fine. I don't want to have a string that long here, but this is just an example of how to trigger a massive FPS hit.

I think that technically text is rendered into an image before being drawn? Is this cached, or is it calculating every frame? Is drawTextInRect having to work really hard each frame to make sure the text fits? Is there something I can do to optimize this?

Thank you!

Not that I recommend this as a long term solution, but is there any difference manually line-breaking the long text?

Have you tried using the Simulator Sampler feature to see where the code is spending most of its time?

For text, in out game we cache most of the rendering. We have functions that render a block of text in a bitmap and use the bitmap as a sprite. I don't think this is done by the SDK, at least this is what we do.

I had to do that because rendering text was not too good for our framerate. I never tested single line vs truncted lines. I am not suprise there is a difference but I would never have expected to be that much (and that would explain why it had such impact on our game too).

Usually lua is not so happy with string manipulation so it might be a reason why drawTextInRect could have an impact on performance.

I think Matt's solution of adding line break manually is also something to consider. Believe it or not but until recently this was still the favored way to layout text in all Nintendo game.

1 Like

I'd assume you could also programmatically calculate and insert the linebreaks at run time.

ah I see! you would cache the line with the linebreaks. That would work too I guess.

Looking at drawTextInRect(), the function still goes to a lot of hoops to format the text. I think caching the rendering would be more efficient overall.

I did a quick comparison for drawing text.

Adding linebreaks doesn't seem to improve the performance of drawTextInRect

drawing each line manually with drawText however is a massive boost in performance but you have to manage the layouting yourself.

I also tested performance depending of the length of the text.
Nothing surprising, the performance is quite linear and on my test having to break a text in multiple lines doesn't seem to trigger a spike in performance. But since drawTextInRect is not a light function, performance will degrade with longer strings.

@nick_splendorr I still think you best bet is to pre-render your e-mail the first time you want to use them. For us it was very helpful.

3 Likes

Just to confirm what Nic said, drawTextInRect is fairly slow, at least partially because it's implemented in lua (in CoreLibs), and uses lua's regex matching, which is not very fast. A simple drawText call with manually-inserted newline characters will be quite a lot faster, however, that's obviously not always convenient. The best solution, as mentioned, is to cache the text drawing into an image, which should be much much faster to redraw.

Now I'm thinking we should probably have a convenience function for doing that to save everyone re-implementing the same code.

6 Likes

Thanks for the help, everybody! I'll look into drawing the text blocks once and caching them, which should help a lot. @dan, something equivalent to the functions that return a blurred or faded image rather than drawing each frame would probably be used often enough to be worth including!

I added an on-screen timer to my game, powered by playdate.timer.new(999)and using gfx.drawText() to draw currentTime to hundredths of a second precision on every frame. On the simulator all is good, but on the device things went from 40fps to 30fps.

Is there anything I can do?

In the sampler I saw that you are calling drawTextAligned is that correct?
I would call plain drawText when possible. And also try to not call getTextSize if this is possible.

I do not have an easy trick to improve performance and instead the same good old, render the text in an image and re-render it only when necessary. So for your timer, you could cache the minutes and seconds and only draw the milliseconds every time.

Thanks Nic. I already switched to drawtext and don't use get text size (no need for either here) but negligible gain, so I guess change is needed.

It's disappointing that drawtext so few pixels requires so much time.

I had the idea to do the 7-segment numbers as sprites. We'll see.

here is some test code (also includes my LCD font and timer routine)

test.zip (21.0 KB)

Test

  1. See attached example, targeting 50fps
  2. Test on device

Look for

  • 10 lines of text drops things to 40fps
  • 5 lines of text can achieve 50fps

in my game i have a bunch of things happening

  • so I target 40fps
  • 1 line of drawtext drops me to 30fps

Thoughts

solutions offered for slow drawtext have been

  • draw to image and cache images
  • use sprites

drawtext should be much faster to be usable without workarounds

  • perhaps drawtext could use one or both of these internally?

I have filed this as a feature request

2 Likes

I spent a couple hours to create a first draft of a "spriteText" module, it takes a string and creates one sprite per character that will then only be drawn when they change.

This could be a really bad idea, but it works well and performance is good (half the fps drop of drawText). I'll happily share it when it has been trimmed and tested and optimised a little more.

It's for use on fixed length text strings whose contents change frequently, like:

  • score
  • lives
  • timers
  • hud text

It uses an image like your font .png (but not an actual font .png because imagetable won't load a .png that has an associated .fnt file, plus font .png files have characters that are not always in alphabetical/ascii order so there's that too. So I'm using a section of the font .png)

Currently it works like this:

  • spriteFont:createText(text, x,y)
    • to create the sprites and set the initial positions
    • optional: correct character widths using playdate.graphics.font:getTextWidth(text)
    • for outside of your loop
  • spriteFont:updateText(text, x,y)
    • to update the sprites to represent the new/updated text and position
    • for inside of your loop

Screenshot

fps ... spriteFont:drawText ... playdate.graphics.font:updateText

Screen shot 2020-06-02 at 23.44.44

5 Likes

Hey, I was wondering if you had made any progress on that "text-as-sprite" module?
Even if the code is not in very good shape I'd be very interested in taking a look at it!

It was written for Daily Driver and is used there. It wasn't generalised at the time.

But I'll try to dig out the original version and see about posting it here. I can't promise when that might be as I have 3 kids and it's the last few days of summer school holidays. :sweat_smile:

Awesome thank you very much, I'm so interested in this book, just pre-ordered it and excited for the tips it will contain!
(also good luck with the kids haha)

1 Like