Welcome to the sponsor-exclusive content for the Ren'Py Patreon. Sponsors like you ensure this page exists. Thank you.
This month’s article talks about framerate glitches in Ren’Py - what they are, what causes them, and most importantly, some ways in which you can make sure your players don’t see them. It focuses on image prediction, describes how image prediction works, why it might fail, and how you can help Ren’Py improve its predictions.
Ren’Py has two modes of operation. When the screen is changing, it tries to update the screen at the monitor’s framerate, which is usually 60 Hz (or 60 times a second). When the screen is not changing rapidly, Ren’Py slows down to about 4.5 Hz, so as to save power and not to spin up computer fans.
A framerate glitch happens when Ren’Py can’t draw a frame in time. When running in 60 Hz mode, when the user clicks or the game advances to the next statement, Ren’Py has to do some or all of:
When the screen is changing, all of these tasks need to be done in 16.6 ms (0.0166 seconds), or the user may see the system skip a frame.
Of these, the task that is the slowest is loading images, as everything else uses the CPU, GPU, and RAM, which are many times faster than a computer’s hard drive. So that’ll be where this article spends most of its time.
Before you read everything, let me just point out that some of this might not be relevant to your game. Framerate glitches are only possible if your game is constantly updating the screen. That happens if a character or background is moving or animating when the game advances. In many simple games, there’s no animation, or the animation can be expected to stop before the player clicks to advance things. If that’s the case, you don’t need to worry about framerate glitches until your game becomes more complex.
The slowest thing Ren’Py can do is to load an image from disk, decompress it, and move it to the GPU. While fast by human standards, it’s slow enough that an unpredicted image load could easily cause a framerate glitch. To prevent this from happening, Ren’Py first predicts images, and then preloads them into the image cache. When this works, it means that the loading occurs before the user clicks to display the image, making such display nearly instantaneous.
Prediction and preloading are different steps. During the prediction step,
Ren’Py will “look forward” by examining multiple Ren’Py statements. (By default
32 statements are looked at, but this is controlled by config.predict_statements)
When a statement Ren’Py understands is encountered, Ren’Py will add any images
and screens it uses to prediction lists.)
To steal a phrase from Yogi Berra, when prediction encounters a fork in the road, it takes it. That means that when it encounters a menu with multiple choices, prediction will look at all possible choices, proportionally less far. (This means that when Ren’Py encounters a menu with two choices, it will go half as far down each branch.)
The one thing Ren’Py can’t predict is Python. This is because Python code might have side-effects that Ren’Py doesn’t understand. While predicting, Ren’Py doesn’t change anything about the state of the system, which can cause the wrong images to be predicted. I’ll describe more about this below.
Ren’Py predicts a bit harder when time is available, meaning that it does not need to update the screen for .2 seconds. When this time is available, Ren’Py will predict screens that are available, then predict which images might be used by those screens.
Since this additional prediction can be helpful, it makes sense to ensure that Ren’Py has the time available. One common thing that would prevent Ren’Py from predicting is a click to continue indicator that constantly updates, such as:
image ctc:
"ctc.png"
alpha 0.0
linear .5 alpha 1.0
linear .5 alpha 0.0
repeat
Adding .25 second pauses will give Ren’Py time to predict screens:
image ctc:
"ctc.png"
alpha 0.0
linear .25 alpha 1.0
pause .25
linear .25 alpha 0.0
pause .25
repeat
Once an image has been predicted, Ren’Py will attempt to preload it into
the image cache, or will keep it in the image cache if it’s already there.
This assumes that space is available in the image cache. The size of the
image cache is set by config.image_cache_size_mb, which defaults
to 300MB. The uncompressed images take 4 bytes per pixel, but only
those inside the smallest box containing non-transparent pixels count.
Ren’Py has an image load log, which can be accessed by pressing the F4 key in a game that has developer mode enabled. This pops up a window that shows each image file for a few seconds as it’s being loaded. Images that are preloaded are shown in green, while images that Ren’Py has to wait for are shown in red.
Note that you can expect images to fail to be predicted when your game first starts. This is normal, as Ren’Py hasn’t had the chance to predict and preload images.
The image load log also shows how full the image cache is. When it is completely full, Ren’Py will stop preloading images until some space can be freed up.
While there are several reasons prediction can fail, the most common one encountered in modern Ren’Py is that Python is used, causing Ren’Py to predict the wrong image.
Ren’Py is designed such that image attributes are used to select which image to display. When Ren’Py encounters the statement:
show eileen happy
it always knows what it can display. By contrast, when there are more dynamic images, such as:
image eileen = "eileen [emotion].png"
or
image eileen = ConditionSwitch(
"emotion == 'happy'", "eileen happy.png",
"emotion == 'concerned'", "eileen concerned.png")
And the predictor encounters:
$ emotion = "happy"
show eileen
The emotion variable might not be set to the correct value until just before the statement executes, meaning the wrong image (or no image at all) could be predicted.
These uses of dynamic images are rather common, due to some differences in what they’re intended for and how they’re used in practice. Dynamic images and ConditionSwitch were intended for things that change once per game (like selecting the look of the protagonist), or at least rarely (the time of day or location). Several libraries and tutorials suggest using variables to model characters’ emotions, but this makes it harder for Ren’Py to predict what should be shown.
Now that we’ve found an image that Ren’Py is failing to predict, the question is what to do about it. There are a lot of choices, so here are a few for you to consider. (I’m assuming that you’re too far along to rewrite your project to use image attributes, but that’s worth trying where it would help.)
In some games, it’s reasonable to do nothing when an image load fails. When the screen is static, a simple failed prediction won’t cause a frame to be skipped, just a slightly longer delay before Ren’Py responds to a click.
Especially when screens are involved, it might make sense to restructure your project so that animations stop before a screen loads. For example, rather than simply calling a screen, one could write:
scene bg interstital
with dissolve
call screen myscreen
By dissolving into a static scene before showing the screen, Ren’Py would stop any ongoing animations, hence avoiding the framerate glitch before it loads. This is a good trick that avoids some other causes of framerate glitches, like Ren’Py rendering too much text at once.
Since the problem with Python is that it might cause images to be predicted wrong, one thing that’s possible is to move Python that’s used earlier. This will work well if the character is off-screen.
Instead of:
"I hear someone knocking on the door."
$ emotion = "happy"
show eileen
e "I'm here!"
try:
$ emotion = "happy"
"I hear someone knocking on the door."
show eileen
e "I'm here!"
This gives Ren’Py more time to predict and preload the correct image.
In Ren’Py 6.99.14 and later, the ConditionSwitch displayable takes
a new predict_all argument. This causes Ren’Py to predict all
possible images reachable from the ConditionSwitch, not just the
single image that would be currently used.
For example, when we have:
image eileen = ConditionSwitch(
"emotion == 'happy'", "eileen happy.png",
"emotion == 'concerned'", "eileen concerned.png",
predict_all=True)
Then the two images will be loaded at the same time. This takes up more memory, though often not that much, especially when the ConditionSwitch controls only a small part of a composited image.
For the best use of the image cache, predict_all should be true when used with ConditionSwitches that change multiple times during the game, and false when used with ConditionSwitches that will rarely or never change, like those used for selecting a character at the start of a game.
It’s possible to simply tell Ren’Py what it should and shouldn’t
predict, using the
renpy.start_predict() and renpy.stop_predict()
functions.
These functions take either the name of an image or an image filename. (An image filename contains at least one dot in it.) For example, one could start predicting the eileen happy image with either of these two lines:
$ renpy.start_predict("eileen happy")
$ renpy.start_predict("images/eileen happy.png")
and end it with the matching one of these:
$ renpy.stop_predict("eileen happy")
$ renpy.stop_predict("images/eileen happy.png")
These functions use * as a wildcard, which can be used to start predicting all of a character’s images:
$ renpy.start_predict("eileen *")
$ renpy.start_predict("images/eileen *.*")
Note how we have to include the dot in the filename version. Stopping wildcard prediction is similar.
For a small enough game, it might be reasonable to predict every image, all the time, using:
init python:
renpy.start_predict("*")
renpy.start_predict("gui/*.*")
that will pull in every image right as the game starts, and keep them all in memory. It makes sense to use the image load log to make sure the image cache isn’t full, but if everything fits, this can be an effective approach.
Hopefully, this gives you an idea of what Ren’Py does behind the scenes to predict images, and things you can to to help Ren’Py make those predictions. In the 6.99.14 series, I’ve been working to make Ren’Py much smoother, and working with the prediction system will help ensure your games benefit from that work.
(Here’s a couple of bonus paragraphs that I wrote. They don’t really fit in this article anymore, but I kept them, since they’re useful.)
Because Ren’Py’s framerate is variable, it’s a little hard to measure with framerate monitoring tools. If you’d like to try, it generally makes sense to turn powersave mode off, by pressing shift+G to access the graphics settings. This will prevent Ren’Py from dropping to 4.5 Hz when the screen is not changing.
Even in this mode, Ren’Py prioritizes interactive performance to visible framerate, by being willing to run expensive tasks when the screen isn’t changing. So you might see the framerate drop from time to time - as long as the screen isn’t changing when this happens, it isn’t a framerate glitch that affects the player.