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

[3.6.beta1] Physics frame rate below idle frame rate on selected iOS devices #76645

Closed
oeleo1 opened this issue May 1, 2023 · 8 comments
Closed

Comments

@oeleo1
Copy link

oeleo1 commented May 1, 2023

Godot version

3.6.beta1

System information

iOS 14, GLES2

Issue description

Spawning a new issue from #76425 which arguably deals with a different problem.

Hi,

We have a similar obscurity with 3.6beta1 on recent iOS devices only. The abnormal behavior doesn't happen on Android, Windows or when using 3.5.2. Only on recent iOS hardware.

What we see is that our player character is being triggered to jump, but it jumps 1/3rd of the time. The touch logic is in a touch controller which arms a jump_trigger and the player logic basically sets a variable jump in _process() to true based on the trigger, then _physics_process() reads that variable to alter the velocity. It's a classic:

var jump : bool = false

func _process(delta):
    ...
    jump = jump_trigger
    ...

func _physics_process(delta):
    ...
    if jump:
        velocity.y -= sqrt(JUMP_HEIGHT * 2*GRAVITY)
    velocity = move_and_slide(...)
    jump = false

What we see on the high-end iOS devices is that the jump variable is read as false in _physics_process() despite being set to true in _process(). This is very odd and happens sporadically.

It basically shows that either:

  1. _process() is executed more often than _physics_process()
  2. there is a memory issue.

In the 1. case, the axiom of having at most 1 idle frame per physics frame is broken (we are at the default 60fps without bumping refresh rates). This means that _process() is executed more often than _physics_process(). Could this ever happen? In any case, this works as intended in 3.5.2 on those devices, so it is really 3.6beta1 specific. Or is someone triggering internally a _process() call in 3.6beta1 under circumstances?

In the 2. case, all bets are off.

Unfortunately, we couldn't reproduce this with a stripped down version of an MRP so 2. is also a possibility.

PS: Some more debugging reveals that 1. is the cause of the problem: The logs show:

jump == false in _physics_process()
jump == false in _physics_process()
[1] jump = true after jump_trigger in _process()
[0] jump == true BEFORE jump_trigger in _process()
jump == false in _physics_process()
jump == false in _physics_process()
jump == false in _physics_process()

In our case, jump is being written twice, at first to true then to false, without _physics_process() noticing the change.

So it boils down to why _process() is being executed more often than _physics_process() or, in other words, how come the physics frame rate is being slowed down below the idle frame rate.

PPS: This occurs on an iPad Pro 3rd gen, iPhone 12 (i.e. fast devices) but does not occur on iPad Pro 1st gen.

Steps to reproduce

It's a tough call. We haven't been able to strip down a minimal version of the above exhibiting the problem on those iOS devices. It is unclear what the cause it in order to reproduce the issue, so for now we just log the problem.

Minimal reproduction project

See steps to reproduce.

@smix8
Copy link
Contributor

smix8 commented May 1, 2023

One reason could be that v-sync does not work on iOS or said device so your frame rate is not really capped and process runs more often.

Never assume a working frame rate cap for your game logic. On highend hardware with not-so-demanding scenes process can and will run far more often than physics_process. You will have multiple frames rendered without a single physics iteration on highend hardware.

physics_process loop runs between 0 - 8 times each frame depending on time passed.
process runs once, and only once, each frame but how many frames is entirely depending on hardware.
physics_process runs before process in the main iteration loop.

I can not comment more on maybe iOS specific issues but that ungodly mix of frame depending process and time depending physics_process is unhealthy game logic.

With this input logic you will have situations where your entire game input logic breaks depending on how well your game runs on the device or if whatever frame rate cap you are using is working or even enabled by the user. If your game can not run without bugs with uncapped frame rate that is what you need to fix. Especially on desktop users regularly disable e.g. v-sync in external hardware drivers so always assume that your game runs without issues without any frame rate cap.

In general your axiom is not correct as even if v-sync or any other "software cap" is working there is never a guarantee that your physics tickrate and your frame rate will stay perfectly in sync or one below the other. Instead, it should be an axiom in this day and age that frame rate depending game and especially input logic is not allowed to exist ... but what can I say ... even triple-AAA studios fail at this quite regularly in glorious fashion.

@oeleo1
Copy link
Author

oeleo1 commented May 1, 2023

Assuming you are correct, it does not explain why the same code works flawlessly on previous versions of Godot on the said devices.

@lawnjelly
Copy link
Member

lawnjelly commented May 6, 2023

Just to be clear, what is the exact code in _process? Also an MRP would be useful.

Are you setting jump to true, then another frame happening before physics process, and jump being set to jump_trigger (false)?

Pattern should be (if you want to do input in process) e.g.:

func _process(delta):
    if Input.action_just_pressed("jump"):
        jump = true

not

func _process(delta):
    jump = Input.action_just_pressed("jump")

(There are also some input bugs currently that are being worked on, especially on Android but these could possibly be affecting iOS too. See #76400 )

@oeleo1
Copy link
Author

oeleo1 commented May 6, 2023

Regardless the fact that I have reported a design bug pointed out by @smix8 :-), the problem is that _process() and _physics_process() are not in lockstep while they used to be, which in itself is a bit of a problem, although there is no official contract for them to be in lockstep at 60 fps, so in principle I shall close this bug. I'll leave it open for the time being as it points out a difference in behavior wrt. previous versions of Godot without an obvious reason. Your pointer to #76400 may indeed influence this.

The exact code in _process() is the one I quoted above, the overall logic being simplified to this:

func _process(delta):
    ...
    jump = jump_trigger
    jump_trigger = false
    ...

func _on_TouchController_sig_jump(): -> void:
    jump_trigger = true

and the sig_jump signal is emitted after TouchEvent processing concluding that we want to jump, and not slide or pinch or whetever other touch event combnations we have there.

@oeleo1
Copy link
Author

oeleo1 commented May 6, 2023

PS: The puzzling bit here is that this occurs (repeatedly) on specific iOS devices only and not on others. I can't say whether it is Godot or Apple iOS version related, but it is clearly a difference in timing (or indeed a difference in threading) issue. We don't see a difference on Windows or on Android, but that may be misleading.

@lawnjelly
Copy link
Member

Yes, whatever else bugs there are, don't reset your input in _process(), reset it only when it has been acted on. That will bite you sooner or later, even if it works on a dev machine.

It's very possible that you will get e.g. double frames before a physics tick (this is largely down to the platform, and not our code, and you need to program with this in mind).

@lawnjelly
Copy link
Member

Google also suggests iPad Pro 3rd gen seems to have 120hz refresh rate. This means it is even more likely there will be 2 frames rendered before a physics frame, and hit the logic bug in the script. This may be part of the reason for the problem occurring more in more up to date hardware (which is more likely to refresh faster).

There is also likely some interaction with the input bug (which may occur more with buffered input on mobile, causing maybe a stall and two rendered frames before a tick), but I don't think these are necessarily the direct cause of the issue, more that they expose the error in the script logic.

If @oeleo1 can confirm that fixing the logic in the script fixes the bug, we should probably close this issue. Even when input is fixed, it would not be the correct fix for (or close) the user script logic bug in this issue. There are already existing issues open for the input bug.

@oeleo1
Copy link
Author

oeleo1 commented May 8, 2023

Indeed, the proper logic to process the input in _physics_process() at a fixed rate is fixing the problem on the said devices. I am closing this since what is reported here is not the cause of the difference in behavior wrt. previous Godot versions.

@oeleo1 oeleo1 closed this as completed May 8, 2023
@YuriSizov YuriSizov removed this from the 3.x milestone Dec 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants