Conversation
|
Can you provide jar file? I really want to test this. |
|
Join ED discord and ping me (or DM at |
f50998f to
52ac793
Compare
seraxis
left a comment
There was a problem hiding this comment.
This looks good. Waiting on tests and bugfixes as discussed internally.
Vsync doesn't really ensure evenly timed `MainController::render()` calls. They jitter, and they jitter a lot. More than you'd expect. (You can measure it yourself) This commit adds `TimeSmoother` that *smooths* jittered time when vsync is enabled. If we don't have vsync, It is expected to have frame drops or changes in render() call interval, so `TimeSmoother` is not enabled when vsync is off. feat(VsyncSmoother): utilize current display Hz feat(VsyncSmoother): prevent double smoothing of time
52ac793 to
002852b
Compare
|
Brilliant stuff. I can vouch for the accuracy of this implementation as it's basically identical to what Zenith does. |
|
the problem continues due input dropped on MIDI keyboard |
|
What's the status of this? |
|
I need to fix bugs that were discovered in testing, and I intend to add config options to let the player select which adjustment options they prefer, since it turned out on some systems these changes do cause issues. So instead of testing all possible configurations to try and find something that works for everyone, we'll instead go with reasonable defaults and let the player disable these features. |

An effort to investigate and fix beatoraja's long-standing timing issues.
Input polling frequency
I have found that, as has been suggested by some players, input processing was indeed synchronized to the game's framerate, and not running at 1000Hz as the intent of the polling implementation would suggest. This is the case for all platforms, and both controller as well as keyboard.
The reason for this differs slightly between keyboard and controller.
MainControllerspawns a thread that callsBMSPlayerInputProcessor.poll()at a high frequency, which in turn usesKeyBoardInputProcesseor.poll()andBMControllerInputProcessor.poll()to update the input device state.However, the keyboard state that
KeyBoardInputProcesseorchecks withGdx.input.isKeyPressedis only internally updated byGLFW.glfwPollEvents();, which is only called in the main thread, in aLwjgl3Applicationmain loop with the game's render calls.Similarly,
BMControllerInputProcessor.pollcan only read whatever state has been set by the polling function spawned byLwjgl3ControllerManager's constructor, which itself usedLwjgl3Application.postRunnableto synchronize its polling with the render loop.(Previously, playing a song with a BPM that aliases to the framerate would produce obvious input grouping)
Solution
Moving
GLFWevent and input device polling to their own thread isn't possible - according to its documentation, all of those functions may only be called from the application's main thread.It is technically possible to configure
Lwjgl3Applicationto use non-continuous rendering, which seemingly would allow polling more frequently in between the render calls, but this would still suffer from timing issues under VSync or with slower rendering.Short of entirely replacing our windowing or input device backends, the only solution I've found that seemed viable is moving rendering to its own thread, allowing polling to continue at a high rate on the main thread.
This required modifying
Lwjgl3Application- I've adapted it asParallelApplication, and for ease of review I include a diff between the two classes at the end of this PR's description. It spawns the rendering thread after window creation, passing it the ownership of the OpenGL context. Analogously topostRunnable, it provides apostPollRunnablefunction that allows scheduling code to run in the polling loop of the main thread, which I've limited to run at 1kHz.Lwjgl3ControllerManagernow polls for controller state at high rates usingpostPollRunnable, while keyboard input naturally became more responsive with no further changes.In order to make window resizing work properly with this new arrangement,
MainLoadercallswhich configures our
Lwjgl3Graphicsbackend to synchronize the window resize code with render calls (doing it on the main thread would cause race conditions and wouldn't work without the GL context, which is exclusive to the rendering thread).Unfortunately, despite several attempts, I was unable to get any of this to work on MacOS. Instead, I've specialized
ParallelApplication's behaviour on MacOS to run rendering and polling serially, as it originally did. Until we find a solution to this, Mac players will simply need to run the game at high framerates to reach an appropriate input polling frequency.VSync frame instability
My testing of beatoraja's VSync showed that the frame timings produced by it an exhibit an alternating pattern - for example, synchronization to 60Hz could result in alternating ~14ms and ~19ms frames, which always correctly adds up to the expected average of 16.66ms, but ends up producing visible stuttering in the movement of notes during play.
After trying out several approaches, as the solution I have decided to adopt - with its author's permission - the upstream PR #835, which augments the time delta values used by skin rendering to significantly improve the apparent smoothness of note motion. This change only takes effect with VSync on, and is only a visual improvement, with no changes to the game's logic.
Other changes
I've experimentally included a change to how the main
TimerManagerobject'supdate()is called, moving it to the polling thread, which should reduce judge processing latency and allow keysounds to no longer by synchronized to rendering. Requires further testing to make sure that this doesn't cause any issues.I've slightly refactored ImGui, making it so that its rendering functions are only called when there are notifications to be displayed or the mod menu is open. Saves about ~100μs during rendering, which is minor but I thought we have no reason not to do this.
Garbage Collector
I've briefly tested the various GC algorithms provided by the JVM, and found that the default
G1garbage collector nearly always causes untolerably long pauses during play - frequently as high as 20 or 30 milliseconds, and in some rare cases even higher. These are easily long enough to be very noticeable during play.In contrast, the latency-focused
ZGCandShenandoahgarbage collectors both provided much more stable performance. My testing was not rigorous enough to be able to tell much of a difference between the performance of the two, but both are clearly far better thanG1.This is unfortunate in that currently, Endless Dream has no control over the JVM's launch settings, and the widely used launch scripts do not change the default garbage collector, which is nearly certain to cause stuttering during play. For now, the best we can do is actively inform users to update their scripts to include either
-XX:+UseZGCor-XX:+UseShenandoahGC.Future work might allow us to have more control over how the game is launched, and we could also try to minimize how much memory is allocated during play in the first place to allow the GC to do less work.
For now, I've included a call to
System.gc()(which does induce a long pause, up to 100ms) at play scene startup in hopes that it will minimize some of the stutter that seems to be frequent early in a song.Needs testing
I've done all of the testing exclusively on my personal Linux machines, and the proposed changes are major. We'll need lots of testing to find any possible issues and to verify that the changes work on other platforms.