Ren'Py Ren'Py optimization tips?

Jman9

Engaged Member
Jul 17, 2019
2,295
961
I kinda need to squeeze some more blood from the stone, so I'd like to know what tips other people have to cut down on any engine-level overhead.

What I already know and have used, with various degrees of success:
  • Changing renderers and powersave (more of a user-level thing, though).
  • Using WebP wherever possible (not 100% sure, but seems to help?). Also, resizing images to fit the intended resolution.
  • Playing around with 'config.image_cache_size' and 'config.cache_surfaces' (mixed results).
  • Disabling autosaves (major win).
  • Disabling rollback (not sure if it really helps) and history buffer size (probably minuscule effect if that).
  • Periodically triggering 'renpy.free_memory()' (seems to be an anti-crashing tool, mostly) and manual garbage collection (doubt if that does much).
  • Paging excessively long lists of useable objects (which is mostly a necessity to keep things even working, and technically a loss due to all the looping from constant UI recalculations).
What else is there? Messing with image prediction?
 

dikau

Member
Dec 16, 2019
316
282
Sorry, got no tips, but I'm really curious, is it not webp use more cpu power comsumption? in terms of decoding the image compression.
 

Rich

Old Fart
Modder
Donor
Respected User
Game Developer
Jun 25, 2017
2,566
7,384
I kinda need to squeeze some more blood from the stone, so I'd like to know what tips other people have to cut down on any engine-level overhead.

What I already know and have used, with various degrees of success:
  • Changing renderers and powersave (more of a user-level thing, though).
  • Using WebP wherever possible (not 100% sure, but seems to help?). Also, resizing images to fit the intended resolution.
  • Playing around with 'config.image_cache_size' and 'config.cache_surfaces' (mixed results).
  • Disabling autosaves (major win).
  • Disabling rollback (not sure if it really helps) and history buffer size (probably minuscule effect if that).
  • Periodically triggering 'renpy.free_memory()' (seems to be an anti-crashing tool, mostly) and manual garbage collection (doubt if that does much).
  • Paging excessively long lists of useable objects (which is mostly a necessity to keep things even working, and technically a loss due to all the looping from constant UI recalculations).
What else is there? Messing with image prediction?
A lot of these you normally shouldn't have to play with much, as Ren'py's defaults are pretty good. But it depends on exactly what you're trying to optimize. For example, trying to eliminate any issues with smooth UI requires different techniques than if you're trying to prevent the game from taking too much memory.
  • WebP - It would seem to me that this would have minimal effect on the vast majority of games, as images are commonly loaded before they're needed, in the background, by the image prediction logic.
  • There are serious tradeoffs in mucking with the caching. If you increase the cache too much, you can run into memory limits. The one exception I can think of is if you are going to create an animation out of a whole series of full-screen images, then there might be an advantage here to prevent the image loading stuff from thrashing. But if you're consuming that much for an image animation, you'd be better to convert it into a video, since Ren'py handles them better than a big ATL-based animation.
  • Disabling autosaves, particularly if the game is large, can definitely prevent some jerkiness in the UI, because the game has to pause while it's doing the save.
  • Disabling rollback and this history buffer size won't have a significant impact on the performance of the game. It might reduce the memory footprint somewhat, but it's also a very non-user-friendly thing to do. Users LIKE rollback.
  • Manually triggering renpy.free_memory() at key locations can help prevent memory buildup, but will put a small UI "hitch" into the game. Where you might want to do this is just before a big image-based animation to maximize the amount of cache available to load the images. But, as I mentioned before, if your animation is consuming that much memory, you'd be better to make it a video. (One game that exhibits this issue is Holiday Island - it uses very big image-based animations that can crash the game if you don't have a lot of RAM.)
  • "Paging excessively long lists of useable objects" would be a REALLY specific use case, and unless the list was REALLY long or the objects were REALLY large, it's probably better to streamline the data design rather than try and hack your way through something like this. But you'd have to have a LOT of BIG objects to challenge the size of the image cache.
All that being said, remember that the amount of memory Ren'py images take when loaded is basically completely determined by their physical size, as compared to their on-disk size. (Meaning that JPG vs WEBP vs PNG encoding doesn't change the in-memory size of the images.) Thus, a game designed at 1920x1080 is going to take 2.25 times as much memory for EVERY in-memory full-screen image as one designed for 1280x720.

Frankly, I wish more developers would pay attention to their on-disk sizes. Taking the JPG images (or, worse, PNG images) that come out of Daz Studio and dropping them directly into Ren'py is what results in games being 2.0 gigabytes before they reach their 0.4. Just a small bit of image compression goes a long, long way on game size, and it's pretty rare that it creates any noticeable visual difference. Of course, that would put our "Compressors" out of business, but it'd do a lot for those people with slow Internet connections...
 
  • Crown
Reactions: Twistty

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
[...] so I'd like to know what tips other people have to cut down on any engine-level overhead.
Regarding what kind of issue, because in your list there's really everything and nothing ?


Using WebP wherever possible (not 100% sure, but seems to help?).
It help reduce the size of the distribution, but it will ask more from the CPU when decoding the image.


Also, resizing images to fit the intended resolution.
It help reduce the size of the distribution and absolutely nothing more. The only case where a resizing wouldn't be needed is:
  • when the game is in full screen and the said screen match the intended resolution ;
  • when the intended resolution is lower than the screen resolution.
Yet I'm not convinced that Ren'Py really care about those two exceptions.


Playing around with 'config.image_cache_size' and 'config.cache_surfaces' (mixed results).
It can possibly help if you made an intensive use of ATL animation, or at the opposite to lower the RAM use, but mostly it's all it effectively help with.


Disabling autosaves (major win).
The autosave have its own thread... Ren'Py need exactly 0.00099 second to proceed all the initial test telling that it's time to generate a new autosave file, everything else will be cost less. Even considering that not everyone have a 3.7GHz CPU, you need to fall back to a Mhz CPU for it to start to be sensible.
As for the save itself Ren'Py need on average 0.2 seconds for a game with a little more than 800 savable variables, same for a game that use complex structures. But the average for an average game is around 0.05 seconde. This said, it doesn't mater since this happen in a thread and therefore do not at all impact the game performances.

So, when I see you talk about a major win, I wonder in what context exactly ?


Disabling rollback (not sure if it really helps) and history buffer size (probably minuscule effect if that).
This can effectively help with the RAM use, and make you win more or less half the time needed for a save.
But rollback points are created with each interaction. This mean that during this time the game is waiting for the user. Said otherwise, you'll win at max 0.1 seconds... at a time where the reader will probably make the game wait for more than a full second.


Periodically triggering 'renpy.free_memory()' (seems to be an anti-crashing tool, mostly) and manual garbage collection (doubt if that does much).
I guess that the garbage collection is also thread (I haven't looked at it, but it make sense), therefore it's cost free and can have an impact on the RAM use. But if this impact is effectively significant, then your game have way bigger problems. Globally speaking, on average use, it would save at most 100MB.


Paging excessively long lists of useable objects (which is mostly a necessity to keep things even working, and technically a loss due to all the looping from constant UI recalculations).
I have two news for you:
Firstly, 90% of the games made with Ren'Py on the scene do not use a single "object". Secondly in Python, and even more Ren'Py, every single thing is an object except pure literal values.
In $ loveCounter += 1, the only thing that isn't an object is "1". The "loveCounter" integer is an object, and even the line itself is an object.
Said otherwise, if your own objects are a bottleneck, it's their design that is in fault, not Ren'Py.


What else is there? Messing with image prediction?
First optimization tips: Stop fuckingly mess with Ren'Py !

It's a 15 years old game engine in constant development used by dozen of thousand creators and played by millions people.
It's obviously not an engine as optimized than Unity or Unreal can be, and more globally than any engine that would compile the game code. It's also an engine that could be a little more optimized. But it's also an engine that already perform really well. Well enough to do all this in pure realtime:
You don't have permission to view the spoiler content. Log in or register now.
And when I say "all this", I really mean it. The rasters are redrawn with every single refresh of the screen, what therefore include the computation of their color and size. The rest (position and animation of the logo) is done purely by using the ATL language.


Second optimization tip: The only effective bottleneck of Ren'Py is screens.

More precisely, it's embedded Python ( $ lines, python: and call to external function defined in a RPY file) inside screens. Those Python parts are in fact compiled code stored in variables and then executed. What mean that they are smaller than their equivalent coming from a PY file, that is directly proceeded.
Since screens are refreshed around every 0.2 seconds, and can be refreshed even more often as seen above, they can effectively slowdown the game.
Same if you make an intensive use of buttons ; yet you need to reach the 400 (or have a tons of Screen action attached to them) for it to be effectively sensible.
So, think your screen more than you think everything else.
 

Jman9

Engaged Member
Jul 17, 2019
2,295
961
Regarding what kind of issue, because in your list there's really everything and nothing ?
Mainly what I'd call 'stutter'. Unexpected and unpredictable 'freezes' in moving from one interaction to another. Kind of like the autosave issue, but smaller-scale.

Edit: I think most stutters are probably from 'renpy.free_memory()'. I guess I'll reduce the frequency. /edit

Also, screens in general, because there's an unholy amount of UI cycles going on and every little bit helps. I know, the best answer is "then don't Ren'Py", but I'm not really a developer, more of a really hardcore modder.

So my choice is to either drop the project and move on to other games, or try fixing things where I can. I guess the next best answer is 'refactor your mess', but said mess is already conceptually so damn complex it's very nearly answer #1. :cry: Also, not particularly fun when I don't get paid for it.


A lot of these you normally shouldn't have to play with much, as Ren'py's defaults are pretty good.

...it's also a very non-user-friendly thing to do. Users LIKE rollback.
I guess the most important thing I didn't mention is: I don't really care about 'users'. :p If it works for me, that's all I need. If others also like it, groovy, but not a requirement.


WebP - It would seem to me that this would have minimal effect on the vast majority of games, as images are commonly loaded before they're needed, in the background, by the image prediction logic.
Excessive predicting of dynamically generated displayables is exactly the issue that might crop up. Or not. As I said, my evidence is really not conclusive that it helps, mainly that I have no crashes with and had occasional crashes without WebP. Might entirely be grasping for straws here.

There are serious tradeoffs in mucking with the caching. If you increase the cache too much, you can run into memory limits.
Sure, that's what it's intended to do. But with a ludicrous 'config.image_cache_size = 4096' I've ever seen the game take ~8Gb once, and it crashed soon after. Usually, it stays entirely under 3Gb.

But I'm not really sure it truly helps, either.

"Paging excessively long lists of useable objects" would be a REALLY specific use case, and unless the list was REALLY long or the objects were REALLY large, it's probably better to streamline the data design rather than try and hack your way through something like this.
Said objects are actual, pretty complex Python objects. Above about 30-50 of those in a viewport or similar tends to be too much clutter from both a design and screen refresh POV. In fact, I think I ran into viewport size limits before I did the paging. o_O

Actually, this is the best reproducible issue I have, opening screens with those takes a noticeable fraction of a second. I guess it's kinda self-inflicted, though. Because I have to recalculate paging info on the fly, removing and reordering the objects dynamically to reflect changed game state.

If you have a suggestion for avoiding that, would be much appreciated.

It help reduce the size of the distribution and absolutely nothing more. The only case where a resizing wouldn't be needed is:
  • when the game is in full screen and the said screen match the intended resolution ;
  • when the intended resolution is lower than the screen resolution.
Given that I can control my window size and resolution (and also tell others to do so), this is not as inapplicable as it seems from a general dev POV. Also, Ren'Py can totally leave an image at its original size and cut off anything that goes over the edge.

It's probably not a real benefit, though. I guess the biggest problem would be loss of image quality from downscaling too many images with no need.

As for the save itself Ren'Py need on average 0.2 seconds for a game with a little more than 800 savable variables...
...
So, when I see you talk about a major win, I wonder in what context exactly ?
Not the game I have. :p Saves take several seconds. And it's very noticeable when autosaves are cut out.

This can effectively help with the RAM use, and make you win more or less half the time needed for a save.
But rollback points are created with each interaction. This mean that during this time the game is waiting for the user. Said otherwise, you'll win at max 0.1 seconds... at a time where the reader will probably make the game wait for more than a full second.
There's a lot of semi-procedural content and event reuse. So these 0.1 seconds can be important when there's lots of skipping.

And even the save-related, er, savings are good. Didn't really expect that, although seems obvious in retrospect. Thanks.

I guess that the garbage collection... if this impact is effectively significant, then your game have way bigger problems.
Of course it has. If it didn't, I wouldn't be here.

The question is, is it actually ever worth it? I'm not really sure it is, and it adds another significant fraction of a second whenever I invoke it.

Firstly, 90% of the games made with Ren'Py on the scene do not use a single "object". Secondly in Python, and even more
Ren'Py, every single thing is an object except pure literal values.
Mine is decidedly in the 90%. Maybe even 99%.

All 'literals' are also objects. '1' is an object. Are you now going to tell me you've never seen the line "TypeError: 'int' object is not iterable"? :p

Said otherwise, if your own objects are a bottleneck, it's their design that is in fault, not Ren'Py.
I'm not blaming Ren'Py. It's obviously not a good fit for the game, but it's what I've got.

I'm asking what - if anything - I can do to mitigate the problems, short of migrating to another engine (which essentially means ceasing to mess with that particular game)?

Second optimization tip: The only effective bottleneck of Ren'Py is screens.
The biggest bottleneck of Ren'Py is the 'Py, i.e. using Python (and even pygame). Python is among the slowest of mainstream languages. The truly critical stuff is in C even in Ren'Py (or pygame, really).

That having been said, you're right. Screen refresh with lots of functions or complex displayables is the worst offender. But there I can at least see and even profile issues directly, and I know I'm going against what Ren'Py stands for.

Random performance drops from autosaves, rollback, image prediction, who knows what else? The 'else' (and maybe prediction) is what I'm really after here.

Same if you make an intensive use of buttons ; yet you need to reach the 400 (or have a tons of Screen action attached to them) for it to be effectively sensible.
There's indeed a lot of activity attached to buttons. It's the root cause of a lot of issues, and unfixable without major refactoring, or maybe unfixable in Ren'Py, period.

Caching some values into variables helps, but this is really not applicable to everything.

I guess that should have been on the list: use variables instead of functions in UI-related parts as much as possible. Easier said than done, though.


I'm actually surprised at how well the whole thing works UI-wise, with all the messy and inefficient crap going on behind the scenes that gets refresed every <= 0.2 seconds.


Edit: Another tip that might be of some minor use is to disable AV for the game. Not really recommended procedure, depends on your AV and I don't really know if Ren'Py has a history of conflicting with any AV at all...

Another one I just recalled: closing any resource-intensive screens in the background if at all feasible. Layering screens is a big performance hog.
 
Last edited:

Rich

Old Fart
Modder
Donor
Respected User
Game Developer
Jun 25, 2017
2,566
7,384
OK, so based on what you've said, you're clearly you ARE in the 1% or 0.05% or whatever in terms of what you're doing with your game. Most of my comments were directed at "mainstream" games, not those that are really pushing the edge of things.

It is entirely possible that Ren'py is not the engine for you. Note that the "Py" portion of Ren'py isn't necessarily your problem - Python itself can be quite performant. (I'm using it in my work for some very Big Data processing, so I know that first-hand.) But Ren'py puts a lot of layers on top of Python, which, in turn, is layered on top of PyGame, and makes some assumptions in the way it deals with the UI that might not be a good fit to you. And, given that Ren'py tries to be "support everything for all environments," and "make it as easy as possible," it introduces abstractions that do, in fact, cost cycles.

So, as I said, if you're really this far to the right of the bell curve, it's quite possible that Ren'py is not the engine for you. You might want to look at either Unity or Unreal Engine. Those engines are more performant, although they tend to require you to do more on your own, since they're a bit more "bare metal" than Ren'py is. (Disclaimer: I've used Unity a fair amount, but not Unreal Engine.)
 

Jman9

Engaged Member
Jul 17, 2019
2,295
961
While it may be true that ultimately the game isn't truly salvageable... As I said, I am not a/the developer. I am not married to the game. If the choice truly becomes between "start over in a new engine" and "or else...", my answer is going to be "or else I'll move on to some other game". I have lots of older games I could return to tweaking.

So I appreciate the answer, but my big question remains: is there anything else I can do that's broadly applicable, similar to:
  • Killing autosaves and rollback.
  • Abandoning manual GC/'renpy.free_memory()', or at least not using either too much.
  • Taking a hard look at image/screen layering.
  • Taking another hard look at functions attached to buttons.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Mainly what I'd call 'stutter'. Unexpected and unpredictable 'freezes' in moving from one interaction to another. Kind of like the autosave issue, but smaller-scale.
What kind of device do you play on ? The autosave is supposed to be threaded for all games, therefore it do not stutter, it can not stutter. And I don't just assume it, I know it for sure, just verified it right now.

I took my variable viewer, enabled all the option, and opened it to fill the dict with the previous value.
What mean that I had a save that contained a dict with an entry for each variables, functions and classes available in the 15 stores, and most of the ones present in the 143 modules of the core ; said otherwise, it had thousands of entries (more 30 000 for this game), for an average length by entry that should be around 40 Bytes.
The autosave file was near to 13 000 KB, for a log file (where the pickled variables are stored) near to 55 000 KB. It's something really big (the average save is around 500 KB at max), that needed exactly 34.9102740288 seconds to be proceeded (pickling of the variables then compression of the file).
There's just no way I could have missed it, right ? If the autosave was effectively impacting the game, making it stutter, there's no way I could have missed it... But the truth is that I didn't noticed it, because as I said, the part effectively happening in the main process is atomic. Everything is performed in a thread and have absolutely no impact for the player.

Choose whatever game that supposedly stutter because of the autosave. Put this in a rpy file:
Code:
init python:
    renpy.config.autosave_frequency = 10
    config.console = True
then put the said file in the "/game" directory and play the game.
The game will perform the autosave every 10 interactions. So, advance a dozen of dialog lines, and verify in the "/game/saves" directory that an autosave have been performed.
Once you've the confirmation that an autosave happened, open the console (shift[icode] + [icode]o) then type: renpy.loadsave.autosave_thread.
If it return you "None", for some reason the autosave is not threaded for this game/OS/Device. But if it return you something like <Thread([...])> then it's not the autosave that make the game stutter because it happen in a thread and is totally cost free for the player.
[Note: The thread is created when the first autosave is performed, reason why you need to pass through it before you can know]

If effectively the autosave is threaded, and that the stuttering effectively happen everytime a autosave is performed, then my guess goes for the hard drive queue, or its cache (either hardware or the one of the OS).

All this being said, by default the autosave happen every 200 interactions. The average reading speed being around 5 words second, and the average dialog line being around 15 words (I hate this actual trend), it mean that on average there's an autosave every 10 minutes.
With an average save taking less than 0.2 seconds to be performed, it's already difficult to notice the stuttering. But it become even more difficult when it happen every 10 minutes. Especially since it happen while you are reading ; what mean that, for you to notice it, it need to take more times that you'll pass reading the dialog line.

I believe you, there's something that make games stutter for you. But I really doubt that it's due to the autosave.


Also, screens in general, because there's an unholy amount of UI cycles going on and every little bit helps.
Hmm...
Quantify "unholy". Right now I have a screen opened that have a vpgrid of 1521 lines, each line having 10 ui. It's not totally smooth of course, but I didn't expected it to be ; it's around 8 000 that it stop to be smooth. Yet I wonder to what extend removing the hovering style on the button wouldn't solve this issue.
So, is "unholy" more than those 15 210 ui, without counting the screen language part of the screen ? Or at least more than the 8 000 ?


[...] but said mess is already conceptually so damn complex it's very nearly answer #1.
Ren'Py is basic engine, build with a basic language. Keep your code basic, and it will works perfectly with Ren'Py. Overdo it, and you'll struggle.
The only games I've seen suffer from Ren'Py are the ones who try to tame it by force instead of trying to understand it. I don't know if it's still the case, but years ago Summertime Saga had a bunch of around 100 Python lines to do something that Ren'Py can do natively with a 5 lines screen. And off course it's this second version that was faster.


Excessive predicting of dynamically generated displayables is exactly the issue that might crop up. Or not. As I said, my evidence is really not conclusive that it helps, mainly that I have no crashes with and had occasional crashes without WebP. Might entirely be grasping for straws here.
It depend of what you call "dynamically generated displayable".
If it's built up image like the layered images offer, the prediction have no consequences. More or less the same if it's dynamism by text substitution (image whatever = "images/[girlName]/nude.png"). If it's the show expression [...] kind, it will depend of the complexity of the expression.
And if it's dynamically drawn displayable, well, it works fine:
You don't have permission to view the spoiler content. Log in or register now.
Mostly because it's not predicted unless you explicitly define the predict method.
[side note: I really hope that one day I'll have the time to write the game that goes with this effect]


Sure, that's what it's intended to do. But with a ludicrous 'config.image_cache_size = 4096' I've ever seen the game take ~8Gb once, and it crashed soon after. Usually, it stays entirely under 3Gb.
For my test regarding the autosave, Ren'Py goes to 10GB use without problems. But I must say that I have 32GB of native RAM, so the OS haven't had to rely on its cache.
So, my guess is that it's an OS problem more than a Ren'Py one.


Said objects are actual, pretty complex Python objects. Above about 30-50 of those in a viewport or similar tends to be too much clutter from both a design and screen refresh POV.
You absolutely don't need this, really.
As I said above, as long as you stay basic, everything will be fine ; it's when you try to overdo it that Ren'Py will start to rebel.
And yes, you can do really complex thing with the most basic code.


In fact, I think I ran into viewport size limits before I did the paging. o_O
No, you haven't. I said it above, I have 15 210 ui in front of my eyes right now and Ren'Py can clearly go further. Not smoothly, but it will not crash.


Because I have to recalculate paging info on the fly, removing and reordering the objects dynamically to reflect changed game state.

If you have a suggestion for avoiding that, would be much appreciated.
Python:
init python:

    def reset():
        for o in object_list:
            o.reset()

    class Object( renpy.python.RevertableObject ):
        def __init__( self ):
            self.__x = None
            self.__y = None
            self.__visible = None

        def reset( self ):
            self.__x = None
            self.__y = None
            self.__visible = None

        def display( self ):
            [...]

        @property
        def x( self ):
            if self.__x: return self.__x
            [compute the value]
            self.__x = the computed value

        @property
        def y( self ):
            if self.__y: return self.__y
            [compute the value]
            self.__y = the computed value

        @property
        def visible( self ):
            if self.__visible: return self.__visible
            [compute the value]
            self.__visible = the computed value

screen whatever:
    
    for o in object_list:
        if o.visible:
            add o.visual pos( o.x, o.y )

    textbutton "close":
        action [ Hide( "whatever" ), Function( reset ) ]
And it's done, your objects are ordered on the screen and only visible if needed. Optimize the computation of the coordinate and visibility, make all your object inherit from this base class, and you are good.

If you fear that the screen look messing for an instant at the opening, then it's this:
Python:
init python:
    def preLoad():
        for o in object_list:
            o.compute()  # replace the computation in /x/, /y/ and /visible/ properties

label whatever:
    $ preLoad()
    call screen whatever
But unless you change the state right before opening the screen, Ren'Py prediction should do its magic here, and the value should already be computed when you'll open the screen. What should also apply for display, that would also benefit from a cache system ; like the __x/__y/__visible being computed only once by screen opening.
If effectively the screen prediction do its magic (it's largely past 4AM here, so I'm not totally sure), but the computations take time, then try to design the game for it to not change its state during the three/four interaction preceding the opening of the screen. Like prediction are also threaded, it will not impact the game, while still letting the time for Ren'Py to compute all the values.


Given that I can control my window size and resolution (and also tell others to do so),
Expecting them to agree to do so and to have a screen that reach this resolution.


this is not as inapplicable as it seems from a general dev POV.
From a general dev point of view, and I talk as a professional, as well as a general player point of view, it's the worse approach. I don't count the number of Unity games I haven't been able to play, because their author didn't cared to make them 4/3 compatible at the time where I still had only 4/3 screens.


Also, Ren'Py can totally leave an image at its original size and cut off anything that goes over the edge.
Oh, but it also do. The author is assumed to know what he do, therefore any image is displayed accordingly to its size, cropped in order to fit the size gave in the configuration. Then the result is resized to fit the effective window size.
And I must say that it's how it have to be done.


It's probably not a real benefit, though. I guess the biggest problem would be loss of image quality from downscaling too much.
I fear to ask the size of your images...


Not the game I have. :p Saves take several seconds. And it's very noticeable when autosaves are cut out.
What's the size of a save file ? If it's below 1 000 KB and it take several seconds, it's the hardware or OS the problem. If it's above 1 000 KB, then it's the game design the problem.
But in both case, it's not Ren'Py and the autosave is noticable only if Ren'Py can not thread them, what again send back to an OS issue.


There's a lot of semi-procedural content and event reuse. So these 0.1 seconds can be important when there's lots of skipping.
If the player encounter some annoyance because he decided to skip, it's his problem, not yours. And I can assure you that he'll complain less about those 0.1 seconds, than about the lack of rollback.


Of course it has. If it didn't, I wouldn't be here.
Then as I said, your game have a bigger problem.

The best solution would be to not generate so much trash, and obviously manually triggering the collection is never the solution. You should change the threshold and let Python/Ren'Py (depending of the configuration) deal with it when the time come:
Code:
init python:
    config.gc_thresholds = (10000, 10, 10)  #Initially 25000

Mine is decidedly in the 90%. Maybe even 99%.
Then you're using Ren'Py wrongly.


I'm not blaming Ren'Py. It's obviously not a good fit for the game, but it's what I've got.
There's a RPG Maker emulator entirely made with Ren'Py... Therefore I wonder what your game can be for Ren'Py not being a good fit for it.
Of course there's its limited performances, but as long as you don't try to do real time rendering, it's not really a problem. At least as long as you code it following Ren'Py flow.


The biggest bottleneck of Ren'Py is the 'Py, i.e. using Python (and even pygame). Python is among the slowest of mainstream languages. The truly critical stuff is in C even in Ren'Py (or pygame, really).
Python is the second smallest language. Yet look at the two videos I provided... It's real time processing through a Python object. The hologram effect take a picture, apply transparency on it, tint it in blue, then deform it, on average 40 time by second... Ren'Py is fast enough for any game that don't need full real time.


There's indeed a lot of activity attached to buttons.
More than 2 400, that correspond to the 800 entries, I talked above, before Ren'Py stop to respond smoothly ?


Caching some values into variables helps, but this is really not applicable to everything.
It is applicable to everything. By default the screens are refreshed 5 times by seconds, and by "refreshed" I don't talk about the frame rate, I talk about the number of time the screens are fully proceeded, recomputing each value in them. If really you are in need for speed, then even the screen actions should be cached.
The only thing that effectively can't be cached are the ui call. Yet, I'm almost sure that you can do something by playing with ui.Warper and Curry.


I guess that should have been on the list: use variables instead of functions in UI-related parts as much as possible. Easier said than done, though.
You don't need to use variable, you need to use none embedded code, and to cache the values (or declare the function as [USER=42055]Pure[/USER]). There's no need to redo all the computation 5 times by seconds when the result will always be the same.
 
  • Like
Reactions: crabsinthekitchen

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Python:
init python:

    def reset():
        for o in object_list:
            o.reset()
[...]

Or also (even better since you clearly over abuse of ui:

Python:
init python:
   class MyObject( renpy.python.RevertableObject ):
        [...]

   class AnImage( MyObject ):
        def __init__( self [...] ):
            self.__name = None
            self.what = "image"

        @property
        def name( self ):
            if self.__name: return self.__name
            [...]
            self.__name = ...

    [...]

screen mainScreen():

    for o in object_list:
        if not o.visible:
            pass
       elif o.what == "image":
           add ( "images/{}".format( o.imageName ) ) pos( o.x, o.y )
       elif o.what == "text":
           text "[o.text]" xpos o.x ypos o.y
       elif o.what == "complex":
            use complex( o )

    textbutton "close":
        action [ Hide( "whatever" ), Function( reset ) ]

screen complex( o ):
   frame:
        xpos o.x
        ypos o.y

        vbox:
            for l in o.lines:
                text "[l]"
            null height 20

            hbox:
                text "Name: [o.name]"

            textbutton "[o.button]":
                if o.tooltip:
                    tooltip o.tooltip
                action o.action
There 75% chance that it will be faster than your actual code.
 
  • Thinking Face
Reactions: Jman9

Jman9

Engaged Member
Jul 17, 2019
2,295
961
What I asked for here was concrete technical advice. I provided a concise list of things I've tried and what the results seemed to be, as both an example and to pre-empt the first round of questions. I also said that I'm working with legacy code, and my willingness to rebuild the whole game is not there. Refactor some things, yes. Redo the whole UI in "basic code", not really.

Thus, most of the above is interesting but ultimately useless to me.

You absolutely don't need this, really.
As I said above, as long as you stay basic, everything will be fine ; it's when you try to overdo it that Ren'Py will start to rebel.
And yes, you can do really complex thing with the most basic code.
Okay, I'm willing to learn.

Let's do a toy example that's close enough to one of my real problems. Since this is an adult forum, I'll invent a Game of Sluts that should exhibit my current most burning issue well enough. You can then show me how to do it the 'basic code' way.

Take a basic Slut object with some properties and image references.

Code:
init python:

    slut_titles = ["Tease","Hussy","Tramp","Slut","Harlot","Whore"]

    class Slut(object):
        def __init__(self, name = "Sluttie", pictures = None, meter = 0):

            self.name = name
            self.meter = meter
            if pictures == None:
                self.pictures = ["missing.webp","missing.webp","missing.webp","missing.webp","missing.webp"]
            else:
                self.pictures = pictures
           
        @property
        def title(self):
            return(slut_titles[self.meter])

        def slutify(self):
            if random.random() < 0.5:
                self.meter += 1
The player should be shown Sluts from a given array, each framed into a box given by an image reference, e.g. "frame.png". They should be able to see and do the following:
  • Name, title, meter value and corresponding image, plus two arrows for each frame. Images can be provided by users, so they might need rescaling. Arrows are game UI.
  • Click on at least some elements of the frame (name, picture) to trigger 'slutify()', with corresponding stat and image changes.
  • Click on the arrows to swap the Slut with the next/previous one. No wraparound.
  • Sluts who hit meter >= 5 are removed from the display and preferably also from the array.
  • When all Sluts are gone, the game flashes "You win" and exits, or whatever.
  • Manage ~1000 sluts without the player's eyes glazing over from so much scrolling or falling out of their head due to tiny UI.
  • Give up and exit.





The rest is here because I just can't resist a good spaghetti posting, and maybe I'll learn something. Given the overall attitude from the above reply, I'm not holding my breath, but I've been pleasantly surprised before.


What kind of device do you play on ?
A PC that was higher mid-end a long time ago.

The autosave ... can not stutter. And I don't just assume it, I know it for sure, just verified it right now.
Game dev is full of stuff programmers missed because it didn't happen to them and therefore "doesn't exist".

I also just verified that it stutters like hell by turning autosave back on and making all settings default. There's a small but noticeable freeze every 3-5 seconds, making the game effectively unplayable. Setting autosave frequency to 1 is about half game and half freeze.

Threading looks to be working, since autosaves are generated, 'renpy.loadsave.autosave_thread' returns a Thread object and the stutters take considerably less time than a full save. It just isn't enough.

For me, it's pretty much an open-and-shut case. I have a problem, disabling autosave fixes a significant part of it. Theorycrafting how "it can't happen" will get us nowhere. Let's leave it at that and move on to other things.


Quantify "unholy".
For example: viewports with cycles over lists generated by more cycles, or lists reversed with [::-1]. Possibly with added conditionals. These display small screens populated with ~10 buttons, some text and a picture. All within some big screen that runs a complex while loop looking for player input and doing other bookkeeping where necessary.

The issue is not really the number of displayables, but rather the related complexity. Although I've also seen similar issues with viewports that contain merely buttons with an image and text, and not very many of those, either. Way under 100, in fact.

I also suspect that lots of screens within screens is a bad idea, but I have no idea how to get the same results without because I need to pass parameters to the sub-displayables. Or, well, at least it's very convenient to be able to do so.

Yet I wonder to what extend removing the hovering style on the button wouldn't solve this issue.
Huh. That might be worth a try.

It depend of what you call "dynamically generated displayable".
Things like ProportionalScale, AlphaMask, displaying images via 'adding' to screens instead of 'show'.

Mostly because it's not predicted unless you explicitly define the predict method.
I have only the vaguest idea what prediction actually does in detail. But I have seen it slow another excessively complex game to a crawl under certain circumstances, so I'm wary.

...memory...
I never said I had memory issues. The opposite, in fact.

It's another tactic I've seen others use and claim to have worked, so I tried. No dice, but you never know.

Pretty much the only time I've had constant crashes in a Ren'Py game, I fixed it by converting everything to WebP. Well, and 'renpy.free_memory()' when the current session was looking about to give up the ghost. It was not my game (again), I don't know why this worked, I don't know if it was not just some silly superstition, or maybe some other change did it and was camouflaged due to happening at the same time. It seemed to work, so now I try both of these when I run into performance issues.

No, you haven't. I said it above, I have 15 210 ui in front of my eyes right now and Ren'Py can clearly go further. Not smoothly, but it will not crash.
Could have been some default value. As I said, I thought it was limits. Might have been wrong.

But this being technically feasible is pretty much immaterial, because nobody wants to scroll through 15 210 objects in one go.


[reset and stuff]
And it's done...
There is nothing 'done' here:
  • Either [compute the value] is always constant while 'screen whatever' is open, in which case I could have just left the visibility to be handled by 'object_list' membership. Which is not what I'm after.
  • Or you've just outsourced the computations to individual array objects, which I suspect is worse performance-wise.
  • I'm now also forced to manually calculate all coordinates, meaning I'll have to skip a great many UI tools. Having these pre-built and ready to use is half the reason to use Ren'Py in the first place!
  • I can't let the player reorder these things without directly changing coordinates, which is a really messy way to do all this. Maybe it's a more efficient than using layers of screens, frames, boxes, grids, etc... But this is exactly the kind of extreme refactoring that isn't going to be workable without someone actually starting to pay me for all this. Which isn't in the cards.

If you fear that the screen look messing for an instant at the opening...

But unless you change the state right before opening the screen...
Well, I very well might. This game has a big 'click buttons to do things' component, and there is no way to insert anything in-between without it becoming a mini-loading-screen.

In fact, that's my most noticeable issue ATM. I can open my version of the Game of Sluts, and it lags only a tiny bit with ~20 of them (they are much more complex than the toy version), and mildly annoyingly with ~50.

But opening up the screen hits me with a moment where the game is visibly struggling to create the whole thing. This is actually working as intended, initialisation can take a little longer if it means a smoother screen. But I'd rather not have the issue at all if possible and I can retain all of current functionality.

Expecting them to agree to do so and to have a screen that reach this resolution.
This is the wrong way to look at it. You've got to have your default resolution set to something, and people who manage to hit that may just as well benefit. Alternatively, there's no default and everyone suffers.

I fear to ask the size of your images...
Mostly 200-300kB for full-screen ones. I could probably cut that down by half, but I see no pressing need to ATM. Why, were you expecting 3000x5000 and 30Mb? :D I've seen a single one of those outright crash Ren'Py, no ifs or buts.

...any image is displayed accordingly to its size, cropped in order to fit the size gave in the configuration. Then the result is resized to fit the effective window size.
Cropping is a much simpler operation than scaling, though. Granted, many people seem to play in a window that doesn't really match the intended resolution. So scaling it is, mostly.

What's the size of a save file ?
Hmm, current ones are ~4500 KB. Earlier, they seemed to be mostly under 2000 KB. Not sure why this inflation of late. But I've been rewriting things, playing with autosaves and rollback, etc during those 4500 KB games.

I'm generally okay with it and don't see it as a 'design problem'. Yet. A bunch of commercial games have hit me worse of late, and I'm willing to suffer a little saving lag if I can have a wildly overcomplicated sandbox to play in in return.

If the player encounter some annoyance because he decided to skip, it's his problem, not yours. And I can assure you that he'll complain less about those 0.1 seconds, than about the lack of rollback.
Uh, well, the thing is that I am the player. I could care less about rollback but I do hate these little jiggers piling up.

Any other players are a secondary priority. Maybe forever, maybe not.

This is why I specifically didn't ask for general game dev advice. Not that it stops people from giving it, of course. :p

The best solution would be to not generate so much trash...
I did not say I had a GC problem. In fact, I don't think I do. But it was something that looked it might help, so I tried it out. Didn't seem to do anything but waste some cycles, so I recorded that in the OP. How you got to advising me 'to not generate so much trash' from that is beyond me.

Therefore I wonder what your game can be for Ren'Py not being a good fit for it.

Of course there's its limited performances, but as long as you don't try to do real time rendering, it's not really a problem. At least as long as you code it following Ren'Py flow.
You're pretty much answering your own question. Yes, Ren'Py can do this and that, but only if you do it the way Ren'Py can handle it. That is, not quite as intended, or with performance hits and compromises. Unless your game is mostly a VN, of course.

Grass is green, sky is blue, bears shit in the forest and Ren'Py is best suited for VNs.

Ren'Py is fast enough for any game that don't need full real time.
Uh-uh. Games are not all about graphics. Something like half of the commercial games I play, even ancient sprite-based stuff, suffer from CPU bottlenecks.

It's kinda the same thing here, but less pronounced because Ren'Py really doesn't let you go too far into the woods. But the underlying problem is the same. Hence, 'poor fit'.

More than 2 400, that correspond to the 800 entries, I talked above, before Ren'Py stop to respond smoothly ?
2400 what? A big chunk of overhead goes straight to the equivalent of your 'object_list'.

The rest, there are some functions attached to buttons. Embedded code setting up various stuff to display. What exactly is supposed to be the equivalent of one of your ui?

...the screen actions should be cached.
Uh, how does one do that?

You don't need to use variable, you need to use none embedded code...
Mmm... Is "for y in [x for x in object_list if x.visible][::-1]" embedded? If so, how do you make it not embedded?

...declare the function as [USER=42055]Pure[/USER]). There's no need to redo all the computation 5 times by seconds when the result will always be the same.
The problem is, it won't.

Now, for it might be unchanged most of the time, and there could be ways around that. I've tried storing some of them in data structures and letting the screen read from those, and only changing the data when a real change happens and not just a screen refresh... But I can't do this for everything, and maintaining all this extra info seems to be a really neat way to shoot yourself in the foot a couple of months down the line.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Thus, most of the above is interesting but ultimately useless to me.
Well, finding what effectively cause the stuttering don't seem at all useless...


Take a basic Slut object with some properties and image references.
[...]
Give up and exit.
"Give up and exit", really ?

I just did it with 1 class, 2 functions and 3 screens, for a total of 77 lines... And yes, it's working.
You don't have permission to view the spoiler content. Log in or register now.
It would greatly benefit from error control, need to be styled, can be tweaked a bit more, and I assumed that the image had the right ratio from start. But those are details and don't really make the code more complex ; in fact, I'm sure that the code can be way better that this.

Same for the fact that it works by default with only 7 girls. Going effectively further just need to put the grid on a viewport, that's all. With almost nothing as changes, you can even remove a zone from the map when there's no more girls.


The rest is here because I just can't resist a good spaghetti posting, and maybe I'll learn something. Given the overall attitude from the above reply, I'm not holding my breath, but I've been pleasantly surprised before.
Well, given those two sentences, I'll stop here.

Do whatever you want with the code. More than anything, I wrote it because I liked the challenge. And I posted it as a proof of concept regarding what I said previously:

Stay basic and don't try to fight Ren'Py. It's how you can succeed and do almost whatever you want.
 

Jman9

Engaged Member
Jul 17, 2019
2,295
961
Kudos for the code and staying above the rest of my reply! Sanity checking and whatever is not important, all I wanted to see was a small working example. I'm glad you liked trying your hand at the GoS.

Unfortunately... it is sort of similar to what I already have, but looks a bit worse:
  • Functions attached to buttons, which Ren'Py will try to peek into during refresh. My current screen returns action-specific strings to the interface loop, which does its thing with them.
  • Manual paging into a grid of up to 4x4 values. This is the really big issue. I want this automatic if I can help it.
  • Triple screens layered inside each other. I suspect that is actually what causes some of the slowdown for me. Hard to prove without a big rewrite, though.
If you're sure any of these assumptions are wrong, do tell.


Now, upon examining the related bit of code in the actual with a fresh eye, it appears that it's entirely within a 'python:' block. Never really thought about that... Does it have any consequences? Is this
Code:
python:
    while True:
        result = ui.interact()
        if result[0] == 'slutify':
           result[1].slutify(...)
functionally any different from this?
Code:
while True:
    $ result = ui.interact()
    if result[0] == 'slutify':
       $ result[1].slutify(...)


Regarding the stutters: I am 90% positive that two offenders for the general CYOA part were autosave and too frequent 'renpy.free_memory()'. Maybe there's something else, too. Or maybe it's just the general number of tiny checks that pile up.

For the GoS analogue: I don't know. I can hazard several guesses: screens within screens, inefficiencies with the main loop, predict having to look into a whole bunch of images all the time. I did find one mistake of mine, a function was written straight into the screen instead of being calculated in advance. :rolleyes: Didn't make much of a difference.
Edit2: Actually, completely removing that function this did make some noticeable difference in the time it takes to open up to the screen. But calculating the thing itself in the console is pretty much instantaneous. I'm not really sure what to think any more.

Limiting the number of Slut equivalents on screen at any one time is very much a net gain, though.
/Edit2

...now that I look into all this again, I can see a whole bunch of what's essentially 'add ProportionalScale(...)'. Now this might be a problem...
Edit: ... Nope, that was not it. These particular images were pre-scaled. \edit


Ultimately, the thing was already complex when I got it, I made it more complex and, well, any advice to 'stay basic' is way too late at this point. I'm doing it in moments of my spare time when the mood strikes me, so the amount of damage control I can do is limited.
 
Last edited:

Jman9

Engaged Member
Jul 17, 2019
2,295
961
It is applicable to everything. By default the screens are refreshed 5 times by seconds, and by "refreshed" I don't talk about the frame rate, I talk about the number of time the screens are fully proceeded, recomputing each value in them. If really you are in need for speed, then even the screen actions should be cached.
Python:
            grid 2 2:
                for i in range( 0, 4 ):
                    if i < len( girlsList ):
                        use girlStat( girlsList[i], girlsList )

...

screen girlStat( girl, girlsList ):

    frame:
        hbox:
            imagebutton:
                idle girl.image
                action Function( girl.slutify, girlsList )
So, how would one go about caching the len( girlsList ) part?

Also, how does one see what kind of crap inflates saves? It's all well and good to say it's a 'game design problem', but there's got to be a more informed way to see which parts of the design contribute the most? Because going from ~600 kB at game start to over 5000 kB really doesn't look normal.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Functions attached to buttons, which Ren'Py will try to peek into during refresh.
No, it will not.

The function can only, and obviously will, be called when the action is effectively triggered. It's why the Function screen action exist.
Screen actions aren't functions, they are classes. What mean that, when you declare your buttons action, you aren't calling something, but building the object that Ren'Py will proceed when the action will be triggered by the player.

My current screen returns action-specific strings to the interface loop, which does its thing with them.
What is another approach, and probably the best one for an effective use.
By returning from the screen, you create a rollback point, what anchors the changes and therefore make the whole screen fully save compliant. This by opposition with the way I wrote it, that would restart the screen at its default state if you save mid process.
But I was focusing on the feasibility and simplicity of the screen, more than on its effective viability ; it's possible, and not too difficult, to build a complex screen fully interactive. The changes being not too difficult to do after this.


Manual paging into a grid of up to 4x4 values. This is the really big issue. I want this automatic if I can help it.
And what prevent you to compute that ?

Code:
screen whatever:
    $ zones = orderedGirls.keys()
    $ line = int( len( zones ) / 4 ) + 1 # or whatever how many column you want

    [...]
    grid 4 line:
        for i in range( 0, 4*line ):
            if i < len( zones ):
                use area( zones[i], orderedGirls[zones[i]] )
            else:
                null width 1
    [...]
I put the computation directly in the screen, but like you return, you can perfectly do them outside of the screen and pass the value as arguments:
Code:
label whatever:
    call screen whatever( orderedGirls.keys(), int( len( orderedGirls.keys() ) / 4 ) + 1 )

screen whatever( zones, line ):
    [...]

Triple screens layered inside each other. I suspect that is actually what causes some of the slowdown for me. Hard to prove without a big rewrite, though.
It doesn't. The way the screen AST is built make it that the used screens are directly embedded inside the main screen. It don't take more time to proceed:
Code:
screen whatever:
    for i in range( 0, 10 ):
        use embedded( i )

screen embedded( value ):
    text "[value]"
than to proceed:
Code:
screen whatever:
    for i in range( 0, 10 ):
        text "[i]"
And obviously it don't change when the embedded screen is more complex.

In fact, since it permit to simplify the screen definition when the embedded screen is more complex, or when you've to switch from one embedded screen to another, it can sometimes be faster.


If you're sure any of these assumptions are wrong, do tell.
"Sure" is a big word.
Computer Science is a exact science that is never fully exact. There's always the possibility that a very particular case react differently, and sometimes the truth evolve with time.
What I can say is just that, at the time I write, what I say is true at 98% ; one percent being removed due to the particular cases, and one due to a possible change in Ren'Py core that I would have missed.


Now, upon examining the related bit of code in the actual with a fresh eye, it appears that it's entirely within a 'python:' block. Never really thought about that... Does it have any consequences? Is this
Code:
python:
    while True:
        result = ui.interact()
        if result[0] == 'slutify':
           result[1].slutify(...)
functionally any different from this?
Code:
while True:
    $ result = ui.interact()
    if result[0] == 'slutify':
       $ result[1].slutify(...)
Well, the fact to put all this in a Python block remove all interest to the fact that you return from your screen.

The python blocks being, well, a block, they represent a single entry point in the AST. This mean that, in terms of rollback (and so also savability), it's an "all or nothing" situation. Either the save happen before the block, or it happen after it. But it will never happen in the middle of it.
Therefore, whatever how many time you'll return from the screen, Ren'py will only save before the screen is shown for the first time, or after you returned from it for the last time.

This being opposed to the Ren'Py approach, where you've a tree of four entries in the AST:
  • while True:
    • $ result = ui.interact()
    • if result[0] == "slutify":
      • $ result[1].slutify(...)
What mean that, in theory, you've four rollback (and so save) points here and therefore you can save mid process. I say "in theory", because rollback point are created when an interaction happen, and here you have only one.
But as implied above, the difference is that with the Ren'Py approach, Ren'Py will create a rollback point each time you return from the screen. You'll then be able to save your progress, and retrieve then when loading.

This being said, the use of ui.interact() is outdated ; it's obsolete since something like 10 years now. Your code should be:
Code:
label whatever:
    while True:
        call screen whatever( [...] )
        if _return[0] == "slutify":
            $ _return[1].slutify( [...] )

Regarding the stutters: I am 90% positive that two offenders for the general CYOA part were autosave and too frequent 'renpy.free_memory()'. Maybe there's something else, too. Or maybe it's just the general number of tiny checks that pile up.
I'm sure at 100% that it's not the autosave. Unless your game don't/can't use threads it's something totally transparent, even when Ren'Py need more than 20 seconds to generate a save file.

As for renpy.free_memory(), naturally it's called when the OS ask for a screen suspension (Android/iOS), when the player change the language, and when he change some preferences. Therefore, unless you call it yourself (what is not to be done regularly since, among other things, it proceed to a garbage collection up to level 2), it never happen during a game.

By design, Ren'Py can only possibly stutter when it try to load something in real time (movies), when it have to load an heavy image that can't have been predicted, and when it have to display a really complex screen for the first time. And when I say "really complex" I mean it. A vpgrid will start to have problems around 8 000 elements, paradoxically a viewport would easily go further than 15 000 without real problems, while a screen with tons of elements but without viewport or vpgrid can perfectly goes further, depending of the way the screen is built.
There's few others elements that can possible be the cause, but the default configuration is designed to works in the most possible cases. People have had more than 15 years to tweak them and find the best compromise, and I trust their values to works 95% of the time.
But this do not mean that Ren'Py will effectively only stutter in those cases. Like any software it's dependent of the OS and the hardware. If Ren'Py saturate the RAM, the OS will start to cache, what can lead to stuttering time to time. If the CPU is under high charge, the OS can delay it's process switching (don't remember the correct term), leading Ren'Py to miss a tick or two. If the hard drive queue is saturated during an instant, preventing Ren'Py prediction to have effectively loaded the next image in time. The cause outside of Ren'Py are multiples.
It's just that those stuttering are not effectively due to Ren'Py. Ren'Py will stutter less if the code of the game was more optimized, but it's not the code of the game by itself, nor Ren'Py, that is the reason of the problem.


[...] predict having to look into a whole bunch of images all the time. [...]
Predict don't works like that. The images are cached and the cache rotate starting by the both biggest and oldest images. What mean that having a bunch of small images aren't really a problem ; they most likely will stay on the cache from start to stop.
It starts to be a problem only when the images are effectively over sized. Here one can possibly benefit from a disk cache. I know that Ren'Py used to have one for some cases (and possibly still use it), and it's possible to build one manually ; don't remember exactly, but it have a function that save an image as seen once proceeded by different displayables (like Frame, Crop and all).


I did find one mistake of mine, a function was written straight into the screen instead of being calculated in advance. :rolleyes: Didn't make much of a difference.
Edit2: Actually, completely removing that function this did make some noticeable difference in the time it takes to open up to the screen. But calculating the thing itself in the console is pretty much instantaneous. I'm not really sure what to think any more.
I said it in my very first answer, Python code embedded in a screen take longer to be proceeded (it's due both to the way they are proceeded and the scope at this time). It's not always a problem, but any time you can, not embedding them is always better.
To be more precise, what have to be avoided is all the processing that isn't effectively related to the screen building. You can have function that will call ui function to build the screen, it's not a problem. It will not slowdown the screen since if it wasn't your function that would be called, it would be Ren'Py equivalent in the screen language. It's an "either one, or the other" situation and the time needed will be more or less equivalent.
But all the computation leading to the choice between this or that ui function should be done before you open the screen. And if you fear that the computation will effectively take time, design the game for it to happen few interaction before the screen is called, and do them by using renpy.invoke_in_thread() for them to be transparent for the user.



So, how would one go about caching the len( girlsList ) part?
Python:
init python:

    def lenList():
        len = cache.get( "len", None )

        if len: return len
        cache["len"] = len( girlsList )
        return cache["len"]

default cache = {}
But it's not the kind of things that would benefit the most from a cache, especially since it's a value that will evolve.
It's more the screen actions and images that would benefit from a cache:
Python:
init python:

   class Slut(object):

        def __init__( [...] ):
            [...]

            self.cache = {}

            self.cache["imageAction"] = Function( self.slutify, store.girlsList )
            self.cache["upAction"] = Function( store.girlUp, self, store.girlsList )
            self.cache["downAction"] = Function( store.girlDown, self, store.girlsList )

screen girlStat( girl, girlsList ):

    [...]
    frame:
        hbox:
            imagebutton:
                idle girl.image
                action girl.cache["imageAction"]

            vbox:
                text "{b}[girl.name]{/b}"
                text "[girl.title] [[[girl.meter]]"

            vbox:
                yalign 0.5
                imagebutton:
                    idle "up.webp"
                    action girl.cache["upAction"]
                null height 20
                imagebutton:
                    idle "down.webp"
                    action girl.cache["downAction"]
And it's evident that you can then simplify the call. store.girlsList can be stored internally, it will just be a "reference" to the list, and so always up to date, while you don't need to pass girl by puting both girlUp and girlDown as class method.
And there's even better I'm sure, but I don't really have the time for this.



Also, how does one see what kind of crap inflates saves?
By using the variable viewer feature (SHIFT + d with config.developer = True, then choose "variable viewer").
Or by using the Extended Variable Viewer (link in my signature, there's a beta version among the last pages, search a big post with a link wrote by me), that will show you all the values that will be saved for each store. It don't include by default the variables purely related to the core that aren't in a store, but there's few and there's a way to see them when you know what they are.


It's all well and good to say it's a 'game design problem', but there's got to be a more informed way to see which parts of the design contribute the most? Because going from ~600 kB at game start to over 5000 kB really doesn't look normal.
It is not normal. As I said, the average saves are below 500 KB, so here right from the start it's over the average.
As reference, for the final test for my Extended Variable Viewer, I use a game that have more than 1300 variables.
Around 90% are boolean values (what don't take too much place to store), but they have names that can goes up to more than 100 characters, and them are stored literally. The other 10% mostly are objects used to reproduce more or less correctly the layered image system. And the biggest save file I have with it is 533 KB and it's a save near the end of the game.

Edit: Sorry, I forgot to close a quote and it messed with everything.
 
Last edited:

Jman9

Engaged Member
Jul 17, 2019
2,295
961
No, it will not.

The function can only, and obviously will, be called when the action is effectively triggered. It's why the Function screen action exist.

Screen actions aren't functions, they are classes. What mean that, when you declare your buttons action, you aren't calling something, but building the object that Ren'Py will proceed when the action will be triggered by the player.
My impression (and some personal experience points to it being true) is that Ren'Py will try to 'predict' what the value of the function call attached to a button via Function will be. Which means a complicated function getting rerun every screen refresh is a massive resource hog.

If I'm wrong, can you be more specific? Because something along these lines is definitely happening.

What is another approach, and probably the best one for an effective use.
Can't take credit for it myself, though. Shoulders of giants and stuff.

By returning from the screen, you create a rollback point...
Remember, I don't have rollback and I don't really want rollback, either.

Otherwise, good points. I never expected a fine-tuned implementation. It's very fine as a demonstration.

And what prevent you to compute that ?

I put the computation directly in the screen, but like you return, you can perfectly do them outside of the screen and pass the value as arguments:
The fact that I want the 'zones' equivalent to change while the screen is still open. I can't precompute it because it's variable, and if I leave it running, screen refresh kills me.

I guess the way to avoid that is to simulate once-per-action activity it by attaching another return value instead, one which will only be processed by the interface loop when the button is, in fact, clicked?

That does run into the issue that the original reason I made all this was to automate away such checks, because 'zones' changes sneak in from all kinds of shady corners.

I suppose I can't have my code cake and eat it too. :cry:

It doesn't. The way the screen AST is built make it that the used screens are directly embedded inside the main screen. It don't take more time to proceed...
Okay, I suppose I can stop worrying about that imaginary bugbear, then. (y)

"Sure" is a big word.
Eh, I don't expect to quote you to my imaginary management to defend my choices. :p Didn't mean I wanted 99% certainty, just fumbled the wording.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
My impression (and some personal experience points to it being true) is that Ren'Py will try to 'predict' what the value of the function call attached to a button via Function will be.
And the reality is that the Function class have no "predict" method ; or more precisely the one of the ui.Action class, that just return. It's code (<renpy/common/00.action.rpy>) is really basic. It store the name of the "reference to the function" and the arguments when create, then call the said "reference" with the said arguments when itself is called, period.
So, I don't know what your personal experiences are, but what you witnessed have absolutely no relation with the Function screen action.
And you can verify this really easily:
You don't have permission to view the spoiler content. Log in or register now.


If I'm wrong, can you be more specific? Because something along these lines is definitely happening.
How can I be more specific than I have already been twice, third time counting this one ? Nothing along these lines is happening, nor can even happen, period.
The only way for a function to be proceeded is to call it, and Function do not call it except explicitly asked by the player when it will click on the button. But the person who wrote the code can do it if it have near to no knowledge regarding programming ; this said, this person would also see that clicking on the button have absolutely no effect.


The fact that I want the 'zones' equivalent to change while the screen is still open. I can't precompute it because it's variable, and if I leave it running, screen refresh kills me.
But, like I said, it's absolutely not this that would kill the screen. Even counting the way Ren'Py proceed Python embedded in a screen, it's nearly atomic, if not purely atomic.
As comparison, Ren'Py need 3 seconds to create 200 000 objects (deeply proceeding variables, while firstly passing through a 6 instance test and 6 deeper test via inspect). It's a little less than 7 objects every 0.1 millisecond. Do you really believe that a len followed by a basic division will have a single impact on the game ?


I guess the way to avoid that is to simulate once-per-action activity it by attaching another return value instead, one which will only be processed by the interface loop when the button is, in fact, clicked?
And multiply by, I don't even want to know how many, the time needed for two basic computation ? Really ?
Just the fact to add the return value would need at least as much time than Ren'Py need to do the computation.


That does run into the issue that the original reason I made all this was to automate away such checks, because 'zones' changes sneak in from all kinds of shady corners.
Python, and therefore Ren'Py, is fully duck typing compliant. You don't care what you've in front of you, as long as it seem to quack just treat it like a duck and everything will be right.
Look I the demonstration code I gave. At what time did it cared what was the zone displayed ? At what time did it cared what was the girl displayed ? The answer is: at no time.
I used a dict for the zone, but you can perfectly have this:
Code:
class Zone( renpy.python.RevertableObject ):
    def __init__( self, name ):
        self.name = name
        self.girls = []

    @property
    def background( self ):
        return "images/background/{}.wepb".format( self.name )

    @property
    def girlsList( self ):
        return self.girls

    def addGirl( self, girl ):
        if len( self.girls ) == 0:
            store.allZonesList.append( self )
        self.girls.append( girl )

    def removeGirl( self, girl ):
        self.girls.remove( girl ) 
        if len( self.girls ) == 0: 
            store.allZonesList.remove( self )
And it would do exactly the same.
The only thing the screens have to care about is to display the zones present in the list, using the information provided by the zones themselves. Like the only thing the screens had to care in my example was to display the girls present in the list, using the value provided by themselves. Everything else to not regard the screens.

Seriously, this object plus the one I gave previously for the girls, with the addition of the three screens I previously gave, and it's done.
After if you want you can have zones that will automatically move each time they have a odd number of girls. Zones that will automatically give one of their girl to another zone when the sum of all the meter is equal to a given value. Or whatever else you want, this is absolutely not a problem. Just make a class that will inherit from zone, change the right method and it's all.
But this only regard the zone itself. Nothing else outside of the two concerned object have to know, and even less to care, that it happened. And this especially apply to the screens, it's not their concern.

The screen just expect the objects to have this/that method, this/that property, period. Is it a girl, a plane or a martian, is it reacting like this or like that, the screens don't care at all about this. It have an image to display, an interface method (slutify), a name, a meter value and a string equivalent for it ? It's alright, the screen can works with them. The name change every time it's displayed ? Not the screen's concern, it just show the string it was given. The image change depending of the meter value ? It's already the case, and at no time the screen I wrote cared about this.

Stay basic, period.
 

Jman9

Engaged Member
Jul 17, 2019
2,295
961
I just discovered the rest of your post in the mega-quote and also think I may have found my save problem. Rolling it into the rest:

It starts to be a problem only when the images are effectively over sized. Here one can possibly benefit from a disk cache. I know that Ren'Py used to have one for some cases (and possibly still use it), and it's possible to build one manually ; don't remember exactly, but it have a function that save an image as seen once proceeded by different displayables (like Frame, Crop and all).
What I have are images with AlphaMask that are already pre-resized and stored in the Slut class analogue. The originals are definitely over-sized, intentionally so because they're reused for several different screens at different sizes.

This might actually be related to the inflated save sizes.

Not sure what I can do here, really. Abandon storing images with Sluts and build a big data structure for them on startup?

What I just tried for a quick fix:
Code:
init -2 python:
    class ProcessedImageDict(object):
        def __init__(self):
            self.dict = defaultdict(dict)

    processed_images = ProcessedImageDict()
When a Slut equivalent needs an image, it gets stored in processed_images.dict[slut]['image_tag'] on the fly and accessed from there for any later calls.

On save, I wipe it: processed_images = ProcessedImageDict().

Unfortunately, this does not seem to be working, at least not for existing saves. :confused: I can load a save and see processed_images .dict is empty, I can see it growing when images are accessed, but save sizes stay the same.

I suspect that might be due to old rollback points? Can I see/delete these?

Variable Viewer
Holy crap, this place is just overflowing with temporary variables. o_O Although there aren't that many, a few hundred at most.

I'm not really sure trying to do anything about those (i.e. manual temp variable removal) would be such a hot idea.

This being said, the use of ui.interact() is outdated ; it's obsolete since something like 10 years now.
Is there a direct downside to it? I'd rather not rewrite all of these if I can help it.

I'm sure at 100% that it's not the autosave.
I referred to that in my big spaghetti post. Toggling autosave has an immediate and visible effect. Even if it's not autosave in a narrow technical sense, in practice it definitely is one of the problems for me and turning it off alleviates the problem.

Therefore, unless you call it yourself (what is not to be done regularly...
Yeah, I cut back on that a lot and it helped. I'll remove it entirely and see if that helps.

To be more precise, what have to be avoided is all the processing that isn't effectively related to the screen building.

...design the game for it to happen few interaction before the screen is called, and do them by using renpy.invoke_in_thread() for them to be transparent for the user.
Well, it kinda is related. The function was 'calculate whether to add this big bad button'.

I can't add any delaying interactions, either, because the gameplay loop there is pretty much 'open screen, click to change screens, come back, change again, back, close, oops, open big screen again, change, back, change to re-check, back, close, etc'.

As I said, this game isn't really Ren'Py-friendly at places.

[caching]
Interesting. I'll have to think about this some. Thank you.



So, I don't know what your personal experiences are, but what you witnessed have absolutely no relation with the Function screen action.
Hmm, I tried something similar myself and it's really not Function. I swear I saw something like that happen, but I suppose that might have been something else like ongoing calculations within a screen itself.

I was wrong, then.

Do you really believe that a len followed by a basic division will have a single impact on the game ?
No, but keeping iterating over a list in a screen definitely does. Regardless of any images involved. I can keep 1000 of these open at once, but going to 2000 is no longer smooth enough to be playable:
Code:
button:
    text 'Text'

The Slut equivalents come in small screens that are apparently so much more computation-intensive that even 50 are annoying. I can't replace those easily, either, since these screens are parametrised and used in several different contexts.

20 seem to be more or less smooth for my machine. Hence the paging.



Bottom line: I tested your original screens, and for me - when replacing grids with viewports -, roughly 200 Sluts perform about as smoothly as ~40 of my Slut equivalents, and that's with all the other inefficiencies I have on top. So even if there's savings to be had here, they're not huge.


Stay basic, period.
I have no ability to 'stay' basic. I might be able to return, at the very best. Repeating this at the end of each post does not change the situation.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
What I have are images with AlphaMask that are already pre-resized and stored in the Slut class analogue. [...]
This might actually be related to the inflated save sizes.
20 Bytes for the name of the Ren'Py class that have been use to resize them. Something line 60 Bytes for the couple path/name pointing to the image. More or less the same for the couple filename/line pointing to the place where the variable have been declared. And around 200 Bytes for the other properties. Add an unknown number of Bytes for the name of the variable, and even in the worse case you stay below 1 KB by images.
It can possibly be the problem, but since once in the ".sav" file, the pickled "log" have its size divided by more or less 5 in average, I doubt that it's effectively the big part of it.


Not sure what I can do here, really.
Well, if effectively they are included in the save file, starts by not saving them... More globally, this apply to all the variables, because with such size for the save file, I assume that there's way too many that are saved.
What do not describe the state of the game at a given moment have no reason to be saved, and therefore shouldn't be saved. It's why there's both a define (generic not saved by default) and a default (will be saved right from the start) statement.


What I just tried for a quick fix:
[...]
On save, I wipe it: processed_images = ProcessedImageDict().
Not only this last line was useless, but in top of that it worsen the situation.

Everything that is created in an init block will be saved only if it's value change. And processed_images being an object, its value will stay constant from start to end. It's the value of his attributes that will change, what is different. What mean that, processed_images wasn't savable... until the moment you decided to give it a new value before a save file is created.


Holy crap, this place is just overflowing with temporary variables. o_O Although there aren't that many, a few hundred at most.

I'm not really sure trying to do anything about those (i.e. manual temp variable removal) would be such a hot idea.
It wouldn't be a bad idea, but it would mean a lot of post processing to correct what is a pure design flaw, therefore a fuckingly big after_load label.


Is there a direct downside to it? I'd rather not rewrite all of these if I can help it.
There's no real downside, just a more complex code and less intuitive one. But in a way it's also the main downside of this ; it make you try to be complex instead of making you focus on simplicity.
But I say this form the point of view of a "recent" game... What I'm not totally sure that this game is.


I referred to that in my big spaghetti post. Toggling autosave has an immediate and visible effect. Even if it's not autosave in a narrow technical sense, in practice it definitely is one of the problems for me and turning it off alleviates the problem.
Here's what happen when autosave happen in the 10 years old 6.14.0 version of Ren'Py [I just reformated the code for it to be compact] :
Python:
def autosave():
    global autosave_counter

    if not renpy.config.autosave_frequency: return 
    if not autosave_not_running.isSet():  return
    if renpy.config.skipping:  return
    if len(renpy.game.contexts) > 1: return
    autosave_counter += 1
    if autosave_counter < renpy.config.autosave_frequency:  return
    force_autosave(True)

def force_autosave(take_screenshot=False):

    if not autosave_not_running.isSet(): return
    autosave_not_running.clear()
    threading.Thread(target=autosave_thread, args=(take_screenshot,)).start()
And it don't really changed since then. At least not in a way that would add more than one or two nano seconds to the process.

There's only two case where the autosave is blocking: Firstly it's when Ren'Py is played throught WebAssembly, therefore from a web server, secondly during the "autosave on quit" procedure.
This second case imply that now the code can be blocking, but it don't happen naturally. For an autosave to be blocking outside of the two cases above, one need to have modified the code of the core.
Plus, as I said, by default the autosave happen every 200 interactions. What mean that it can't have an immediate effect. 199 time over 200 interactions, it will stop at the line testing if the counter is equal to the asked frequency. And there's no way for four basic tests and one increment to have a visible effect.

As I previously said, I believe it, there's stuttering when the autosave is enabled, but I really doubt that it's due to the autosave as it is designed in Ren'Py. It's something else.


Well, it kinda is related. The function was 'calculate whether to add this big bad button'.
And you need such so complex computation for this ?

Let's say that the "big bad button" is visible when all the girls for all the zones have a particular state, and that this state is linked to their meter value. The code should looks more or less like this:
Python:
class Zone( [...] ):
{...]
    @property
    def particularState( self ):
        return self.particularState

    def changeParticularState( self, value ):
        if value is True:
            self.girlInParticularState += 1
        else : 
            self.girlInParticularState -= 1
        self.particularState = self.girlInParticularState == len( self.hostedGirls )
{...]

class Slut( [...] ):
[...]
    def slutify( self ):
        [...]
        self.father.changeParticularState( self.meter >= 4 )

def showRedButton():
    for z in listOfZones:
        if not z.particularState: return False
    return True
The value would be always up to date, the process would add a nano second to slutify and it would need thousands of zones, all in the particular state, for showRedButton to start to have an effective impact.


No, but keeping iterating over a list in a screen definitely does. Regardless of any images involved. I can keep 1000 of these open at once, but going to 2000 is no longer smooth enough to be playable:
Code:
button:
    text 'Text'
Look at this:
all opened.png
In addition to its header, this screen contain 5 752 times four imagebutton (actually the blank spaces are also imagebutton, it should change) and two text. There's also around 4 000 lines that have an additional null. Only the visible part will be effectively displayed by Ren'Py, but all are generated and proceeded each time the screen refresh.
The processing on my side take less than 0.2 second and Ren'Py then need more or less 0.8 second to digest all what have been gave to him.


This being said, how fuckingly old is the game ? First ui.interact(), now the couple button/text. This is obsolete since more than 10 years ; the 6.14.0, that is the oldest version I have on my computer, was already using textbutton and already had call screen.


The Slut equivalents come in small screens that are apparently so much more computation-intensive that even 50 are annoying. I can't replace those easily, either, since these screens are parametrised and used in several different contexts.
The more you talk about it, the more it feel that the game is either an antiquity, or one of the most horrible that have been wrote during the last 5 years. In both case the only viable solution would be a total rewrite.


Bottom line: I tested your original screens, and for me - when replacing grids with viewports -, roughly 200 Sluts perform about as smoothly as ~40 of my Slut equivalents, and that's with all the other inefficiencies I have on top. So even if there's savings to be had here, they're not huge.
vpgrid is your friend then. Replace the couple viewport + grid by it, and the performance will drastically improve. You just need for all the entries in the grid to have the same size, what is the case in the code I wrote.
 

Jman9

Engaged Member
Jul 17, 2019
2,295
961
Thanks for the info!

Well, if effectively they are included in the save file, starts by not saving them... More globally, this apply to all the variables, because with such size for the save file, I assume that there's way too many that are saved.
I am not quite sure how to remove variables from an ongoing game. When I del-ed virtually everything that was not in a new game (size <550 kB), i.e. most of the temporary ones, save sizes remained just about the same. So either del doesn't work for that purpose, the real problems are hidden in the more complex classes, or there's something else inflating the saves.

define ... default
But I don't really want to do either. And isn't define conceptually meant for unchanging entities? Because all these temp variables very much do change around all the time.

This stuff is usually supposed to be garbage collected in Python, but apparently some things are and some aren't.

Not only this last line was useless, but in top of that it worsen the situation.

... until the moment you decided to give it a new value before a save file is created.
But that value is near-empty. Unless its former contents are now up for grabs, and they should not.

In any case, removing that last bit is easy enough.

[autosave]
There is also config.autosave_on_choice and the game is basically driven by either screens or menus. Perhaps that was (some of) it.

And you need such so complex computation for this ?
Honestly, no. I rewrote it along the lines of your example and killed a lot of extra calculations that weren't actually necessary to check just visibility. Although I have trouble seeing why outsourcing calculations to a million objects would be noticeably more efficient than iterating over them. But if it is, then it is. Wouldn't be the first weird thing I've learned about Python/Ren'Py.

Unfortunately, the end result was not really measurably better. I think the bottlenecks are still in those small screens, and I doubt I can get at them without completely redesigning the things.

The processing on my side...
Okay, can you indicate how beefy your computer is? Because this 'on my side' might very well account for a sizeable portion of all the differences between us. Mine is a first-gen i5, I think?

This being said, how fuckingly old is the game ? First ui.interact(), now the couple button...
The buttons were ad-hoc. I just grabbed the first ui element whose name I could recall and put something inside just to see what kind of performance a simple object can have.

The game is, uh, under five years old, I think? I can't vouch for the author's experience or design credentials, though.

In both case the only viable solution would be a total rewrite.
Which would probably take me about a year's worth of free time to redesign and recode. No, thanks.

The game is still playable even after I've added overcomplicated extra code into all kinds of nooks and crannies, even if the original architecture wasn't really meant for that nor particularly good in the first place.

So, as you said, Ren'Py can still do a lot of things even with crap code.

vpgrid is your friend then.
I don't have a grid in the first place. The zone equivalents are only used to gather a list of screens to display.

Maybe I need to create and manage a global list.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
I am not quite sure how to remove variables from an ongoing game.
It's not deleting the variables that matter the most, but removing them from the list of variables that will be saved.

Code:
label after_load:
    for n in [ "name", "of", "the", "variables", "you", "want", "to", "remove"  ]:
        if n in renpy.python.store_dicts["store"].ever_been_changed:
            $ renpy.python.store_dicts["store"].ever_been_changed.remove( n )

But I don't really want to do either. And isn't define conceptually meant for unchanging entities? Because all these temp variables very much do change around all the time.
It depend and, since technically everything is an object, it's a little difficult to explain why... But look at it that way:

Is "changed" a variable that is given a new or different value.
A list to which you add an entry will be changed, because it's value past from [ 1, 2, ..., 10 ] to [1, 2, ..., 10, 11 ]. Same for a dictionary to which you would change the value of a key ; it value would pass from { "a": "whatever", "b": "this"} to { "a": "whatever", "b": "that"}.
But an object to which you change the value of a attribute will it stay unchanged. Before the change, its value was < object whatever at somewhere>, and it will still be its value after the change.
So, all depend of the nature of those temporary variables.


This stuff is usually supposed to be garbage collected in Python, but apparently some things are and some aren't.
Labels are at global level, there's no reason for their variables to be seen as garbage.


There is also config.autosave_on_choice and the game is basically driven by either screens or menus. Perhaps that was (some of) it.
It's even less time consuming than the regular autosave. The only place it's used it there:
Code:
def choice_for_skipping():
    if renpy.config.skipping and not renpy.game.preferences.skip_after_choices:  renpy.config.skipping = None
    if renpy.config.autosave_on_choice and not renpy.game.after_rollback:  renpy.loadsave.force_autosave(True)
And like you can see, it pass through the exact same process than a regular autosave ; test if it apply, then call the function that will start/call the autosave thread.


Honestly, no. I rewrote it along the lines of your example and killed a lot of extra calculations that weren't actually necessary to check just visibility. Although I have trouble seeing why outsourcing calculations to a million objects would be noticeably more efficient than iterating over them. But if it is, then it is. Wouldn't be the first weird thing I've learned about Python/Ren'Py.
It's not just about the time needed to do the processing, but also when this processing happen.

With a default approach, every time you open the screen, you pass through a complex process:
  • for each zone
    • for each girl in this zone
      • compute its state
      • return the result of this computation
    • compute the result of the loop
  • compute the result of the loop

With my approach, every time you open the screen, there's only this that is done:
  • for each zone
    • test a flag value
    • stop the process if this value do not match

Plus, the said flag being proceeded in real time, the whole "for each girl in the zone" part is never done.
You pass from an algorithm that correspond to a "hey, girls, can you tell me what is your state ?" performed every round, to one that correspond to "hey, my state have changed, just in case it interest you to know it", that will be only performed once in a while. So, instead of asking every time to 2 000 objects what is their state, you've 1 object that, time to time, tell you that its state have changed.
Not only the make the time needed for the processing totally independent of the number of girls, but it will also make most of this processing happen at a time where it will have no impact.

After, all depend of the effective time lost at this moment. It's faster, but is it sensibly faster or not, all depend of the effective code ; dividing something by 10 will be noticeable only if this something is big enough to be noticed in the first place.
It's the problem with speed optimization of some code. It's rarely by optimizing one algorithm that you'll notice a change, and sometimes when you've optimized them all you discover that the impact is in fact smaller than you thought it could be.


Okay, can you indicate how beefy your computer is? Because this 'on my side' might very well account for a sizeable portion of all the differences between us. Mine is a first-gen i5, I think?
As I said, I have a 3.7 GHz CPU, and it's a basic 6 cores i7, not a threadreaper. It's a good CPU, but far to be one on top of the chain.
It's better than yours, but not "this better". Globally speaking, at full charge yours would be 1/2 as fast, and at average charge it's around 1/4. So, when I need 1 second, you would generally need 1.3, what is, of course, big and noticeable, but the difference itself isn't really noticeable.


The buttons were ad-hoc. I just grabbed the first ui element whose name I could recall and put something inside just to see what kind of performance a simple object can have.
Well, it obviously have a bigger influence than a textbutton, that do the same but in a purely optimized way. And here it can effectively be noticeable, because there's more to proceed.


I don't have a grid in the first place. The zone equivalents are only used to gather a list of screens to display.
What is a killing point. It mean that Ren'Py will built and display the whole screen... but most of it will be not seen because outside of the window.
A vpgrid would still take time, but for each entity in it, Ren'Py will first look if it's inside the window or not ; and if it's not it will just ignore it.

Said otherwise:
Code:
screen whatever:
    grid X Y:
        for i in [ ... ]:
            vbox:
                [whatever]
would build all the vbox and their whole content, place them, then show you only the part that fit the window.
Same for:
Code:
screen whatever:
    viewport:
        grid X Y:        
            for i in [ ... ]:
                vbox:
                    [whatever]
But:
Code:
screen whatever:
    vpgrid:
        col X
        for i in [ ... ]:
           vbox:
               [whatever]
Would pass through all the vbox but only process them, and their content, if they fit on the window.

It would still need a time directly dependent of the number of vbox, but in the same time, the more vbox you have that don't fit the window, the more time you gain in comparison of the two other approaches.
If everything fit the window, a vpgrid will be a little bit (generally unnoticeable) slower. But once there's at least one element that don't fit, it become faster ; and the more there's elements that don't fit, the faster it goes.