Open djrain opened 1 year ago
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 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...
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 I tried several runs, and it doesn't show any hitches.
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).
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:
_process()
is executed more often than _physics_process()
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.
Can anyone reproduce this on 4.0.2?
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 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 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 Can you reproduce this issue on 3.5.2 or older?
@Calinou yes, I've reproduced this in 3.5.1 and 4.0.2 RC2.
@oeleo1 I agree that a new topic is appropriate for your issue.
In the meantime, jump = jump or jump_trigger
should unblock your situation.
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.
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)
Godot 4.2 same issue. Is there any chance this will get fixed? Very bad user experience on iOS.
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.
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 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.
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.
@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:
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.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.
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 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.
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.
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?
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.
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!
Application -> Run -> Delta Smoothing can be disabled for mobile only if that is a problem for mobile.
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.
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
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:
The delta smoothing should turn off automatically with VRR (it's not intended to work in that scenario). If it doesn't that could be a bug, so worth reporting so I can investigate.
If you can compile from source, you can enable this define in main_timer_sync.h
, which will report on whether it has locked, turn on / off etc:
//#define GODOT_DEBUG_DELTA_SMOOTHER
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?
I guess that's an Apple secret :-) In practice, it looks like a switch 60/120. We observe whatever values are reported every second by Engine.get_frames_per_second()
. When loading heavy scenes the value drops down to 105 fps then goes up to 120 within 2 seconds or so. That 1 sec interval is not very friendly to see exactly how variable the VRR is, but in reality it sticks to 120 fps and stays constant unless there are CPU/GPU load spikes.
That 1 sec interval is not very friendly to see exactly how variable the VRR is
You can measure the exact time taken between rendering two frames by subtracting Engine.get_ticks_usec()
with the value from the previous frame (stored at the end of _process()
in a member variable).
@oeleo1 So I opened your new project and exported it without any changes. Unfortunately, I'm still getting obvious stutters. Maybe I'm just tapping with more fingers or more rapidly than you are in order to see them?
Here is how that looks. On the 3rd pass of the icons, you can see the stutters after I tapped with 3 fingers, about 3 seconds in:
https://github.com/godotengine/godot/assets/33777501/56e908dd-7193-46b1-bc6c-4de024c0a618
Potentially related issue is why the FPS is fixed at 60 while idle, and it jumps up to 120 temporarily after the touches?? I found that if I disable "hide home indicator", then the behavior is sort of opposite - it idles at 120, and drops a bit when tapped. Also, for some reason it seems I can only access the 120 FPS if the phone is screen recording, like I was here. If not recording, it only ever goes up to 80 fps... And all that only applies in Godot 3, I can't reproduce this odd FPS limit when not tapping in 4.2.1, there it always seems to run at a base of 120 as expected (though it still drops and stutters on taps).
All this to say, the fix isn't working for me, and it seems something is wrong with the high refresh rate in Godot 3 compared to 4.
@djrain To be honest, your latest stutter video looks much better to me than the previous ones, so I believe the new settings actually do work in your favor.
That said, your report sounds like a big mess of uncorrelated events. I believe you because real life is a mess of such events. :-)
Now, the only reasonable explanation is that currently Godot doesn't honor the VRR goodies dedicated to ProMotion for iPad/iPhone Pro models, nor does it support Adaptive-Sync displays on Macs. Lack of expertise, lack of time, lack of ressources, whatever - we just don't have it at this time.
What Godot actually does and excells at is that it is reasoning about frame rates based on the effective occurrence of the frames as time goes. The delta smoothing option is a statistical rate observer of past timings aiming at correcting deviations for a fixed frame rendering scenario (so again, no good for VRR). The FTI option takes the approach of taking the fixed rate physics ticks as a basis, then compensate the variable rendering by interpolating positions (which is much better for VRR). Etc. But all these techniques suffer from the lack of real hardware hints on what the actual rendering rate is or how and when it changes.
That's a problem indeed but I am sure that if you partner with @lawnjelly and the Godot iOS team you can put all the bits together in one place. So heads up on this!
FYI, Apple has actually published some very useful technical bits related to ProMotion. Please check this developer article and especially this excellent video about optimizing for VRR, Adaptive-Sync and ProMotion. There are enough technical details on how to detect the presence of VRR, how to configure the CADisplayLink
with the goodies available and also best practices for securing a smooth custom rendering loop. That's spot on for what we are discusiing here.
That being said, I had a quick look at the current 3.x iphone code, and I didn't quite like the following bit in godot_view.mm
:
- (void)drawView {
...
if (self.useCADisplayLink) {
// Pause the CADisplayLink to avoid recursion
[self.displayLink setPaused:YES];
// Process all input events
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource)
;
// We are good to go, resume the CADisplayLink
[self.displayLink setPaused:NO];
}
...
Why is there a while loop about input events in the middle of a drawView call ? Unclear... And this bit precisely is probably causing some of the stutter we see.
Also, the app_delegate.mm
defines the godotView.useCADisplayLink
flag in terms of some project setting which was unknown to me this far (and doesn't exist in the user menus):
viewController.godotView.useCADisplayLink = bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO;
All this to say that 1) we already benefit from the best settings at the time of this writing, 2) there is pending work to do in order to honor VRR for MacOS and iOS properly, 3) the above code probably neads a new look and a thorough review from the iOS folks.
Last but not least, we shall gift Apple Pro hardware to all of our Godot developer friends. Not sure how I can buy @lawnjelly an iPhone or an iPad Pro but if there are practical ways to do it, I am all ears and I'll do it :-)
While I haven't looked at Godot's source code yet here are some things which need to be considered based on my empirical knowledge (I've started learning Godot less than a month ago, so maybe I'm super wrong).
My game needs requires input fine touch resolution (like in fine scrolling smth on very short lengths) for a good experience.
The screen itself influences how many touch events can be generated.
iOS On an iPhone XS Max with a no name aftermarket screen (not Apple's) or a cheap Android phone: not enough touch move are sent (too much time between two touch move events).
Android On the cheap Android phone things are even worse: a drop in FPS caused by anything (not touch) drops the number of touch events too.
As such, not enough touch events (for my case) because of a drop in FPS is something real to complain about and should be a bug as it would cause the user to compensate making things worse because the touch move events appear to first accumulate in a 10 events long buffer which is only sent to code in Godot after it fills - the worser the screen OR the worser the FPS then the more the user will overcompensate a touch move by moving the finger more. In extreme cases (lower than 20 FPS) this results in the user making a mistake because the 10 events buffer is eventually sent (with a big lag) to code (only touch move events
Conclusion That is, it would be great if touch events had max priority in Godot and would be completely decoupled from how many frames of any kind are rendered, and would not be accumulated in buffers but sent to code as fast as possible without any lag induced in any way (such as waiting for a frame). This is the opposite of what the OP suggests.
So, based on what I've read above and the coupling of touch move events to screen quality AND the Godot's coupling to the rendered rendered frames I'd say the OP is going down a vicious cycle (I didn't look at his source code):
The OP may be causing AND accelerating the FPS drop by spiking the CPU on every frame (on every touch event) resulting in a vicious cycle which eventually results in hitting the max processing capacity of the CPU and thus stuttering. If that's so then he can easily fix his issue (ProMotion displays come with powerful Apple CPUs, don't they?).
Maybe an empty project logging FPS, counting touch move events and profiling CPU usage on a jailbroken device would be needed to prove the OP's case if indeed Godot is taking too much CPU). The CPU profiler in the debugger is useless in his case: when working with iOS I have to detach the debugger in XCode because it is wasting so much CPU (depending on how much CPU processing my game makes) that it causes some SIGUSR1 signal which pauses the app (killing xcode resumes the game, couldn't figure out how to resume otherwise as the game would be killed).
Feel free to diss me.
*only touch move events are buffered (which sucks), other events are sent in immediately (which is good).
*I'm not sure if there is a 10 events touch move buffer on iOS, but if one exists then sending all those events at once would cause the CPU spike depending on what's processed on every frame (I guess one event would be sent for every consecutive frame).
This should be considered a bug in Godot (the buffer should be eliminated if the user isn't listening for gestures - then, a larger refactor should eliminate the coupling of touch events to rendered frames if such a coupling exists).
That is, it would be great if touch events had max priority in Godot and would be completely decoupled from how many frames of any kind are rendered, and would not be accumulated in buffers but sent to code as fast as possible without any lag induced in any way (such as waiting for a frame). This is the opposite of what the OP suggests.
Setting Input.use_accumulated_input = false
in an autoload's _ready()
method will disable input accumulation, so you can receive multiple input events for mouse/touch movement in the same rendered frame. This comes at the cost of increased CPU utilization, particularly if you have slow _input()
methods in scripts. Therefore, input accumulation is enabled by default (it's a good idea to disable it for things like drawing apps).
@Calinou Thank you, you saved me a lot of headache as I needed to measure the time passed between touch move events for some calculation. Also, I am now certain there is a touch move buffer on Android and no such buffer on iOS/Windows/Mac so it is an OS level Android issue.
Also, I am now certain there is a touch move buffer on Android and no such buffer on iOS/Windows/Mac so it is an OS level Android issue.
Agile input event flushing might help (it's only implemented on Android). It's disabled by default -- check the advanced Project Settings.
@Calinou Thanks, its enabled already so that's not it. I've found lots of posts about this Android issue (feature), some call it "touch slop". I've found these docs, I'm still reading about it:
https://developer.android.com/develop/ui/views/touch-and-input/gestures/viewgroup#vc
""Touch slop" refers to the distance in pixels a user's touch can wander before the gesture is interpreted as scrolling. Touch slop is typically used to prevent accidental scrolling when the user is performing another touch operation, such as touching on-screen elements."
If this could be configured out of Godot it would make Godot more consistent accros platforms (and problably properly regulate the rate of touch events on Android).
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