Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drastically optimize pause menu elements #300

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

Koopa1018
Copy link
Collaborator

Description of changes

On profiling the normal gameplay loop, it was discovered that several pause menu elements had startlingly high performance costs. This PR optimizes them down to reasonable levels.

The bigger offender was the function _get_joypad_buttons in rebind_option.gd, which was profiled as using 11(!) ms of CPU time in each frame. This function returns an array of arrays of button names, indexed by logical button index and by controller brand. On inspection, it was found that this function was called once for every binding of every action, every frame, and doing the same 25 string translations every time (which suggests 25 disk reads). Assuming one binding for every action--which I know is lowballing it--that's 25 * 25 = 625 total translations per frame, literally 98% of which are repeated work.

Optimizing the script thus becomes a matter of reusing work. This can be done:

  • within _get_joypad_buttons. Because some buttons share names between brands, some translated strings can be cached before constructing the return array, which skips 15 extra translations per function call. This step alone reduced CPU time from 11ms to 4ms.
  • between actions and bindings. The result of calling _get_joypad_buttons doesn't depend on which action or mapping it's used for--only the selected locale. By storing the result in a static variable, it can be reused between instances, and the disk reads(?) involved in translation lookup can be skipped for all but the first user.
  • between frames. Locale really only changes when the user sets it in the options menu, so for 99% of frames, _get_joypad_buttons returns the same result as last frame. rebind_option already has machinery to receive the engine's locale-change notification, so it's fairly trivial to only set _get_joypad_buttons when the game starts or the locale changes. Combined with the previous step, this reduced CPU time from 4ms to ~0.3ms, about level with Entity._physics_process and DejitterGroup._physics_process.
  • Actually, the majority of rebind_option's state doesn't change between frames. By adding (and connecting to) a signal to the reset-mappings button, it becomes possible to completely remove _process and instead update rebind options reactively. Naturally, this means rebind_option is no longer charting at all on most profiler frames.

Some similar string-caching-via-statics work was also done to get_joypad_motion_name. This is mostly for methodological consistency, but if somebody were to make very strange mapping choices ("wassup YouTube, today I'm gonna be playing SM63R using just the left and right analog stick!"), I could see it saving actual performance.

I also chose to optimize level_info.gd. This was much less of a problem script, but it still seemed appropriate to switch it from detecting locale via _process to detecting via engine notification, especially since rebind_option had shown me how to do that. This saves about 0.15ms--not much, but still seemed excessive for a static menu element.

Some mild refactors have also been sprinkled in, including obligatory reordering functions to standard and commenting everything I touch.

Koopa1018 added 10 commits June 7, 2024 15:37
_process runs nothing but this function, and it's not used elsewhere. No need to separate it.
WHOA. Just by doing this, I drop the total process time for this script from ~11 ms to ~4.5 ms. I gotta keep this going!
For use later in refactoring button name fetches.
This drops processing time from ~4.5 ms down to ~0.25 ms. This puts it level with Entity._physics_process in T1R1. Frankly a bit shocking that an options menu should even be that intensive, though.

The script already has machinery to mostly operate without _process(). Looking into how to take that to 100% so we can skip that function entirely.
...instead of proactively running update_list() for every rebind option, every frame. This allows us to completely remove the _process() function from rebind_options.

Script functions now profile as sub-1 ms on my machine (~0.8).
If we're already caching other input translations, might as well finish the job. Should be zero redundant translation calls now, apart from when translations changed (and I know how to fix that).
Accomplished by statically caching the name of the current locale, only updating the strings if the cache is out of date, and updating the cached locale as soon as the first rebind option finishes (so the others don't see it).
Perhaps it's not taking 11 milliseconds, but this script's process function was up there with water and entities last I checked--which, again, is pretty outrageous for a static pause menu element! Now it only updates when the language changes, not once every frame.

While I'm here, refactor the script to potentially support the fancy font in other languages someday. That's really just using a match statement instead of an if, though.... (and commenting it)
Bit of "why use a separate function when a comment will do" here. Also, updating for-loop variable names to accurately describe what's actually contained in them (as far as we care). And commenting, of course.
Copy link
Contributor

@GTcreyon GTcreyon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally all good, just some crashes that need resolving.

output = output.trim_suffix(", ")
return output

key_list.text = output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The game crashes here for me. "Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'."

I believe this happens because update_list() is called before @onready can trigger, when the translation change notification is received. I'm not sure why this didn't occur while you were working on it, perhaps Godot updates, or perhaps something to do with my own language settings? (Apologies for the belated response to your PR.)

refresh_caps()
func _notification(what):
if what == NOTIFICATION_TRANSLATION_CHANGED:
match TranslationServer.get_locale().substr(0, 2):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar issue occurs here, with a similar solution. (See my other comments.)

Suggested change
match TranslationServer.get_locale().substr(0, 2):
if level_name == null:
await ready
match TranslationServer.get_locale().substr(0, 2):

# already done that).
if locale_current != TranslationServer.get_locale().substr(0, 2):
_update_translations()
# Update the list.
update_list()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aforementioned crash can be resolved like so:

Suggested change
update_list()
if key_list == null:
await ready
update_list()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants