beatoraja icon indicating copy to clipboard operation
beatoraja copied to clipboard

KeyPressedPreferNative: More performat key input

Open phu54321 opened this issue 1 year ago • 7 comments

On windows, Gdx.input.isKeyPressed uses lwjgl's event system for fetching key inputs. This event-based system sometimes lags due to OS issues, so beatoraja had problem getting key state ASAP. When pressing multiple keys, some keys were registered later than other keys.

This commit fixes the issue by utilizing Windows's GetAsyncKeyState function for fetching current keyboard state itself.

  • Before: note that simultaneous keys are not registered simultaneously.
  • After: simultaneous keys are registered simultaneously.

Comparison before & after

Reference: previous libgdx input polling system (from LWJGL2)

Notes:

  • ~~This adds jna to the dependencies.~~ Fixed. No additional dependencies.
  • Whether the code works on mac/linux is not yet tested. Testing is required.

https://github.com/LWJGL/lwjgl/blob/master/src/java/org/lwjgl/opengl/WindowsKeyboard.java

phu54321 avatar Oct 15 '24 02:10 phu54321

Fix) beatoraja now only accept keys when the window is focused.

  • GetAsyncKeyState works regardless of whether the window is focused.
  • Added a check so that the key will input only when the window is focused.

phu54321 avatar Oct 21 '24 14:10 phu54321

I had noticed this issue before, which led me to write this project https://github.com/Merrg1n/beatoraja-ime-fix. After reading the source code, I found that libGDX handles keyboard input events tied to the game loop. This means the actual keyboard polling rate is bound to the game's frame rate. However, the GetAsyncKeyState API only exists on Windows, and more consideration is needed for Mac/Linux (since this project is cross-platform).

Merrg1n avatar Oct 26 '24 04:10 Merrg1n

This means the actual keyboard polling rate is bound to the game's frame rate.

https://github.com/exch-bms2/beatoraja/blob/91d975358619fb0cb9acc3b267897a2872d29ec5/src/bms/player/beatoraja/MainController.java#L363 Polling happens on a separate thread, so polling rate is much faster than the display refresh rate.

Separate consideration is required for macOS/Linux. Every windows-specific aspects are enclosed on the KeyPressedPreferNative class, and support for other platforms also can be encapsulated.

phu54321 avatar Oct 26 '24 04:10 phu54321

Polling happens on a separate thread, so polling rate is much faster than the display refresh rate.

Let me add some implementation details about LibGDX. Even though it appears that keyboard events are polled by another thread in the game, this is actually how it works.

In LibGDX 1.9.9 (the version actually used in this repository), Gdx.input.isKeyPressed() actually calls com.badlogic.gdx.backends.lwjgl.LwjglInput.isKeyPressed(). (In later versions of LibGDX, this call path has changed.)

        public boolean isKeyPressed (int key) {
		if (!Keyboard.isCreated()) return false;

		if (key == Input.Keys.ANY_KEY)
			return pressedKeys > 0;
		else
			return Keyboard.isKeyDown(getLwjglKeyCode(key));
	}

This method actually calls org.lwjgl.input.Keyboard.isKeyDown(), and then attempts to retrieve a key from the keyDownBuffer.

    public static boolean isKeyDown(int key) {
        synchronized(OpenGLPackageAccess.global_lock) {
            if (!created) {
                throw new IllegalStateException("Keyboard must be created before you can query key state");
            } else {
                return keyDownBuffer.get(key) != 0;
            }
        }
    }

So next, we should focus on where the keyDownBuffer is updated. It's actually updated in org.lwjgl.input.Keyboard.poll(), This method calls org.lwjgl.opengl.InputImplementation.pollKeyboard(), which is implemented by platform-specific classes. On Windows, it's implemented in org.lwjgl.opengl.WindowsDisplay, and ultimately, it calls GetAsyncKeyState API.

    public static void poll() {
        synchronized(OpenGLPackageAccess.global_lock) {
            if (!created) {
                throw new IllegalStateException("Keyboard must be created before you can poll the device");
            } else {
                implementation.pollKeyboard(keyDownBuffer);
                read();
            }
        }
    }

It seems like it should work correctly, but the Keyboard.poll() method is actually called within org.lwjgl.opengl.Display.pollDevices(), which is then invoked by org.lwjgl.opengl.Display.processMessages(). Finally, this brings us back to LibGDX, where it is called in com.badlogic.gdx.backends.lwjgl.LwjglApplication.mainLoop(), So, in reality, the keyboard event is polled within the main game loop.

	void mainLoop () {
                // ...
		graphics.lastTime = System.nanoTime();
		boolean wasActive = true;
		while (running) {
			Display.processMessages();
                        // ...
                }
       }

LwjglApplication.mainLoop() includes the actual rendering loop, so the polling of keyboard events is tied to the game’s rendering loop. As a result, the actual keyboard polling rate is linked to the game's frame rate.

Merrg1n avatar Oct 26 '24 05:10 Merrg1n

Wow, awesome description :) I haven't noticed that behavior, but it seems very plausible. Probably I'll need to test it too!

My PR could also help mitigate that, as my code doesn't rely on the lwjgl's event loop system.


Minor corrections: On WindowKeyboard.poll, this code fills up the keyDownBuffer. https://github.com/LWJGL/lwjgl/blob/2df01dd762e20ca0871edb75daf670ccacc89b60/src/java/org/lwjgl/opengl/WindowsKeyboard.java#L80C3-L80C38

key_down_buffer is filled by handleKey function, which is in turn called in response to WM_KEYDOWN and WM_KEYUP events. https://github.com/LWJGL/lwjgl/blob/2df01dd762e20ca0871edb75daf670ccacc89b60/src/java/org/lwjgl/opengl/WindowsDisplay.java#L892

So left and right shift keys are handled with GetAsyncKeyState, but others are handled by Win32 event system. This explains why simultaneous keys were not able to be processed simultaneously.

phu54321 avatar Oct 26 '24 07:10 phu54321

Confirmed...

Note the quantized timing graph (bottom right) when playing 180 BPM songs with 60Hz monitor vsynced game.

20241026_161914_mpc-be64_bDrk9Pclgm

phu54321 avatar Oct 26 '24 07:10 phu54321

OK everything else is quite optimized, and if I replace the polling thread's Thread.sleep to Thread.yield. I can easily achieve 8k polling rate. I'm not sure whether this is the right direction.

Example implementation.

Thread polling = new Thread(() -> {
	for (;;) {
		input.poll();
		Thread.yield();
	}
});

phu54321 avatar Oct 26 '24 14:10 phu54321