PlayCover / PlayTools

Tools for keymapping, dynamic resolution, and more
GNU Affero General Public License v3.0
71 stars 56 forks source link

Fix controller input lag or unresponsive in some games #162

Closed XuYicong closed 4 months ago

XuYicong commented 4 months ago

Fixes Playcover/Playcover/issues/1550

The issue is basically the same as #83. Testing and analysis shows that some games rely on GameController APIs (which in turn relies on main dispatch queue) to process controller input. But main dispatch queue may not be served in time for some reason.

CFRunLoop of the main thread typically drains the main dispatch queue. It eventually calls _dispatch_main_queue_callback_4CF() to do its work. In PlayTools, by calling this function every frame, the input can be processed timely. This is done via the CADisplayLink, which is a timer that gets called every frame, originally designed for handling UI drawing routines.

With this fix, I have played ZZZ with an XBox controller on my M1 pro Macbook, with ProMotion on, low power mode on, keymapping on, game in fullscreen and game fps set to 120Hz, for 4 hours and didn't encounter any input lag or unresponsiveness.

wasd96040501 commented 4 months ago

How did you figured out this. Awesome!

XuYicong commented 4 months ago

How did you figured out this

I was mainly analyzing the game's call stacks by sampling the running game. And I found some calls related to GameController framework, but I don't have sufficient evidence to say that's the reason of the input issue. I kinda guessed things and tested a fix, and it just works. So I assume my guesses are right.

Call stack of play input draining main queue

This is the call stack after applying this fix. The main queue executed a block that seems to be recording current controller input state (the [_GCDevicePhysicalInputGroup handleGamepadEvent:] one). Although the handling here doesn't pass the recorded data into the game's private functions, I assume the data is stored somewhere internally, and the game is actively polling it every frame, in the UnityFramework calls:

Unity framework access controller button

If we search for the word "GameController" in the sample plain text, we can find some calls like [GCControllerButtonInput isPressed] under the UnityFramework call, which gets executed every frame. I think it actually polls more than just ControllerButton, but those calls happen so fast that the sample didn't capture them. If you sample it multiple times there are chances to capture it polling other controller keys like thumbsticks.

So here is the guess: Although UnityFramework polls the controller state every frame, it's not actually checking the hardware state, but rather checking a controller state stored inside GameController. GameController just updates its stored state in a routine executed in main dispatch queue, which may not get called in some cases. So UnityFramework may be checking every frame on an outdated state.

The reason I suspect main dispatch queue is from the experience of #83. The controller symptoms now are very similar to the keymapping symptoms then.

To verify this hypothesis, I just wrote this fix and tested it. I was actually prepared for the fix to fail, so that I can prove a hypothesis wrong quickly, but it seems to work. I'm even not sure whether or not it works in the way I think it is.