godot icon indicating copy to clipboard operation
godot copied to clipboard

Touch input causes FPS drop and stuttering on some iOS devices (Pro Motion?)

Open djrain opened this issue 1 year ago • 38 comments

Godot version

3.6 beta 1

System information

iOS 16, GLES3

Issue description

Discussion started in #32139 but now seems like a separate issue.

I believe #69200 fixed that issue, but now I'm still seeing stutter and FPS drop from touch input on iPhone 14 Pro Max. The issue is not reproducible on iPhone 12 or iPhone 6s, so I guess maybe this has to do with Pro Motion (high refresh rate).

Here are some screen recordings comparing a near-empty scene on iPhone 14 Pro Max and iPhone 6s. Godot icons indicate tapping input.

iPhone 6s - performance unaffected by touch input, as it should be:

https://user-images.githubusercontent.com/33777501/234172363-56198b63-d709-4b8d-b4c9-4abd48f8d573.MP4



iPhone 14 Pro Max (high refresh rate DISABLED in Godot) - notice severe FPS drop:

https://user-images.githubusercontent.com/33777501/234172378-4ac52bd0-435e-485f-b2b1-6e6ecbaa4975.mov



iPhone 14 Pro Max (high refresh rate enabled in Godot) - again, significant FPS drop:

https://user-images.githubusercontent.com/33777501/234172394-a19cbd91-be93-4950-a3dc-60f20d76a2c0.mov

iPhone 14 is of course many times more powerful than the 6s, so this makes no sense at all. Especially the case where pro motion is not even enabled in Godot and the 14 is struggling.

Our game requires a lot of tapping, and these stutters are unfortunately making the difference between a nice smooth game, and a non-shippable one :( so we'd be super grateful for any help on this.

Steps to reproduce

Export and run MRP main scene on a recent iOS device (may require Pro Motion)

Minimal reproduction project

iOSTouchStutter.zip

djrain avatar Apr 25 '23 04:04 djrain

I wonder if this would help: https://github.com/godotengine/godot/pull/76399

There's a 3.x variant of the PR you could test.

That's just a hunch but maybe with the high refresh rate you're getting too many inputs processed and running potentially expensive logic for each event?

akien-mga avatar Apr 25 '23 07:04 akien-mga

@akien-mga Could be worth a shot, thanks - weird thing is that the issue persists even if I disable high refresh rate in the phone settings. So perhaps it's not directly related...

djrain avatar Apr 25 '23 15:04 djrain

That's just a hunch but maybe with the high refresh rate you're getting too many inputs processed and running potentially expensive logic for each event?

I believe the attached project doesn't have any logic run on input events yet still causes a stutter.

I looked at the Pro Motion support in the SDK. It allows for variable refresh rates to be set (see preferredFrameRateRange). But in practice, preferredFramesPerSecond will set the minimum, maximum, and preferred FPS to the value given.

Have you run Instruments with the Animation Hitches tool? It captures Core Animation data and gives insight into the CADisplayLink timing.

tbveralrud avatar Apr 26 '23 05:04 tbveralrud

@tbveralrud I tried several runs, and it doesn't show any hitches.

Screen Shot 2023-04-26 at 11 09 33 AM

djrain avatar Apr 26 '23 18:04 djrain

I wonder if this would help: #76399

Tried this, did not help unfortunately.

Also, I've tested on an iPhone 14 Plus (doesn't have ProMotion) and confirmed that the stutter doesn't happen there. So the problem must be related to ProMotion, or some other difference between 14 Pro Max and the 14 Plus (which there aren't many - only other big things are the dynamic island, and always-on display).

djrain avatar Apr 28 '23 18:04 djrain

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.

oeleo1 avatar Apr 29 '23 08:04 oeleo1

Can anyone reproduce this on 4.0.2?

Calinou avatar Apr 29 '23 18:04 Calinou

After doing a bit more digging, this may just be an issue on Apple's side. There are a number of reports about iPhone 14 Pro stuttering, some specifically regarding touch: https://forums.macrumors.com/threads/why-isnt-apple-fixing-the-touch-input-stutter-on-iphone-14-pro-models.2370587/ https://developer.apple.com/forums/thread/718721 https://www.reddit.com/r/iOSProgramming/comments/10gatwu/iphone_14_pro_stutter_when_tapping_display/ https://www.reddit.com/r/iphone/comments/yyv06x/why_isnt_apple_fixing_the_touch_input_stutter_on/

And I just noticed that I can see the same kind of stuttering in some other games, for instance Tiny Wings.

It sounds like Apple has been aware of this for some time, but it still hasn't been fixed properly. I just updated my phone to latest iOS (16.4.1) and it did not help.

djrain avatar Apr 29 '23 18:04 djrain

@djrain It sounds tempting to assume that the problem is on Apple’s side, but I see no rational explanation on why the problem is not present on previous Godot versions on the same hardware. BTW the touch logic works fine for us on both 3.5.2 and 3.6beta1. So for me this remains a major obscurity. Maybe a bisect on potential commits which may have broken it would be in order…

oeleo1 avatar Apr 29 '23 19:04 oeleo1

@oeleo1 I'm not sure, but it sounds to me like you may have a different problem there. It could be related, but I would suggest opening a new issue to look into that, as it does sound concerning!

djrain avatar Apr 29 '23 19:04 djrain

@djrain Can you reproduce this issue on 3.5.2 or older?

Calinou avatar Apr 29 '23 21:04 Calinou

@Calinou yes, I've reproduced this in 3.5.1 and 4.0.2 RC2.

djrain avatar Apr 30 '23 01:04 djrain

@oeleo1 I agree that a new topic is appropriate for your issue.

In the meantime, jump = jump or jump_trigger should unblock your situation.

tbveralrud avatar May 01 '23 00:05 tbveralrud

Yes, I need to spawn another ticket.

@tbveralrud Yes, we already tried that jump = jump or jump_trigger idea but it shifts the same problem to double taps. Better not go down this road at this point unless we have some more visibility on what's going on here. In the meantime we're happy with 3.5.2.

oeleo1 avatar May 01 '23 10:05 oeleo1

Godot v4.1.1 Same issue on iPhone 14. iPhone 12 runs the game smoothly. Tested via TestFlight, so it's the "production" build. Turning on 60 hertz makes the frame drops a little less visible, but they still appear. The frame drops appear when processing input (tested with custom in-game FPS profiler)

k0rean-rand0m avatar Oct 03 '23 12:10 k0rean-rand0m

Godot 4.2 same issue. Is there any chance this will get fixed? Very bad user experience on iOS.

MarcusDobler avatar Dec 28 '23 10:12 MarcusDobler

I am not positive the problem is identified or understood in order to propose a fix. Maybe we shall provide @lawnjelly with an iPad Pro (all iPad Pros from Gen 1 have ProMotion) so he can at least see the problem. Then again, not sure Godot can do something better than the logic in place with the frame rates (detection + adjustment) to remedy the situation.

oeleo1 avatar Jan 16 '24 08:01 oeleo1

PS: I am very happy to report that the iPad Pro / iPhone stutter (presumably due to ProMotion rendering rate variations and input lags as reported here) are gone when we switched to FTI for 2D (Fixed Timestamp Interpolation) with the release of Godot 3.6beta4 - the 1st and long awaited 3.x release supporting FTI for 2D. Stutter completely gone, as FTI deals with it perfectly and the game runs smoothly as it run on other devices without ProMotion. So I encourage everyone here to retest their projects with FTI enabled.

For us switching to FTI was a no brainer. Just enabling the global setting, renaming a couple of _process() functions and logic to _physics_process() and a few node.reset_physics_interplation() calls here and there, essentially at places where we have set_global_position() calls for some objects or (CPU) Particles with local coordinates set to off.

Excellent work @lawnjelly and Team! Thank you very much! For us this 3.6beta4 release is a huge milestone in both quality and functionality!

oeleo1 avatar Jan 28 '24 12:01 oeleo1

@oeleo1 Are you sure the interpolation didn't solve a different issue in your game? The MRP here still reproduces the touch stutter for me in 3.6 beta 4 with physics interpolation enabled. The project doesn't use any physics, so I don't see how that setting would affect it.

djrain avatar Jan 29 '24 05:01 djrain

The project doesn't use any physics, so I don't see how that setting would affect it.

The name "physics interpolation" is misleading, but Juan insisted, as it is a simpler term for beginners. It is usually known as "fixed timestep interpolation". It has nothing to do with physics (except in this case the fixed timesteps coincide with physics steps in Godot).

That said there is likely more than one issue being reported here. Some problems may be due to input threading (which has some fixes already in 3.6) and some problems may be due to lack of FTI. There may also be additional factors.

lawnjelly avatar Jan 29 '24 08:01 lawnjelly

@djrain I am quite confident the stutter we had was related to the varying 115-120 fps ProMotion devices with 60 tps physics.

The MRP here still reproduces the touch stutter for me in 3.6 beta 4 with physics interpolation enabled. The project doesn't use any physics, so I don't see how that setting would affect it.

Are you sure you still have stutter ? Or are you referring to the FPS drops due to input which do not necessarily result in stutter ? These two are different issues. With FTI, the FPS variations are still there, but the stutter is gone. That's what I am reporting.

Now, on the iOS input resulting in FPS drops, two things are worth mentioning here:

  1. The iOS specific input code has been removed recently and unified with the rest of the supported devices. no more specific buffering or iOS specific delays which we had in previous versions of Godot and we could configure in settings. So Godot can't du much more than that codewise at this point, but we could indeed analyze the problem further on iOS devices on why it happens.
  2. Your MRP exhibits the lot's of work in the input handler pattern @akien-mga mentioned earlier. On every key press, you are loading dynamically a scene from the file system, which in turn is instanced dynamically to create objects, which in your case happens to be an AnimationPlayer with properties set to di a fadeout. That's quite a lot of work for an input processor an you definitely might want to optimize that in order to reduce any potential lags here. For instance, you could a) preload your scene, b) instance it outside the input handler, c) just clone the instance in the input handler and use the clone, which is a thousand times faster than instancing the scene after loading it dynamically. Etc.
  3. Generally speaking of input processing in Godot, it is worth mentioning a few optimizations one may consider for real-time games
  • Put you touch input logic at the bottom of the tree. This is because Godot processes its input events bottom-up and you want your input event to be processed as early as possible, and not after walking through thousands of nodes.
  • Put an input processing "pit stop" node above your touch controller, which catches all input and stops its propagation upwards in the tree, thus preventing the event to walk through all the nodes in your game. Your mileage may differ, but you gate the idea. You can achieve the same result by putting you main non-input related part of the game in viewport(s) with input disabled.
  • Make sure Input.set_input_as_handled() is used redularily when you catch an event of interest that you are processing.

All this to say that the MRP here is causing delays for sure, but I am not positive it is causing and exhibiting stutter.

oeleo1 avatar Jan 29 '24 08:01 oeleo1

Just to clarify. In order to observe stutter, there has to be a moving object. The MRP doesn't have one. Just fading sprites appearing at the rate of the screen taps.

The MRP and this bug report is about an FPS lag which is directly related to the input processing on iOS devices with ProMotion. The Fixed Timestamp Interpolation enabled in settings definitely solves most, if not ALL of the stutter one may have on a variable frame rate rendering device. So with FTI there is no stutter issue. There is an input processing issue which I have tried to break down above with some practical tips on reducing the phenomenon.

What is still puzzling here is that on iOS devices without ProMotion, there are no input lags, while there ae lag on device with ProMotion - characterized with a (potentially variable, as per spec) rendering rate of 120 fps. But given that Godot's input code is standardized across devices and the observed phenomenon seems specific to Apple devices with ProMotion, Godot's due diligence homework is about figuring out why ProMotion results in screen touch input lags. This may be an Apple-specific problem, or a Godot threading priority problem showing up in this specific scenario.

Hope this helps explaining where we stand on this topic.

oeleo1 avatar Jan 29 '24 14:01 oeleo1

@oeleo1 okay, so in the MRP I added some sprites moving across the screen and removed all logic on input. And after enabling the interpolation, I'm still seeing plenty of visual stuttering on touch (in addition to the frame drops). I'm moving the sprites in _physics_process(). Is there another step I'm missing? I guess I don't understand what the new setting does exactly.

djrain avatar Jan 29 '24 17:01 djrain

Is there another step I'm missing?

Who knows :-) Can't say without looking at the new MRP. Moving the sprites is a vague notion here. Usually moving means tweening the position or using the move_and_slide() family of functions within _physics_process(). Setting the position explicitly, be it in _process() or _physics_process() is a no go. Do you have a Camera2D node? You may want to add one in order to have explicit control on the visuals and the target to follow.

I guess I don't understand what the new setting does exactly.

The new setting smoothes movements for you automatically by using the so called Engine.get_physics_interpolated_fraction() without you having to worry when and how to use it. The net effect is that any rendering occurring between 2 physics ticks is interpolated automatically for you. Before FTI for 2D, to achieve the same effect, one had to lerp() positions manually and maintain transform state variables between _process() and _physics_process() in lockstep. This is now done automatically for you by ticking on the new setting.

ProMotion typically uses adaptive rate rendering "up to 120 fps" while your Godot physics ticks are nominally fixed at 60 tps. So in theory, you shall have something like X-2.0 rendered rames per physics tick, where X varies typically from 1.5 to 1.9 with ProMotion. In short, when a frame is rendered on the screen, FTI makes sure the moving objects positions are computed properly so they appear exactly where they should be at the time the frame is rendered (which is a variable instant in time, happening before or after the physics tick). With FTI and a frame refresh rate of 110-120 fps with ProMotion, you shouldn't see a blink but a bunch of very smoothly moving objects.

oeleo1 avatar Jan 29 '24 18:01 oeleo1

Thanks for the info! I'm just still confused about how I should be moving stuff, since you said that setting position directly won't work? For example in my game, I don't use physics nodes. My "Player" entity is basically just a Node2D with a Sprite2D child. To move the whole player, I directly set the position of the Node2D in _physics_process(). Evidently this does not do the trick... So what would I do differently to let the interpolation magic happen?

djrain avatar Jan 29 '24 18:01 djrain

So what would I do differently to let the interpolation magic happen?

Like I said, one of the simplest things you could do is to tween your player from pos_A to pos_B with a tween duration corresponding to your desired speed of movement. The granularity of the position and duration deltas is up to you. One usually uses velocity with move_and_slide() in the context of a KinematicBody2D. Not sure why you are not using it since it comes with plenty of goodies about collisions, frictions and the like.

oeleo1 avatar Jan 29 '24 19:01 oeleo1

Well, even using a KinematicBody2D and move_and_slide in _physics_process(), I'm not getting smooth movement.

Here's a video from my iPhone 14 Pro Max. The first cycle is smooth without touch input, and on the second cycle I start tapping and get a significant FPS drop and visual stutters:

https://github.com/godotengine/godot/assets/33777501/f72ef7e1-cf6b-405a-bb0d-48e42b4d040b

@oeleo1 Would you mind looking at the project and showing me what I'm doing wrong? That would be a huge help!

iOSTouchStutter2.zip

djrain avatar Jan 29 '24 22:01 djrain

Application -> Run -> Delta Smoothing can be disabled for mobile only if that is a problem for mobile.

normano avatar Jan 31 '24 17:01 normano

Messed up my multiple GitHub logins and identities, so repeating my post with the Godot version of myself ;-) Sorry about that.

@djrain Finally got a minute to look into your project. With a few project settings adjustments, it's smooth like a Greek olive sliding on a French butter toast :-) No stutter for me.

  • The general rule of thumb here is to enable Physics -> Common -> Physics Interpolation
  • The no less important bit is to enable Display -> Window -> iOS -> Allow High Refresh Rate (ProMotion 120 fps)
  • The crucial setting here is to disable Applicatin -> Run -> Delta Smoothing. This one is not your friend when you have a variable refresh rate like ProMotion. Not sure why it is enabled by default. This one constantly tries to compensate for variable refresh rate deviations and although I undesrtand the intent and the underlying logic, I disagree it shall be enabled by default... That's my opinion though and @lawnjelly & co. may disagree with it. I think it effectively does more harm than good, especially in the case of ProMotion.

Another piece of advice against stutter is to disable stdout and stderr :-). Although this doesn't apply to your project, any print output triggers useless complex formatting logic so if printing stuff is not absolutely necessary for debugging, stdout and stderr shall be off.

Your project with these remarks applied doesn't exhibit any stutter for me. iOSTouchStutter2_fix.zip

oeleo1 avatar Jan 31 '24 17:01 oeleo1

The crucial setting here is to disable Applicatin -> Run -> Delta Smoothing. This one is not your friend when you have a variable refresh rate like ProMotion.

I wonder if there's a reliable way to detect VRR on all platforms, but last time I checked, it seemed difficult to impossible. That said, is ProMotion "true" VRR with a variable range between say, 48 and 120 Hz, or is it just a switch between 60 Hz and 120 Hz?

The way delta smoothing works reminds me of DuckStation's Sync to Host Refresh Rate feature, which is also recommended to be disabled on VRR displays:

image

Calinou avatar Jan 31 '24 17:01 Calinou