diff --git a/src/xenia/CMakeLists.txt b/src/xenia/CMakeLists.txt index 80092001f..0efc5fc55 100644 --- a/src/xenia/CMakeLists.txt +++ b/src/xenia/CMakeLists.txt @@ -58,4 +58,6 @@ if(WIN32) add_subdirectory(hid/winkey) add_subdirectory(hid/xinput) add_subdirectory(ui/d3d12) +else() + add_subdirectory(hid/keyboard) endif() diff --git a/src/xenia/app/CMakeLists.txt b/src/xenia/app/CMakeLists.txt index e7d397a41..005c5f7bc 100644 --- a/src/xenia/app/CMakeLists.txt +++ b/src/xenia/app/CMakeLists.txt @@ -105,6 +105,7 @@ if(WIN32) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(xenia-app PRIVATE xenia-apu-alsa + xenia-hid-keyboard X11 xcb X11-xcb @@ -113,6 +114,7 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif(APPLE) target_link_libraries(xenia-app PRIVATE xenia-gpu-metal + xenia-hid-keyboard xenia-ui-metal ) endif() diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index 8f8d1e9d3..4bc271e9a 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -63,6 +63,9 @@ #if !XE_PLATFORM_ANDROID #include "xenia/hid/sdl/sdl_hid.h" #endif // !XE_PLATFORM_ANDROID +#if !XE_PLATFORM_WIN32 +#include "xenia/hid/keyboard/keyboard_hid.h" +#endif #if XE_PLATFORM_WIN32 #include "xenia/hid/winkey/winkey_hid.h" #include "xenia/hid/xinput/xinput_hid.h" @@ -301,7 +304,19 @@ class EmulatorApp final : public xe::ui::WindowedApp { Args... args) { std::vector> instances; - if (name != "winkey") { + // Drivers that are always loaded as a keyboard fallback alongside + // whatever the user selected via `name`. winkey on Windows; keyboard + // on Linux/macOS. Only one is registered on any given build. + static constexpr std::string_view kAlwaysOn[] = {"winkey", "keyboard"}; + + auto is_always_on = [&](std::string_view n) { + for (auto a : kAlwaysOn) { + if (n == a) return true; + } + return false; + }; + + if (!is_always_on(name)) { auto it = std::find_if( creators_.cbegin(), creators_.cend(), [&name](const auto& f) { return name.compare(f.name) == 0; }); @@ -314,13 +329,15 @@ class EmulatorApp final : public xe::ui::WindowedApp { } } - auto it = std::find_if( - creators_.cbegin(), creators_.cend(), - [&name](const auto& f) { return f.name.compare("winkey") == 0; }); - if (it != creators_.cend() && (*it).is_available()) { - auto instance = (*it).instantiate(std::forward(args)...); - if (instance) { - instances.emplace_back(std::move(instance)); + for (auto always : kAlwaysOn) { + auto it = std::find_if( + creators_.cbegin(), creators_.cend(), + [&always](const auto& f) { return f.name.compare(always) == 0; }); + if (it != creators_.cend() && (*it).is_available()) { + auto instance = (*it).instantiate(std::forward(args)...); + if (instance) { + instances.emplace_back(std::move(instance)); + } } } return instances; @@ -530,6 +547,9 @@ std::vector> EmulatorApp::CreateInputDrivers( #if !XE_PLATFORM_ANDROID factory.Add("sdl", xe::hid::sdl::Create); #endif // !XE_PLATFORM_ANDROID +#if !XE_PLATFORM_WIN32 + factory.Add("keyboard", xe::hid::keyboard::Create); +#endif #if XE_PLATFORM_WIN32 // WinKey input driver should always be the last input driver added! factory.Add("winkey", xe::hid::winkey::Create); diff --git a/src/xenia/hid/hid_demo.cc b/src/xenia/hid/hid_demo.cc index c74935aea..62fe33d02 100644 --- a/src/xenia/hid/hid_demo.cc +++ b/src/xenia/hid/hid_demo.cc @@ -40,6 +40,9 @@ #if !XE_PLATFORM_ANDROID #include "xenia/hid/sdl/sdl_hid.h" #endif // !XE_PLATFORM_ANDROID +#if !XE_PLATFORM_WIN32 +#include "xenia/hid/keyboard/keyboard_hid.h" +#endif #if XE_PLATFORM_WIN32 #include "xenia/hid/winkey/winkey_hid.h" #include "xenia/hid/xinput/xinput_hid.h" @@ -132,6 +135,13 @@ std::vector> HidDemoApp::CreateInputDrivers( drivers.emplace_back(std::move(driver)); } #endif // !XE_PLATFORM_ANDROID +#if !XE_PLATFORM_WIN32 + } else if (cvars::hid.compare("keyboard") == 0) { + auto driver = xe::hid::keyboard::Create(window, kZOrderHidInput); + if (driver && XSUCCEEDED(driver->Setup())) { + drivers.emplace_back(std::move(driver)); + } +#endif #if XE_PLATFORM_WIN32 } else if (cvars::hid.compare("winkey") == 0) { auto driver = xe::hid::winkey::Create(window, kZOrderHidInput); @@ -151,6 +161,12 @@ std::vector> HidDemoApp::CreateInputDrivers( drivers.emplace_back(std::move(sdl_driver)); } #endif // !XE_PLATFORM_ANDROID +#if !XE_PLATFORM_WIN32 + auto keyboard_driver = xe::hid::keyboard::Create(window, kZOrderHidInput); + if (keyboard_driver && XSUCCEEDED(keyboard_driver->Setup())) { + drivers.emplace_back(std::move(keyboard_driver)); + } +#endif #if XE_PLATFORM_WIN32 auto xinput_driver = xe::hid::xinput::Create(window, kZOrderHidInput); if (xinput_driver && XSUCCEEDED(xinput_driver->Setup())) { diff --git a/src/xenia/hid/keyboard/CMakeLists.txt b/src/xenia/hid/keyboard/CMakeLists.txt new file mode 100644 index 000000000..e7ddb6b15 --- /dev/null +++ b/src/xenia/hid/keyboard/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(xenia-hid-keyboard STATIC) +xe_platform_sources(xenia-hid-keyboard ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(xenia-hid-keyboard PUBLIC xenia-base xenia-hid xenia-ui) +xe_target_defaults(xenia-hid-keyboard) diff --git a/src/xenia/hid/keyboard/keyboard_binding_table.inc b/src/xenia/hid/keyboard/keyboard_binding_table.inc new file mode 100644 index 000000000..4e4db3d1c --- /dev/null +++ b/src/xenia/hid/keyboard/keyboard_binding_table.inc @@ -0,0 +1,38 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2022 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +// This is a partial file designed to be included by other files when +// constructing various tables. + +// clang-format off +XE_HID_KEYBOARD_BINDING(DpadLeft, "DPAD_LEFT" , keybind_dpad_left , "^A" ) +XE_HID_KEYBOARD_BINDING(DpadRight, "DPAD_RIGHT" , keybind_dpad_right , "^D" ) +XE_HID_KEYBOARD_BINDING(DpadDown, "DPAD_DOWN" , keybind_dpad_down , "^S" ) +XE_HID_KEYBOARD_BINDING(DpadUp, "DPAD_UP" , keybind_dpad_up , "^W" ) +XE_HID_KEYBOARD_BINDING(LThumbLeft, "LEFT_THUMB_LEFT" , keybind_left_thumb_left , "_A" ) +XE_HID_KEYBOARD_BINDING(LThumbRight, "LEFT_THUMB_RIGHT" , keybind_left_thumb_right , "_D" ) +XE_HID_KEYBOARD_BINDING(LThumbDown, "LEFT_THUMB_DOWN" , keybind_left_thumb_down , "_S" ) +XE_HID_KEYBOARD_BINDING(LThumbUp, "LEFT_THUMB_UP" , keybind_left_thumb_up , "_W" ) +XE_HID_KEYBOARD_BINDING(LThumbPress, "LEFT_THUMB_PRESSED" , keybind_left_thumb , "F" ) +XE_HID_KEYBOARD_BINDING(RThumbUp, "RIGHT_THUMB_UP" , keybind_right_thumb_up , "0x26") +XE_HID_KEYBOARD_BINDING(RThumbDown, "RIGHT_THUMB_DOWN" , keybind_right_thumb_down , "0x28") +XE_HID_KEYBOARD_BINDING(RThumbRight, "RIGHT_THUMB_RIGHT" , keybind_right_thumb_right, "0x27") +XE_HID_KEYBOARD_BINDING(RThumbLeft, "RIGHT_THUMB_LEFT" , keybind_right_thumb_left , "0x25") +XE_HID_KEYBOARD_BINDING(RThumbPress, "RIGHT_THUMB_PRESSED", keybind_right_thumb , "K" ) +XE_HID_KEYBOARD_BINDING(X, "X" , keybind_x , "L" ) +XE_HID_KEYBOARD_BINDING(B, "B" , keybind_b , "0xDE") +XE_HID_KEYBOARD_BINDING(A, "A" , keybind_a , "0xBA") +XE_HID_KEYBOARD_BINDING(Y, "Y" , keybind_y , "P" ) +XE_HID_KEYBOARD_BINDING(LTrigger, "LEFT_TRIGGER" , keybind_left_trigger , "Q I" ) +XE_HID_KEYBOARD_BINDING(RTrigger, "RIGHT_TRIGGER" , keybind_right_trigger , "E O" ) +XE_HID_KEYBOARD_BINDING(Back, "BACK" , keybind_back , "Z" ) +XE_HID_KEYBOARD_BINDING(Start, "START" , keybind_start , "X" ) +XE_HID_KEYBOARD_BINDING(LShoulder, "LEFT_SHOULDER" , keybind_left_shoulder , "1" ) +XE_HID_KEYBOARD_BINDING(RShoulder, "RIGHT_SHOULDER" , keybind_right_shoulder , "3" ) +// clang-format on diff --git a/src/xenia/hid/keyboard/keyboard_hid.cc b/src/xenia/hid/keyboard/keyboard_hid.cc new file mode 100644 index 000000000..283ebdf6c --- /dev/null +++ b/src/xenia/hid/keyboard/keyboard_hid.cc @@ -0,0 +1,26 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/hid/keyboard/keyboard_hid.h" + +#include "xenia/hid/keyboard/keyboard_input_driver.h" + +namespace xe { +namespace hid { +namespace keyboard { + +std::unique_ptr Create(xe::ui::Window* window, + size_t window_z_order) { + return std::make_unique( + window, window_z_order); +} + +} // namespace keyboard +} // namespace hid +} // namespace xe diff --git a/src/xenia/hid/keyboard/keyboard_hid.h b/src/xenia/hid/keyboard/keyboard_hid.h new file mode 100644 index 000000000..a2fd65662 --- /dev/null +++ b/src/xenia/hid/keyboard/keyboard_hid.h @@ -0,0 +1,28 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_HID_KEYBOARD_KEYBOARD_HID_H_ +#define XENIA_HID_KEYBOARD_KEYBOARD_HID_H_ + +#include + +#include "xenia/hid/input_system.h" + +namespace xe { +namespace hid { +namespace keyboard { + +std::unique_ptr Create(xe::ui::Window* window, + size_t window_z_order); + +} // namespace keyboard +} // namespace hid +} // namespace xe + +#endif // XENIA_HID_KEYBOARD_KEYBOARD_HID_H_ diff --git a/src/xenia/hid/keyboard/keyboard_input_driver.cc b/src/xenia/hid/keyboard/keyboard_input_driver.cc new file mode 100644 index 000000000..68b988738 --- /dev/null +++ b/src/xenia/hid/keyboard/keyboard_input_driver.cc @@ -0,0 +1,411 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/hid/keyboard/keyboard_input_driver.h" + +#include + +#include "xenia/base/logging.h" +#include "xenia/hid/hid_flags.h" +#include "xenia/hid/input_system.h" +#include "xenia/ui/virtual_key.h" +#include "xenia/ui/window.h" + +#define XE_HID_KEYBOARD_BINDING(button, description, cvar_name, \ + cvar_default_value) \ + DEFINE_string(cvar_name, cvar_default_value, \ + "List of keys to bind to " description \ + ", separated by spaces", \ + "HID.Key") +#include "keyboard_binding_table.inc" +#undef XE_HID_KEYBOARD_BINDING + +DEFINE_int32(keyboard_mode, 0, + "Allows user do specify keyboard working mode. Possible values: 0 " + "- Disabled, 1 - Enabled, 2 - Passthrough. Passthrough requires " + "controller being connected!", + "HID"); + +DEFINE_int32( + keyboard_user_index, 0, + "Controller port that keyboard emulates. [0, 3] - Keyboard is assigned to " + "selected slot. Passthrough does not require assigning slot.", + "HID"); + +namespace xe { +namespace hid { +namespace keyboard { + +static bool IsPassthroughEnabled() { + return static_cast(cvars::keyboard_mode) == + KeyboardMode::Passthrough; +} + +static bool IsKeyboardForUserEnabled(uint32_t user_index) { + if (static_cast(cvars::keyboard_mode) != + KeyboardMode::Enabled) { + return false; + } + return cvars::keyboard_user_index == static_cast(user_index); +} + +void KeyboardInputDriver::ParseKeyBinding( + ui::VirtualKey output_key, const std::string_view description, + const std::string_view source_tokens) { + for (const std::string_view source_token : + utf8::split(source_tokens, " ", true)) { + KeyBinding key_binding; + key_binding.output_key = output_key; + + std::string_view token = source_token; + + if (utf8::starts_with(token, "_")) { + key_binding.uppercase = false; + token = token.substr(1); + } else if (utf8::starts_with(token, "^")) { + key_binding.uppercase = true; + token = token.substr(1); + } + + if (utf8::starts_with(token, "0x")) { + token = token.substr(2); + key_binding.input_key = static_cast( + string_util::from_string(token, true)); + } else if (token.size() == 1 && ((token[0] >= 'A' && token[0] <= 'Z') || + (token[0] >= '0' && token[0] <= '9'))) { + key_binding.input_key = static_cast(token[0]); + } + + if (key_binding.input_key == ui::VirtualKey::kNone) { + XELOGW( + "keyboard HID: failed to parse binding \"{}\" for controller input " + "{}.", + source_token, description); + continue; + } + + key_bindings_.push_back(key_binding); + XELOGI("keyboard HID: \"{}\" binds key 0x{:X} to controller input {}.", + source_token, static_cast(key_binding.input_key), + description); + } +} + +KeyboardInputDriver::KeyboardInputDriver(xe::ui::Window* window, + size_t window_z_order) + : InputDriver(window, window_z_order), window_input_listener_(*this) { +#define XE_HID_KEYBOARD_BINDING(button, description, cvar_name, \ + cvar_default_value) \ + ParseKeyBinding(xe::ui::VirtualKey::kXInputPad##button, description, \ + cvars::cvar_name); +#include "keyboard_binding_table.inc" +#undef XE_HID_KEYBOARD_BINDING + + window->AddInputListener(&window_input_listener_, window_z_order); +} + +KeyboardInputDriver::~KeyboardInputDriver() { + window()->RemoveInputListener(&window_input_listener_); +} + +X_STATUS KeyboardInputDriver::Setup() { return X_STATUS_SUCCESS; } + +X_RESULT KeyboardInputDriver::GetCapabilities(uint32_t user_index, + uint32_t flags, + X_INPUT_CAPABILITIES* out_caps) { + if (!IsKeyboardForUserEnabled(user_index) && !IsPassthroughEnabled()) { + return X_ERROR_DEVICE_NOT_CONNECTED; + } + + if (IsPassthroughEnabled()) { + out_caps->type = X_INPUT_DEVTYPE::XINPUT_DEVTYPE_KEYBOARD; + out_caps->sub_type = X_INPUT_DEVSUBTYPE::XINPUT_DEVSUBTYPE_USB_KEYBOARD; + return X_ERROR_SUCCESS; + } + + out_caps->type = X_INPUT_DEVTYPE::XINPUT_DEVTYPE_GAMEPAD; + out_caps->sub_type = X_INPUT_DEVSUBTYPE::XINPUT_DEVSUBTYPE_GAMEPAD; + out_caps->flags = 0; + out_caps->gamepad.buttons = 0xFFFF; + out_caps->gamepad.left_trigger = 0xFF; + out_caps->gamepad.right_trigger = 0xFF; + out_caps->gamepad.thumb_lx = (int16_t)0xFFFFu; + out_caps->gamepad.thumb_ly = (int16_t)0xFFFFu; + out_caps->gamepad.thumb_rx = (int16_t)0xFFFFu; + out_caps->gamepad.thumb_ry = (int16_t)0xFFFFu; + out_caps->vibration.left_motor_speed = 0; + out_caps->vibration.right_motor_speed = 0; + return X_ERROR_SUCCESS; +} + +X_RESULT KeyboardInputDriver::GetState(uint32_t user_index, + X_INPUT_STATE* out_state) { + if (!IsKeyboardForUserEnabled(user_index)) { + return X_ERROR_DEVICE_NOT_CONNECTED; + } + + packet_number_++; + + uint16_t buttons = 0; + uint8_t left_trigger = 0; + uint8_t right_trigger = 0; + int16_t thumb_lx = 0; + int16_t thumb_ly = 0; + int16_t thumb_rx = 0; + int16_t thumb_ry = 0; + + if (window()->HasFocus()) { + for (const KeyBinding& b : key_bindings_) { + if (b.is_pressed) { + switch (b.output_key) { + case ui::VirtualKey::kXInputPadA: + buttons |= X_INPUT_GAMEPAD_A; + break; + case ui::VirtualKey::kXInputPadY: + buttons |= X_INPUT_GAMEPAD_Y; + break; + case ui::VirtualKey::kXInputPadB: + buttons |= X_INPUT_GAMEPAD_B; + break; + case ui::VirtualKey::kXInputPadX: + buttons |= X_INPUT_GAMEPAD_X; + break; + case ui::VirtualKey::kXInputPadGuide: + buttons |= X_INPUT_GAMEPAD_GUIDE; + break; + case ui::VirtualKey::kXInputPadDpadLeft: + buttons |= X_INPUT_GAMEPAD_DPAD_LEFT; + break; + case ui::VirtualKey::kXInputPadDpadRight: + buttons |= X_INPUT_GAMEPAD_DPAD_RIGHT; + break; + case ui::VirtualKey::kXInputPadDpadDown: + buttons |= X_INPUT_GAMEPAD_DPAD_DOWN; + break; + case ui::VirtualKey::kXInputPadDpadUp: + buttons |= X_INPUT_GAMEPAD_DPAD_UP; + break; + case ui::VirtualKey::kXInputPadRThumbPress: + buttons |= X_INPUT_GAMEPAD_RIGHT_THUMB; + break; + case ui::VirtualKey::kXInputPadLThumbPress: + buttons |= X_INPUT_GAMEPAD_LEFT_THUMB; + break; + case ui::VirtualKey::kXInputPadBack: + buttons |= X_INPUT_GAMEPAD_BACK; + break; + case ui::VirtualKey::kXInputPadStart: + buttons |= X_INPUT_GAMEPAD_START; + break; + case ui::VirtualKey::kXInputPadLShoulder: + buttons |= X_INPUT_GAMEPAD_LEFT_SHOULDER; + break; + case ui::VirtualKey::kXInputPadRShoulder: + buttons |= X_INPUT_GAMEPAD_RIGHT_SHOULDER; + break; + case ui::VirtualKey::kXInputPadLTrigger: + left_trigger = 0xFF; + break; + case ui::VirtualKey::kXInputPadRTrigger: + right_trigger = 0xFF; + break; + case ui::VirtualKey::kXInputPadLThumbLeft: + thumb_lx += SHRT_MIN; + break; + case ui::VirtualKey::kXInputPadLThumbRight: + thumb_lx += SHRT_MAX; + break; + case ui::VirtualKey::kXInputPadLThumbDown: + thumb_ly += SHRT_MIN; + break; + case ui::VirtualKey::kXInputPadLThumbUp: + thumb_ly += SHRT_MAX; + break; + case ui::VirtualKey::kXInputPadRThumbUp: + thumb_ry += SHRT_MAX; + break; + case ui::VirtualKey::kXInputPadRThumbDown: + thumb_ry += SHRT_MIN; + break; + case ui::VirtualKey::kXInputPadRThumbRight: + thumb_rx += SHRT_MAX; + break; + case ui::VirtualKey::kXInputPadRThumbLeft: + thumb_rx += SHRT_MIN; + break; + default: + assert_unhandled_case(b.output_key); + } + } + } + } + + out_state->packet_number = packet_number_; + out_state->gamepad.buttons = buttons; + out_state->gamepad.left_trigger = left_trigger; + out_state->gamepad.right_trigger = right_trigger; + out_state->gamepad.thumb_lx = thumb_lx; + out_state->gamepad.thumb_ly = thumb_ly; + out_state->gamepad.thumb_rx = thumb_rx; + out_state->gamepad.thumb_ry = thumb_ry; + + if (IsPassthroughEnabled()) { + std::memset(out_state, 0, sizeof(*out_state)); + } + + return X_ERROR_SUCCESS; +} + +X_RESULT KeyboardInputDriver::SetState(uint32_t user_index, + X_INPUT_VIBRATION* vibration) { + if (!IsKeyboardForUserEnabled(user_index) && !IsPassthroughEnabled()) { + return X_ERROR_DEVICE_NOT_CONNECTED; + } + + return X_ERROR_SUCCESS; +} + +X_RESULT KeyboardInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags, + X_INPUT_KEYSTROKE* out_keystroke) { + if (!IsKeyboardForUserEnabled(user_index) && !IsPassthroughEnabled()) { + return X_ERROR_DEVICE_NOT_CONNECTED; + } + + X_RESULT result = X_ERROR_EMPTY; + + ui::VirtualKey xinput_virtual_key = ui::VirtualKey::kNone; + uint16_t unicode = 0; + uint16_t keystroke_flags = 0; + uint8_t hid_code = 0; + + // Pop from the queue. + KeyEvent evt; + { + auto global_lock = global_critical_region_.Acquire(); + if (key_events_.empty()) { + // No keys! + return X_ERROR_EMPTY; + } + evt = key_events_.front(); + key_events_.pop(); + } + + if (!IsPassthroughEnabled()) { + if (IsKeyboardForUserEnabled(user_index)) { + for (const KeyBinding& b : key_bindings_) { + if (b.input_key == evt.virtual_key && b.uppercase == evt.is_capital) { + xinput_virtual_key = b.output_key; + } + } + } + } else { + // Passthrough: surface the raw key plus modifier flags. + // TODO(has207): plumb unicode (via OnKeyChar text()) and a VirtualKey -> + // HID usage table so passthrough matches winkey's full keystroke shape. + xinput_virtual_key = evt.virtual_key; + + if (evt.is_capital) { + keystroke_flags |= 0x0008; // XINPUT_KEYSTROKE_SHIFT + } + if (evt.is_ctrl_pressed) { + keystroke_flags |= 0x0010; // XINPUT_KEYSTROKE_CTRL + } + if (evt.is_alt_pressed) { + keystroke_flags |= 0x0020; // XINPUT_KEYSTROKE_ALT + } + } + + if (xinput_virtual_key != ui::VirtualKey::kNone) { + if (evt.transition == true) { + keystroke_flags |= 0x0001; // XINPUT_KEYSTROKE_KEYDOWN + if (evt.prev_state == evt.transition) { + keystroke_flags |= 0x0004; // XINPUT_KEYSTROKE_REPEAT + } + } else if (evt.transition == false) { + keystroke_flags |= 0x0002; // XINPUT_KEYSTROKE_KEYUP + } + + result = X_ERROR_SUCCESS; + } + + out_keystroke->virtual_key = uint16_t(xinput_virtual_key); + out_keystroke->unicode = unicode; + out_keystroke->flags = keystroke_flags; + out_keystroke->user_index = user_index; + out_keystroke->hid_code = hid_code; + + // X_ERROR_EMPTY if no new keys + // X_ERROR_DEVICE_NOT_CONNECTED if no device + // X_ERROR_SUCCESS if key + return result; +} + +void KeyboardInputDriver::KeyboardWindowInputListener::OnKeyDown( + ui::KeyEvent& e) { + driver_.OnKey(e, true); +} + +void KeyboardInputDriver::KeyboardWindowInputListener::OnKeyUp( + ui::KeyEvent& e) { + driver_.OnKey(e, false); +} + +void KeyboardInputDriver::OnKey(ui::KeyEvent& e, bool is_down) { + if (static_cast(cvars::keyboard_mode) == + KeyboardMode::Disabled) { + return; + } + + auto global_lock = global_critical_region_.Acquire(); + + const bool is_capital = e.is_shift_pressed(); + + KeyEvent key; + key.virtual_key = e.virtual_key(); + key.transition = is_down; + key.prev_state = e.prev_state(); + key.repeat_count = e.repeat_count(); + key.is_capital = is_capital; + key.is_ctrl_pressed = e.is_ctrl_pressed(); + key.is_alt_pressed = e.is_alt_pressed(); + + key_events_.push(key); + for (auto& key_binding : key_bindings_) { + if (key_binding.input_key != key.virtual_key) { + continue; + } + if (is_down) { + if (key_binding.uppercase == is_capital) { + key_binding.is_pressed = true; + } + } else { + // Clear all variants on release so a shift/caps change mid-press + // can't leave a binding stuck. + key_binding.is_pressed = false; + } + } +} + +InputType KeyboardInputDriver::GetInputType() const { + switch (static_cast(cvars::keyboard_mode)) { + case KeyboardMode::Disabled: + return InputType::None; + case KeyboardMode::Enabled: + return InputType::Controller; + case KeyboardMode::Passthrough: + return InputType::Keyboard; + default: + break; + } + return InputType::Controller; +} + +} // namespace keyboard +} // namespace hid +} // namespace xe diff --git a/src/xenia/hid/keyboard/keyboard_input_driver.h b/src/xenia/hid/keyboard/keyboard_input_driver.h new file mode 100644 index 000000000..a04ca96a6 --- /dev/null +++ b/src/xenia/hid/keyboard/keyboard_input_driver.h @@ -0,0 +1,90 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_HID_KEYBOARD_KEYBOARD_INPUT_DRIVER_H_ +#define XENIA_HID_KEYBOARD_KEYBOARD_INPUT_DRIVER_H_ + +#include + +#include "xenia/base/mutex.h" +#include "xenia/hid/input_driver.h" +#include "xenia/ui/virtual_key.h" + +namespace xe { +namespace hid { +namespace keyboard { + +enum class KeyboardMode { Disabled, Enabled, Passthrough }; + +class KeyboardInputDriver final : public InputDriver { + public: + explicit KeyboardInputDriver(xe::ui::Window* window, size_t window_z_order); + ~KeyboardInputDriver() override; + + X_STATUS Setup() override; + + X_RESULT GetCapabilities(uint32_t user_index, uint32_t flags, + X_INPUT_CAPABILITIES* out_caps) override; + X_RESULT GetState(uint32_t user_index, X_INPUT_STATE* out_state) override; + X_RESULT SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) override; + X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags, + X_INPUT_KEYSTROKE* out_keystroke) override; + virtual InputType GetInputType() const override; + + protected: + class KeyboardWindowInputListener final : public ui::WindowInputListener { + public: + explicit KeyboardWindowInputListener(KeyboardInputDriver& driver) + : driver_(driver) {} + + void OnKeyDown(ui::KeyEvent& e) override; + void OnKeyUp(ui::KeyEvent& e) override; + + private: + KeyboardInputDriver& driver_; + }; + + struct KeyEvent { + ui::VirtualKey virtual_key = ui::VirtualKey::kNone; + int repeat_count = 0; + bool transition = false; // going up(false) or going down(true) + bool prev_state = false; // down(true) or up(false) + bool is_capital = false; // shift held at event time + bool is_ctrl_pressed = false; + bool is_alt_pressed = false; + }; + + struct KeyBinding { + ui::VirtualKey input_key = ui::VirtualKey::kNone; + ui::VirtualKey output_key = ui::VirtualKey::kNone; + bool uppercase = false; + bool is_pressed = false; + }; + + void ParseKeyBinding(ui::VirtualKey virtual_key, + const std::string_view description, + const std::string_view binding); + + void OnKey(ui::KeyEvent& e, bool is_down); + + xe::global_critical_region global_critical_region_; + + std::queue key_events_; + std::vector key_bindings_; + + KeyboardWindowInputListener window_input_listener_; + + uint32_t packet_number_ = 1; +}; + +} // namespace keyboard +} // namespace hid +} // namespace xe + +#endif // XENIA_HID_KEYBOARD_KEYBOARD_INPUT_DRIVER_H_ diff --git a/src/xenia/ui/window_qt.cc b/src/xenia/ui/window_qt.cc index a3e1bf278..c3e7b30cc 100644 --- a/src/xenia/ui/window_qt.cc +++ b/src/xenia/ui/window_qt.cc @@ -840,6 +840,26 @@ VirtualKey QtWindow::TranslateVirtualKey(int qt_key) { return VirtualKey::kAdd; case Qt::Key_Minus: return VirtualKey::kSubtract; + case Qt::Key_Semicolon: + return VirtualKey::kOem1; // ;: (US) + case Qt::Key_Equal: + return VirtualKey::kOemPlus; + case Qt::Key_Comma: + return VirtualKey::kOemComma; + case Qt::Key_Period: + return VirtualKey::kOemPeriod; + case Qt::Key_Slash: + return VirtualKey::kOem2; // /? (US) + case Qt::Key_QuoteLeft: + return VirtualKey::kOem3; // `~ (US) + case Qt::Key_BracketLeft: + return VirtualKey::kOem4; // [{ (US) + case Qt::Key_Backslash: + return VirtualKey::kOem5; // \| (US) + case Qt::Key_BracketRight: + return VirtualKey::kOem6; // ]} (US) + case Qt::Key_Apostrophe: + return VirtualKey::kOem7; // '" (US) default: return VirtualKey::kNone; }