Android: give the guest a real keyboard

cellKb reported no keyboard at all, so games that need one were unreachable: NFS Most
Wanted's beta debug menu, and native keyboard support in games like Counter-Strike. The
only handler upstream ships, basic_keyboard_handler, derives from QObject and filters
QKeyEvent off a QWindow, and android/CMakeLists.txt excludes it with the rest of the Qt
input layer -- init_kb_handler was hardcoded to NullKeyboardHandler as a result.

Almost none of that handler is actually Qt-bound. KeyboardHandlerBase::HandleKey already
takes plain u32 codes and keyboard_consumer::ConsumeKey resolves them through
m_keys.find(code), so the code space only has to agree between whatever registers the
buttons and whatever injects them. android_keyboard_handler therefore registers ANDROID
KeyEvent keycodes directly rather than impersonating Qt. The PS3 side uses USB HID usage
IDs and Android's letters and digits are contiguous too, so those map arithmetically and
only the remainder needs a table. Android also distinguishes left from right modifiers,
which Qt cannot, so all eight are wired rather than four.

init_kb_handler now honours the Keyboard setting instead of always reporting none, and
_rpcsx_keyboardKey delivers one key through the usual dlsym bridge, returning false when
no keyboard is active so a caller can tell the difference.

A physical keyboard reaches the guest through dispatchKeyEvent. The test there is
KEYBOARD_TYPE_ALPHABETIC, not the event source: gamepads also report SOURCE_KEYBOARD for
their buttons, so filtering on source alone would send every controller press to the guest
keyboard as well as the pad. The event is consumed only when the native side reports the
key landed, which keeps a physical keyboard usable for UI navigation everywhere else.

Not yet done: the on-screen keyboard overlay, and a UI setting for the handler. The core
default is still keyboard_handler::null, so this is inert until Keyboard is set to Basic.
This commit is contained in:
jpolo1224
2026-08-18 12:57:34 -04:00
parent 55a54c924e
commit 422d831eff
5 changed files with 271 additions and 2 deletions
@@ -24,6 +24,7 @@ struct RPCSXApi {
bool (*overlayPadData)(int port, int digital1, int digital2, int leftStickX,
int leftStickY, int rightStickX, int rightStickY);
bool (*overlayPadPressure)(int port, const int *values, int count);
bool (*keyboardKey)(int keyCode, bool pressed, int unicode);
bool (*initialize)(std::string_view rootDir, std::string_view user);
void (*setSocInfo)(std::string_view socInfo);
bool (*processCompilationQueue)(JNIEnv *env);
@@ -121,6 +122,7 @@ struct RPCSXLibrary : RPCSXApi {
// clang-format off
result.overlayPadData = reinterpret_cast<decltype(overlayPadData)>(dlsym(handle, "_rpcsx_overlayPadData"));
result.keyboardKey = reinterpret_cast<decltype(keyboardKey)>(dlsym(handle, "_rpcsx_keyboardKey"));
result.overlayPadPressure = reinterpret_cast<decltype(overlayPadPressure)>(dlsym(handle, "_rpcsx_overlayPadPressure"));
result.initialize = reinterpret_cast<decltype(initialize)>(dlsym(handle, "_rpcsx_initialize"));
result.setSocInfo = reinterpret_cast<decltype(setSocInfo)>(dlsym(handle, "_rpcsx_setSocInfo"));
@@ -234,6 +236,17 @@ extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadData(
leftStickY, rightStickX, rightStickY);
}
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_keyboardKey(
JNIEnv *, jobject, jint keyCode, jboolean pressed, jint unicode) {
// Absent on a core older than this export, and null before the core is dlopen()ed. Report
// failure rather than faulting, so the on-screen keyboard can show itself as inert.
if (rpcsxLib.keyboardKey == nullptr) {
return false;
}
return rpcsxLib.keyboardKey(keyCode, pressed == JNI_TRUE, unicode);
}
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadPressure(
JNIEnv *env, jobject, jint port, jintArray values) {
// Absent on a core older than this export: the pad still works, every button
@@ -2716,6 +2716,48 @@ open class MainActivityRuntime : ComponentActivity() {
KeyEvent.ACTION_UP -> heldKeys.remove(kc)
}
}
// Route a real keyboard to the guest's emulated keyboard.
//
// cellKb reported no keyboard at all before this: the only handler upstream ships derives
// from QObject and filters QKeyEvent, and the Android build excludes the whole Qt input
// layer, so games needing one were unreachable -- NFS Most Wanted's beta debug menu, and
// native keyboard support in games like Counter-Strike.
//
// KEYBOARD_TYPE_ALPHABETIC is the part that matters. Gamepads also report SOURCE_KEYBOARD
// for their buttons, so testing the source alone would send every controller press to the
// guest keyboard as well as the pad. Only a device that actually has alphabetic keys
// qualifies.
//
// Consumed only when the native side reports the key landed, which it does not when the
// Keyboard setting is Null, no game is running, or the core predates the export. That
// keeps a physical keyboard usable for UI navigation everywhere else.
if (event.keyCode != KeyEvent.KEYCODE_UNKNOWN &&
(event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP) &&
eState.value == EmuState.RUNNING &&
WindowImpl.inGameScreen.value == null &&
!WindowImpl.overlayVisible.value
) {
val dev = runCatching { InputDevice.getDevice(event.deviceId) }.getOrNull()
if (dev != null &&
dev.keyboardType == InputDevice.KEYBOARD_TYPE_ALPHABETIC &&
!event.isFromSource(InputDevice.SOURCE_GAMEPAD) &&
!event.isFromSource(InputDevice.SOURCE_JOYSTICK)
) {
val landed = runCatching {
net.rpcsx.RPCSX.instance.keyboardKey(
event.keyCode,
event.action == KeyEvent.ACTION_DOWN,
event.unicodeChar,
)
}.getOrDefault(false)
if (landed) {
return true
}
}
}
// Track the active gamepad so PS2 rumble routes to its vibrator.
if (event.isFromSource(InputDevice.SOURCE_GAMEPAD) ||
event.isFromSource(InputDevice.SOURCE_JOYSTICK)) {
@@ -107,6 +107,18 @@ class RPCSX {
external fun processCompilationQueue(): Boolean
external fun startMainThreadProcessor(): Boolean
external fun overlayPadData(port: Int, digital1: Int, digital2: Int, leftStickX: Int, leftStickY: Int, rightStickX: Int, rightStickY: Int): Boolean
/**
* Deliver one key to the guest keyboard.
*
* keyCode is an Android KeyEvent keycode -- the native handler registers those directly, so
* nothing needs translating on the way through. unicode is the character the key produces, or
* 0 for keys that produce none (modifiers, arrows, function keys).
*
* Returns false when there is no active keyboard: the Keyboard setting is Null, no game is
* running, or the core predates this export.
*/
external fun keyboardKey(keyCode: Int, pressed: Boolean, unicode: Int): Boolean
/** Analog pressure per pressure-capable button, in CELL_PAD press-offset order
* (RIGHT, LEFT, UP, DOWN, TRIANGLE, CIRCLE, CROSS, SQUARE, L1, R1, L2, R2),
* each 1..255, or 0 to leave that button digital. */
+159
View File
@@ -0,0 +1,159 @@
#pragma once
#include "util/types.hpp"
#include "Emu/Io/KeyboardHandler.h"
#include "Emu/system_config.h"
#include "Emu/Io/interception.h"
// A keyboard the guest believes is physically attached, driven from the Android UI.
//
// RPCS3's only real handler is basic_keyboard_handler, which derives from QObject and filters
// QKeyEvent off a QWindow -- android/CMakeLists.txt excludes it along with the rest of the Qt
// input layer, so cellKb previously had nothing but NullKeyboardHandler and reported no keyboard
// at all. Games that require one are then unreachable: NFS Most Wanted's beta debug menu, and
// native keyboard support in the likes of Counter-Strike.
//
// Almost none of basic_keyboard_handler is actually Qt-bound. KeyboardHandlerBase::HandleKey
// already takes plain u32 codes and keyboard_consumer::ConsumeKey resolves them with
// m_keys.find(code), so the code space only has to agree between whatever registers the buttons
// and whatever injects them. This handler therefore registers ANDROID KeyEvent keycodes directly
// rather than pretending to be Qt, and the UI passes the keycodes it already has.
//
// The PS3 side uses USB HID usage IDs (A = 0x04 .. Z = 0x1d, 1 = 0x1e .. 0 = 0x27), and Android's
// letter and digit keycodes are contiguous too, so those map arithmetically; only the rest needs a
// table.
class android_keyboard_handler final : public KeyboardHandlerBase
{
using KeyboardHandlerBase::KeyboardHandlerBase;
// Android KeyEvent keycodes. Named here rather than pulled from a header because the native
// side has no Android SDK constants, and these are ABI-stable platform values.
enum : u32
{
AKEY_0 = 7, AKEY_9 = 16,
AKEY_DPAD_UP = 19, AKEY_DPAD_DOWN = 20, AKEY_DPAD_LEFT = 21, AKEY_DPAD_RIGHT = 22,
AKEY_A = 29, AKEY_Z = 54,
AKEY_COMMA = 55, AKEY_PERIOD = 56,
AKEY_ALT_LEFT = 57, AKEY_ALT_RIGHT = 58,
AKEY_SHIFT_LEFT = 59, AKEY_SHIFT_RIGHT = 60,
AKEY_TAB = 61, AKEY_SPACE = 62,
AKEY_ENTER = 66, AKEY_DEL = 67, AKEY_GRAVE = 68, AKEY_MINUS = 69, AKEY_EQUALS = 70,
AKEY_LEFT_BRACKET = 71, AKEY_RIGHT_BRACKET = 72, AKEY_BACKSLASH = 73,
AKEY_SEMICOLON = 74, AKEY_APOSTROPHE = 75, AKEY_SLASH = 76,
AKEY_PAGE_UP = 92, AKEY_PAGE_DOWN = 93,
AKEY_ESCAPE = 111, AKEY_FORWARD_DEL = 112,
AKEY_CTRL_LEFT = 113, AKEY_CTRL_RIGHT = 114,
AKEY_CAPS_LOCK = 115,
AKEY_META_LEFT = 117, AKEY_META_RIGHT = 118,
AKEY_MOVE_HOME = 122, AKEY_MOVE_END = 123, AKEY_INSERT = 124,
AKEY_F1 = 131, AKEY_F12 = 142,
};
public:
void Init(keyboard_consumer& consumer, const u32 max_connect) override
{
KbInfo& info = consumer.GetInfo();
std::vector<Keyboard>& keyboards = consumer.GetKeyboards();
info = {};
keyboards.clear();
for (u32 i = 0; i < max_connect; i++)
{
Keyboard kb{};
kb.m_config.arrange = g_cfg.sys.keyboard_type;
if (consumer.id() == keyboard_consumer::identifier::overlays)
{
// Enable key repeat, matching basic_keyboard_handler: the OSK and the other
// overlays rely on repeat for held arrows and backspace.
kb.m_key_repeat = true;
}
LoadSettings(kb);
keyboards.emplace_back(kb);
}
info.max_connect = max_connect;
info.now_connect = std::min(::size32(keyboards), max_connect);
// Ownership of keyboard data: 0 = application, 1 = system.
info.info = input::g_keyboards_intercepted ? CELL_KB_INFO_INTERCEPTED : 0;
info.status[0] = CELL_KB_STATUS_CONNECTED;
}
private:
static void LoadSettings(Keyboard& keyboard)
{
std::vector<KbButton> buttons;
const auto add = [&buttons](u32 android_code, u32 cell_code)
{
buttons.emplace_back(android_code, cell_code);
};
// Modifiers. Unlike Qt, Android does tell left from right, so all eight are real here.
add(AKEY_CTRL_LEFT, CELL_KB_MKEY_L_CTRL);
add(AKEY_CTRL_RIGHT, CELL_KB_MKEY_R_CTRL);
add(AKEY_SHIFT_LEFT, CELL_KB_MKEY_L_SHIFT);
add(AKEY_SHIFT_RIGHT, CELL_KB_MKEY_R_SHIFT);
add(AKEY_ALT_LEFT, CELL_KB_MKEY_L_ALT);
add(AKEY_ALT_RIGHT, CELL_KB_MKEY_R_ALT);
add(AKEY_META_LEFT, CELL_KB_MKEY_L_WIN);
add(AKEY_META_RIGHT, CELL_KB_MKEY_R_WIN);
// Letters and digits are contiguous on both sides. PS3 digits run 1..9 then 0, which is
// why zero is handled apart from the rest.
for (u32 i = 0; i <= (AKEY_Z - AKEY_A); i++)
{
add(AKEY_A + i, CELL_KEYC_A + i);
}
for (u32 i = 1; i <= 9; i++)
{
add(AKEY_0 + i, CELL_KEYC_1 + (i - 1));
}
add(AKEY_0, CELL_KEYC_0);
// Function keys, also contiguous.
for (u32 i = 0; i <= (AKEY_F12 - AKEY_F1); i++)
{
add(AKEY_F1 + i, CELL_KEYC_F1 + i);
}
add(AKEY_ENTER, CELL_KEYC_ENTER);
add(AKEY_ESCAPE, CELL_KEYC_ESCAPE);
add(AKEY_DEL, CELL_KEYC_BS);
add(AKEY_TAB, CELL_KEYC_TAB);
add(AKEY_SPACE, CELL_KEYC_SPACE);
add(AKEY_MINUS, CELL_KEYC_MINUS);
add(AKEY_EQUALS, CELL_KEYC_EQUAL_101);
add(AKEY_LEFT_BRACKET, CELL_KEYC_LEFT_BRACKET_101);
add(AKEY_RIGHT_BRACKET, CELL_KEYC_RIGHT_BRACKET_101);
add(AKEY_BACKSLASH, CELL_KEYC_BACKSLASH_101);
add(AKEY_SEMICOLON, CELL_KEYC_SEMICOLON);
add(AKEY_APOSTROPHE, CELL_KEYC_QUOTATION_101);
add(AKEY_COMMA, CELL_KEYC_COMMA);
add(AKEY_PERIOD, CELL_KEYC_PERIOD);
add(AKEY_SLASH, CELL_KEYC_SLASH);
add(AKEY_CAPS_LOCK, CELL_KEYC_CAPS_LOCK);
add(AKEY_INSERT, CELL_KEYC_INSERT);
add(AKEY_FORWARD_DEL, CELL_KEYC_DELETE);
add(AKEY_MOVE_HOME, CELL_KEYC_HOME);
add(AKEY_MOVE_END, CELL_KEYC_END);
add(AKEY_PAGE_UP, CELL_KEYC_PAGE_UP);
add(AKEY_PAGE_DOWN, CELL_KEYC_PAGE_DOWN);
add(AKEY_DPAD_LEFT, CELL_KEYC_LEFT_ARROW);
add(AKEY_DPAD_RIGHT, CELL_KEYC_RIGHT_ARROW);
add(AKEY_DPAD_UP, CELL_KEYC_UP_ARROW);
add(AKEY_DPAD_DOWN, CELL_KEYC_DOWN_ARROW);
keyboard.m_keys.clear();
for (const KbButton& button : buttons)
{
keyboard.m_keys[button.m_keyCode] = button;
}
}
};
+45 -2
View File
@@ -11,6 +11,7 @@
#include "Emu/IdManager.h"
#include "Emu/Io/KeyboardHandler.h"
#include "Emu/Io/Null/NullKeyboardHandler.h"
#include "android_keyboard_handler.h"
#include "Emu/Io/Null/NullMouseHandler.h"
#include "Emu/Io/Null/NullPadHandler.h"
#include "Emu/Io/Null/null_camera_handler.h"
@@ -2276,8 +2277,18 @@ static void setupCallbacks() {
.handle_taskbar_progress = [](auto...) {},
.init_kb_handler =
[](auto...) {
ensure(g_fxo->init<KeyboardHandlerBase, NullKeyboardHandler>(
Emu.DeserialManager()));
// Honour the Keyboard setting instead of always reporting none.
//
// This was hardcoded to NullKeyboardHandler, so cellKb told every game there was no
// keyboard attached. Games that need one were simply unreachable: NFS Most Wanted's
// beta debug menu, and native keyboard support in games like Counter-Strike.
if (g_cfg.io.keyboard == keyboard_handler::basic) {
ensure(g_fxo->init<KeyboardHandlerBase, android_keyboard_handler>(
Emu.DeserialManager()));
} else {
ensure(g_fxo->init<KeyboardHandlerBase, NullKeyboardHandler>(
Emu.DeserialManager()));
}
},
.init_mouse_handler =
[](auto...) {
@@ -2532,6 +2543,38 @@ static bool initVirtualPad(const std::shared_ptr<Pad> &pad) {
return true;
}
// Deliver one key from the Android UI to the guest keyboard.
//
// keyCode is an Android KeyEvent keycode, which is what android_keyboard_handler registers, so no
// translation happens here. unicode is the character the key produces (0 when it produces none);
// the guest needs it for text fields, and cellKb reports it alongside the keycode.
//
// Returns false when no keyboard is active -- either the Keyboard setting is Null, or no game is
// running -- so the caller can leave the on-screen keyboard visibly inert rather than pretending
// the key landed.
extern "C" bool _rpcsx_keyboardKey(int keyCode, bool pressed, int unicode) {
if (keyCode < 0) {
return false;
}
auto *handler = g_fxo->try_get<KeyboardHandlerBase>();
if (handler == nullptr) {
return false;
}
std::u32string text;
if (unicode > 0) {
text.push_back(static_cast<char32_t>(unicode));
}
// native_code is the platform scancode on desktop; the guest only reads it for raw-mode
// keyboards, and Android gives us the keycode rather than a scancode, so pass it through.
return handler->HandleKey(static_cast<u32>(keyCode), static_cast<u32>(keyCode), pressed,
/*is_auto_repeat=*/false, text);
}
extern "C" bool _rpcsx_overlayPadData(int port, int digital1, int digital2,
int leftStickX, int leftStickY,
int rightStickX, int rightStickY) {