Skip to content

Comments

[Windows,Linux] [Input] Return the keyboard ID for raw input#110114

Open
knn217 wants to merge 1 commit intogodotengine:masterfrom
knn217:Return-raw-keyboard-ID-on-Windows
Open

[Windows,Linux] [Input] Return the keyboard ID for raw input#110114
knn217 wants to merge 1 commit intogodotengine:masterfrom
knn217:Return-raw-keyboard-ID-on-Windows

Conversation

@knn217
Copy link

@knn217 knn217 commented Aug 30, 2025

Based on #13083

  • Return the keyboard ID from raw input in InputEventKey as device
  • When adding Input mapping, the Input events's device will be normalized to ALL_DEVICE,
  • Normalizing to a default device value for input map id safer, since raw input's id can change in runtime by unplugging/plugging back the keyboard
  • If users wish to separate by keyboard ID, they need to get the event 's device to tell which keyboard the event is from, and handle hot-remapping if the ID was changed in runtime
  • Added in 2nd commit: The returned device is now ordered starting from 0, and registered by plugin/input. Unplugging a keyboard will leave its device open for a new keyboard to register to

Test with GDScript:

extends Node

func _input(event):
	if event is InputEventKey and event.pressed and not event.echo:
		print("Pressed: ", OS.get_keycode_string(event.keycode), "| ID: ", event.device)
image

Test with GDExtension:

void App::_input(const godot::Ref<godot::InputEvent> &event)
{
    godot::Ref<godot::InputEventKey> key_event = event;
    if (key_event.is_valid()){
        if (key_event->is_pressed() && !key_event->is_echo()) {
            godot::String key_name = godot::OS::get_singleton()->get_keycode_string(key_event->get_keycode());
            std::int64_t keyboard_id = key_event->get_device();
            godot::UtilityFunctions::print("Pressed: ", key_name, " | ID: ", keyboard_id);
        }
    }
}
image image

Test input mapping:
Tested action created from GDScript, with an event for pressing key right, the action triggered with multiple keyboard ids (0, 1)

extends Node2D

func _ready():
	InputMap.add_action("ui_right_custom")
	var event := InputEventKey.new()
	event.physical_keycode = KEY_RIGHT
	event.pressed = true
	event.echo = false
	InputMap.action_add_event("ui_right_custom", event)

func _process(_delta):
	if Input.is_action_pressed("ui_right_custom", false):
		print("GDScript moving right")

func _input(event):
	if event is InputEventKey:
		if not event.echo:
			if event.pressed:
				print("GDScript Pressed: ", OS.get_keycode_string(event.keycode), "| ID: ", event.device)
			else:
				print("GDScript Released: ", OS.get_keycode_string(event.keycode), "| ID: ", event.device)

image

Tested with an action from the input map setting

extends Node2D

func _process(_delta):
	if Input.is_action_pressed("ui_left_custom", false):
		print("GDScript moving left")

func _input(event):
	if event is InputEventKey:
		if not event.echo:
			if event.pressed:
				print("GDScript Pressed: ", OS.get_keycode_string(event.keycode), "| ID: ", event.device)
			else:
				print("GDScript Released: ", OS.get_keycode_string(event.keycode), "| ID: ", event.device)

image image

Test on Linux X11 (Ubuntu 24.04):
image

@knn217 knn217 requested review from a team as code owners August 30, 2025 13:45
return physical_keycode;
}

void InputEventKey::set_keyboard_id(int64_t p_id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make more sense to have device_id (or use it with existing device, but this will require more changes) in the base InputEvent, the same can be used for mouse as well.

For the reference, native device ID on macOS can be obtained from keyboard/mouse events, using the following:

int64 _get_device_id(NSEvent *p_event) {
	return CGEventGetIntegerValueField([p_event CGEvent], /*kCGEventRegistryID*/ CGEventField(87));
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense.
Should've look deeper into the parents.
Do I need to wait until check flows are completed to push?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to wait until check flows are completed to push?

No, it will cancel currently running tasks if you push.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated to use device instead, thanks for the review

@bruvzg
Copy link
Member

bruvzg commented Aug 30, 2025

Simply using set_device will likely break a lot of stuff, input map actions default to device 0 when matching actions, so won't work with any other ID, and probably should be changed to default to -1 (all devices, which might conflict with DEVICE_ID_EMULATION with is also -1).

diff --git a/core/input/input_event.h b/core/input/input_event.h
index 62b89bf0fd..6fc31b802f 100644
--- a/core/input/input_event.h
+++ b/core/input/input_event.h
@@ -52,7 +52,7 @@ class Shortcut;
 class InputEvent : public Resource {
        GDCLASS(InputEvent, Resource);

-       int device = 0;
+       int device = -1; // ALL_DEVICES

 protected:
        bool canceled = false;

@AThousandShips AThousandShips added this to the 4.x milestone Sep 1, 2025
@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch 3 times, most recently from b302b38 to bf40ac8 Compare September 1, 2025 14:33
@knn217
Copy link
Author

knn217 commented Sep 1, 2025

I looked into InputMap and it seems safer to only use ALL_DEVICE for input mapping, since keyboard ID can change when user unplug -> plug keyboard back in, which invalidates the mapping if we use keyboard ID (device)
For now, I normalize InputEventKey when it is added/searched in input map

probably should be changed to default to -1 (all devices, which might conflict with DEVICE_ID_EMULATION with is also -1).

I made the change and did not find any issue.

@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch from bf40ac8 to b4613af Compare September 1, 2025 16:03
@knn217 knn217 requested a review from a team as a code owner September 1, 2025 16:03
@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch from b4613af to f46c9a3 Compare September 6, 2025 01:19
@Gnumaru
Copy link
Contributor

Gnumaru commented Oct 2, 2025

@knn217 In your first screenshot there are three diferent keyboard ids. Is this because you really plugged 3 different keyboards to test, or perhaps windows simply did changed the keyboard id in between keystrokes?

Also, does the windows api expose some way of knowing the device ordering? to know what is the first, the second, the third keyboard and os on? the device ids for joypads that godot returns are always ordered starting from zero, so that if you have 4 joypads they will have device ids 0, 1, 2 and 3. Is it possible to at least emulate this with keyboards? even if the OSs api don't tell you the keyboard ordering, couldn't we store a dictionary mapping the raw keyboard ids returned by the OS into a zero based ordered id? even if we just make up this ordering ourselves? for example, if windows returns ids 687, -94601 and 12687 whe order them ascendingly and give them the ids 0 (for -94601), 1 (for 687) and 2 (for 12687).

@knn217
Copy link
Author

knn217 commented Oct 2, 2025

@Gnumaru

@knn217 In your first screenshot there are three diferent keyboard ids. Is this because you really plugged 3 different keyboards to test, or perhaps windows simply did changed the keyboard id in between keystrokes?

Windows can change the keyboard ID (handle), but only when user unplug -> plug keyboard back in. The ID stays the same while plugged in though.
I have updated the test to show this, by unpluging the keyboard, I was able to change the keyboard ID 10 times

Also, does the windows api expose some way of knowing the device ordering? to know what is the first, the second, the third keyboard and os on? the device ids for joypads that godot returns are always ordered starting from zero, so that if you have 4 joypads they will have device ids 0, 1, 2 and 3

I have not found any API like that.

Is it possible to at least emulate this with keyboards? even if the OSs api don't tell you the keyboard ordering, couldn't we store a dictionary mapping the raw keyboard ids returned by the OS into a zero based ordered id?
Like I have shown before, after unplugged, Windows assigns a new ID to the keyboard when plugged back in, making the previous ID deprecated.

I think it's possible to emulate this behavior, by using this API:
https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-input-device-change
I have not used this myself but this seems to provide notifications for whenever a device is plugged/unplugged

Here's how I understand the flowchart should be implemented from your description:

  • device plugged in -> Add to map { "raw kb id": "lowest available slot" } ->update "lowest available slot"
  • device unplugged -> Remove from map at key "raw kb id" ->update "lowest available slot"
  • If a key is pressed, contains a raw ID not existed in map -> insert it the same as "device plugged in"

The last step is to solve the issue with laptop's keyboards being always plugged in, and also any keyboards that are already plugged in.

@knn217
Copy link
Author

knn217 commented Oct 9, 2025

I have added another commit to support:

  • Return incremental ID for raw keyboards, starting from 0
  • Register ID by plugging in the keyboard or pressing a key
  • Unregister by unplugging the keyboard

Test:

  • I have updated the test cases to match this. I also tested unplug->plugin again, the keyboard was able to regain its device ID before unplug, so in the test the ID never get above 2 (I have 2 keyboards plugged in, and the laptop's keyboard).
  • The only issue with this approach that I can think of is that the device IDs are reset each time the app closes (since they are managed in a map), so they need to be registered again.
  • I have no joypads, so I'm not sure if this is the same as how joypads work. Does godot have a way to memorize the joypads' registered order in previous process?

*NOTE:

  • I didn't find any Win32 API that returns the device ordering, so it needs to be handled by Godot.
  • I tried GetRawInputDeviceList, but the ordering of the device list can be changed by unplugging a keyboard. So unplugging keyboard A will affect the index of keyboard B in the list. The order is also unclear to me, it does not follow the order of plugging in.
  • Here's the test branch: https://github.com/knn217/godot/tree/raw-keyboard-id-test-branch

@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch 2 times, most recently from 7ec48dc to 4723971 Compare October 11, 2025 17:17
@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch from 4723971 to f444993 Compare October 26, 2025 03:27
@knn217 knn217 requested a review from a team as a code owner October 26, 2025 03:27
@knn217
Copy link
Author

knn217 commented Oct 26, 2025

I have added another commit to support:

  • Adding the same functionality on Linux (only X11, Wayland doesn't have support for this as far as I know)
  • Register ID is now only by pressing a key on a keyboard, NOT plugging in.
  • I removed registering by plugging since on Linux since a lot of devices showed up as keyboard. I couldn't find a way to differentiate the actual physical keyboard from the members in XIDeviceInfo
  • I also removed registering by plugging on Windows to sync the functionality with Linux. Now on register by key press is supported

Future enhancement

  • Wayland: Currently, Wayland doesn't support distinguishing key-presses based on device. Maybe in the future they will consider it
  • Mac: I don't have a Mac so I will leave it to another contributor. I have considered virtual machine, but on VMs, it is not guaranteed to work exactly as an actual Mac, since this is related to raw keyboard inputs.

Here's the test on Linux X11 (Ubuntu 24.04):
image

@deralmas
Copy link
Contributor

Currently, Wayland doesn't support distinguishing key-presses based on device. Maybe in the future they will consider it

Hi, Wayland should support this behavior already. Compositors can expose multiple wl_seat globals, each with a different keyboard, mouse, and touchscreen. Note though that some compositors (e.g. sway) merge multiple devices under a single seat unless the user says otherwise. This is not mandatory according to the spec, it's just compositor-specific policy.

@knn217
Copy link
Author

knn217 commented Oct 30, 2025

Hi, Wayland should support this behavior already. Compositors can expose multiple wl_seat globals, each with a different keyboard, mouse, and touchscreen. Note though that some compositors (e.g. sway) merge multiple devices under a single seat unless the user says otherwise. This is not mandatory according to the spec, it's just compositor-specific policy.

I see, but is there a way to make the compositor expose seats from Godot? Compositors are outside of application's control as I understand.

@deralmas
Copy link
Contributor

deralmas commented Nov 1, 2025

I see, but is there a way to make the compositor expose seats from Godot? Compositors are outside of application's control as I understand.

Hi, I'm not really sure what you mean. Godot can't play with input settings as they're non-standard and outside of the scope of the Wayland protocol. I also don't know how other compositors outside of sway handle it, they could as well make a new seat every time, it's up to them.

If the compositor is configured (by default or by the user) to expose multiple input devices, godot will be able to detect and use all of them. In fact, we already do that, we already have logic for handling multiple input devices as we're required to do that.

@knn217
Copy link
Author

knn217 commented Nov 14, 2025

Support for Wayland seems too complicated for me currently, so I will leave the PR here for now.
Maybe Wayland support can be added in another PR.

If the compositor is configured (by default or by the user) to expose multiple input devices, godot will be able to detect and use all of them. In fact, we already do that, we already have logic for handling multiple input devices as we're required to do that.

@deralmas
I'm still new to Godot so I'm not sure what this logic is for. Please let me know what feature this is and how to set it up. I would like to play around with this first to see how Godot handle it and what compositor config Godot is expecting.
Thank you!

@deralmas
Copy link
Contributor

Support for Wayland seems too complicated for me currently, so I will leave the PR here for now.
Maybe Wayland support can be added in another PR.

Yeah that's fine, don't worry about that :D

I'm still new to Godot so I'm not sure what this logic is for. Please let me know what feature this is and how to set it up. I would like to play around with this first to see how Godot handle it and what compositor config Godot is expecting.
Thank you!

Hi. To be clear, Wayland works this way but we don't expose this distinction to the Godot game, since there are no public Godot APIs. I can make a rough overview of the internal Wayland communication if you'd like.

Regarding config, as I said I only know about sway, which has commands like seat <name> attach <input_identifier>: https://man.archlinux.org/man/sway-input.5#SEAT_CONFIGURATION , which when done correctly will spawn multiple cursors (each with its own focus) at the compositor level, a bit like this example with Weston (another Wayland compositor): https://www.youtube.com/watch?v=WO2L_ihO_rI

Let me know if you need more info, I'm glad to help.

@knn217
Copy link
Author

knn217 commented Nov 28, 2025

@deralmas
I have tested on sway, and this PR's current commit can still return the keyboard device id

Here's the test:
godot

GDscript for test:

extends Node

func _input(event):
	if event is InputEventKey and event.pressed and not event.echo:
		print("Pressed: ", OS.get_keycode_string(event.keycode), "| ID: ", event.device)

Here's what I added in my sway config:

# Seat 1 = Logitech external keyboard (attach all parts of the same device)
seat seat1 attach "1133:49948:Logitech_USB_Keyboard"
seat seat1 attach "1133:49948:Logitech_USB_Keyboard_System_Control"
seat seat1 attach "1133:49948:Logitech_USB_Keyboard_Consumer_Control"

Here's the seats from: swaymsg -t get_seats

Seat: seat1
  Capabilities: 2
  Devices:
    Logitech USB Keyboard Consumer Control
    Logitech USB Keyboard System Control
    Logitech USB Keyboard

Seat: seat0
  Capabilities: 3
  Devices:
    HP WMI hotkeys
    Wireless hotkeys
    AT Translated Set 2 keyboard
    SYNA32D8:00 06CB:CEE7 Touchpad
    SYNA32D8:00 06CB:CEE7 Mouse
    Logitech USB Optical Mouse
    Lid Switch
    Power Button
    Video Bus

Does it work on your env?

@deralmas
Copy link
Contributor

Hi @knn217, are you using the Wayland backend? You might be running under XWayland, which is the default right now.

You can change the backend with the editor setting "Prefer Wayland" for the editor, the "display server" project setting for the running game or simply by running the engine binary with the --display-driver wayland option.

@knn217
Copy link
Author

knn217 commented Nov 28, 2025

@deralmas
I checked the session type, it's Wayland:
wl

I also used --display-driver wayland and the app window did look a bit different, but the test result is still the same, it can still distinguish keyboard if compositor config support multiple seats

wayland

Please help me test this on your local if possible.

@deralmas
Copy link
Contributor

Hi, sorry for the delay. I think that the project might be still running under X11. swaymsg -t get_tree should output the window tree, with the respective type (xdg_shell for Wayland, xwayland for X11).

Note that passing that flag will only affect the editor, unless you run a game directly from the command line. To run a project under Wayland, you'll need to change the linuxbsd display driver property in the project settings. They are separated to allow a developer to use Wayland while exporting to X11, for stability reasons.

@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch from f444993 to 4f1bd1c Compare January 25, 2026 10:09
@knn217 knn217 requested a review from a team as a code owner January 25, 2026 10:09
@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch from 4f1bd1c to 4641836 Compare January 25, 2026 10:20
@knn217
Copy link
Author

knn217 commented Jan 25, 2026

@deralmas Thanks for the help, I was able to run the project under wayland now

I have updated the code and it now works on both X11 and Wayland. Here's swaymsg -t get_tree to check the project godot_project (DEBUG):

  • X11 display driver project has xwayland:
3333
  • Wayland display driver project has xdg_shell:
4444

The code for indexing device id is in InputEvent, and the current implementation hides the raw device ID from users, it only return the index of the device (Since I'm not sure if its safe to expose the raw ID)

@deralmas
Copy link
Contributor

Awesome!

The code for indexing device id is in InputEvent, and the current implementation hides the raw device ID from users, it only return the index of the device (Since I'm not sure if its safe to expose the raw ID)

Depends on what you mean by "safe". It's just a int, users can't do much with it. Now, if the desired API must have sequential IDs then yes, you indeed need to do some housekeeping.

That said from the Wayland side of things I can say that using the name of each wl_seat object as an ID sounds perfectly fine :D

API-wise I wonder, if the raw ID is hidden from the user, why expose the relevant methods? I think that you can simply keep them internal by not binding them in ClassDB. It becomes an implementation detail FWIW.

For everything else I'd need to make a proper review, so I can't vouch for the approach in its entirety.

@knn217
Copy link
Author

knn217 commented Jan 25, 2026

API-wise I wonder, if the raw ID is hidden from the user, why expose the relevant methods?

Good point, I will remove them in the next push, when resolving PR reviews. This also remind me that get_active_devices does reveal the raw device id, I forgot to remove this function

…from multiple keyboards (seats in Wayland case)
@knn217 knn217 force-pushed the Return-raw-keyboard-ID-on-Windows branch from 4641836 to 45b3967 Compare January 26, 2026 13:22
@Nintorch Nintorch changed the title [Windows] [Input] Return the keyboard ID for raw input [Windows and Linux (Wayland/X11)] [Input] Return the keyboard ID for raw input Feb 17, 2026
@Nintorch Nintorch changed the title [Windows and Linux (Wayland/X11)] [Input] Return the keyboard ID for raw input [Windows,Linux] [Input] Return the keyboard ID for raw input Feb 17, 2026
@Nintorch
Copy link
Contributor

Hello! We recently introduced separate device IDs for keyboard and mouse (see #116274 ), and I think it might make sense for this PR to use IDs 16-31 for keyboards. What do you think? :)

protected:
bool canceled = false;
bool pressed = false;
static inline AHashMap<int, bool> device_index; // bool is to mark a device ID as inactive, available for overwriting
Copy link
Contributor

@Nintorch Nintorch Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the new functionality in this PR should be inside of Input class instead of InputEvent, since joypads are also registered inside Input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a way to get raw keyboard input ID on Windows, to separate different keyboards

7 participants