Files
ppsspp/Core/KeyMap.cpp
T
libretroadmin f1208a7a5a Make threading lock-free
The core and the headless tools no longer use mutexes, condition variables,
spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through
libretro-common's retro_atomic and rthreads plus a few small pieces on top of
them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and
ReaderGate. Every wait blocks until it's woken; nothing polls on a timer.

State that only one thread touches just loses its lock. The PSP filesystem,
sceIo and the savedata dialog now belong to the emulation thread: async reads
and writes run when they're queued and complete later in emulated time, and
the IO and savedata threads are gone. The friend finder only talks to the
adhoc server and hands what it gets to the emulation thread. GPU init always
runs on the thread that starts the game.

The Vulkan render and compile queues, pipeline and render pass caches, delete
list and SPIR-V cache, the GL render queue, the software renderer's jit caches,
the file loader caches, the web debugger, UPnP, input, savestates, logging and
the GE debugger all hand off through queues and atomics. The IR jit's spinning
profiler thread goes.

A CPU thread with nothing to run now sleeps until work is queued instead of
spinning. Headless with --debugger waits for a client while stopped, and ends
the run once every client that connected has left. Web server shutdown keeps
servicing the CPU queue until the server has finished.
2026-10-09 04:56:16 +02:00

1026 lines
30 KiB
C++

// Copyright (c) 2013- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <set>
#include <unordered_map>
#include "ppsspp_config.h"
#include "Common/System/NativeApp.h"
#include "Common/System/System.h"
#include "Common/System/OSD.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Format/IniFile.h"
#include "Common/Input/InputState.h"
#include "Common/VR/PPSSPPVR.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Core/HLE/sceUtility.h"
#include "Core/HLE/sceCtrl.h" // psp keys
#include "Core/Config.h"
#include "Core/KeyMap.h"
#include "Core/KeyMapDefaults.h"
namespace KeyMap {
// Belongs to the thread that loads and saves the config.
KeyMapping g_controllerMap;
// Incremented on modification, so we know when to update menus.
int g_controllerMapGeneration = 0;
std::set<std::string> g_seenPads;
std::map<InputDeviceID, std::string> g_padNames;
std::set<InputDeviceID> g_seenDeviceIds;
AxisType GetAxisType(InputAxis input) {
switch (input) {
case JOYSTICK_AXIS_GAS:
case JOYSTICK_AXIS_BRAKE:
case JOYSTICK_AXIS_LTRIGGER:
case JOYSTICK_AXIS_RTRIGGER:
return AxisType::TRIGGER;
case JOYSTICK_AXIS_X:
case JOYSTICK_AXIS_Y:
case JOYSTICK_AXIS_Z:
case JOYSTICK_AXIS_RX:
case JOYSTICK_AXIS_RY:
case JOYSTICK_AXIS_RZ:
return AxisType::STICK;
default:
return AxisType::OTHER;
}
}
// Utility for UI navigation
void SingleInputMappingFromPspButton(int btn, std::vector<InputMapping> *mappings, bool ignoreMouse) {
std::vector<MultiInputMapping> multiMappings;
InputMappingsFromPspButton(btn, &multiMappings, ignoreMouse);
mappings->clear();
for (auto &mapping : multiMappings) {
// Ignore combos
if (mapping.mappings.size() > 1) {
continue;
}
if (!mapping.empty()) {
mappings->push_back(mapping.mappings[0]);
} else {
WARN_LOG(Log::Common, "Encountered empty mapping in multi-mapping for button %d", btn);
}
}
}
static void InsertIntoVector(std::vector<InputMapping> *vec, const InputMapping &mapping) {
if (std::find(vec->begin(), vec->end(), mapping) == vec->end())
vec->push_back(mapping);
}
// Used to avoid multiple main buttons both being mapped to cancel or confirm.
bool HasMainButtonMapping(const std::vector<InputMapping> &mappings) {
for (auto mapping : mappings) {
switch (mapping.keyCode) {
case NKCODE_BUTTON_A:
case NKCODE_BUTTON_B:
case NKCODE_BUTTON_C:
case NKCODE_BUTTON_X:
case NKCODE_BUTTON_Y:
case NKCODE_BUTTON_Z:
case NKCODE_BUTTON_1:
case NKCODE_BUTTON_2:
case NKCODE_BUTTON_3:
case NKCODE_BUTTON_4:
case NKCODE_BUTTON_5:
case NKCODE_BUTTON_6:
return true;
}
}
return false;
}
static void RemoveKeyboardLetterKeys(std::vector<InputMapping> &mappings) {
std::vector<InputMapping> result;
for (auto &mapping : mappings) {
if (mapping.deviceId == DEVICE_ID_KEYBOARD) {
if (mapping.keyCode >= NKCODE_A && mapping.keyCode <= NKCODE_Z) {
continue;
}
}
result.push_back(mapping);
}
mappings = result;
}
// TODO: This is such a mess...
void UpdateNativeMenuKeys() {
std::vector<InputMapping> confirmKeys, cancelKeys;
std::vector<InputMapping> tabLeft, tabRight;
std::vector<InputMapping> upKeys, downKeys, leftKeys, rightKeys;
std::vector<InputMapping> infoKeys;
// Mouse mapping might be problematic in UI, so let's ignore mouse for UI
const bool confirmWithCross = g_Config.iButtonPreference == PSP_SYSTEMPARAM_BUTTON_CROSS;
const int confirmKey = confirmWithCross ? CTRL_CROSS : CTRL_CIRCLE;
const int cancelKey = confirmWithCross ? CTRL_CIRCLE : CTRL_CROSS;
SingleInputMappingFromPspButton(confirmKey, &confirmKeys, true);
SingleInputMappingFromPspButton(cancelKey, &cancelKeys, true);
SingleInputMappingFromPspButton(CTRL_TRIANGLE, &infoKeys, true);
SingleInputMappingFromPspButton(CTRL_LTRIGGER, &tabLeft, true);
SingleInputMappingFromPspButton(CTRL_RTRIGGER, &tabRight, true);
SingleInputMappingFromPspButton(CTRL_UP, &upKeys, true);
SingleInputMappingFromPspButton(CTRL_DOWN, &downKeys, true);
SingleInputMappingFromPspButton(CTRL_LEFT, &leftKeys, true);
SingleInputMappingFromPspButton(CTRL_RIGHT, &rightKeys, true);
#if PPSSPP_PLATFORM(ANDROID)
// Hardcode DPAD on Android
upKeys.push_back(InputMapping(DEVICE_ID_ANY, NKCODE_DPAD_UP));
downKeys.push_back(InputMapping(DEVICE_ID_ANY, NKCODE_DPAD_DOWN));
leftKeys.push_back(InputMapping(DEVICE_ID_ANY, NKCODE_DPAD_LEFT));
rightKeys.push_back(InputMapping(DEVICE_ID_ANY, NKCODE_DPAD_RIGHT));
#endif
// Push several hard-coded keys before submitting to native.
const InputMapping hardcodedConfirmKeys[] = {
InputMapping(DEVICE_ID_KEYBOARD, NKCODE_SPACE),
InputMapping(DEVICE_ID_KEYBOARD, NKCODE_ENTER),
InputMapping(DEVICE_ID_KEYBOARD, NKCODE_NUMPAD_ENTER),
InputMapping(DEVICE_ID_PAD_0, NKCODE_DPAD_CENTER), // A number of old obscure Android devices need this.
};
// If they're not already bound, add them in.
for (size_t i = 0; i < ARRAY_SIZE(hardcodedConfirmKeys); i++) {
InsertIntoVector(&confirmKeys, hardcodedConfirmKeys[i]);
}
if (!HasMainButtonMapping(confirmKeys)) {
confirmKeys.push_back(InputMapping(DEVICE_ID_ANY, confirmWithCross ? NKCODE_BUTTON_A : NKCODE_BUTTON_B));
}
const InputMapping hardcodedCancelKeys[] = {
InputMapping(DEVICE_ID_KEYBOARD, NKCODE_ESCAPE),
InputMapping(DEVICE_ID_ANY, NKCODE_BACK),
InputMapping(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_4),
};
for (size_t i = 0; i < ARRAY_SIZE(hardcodedCancelKeys); i++) {
InsertIntoVector(&cancelKeys, hardcodedCancelKeys[i]);
}
if (!HasMainButtonMapping(cancelKeys)) {
// This used to push the confirm button into confirmKeys - wrong list, wrong button.
cancelKeys.push_back(InputMapping(DEVICE_ID_ANY, confirmWithCross ? NKCODE_BUTTON_B : NKCODE_BUTTON_A));
}
const InputMapping hardcodedInfoKeys[] = {
InputMapping(DEVICE_ID_KEYBOARD, NKCODE_S),
InputMapping(DEVICE_ID_KEYBOARD, NKCODE_NUMPAD_ADD),
InputMapping(DEVICE_ID_PAD_0, NKCODE_BUTTON_Y), // Also triangle
};
for (size_t i = 0; i < ARRAY_SIZE(hardcodedInfoKeys); i++) {
InsertIntoVector(&infoKeys, hardcodedInfoKeys[i]);
}
/*
for (size_t i = 0; i < confirmKeys.size(); i++) {
INFO_LOG(Log::System, "Confirm key: %s", MultiInputMapping(confirmKeys[i]).ToVisualString().c_str());
}
for (size_t i = 0; i < cancelKeys.size(); i++) {
INFO_LOG(Log::System, "Cancel key: %s", MultiInputMapping(cancelKeys[i]).ToVisualString().c_str());
}
*/
// Remove keyboard letter keys from some arrays, as they will collide with text input for instant search.
RemoveKeyboardLetterKeys(tabLeft);
RemoveKeyboardLetterKeys(tabRight);
RemoveKeyboardLetterKeys(confirmKeys);
RemoveKeyboardLetterKeys(cancelKeys);
RemoveKeyboardLetterKeys(infoKeys);
SetDPadKeys(upKeys, downKeys, leftKeys, rightKeys);
SetConfirmCancelKeys(confirmKeys, cancelKeys);
SetTabLeftRightKeys(tabLeft, tabRight);
SetInfoKeys(infoKeys);
std::unordered_map<InputDeviceID, int> flipYByDeviceId;
for (InputDeviceID deviceId : g_seenDeviceIds) {
auto analogs = MappedAxesForDevice(deviceId);
flipYByDeviceId[deviceId] = analogs.leftY.direction;
}
SetAnalogFlipY(flipYByDeviceId);
}
static const KeyMap_IntStrPair key_names[] = {
{NKCODE_A, "A"},
{NKCODE_B, "B"},
{NKCODE_C, "C"},
{NKCODE_D, "D"},
{NKCODE_E, "E"},
{NKCODE_F, "F"},
{NKCODE_G, "G"},
{NKCODE_H, "H"},
{NKCODE_I, "I"},
{NKCODE_J, "J"},
{NKCODE_K, "K"},
{NKCODE_L, "L"},
{NKCODE_M, "M"},
{NKCODE_N, "N"},
{NKCODE_O, "O"},
{NKCODE_P, "P"},
{NKCODE_Q, "Q"},
{NKCODE_R, "R"},
{NKCODE_S, "S"},
{NKCODE_T, "T"},
{NKCODE_U, "U"},
{NKCODE_V, "V"},
{NKCODE_W, "W"},
{NKCODE_X, "X"},
{NKCODE_Y, "Y"},
{NKCODE_Z, "Z"},
{NKCODE_0, "0"},
{NKCODE_1, "1"},
{NKCODE_2, "2"},
{NKCODE_3, "3"},
{NKCODE_4, "4"},
{NKCODE_5, "5"},
{NKCODE_6, "6"},
{NKCODE_7, "7"},
{NKCODE_8, "8"},
{NKCODE_9, "9"},
{NKCODE_F1, "F1"},
{NKCODE_F2, "F2"},
{NKCODE_F3, "F3"},
{NKCODE_F4, "F4"},
{NKCODE_F5, "F5"},
{NKCODE_F6, "F6"},
{NKCODE_F7, "F7"},
{NKCODE_F8, "F8"},
{NKCODE_F9, "F9"},
{NKCODE_F10, "F10"},
{NKCODE_F11, "F11"},
{NKCODE_F12, "F12"},
{NKCODE_GRAVE, "`"},
{NKCODE_SLASH, "/"},
{NKCODE_BACKSLASH, "\\"},
{NKCODE_SEMICOLON, ";"},
{NKCODE_COMMA, ","},
{NKCODE_PERIOD, "."},
{NKCODE_LEFT_BRACKET, "["},
{NKCODE_RIGHT_BRACKET, "]"},
{NKCODE_APOSTROPHE, "'"},
{NKCODE_MINUS, "-"},
{NKCODE_PLUS, "+"},
{NKCODE_PRINTSCREEN, "Print"},
{NKCODE_SCROLL_LOCK, "ScrLock"},
{NKCODE_BREAK, "Pause"},
{NKCODE_BACK, "Back"},
{NKCODE_TAB, "Tab"},
{NKCODE_ENTER, "Enter"},
{NKCODE_SHIFT_LEFT, "LShift"},
{NKCODE_SHIFT_RIGHT, "RShift"},
{NKCODE_CTRL_LEFT, "LCtrl"},
{NKCODE_CTRL_RIGHT, "RCtrl"},
{NKCODE_ALT_LEFT, "LAlt"},
{NKCODE_ALT_RIGHT, "RAlt"},
{NKCODE_META_LEFT, "LMeta"},
{NKCODE_META_RIGHT, "RMeta"},
{NKCODE_SPACE, "Space"},
{NKCODE_WINDOW, "Windows"},
{NKCODE_DEL, "Backspace"},
{NKCODE_FORWARD_DEL, "Delete"},
{NKCODE_MOVE_HOME, "Home"},
{NKCODE_MOVE_END, "End"},
{NKCODE_ESCAPE, "Esc"},
{NKCODE_CAPS_LOCK, "CapsLock"},
{NKCODE_VOLUME_UP, "Vol +"},
{NKCODE_VOLUME_DOWN, "Vol -"},
{NKCODE_HOME, "Home"},
{NKCODE_INSERT, "Ins"},
{NKCODE_PAGE_UP, "PgUp"},
{NKCODE_PAGE_DOWN, "PgDn"},
{NKCODE_CLEAR, "Clear"}, // 5 when numlock off
{NKCODE_CALL, "Call"},
{NKCODE_ENDCALL, "End Call"},
{NKCODE_DPAD_LEFT, "Left"},
{NKCODE_DPAD_UP, "Up"},
{NKCODE_DPAD_RIGHT, "Right"},
{NKCODE_DPAD_DOWN, "Down"},
{NKCODE_BUTTON_L1, "L1"},
{NKCODE_BUTTON_L2, "L2"},
{NKCODE_BUTTON_R1, "R1"},
{NKCODE_BUTTON_R2, "R2"},
{NKCODE_BUTTON_A, "[A]"},
{NKCODE_BUTTON_B, "[B]"},
{NKCODE_BUTTON_C, "[C]"},
{NKCODE_BUTTON_X, "[X]"},
{NKCODE_BUTTON_Y, "[Y]"},
{NKCODE_BUTTON_Z, "[Z]"},
{NKCODE_BUTTON_1, "b1"},
{NKCODE_BUTTON_2, "b2"},
{NKCODE_BUTTON_3, "b3"},
{NKCODE_BUTTON_4, "b4"},
{NKCODE_BUTTON_5, "b5"},
{NKCODE_BUTTON_6, "b6"},
{NKCODE_BUTTON_7, "b7"},
{NKCODE_BUTTON_8, "b8"},
{NKCODE_BUTTON_9, "b9"},
{NKCODE_BUTTON_10, "b10"},
{NKCODE_BUTTON_11, "b11"},
{NKCODE_BUTTON_12, "b12"},
{NKCODE_BUTTON_13, "b13"},
{NKCODE_BUTTON_14, "b14"},
{NKCODE_BUTTON_15, "b15"},
{NKCODE_BUTTON_16, "b16"},
{NKCODE_BUTTON_START, "Start"},
{NKCODE_BUTTON_SELECT, "Select"},
{NKCODE_BUTTON_CIRCLE, "Circle"},
{NKCODE_BUTTON_CIRCLE_PS3, "Circle3"},
{NKCODE_BUTTON_CROSS, "Cross"},
{NKCODE_BUTTON_CROSS_PS3, "Cross3"},
{NKCODE_BUTTON_TRIANGLE, "Triangle"},
{NKCODE_BUTTON_SQUARE, "Square"},
{NKCODE_BUTTON_THUMBL, "ThumbL"},
{NKCODE_BUTTON_THUMBR, "ThumbR"},
{NKCODE_BUTTON_MODE, "Mode"},
{NKCODE_EXT_PIPE, "|"},
{NKCODE_NUMPAD_DIVIDE, "Num/"},
{NKCODE_NUMPAD_MULTIPLY, "Num*"},
{NKCODE_NUMPAD_ADD, "Num+"},
{NKCODE_NUMPAD_SUBTRACT, "Num-"},
{NKCODE_NUMPAD_DOT, "Num."},
{NKCODE_NUMPAD_COMMA, "Num,"},
{NKCODE_NUMPAD_ENTER, "NumEnter"},
{NKCODE_NUMPAD_EQUALS, "Num="},
{NKCODE_NUMPAD_LEFT_PAREN, "Num("},
{NKCODE_NUMPAD_RIGHT_PAREN, "Num)"},
{NKCODE_NUMPAD_0, "Num0"},
{NKCODE_NUMPAD_1, "Num1"},
{NKCODE_NUMPAD_2, "Num2"},
{NKCODE_NUMPAD_3, "Num3"},
{NKCODE_NUMPAD_4, "Num4"},
{NKCODE_NUMPAD_5, "Num5"},
{NKCODE_NUMPAD_6, "Num6"},
{NKCODE_NUMPAD_7, "Num7"},
{NKCODE_NUMPAD_8, "Num8"},
{NKCODE_NUMPAD_9, "Num9"},
{NKCODE_LANGUAGE_SWITCH, "Language"},
{NKCODE_MANNER_MODE, "Manner"},
{NKCODE_3D_MODE, "3D Mode"},
{NKCODE_CONTACTS, "Contacts"},
{NKCODE_CALENDAR, "Calendar"},
{NKCODE_MUSIC, "Music"},
{NKCODE_CALCULATOR, "Calc"},
{NKCODE_ZENKAKU_HANKAKU, "Zenkaku"},
{NKCODE_EISU, "Eisu"},
{NKCODE_MUHENKAN, "Muhenkan"},
{NKCODE_HENKAN, "Henkan"},
{NKCODE_KATAKANA_HIRAGANA, "Katakana"},
{NKCODE_YEN, "Yen"},
{NKCODE_RO, "Ro"},
{NKCODE_KANA, "Kana"},
{NKCODE_ASSIST, "Assist"},
{NKCODE_EXT_MOUSEBUTTON_1, "MB1"},
{NKCODE_EXT_MOUSEBUTTON_2, "MB2"},
{NKCODE_EXT_MOUSEBUTTON_3, "MB3"},
{NKCODE_EXT_MOUSEBUTTON_4, "MB4"},
{NKCODE_EXT_MOUSEBUTTON_5, "MB5"},
{NKCODE_EXT_MOUSEWHEEL_UP, "MWheelU"},
{NKCODE_EXT_MOUSEWHEEL_DOWN, "MWheelD"},
{NKCODE_START_QUESTION, "¿"},
{NKCODE_LEFTBRACE, "{"},
{NKCODE_RIGHTBRACE, "}"},
{NKCODE_GUIDE, "Guide"},
{NKCODE_INFO, "Info"},
};
static const KeyMap_IntStrPair axis_names[] = {
{JOYSTICK_AXIS_X, "X Axis"},
{JOYSTICK_AXIS_Y, "Y Axis"},
{JOYSTICK_AXIS_PRESSURE, "Pressure"},
{JOYSTICK_AXIS_SIZE, "Size"},
{JOYSTICK_AXIS_TOUCH_MAJOR, "Touch Major"},
{JOYSTICK_AXIS_TOUCH_MINOR, "Touch Minor"},
{JOYSTICK_AXIS_TOOL_MAJOR, "Tool Major"},
{JOYSTICK_AXIS_TOOL_MINOR, "Tool Minor"},
{JOYSTICK_AXIS_ORIENTATION, "Orient"},
{JOYSTICK_AXIS_VSCROLL, "Vert Scroll"},
{JOYSTICK_AXIS_HSCROLL, "Horiz Scroll"},
{JOYSTICK_AXIS_Z, "Z Axis"}, // Also used as second stick X on many controllers - rename?
{JOYSTICK_AXIS_RX, "X Rotation"},
{JOYSTICK_AXIS_RY, "Y Rotation"},
{JOYSTICK_AXIS_RZ, "Z Rotation"}, // Also used as second stick Y on many controllers - rename?
{JOYSTICK_AXIS_HAT_X, "X HAT"},
{JOYSTICK_AXIS_HAT_Y, "Y HAT"},
{JOYSTICK_AXIS_LTRIGGER, "TriggerL"},
{JOYSTICK_AXIS_RTRIGGER, "TriggerR"},
{JOYSTICK_AXIS_THROTTLE, "Throttle"},
{JOYSTICK_AXIS_RUDDER, "Rudder"},
{JOYSTICK_AXIS_WHEEL, "Wheel"},
{JOYSTICK_AXIS_GAS, "Gas"},
{JOYSTICK_AXIS_BRAKE, "Brake"},
{JOYSTICK_AXIS_DISTANCE, "Distance"},
{JOYSTICK_AXIS_TILT, "Tilt"},
{JOYSTICK_AXIS_MOUSE_REL_X, "MouseDX"},
{JOYSTICK_AXIS_MOUSE_REL_Y, "MouseDY"},
{JOYSTICK_AXIS_ACCELEROMETER_X, "AccelX"},
{JOYSTICK_AXIS_ACCELEROMETER_Y, "AccelY"},
{JOYSTICK_AXIS_ACCELEROMETER_Z, "AccelZ"},
};
const KeyMap_IntStrPair psp_button_names[] = {
{CTRL_UP, "Up"},
{CTRL_DOWN, "Down"},
{CTRL_LEFT, "Left"},
{CTRL_RIGHT, "Right"},
{CTRL_CIRCLE, "Circle"},
{CTRL_CROSS, "Cross"},
{CTRL_SQUARE, "Square"},
{CTRL_TRIANGLE, "Triangle"},
{CTRL_START, "Start"},
{CTRL_SELECT, "Select"},
{CTRL_LTRIGGER, "L"},
{CTRL_RTRIGGER, "R"},
{VIRTKEY_AXIS_Y_MAX, "An.Up"},
{VIRTKEY_AXIS_Y_MIN, "An.Down"},
{VIRTKEY_AXIS_X_MIN, "An.Left"},
{VIRTKEY_AXIS_X_MAX, "An.Right"},
{VIRTKEY_ANALOG_ROTATE_CW, "Rotate Analog (CW)"},
{VIRTKEY_ANALOG_ROTATE_CCW, "Rotate Analog (CCW)"},
{VIRTKEY_ANALOG_LIGHTLY, "Analog limiter"},
{VIRTKEY_RAPID_FIRE, "RapidFire"},
{VIRTKEY_AXIS_SWAP_HOLD, "Axis swap (hold)"},
{VIRTKEY_AXIS_SWAP_TOGGLE, "Axis swap (toggle)"},
{VIRTKEY_FASTFORWARD, "Fast-forward"},
{VIRTKEY_PAUSE, "Pause"},
{VIRTKEY_PAUSE_NO_MENU, "Pause (no menu)"},
{VIRTKEY_SPEED_TOGGLE, "SpeedToggle"},
{VIRTKEY_SPEED_CUSTOM1, "Alt speed 1"},
{VIRTKEY_SPEED_CUSTOM2, "Alt speed 2"},
{VIRTKEY_SPEED_ANALOG, "Analog speed"},
{VIRTKEY_RESET_EMULATION, "Reset"},
{VIRTKEY_FRAME_ADVANCE, "Frame Advance"},
#if !defined(MOBILE_DEVICE)
{VIRTKEY_RECORD, "Audio/Video Recording" },
#endif
{VIRTKEY_REWIND, "Rewind"},
{VIRTKEY_SAVE_STATE, "Save State"},
{VIRTKEY_LOAD_STATE, "Load State"},
{VIRTKEY_PREVIOUS_SLOT, "Previous Slot"},
{VIRTKEY_NEXT_SLOT, "Next Slot"},
#if !defined(MOBILE_DEVICE)
{VIRTKEY_TOGGLE_FULLSCREEN, "Toggle Fullscreen"},
#endif
{VIRTKEY_TOGGLE_DEBUGGER, "Toggle Debugger"},
{VIRTKEY_TOGGLE_TILT, "Toggle tilt control"},
{VIRTKEY_OPENCHAT, "OpenChat" },
{VIRTKEY_DEVMENU, "DevMenu"},
{VIRTKEY_TEXTURE_DUMP, "Texture Dumping"},
{VIRTKEY_TEXTURE_REPLACE, "Texture Replacement"},
{VIRTKEY_SCREENSHOT, "Screenshot"},
{VIRTKEY_MUTE_TOGGLE, "Mute toggle"},
#ifdef OPENXR
{VIRTKEY_VR_CAMERA_ADJUST, "VR camera adjust"},
{VIRTKEY_VR_CAMERA_RESET, "VR camera reset"},
#else
{VIRTKEY_SCREEN_ROTATION_VERTICAL, "Display Portrait"},
{VIRTKEY_SCREEN_ROTATION_VERTICAL180, "Display Portrait Reversed"},
{VIRTKEY_SCREEN_ROTATION_HORIZONTAL, "Display Landscape"},
{VIRTKEY_SCREEN_ROTATION_HORIZONTAL180, "Display Landscape Reversed"},
#endif
{VIRTKEY_TOGGLE_WLAN, "Toggle WLAN"},
{VIRTKEY_EXIT_APP, "Exit App"},
{VIRTKEY_TOGGLE_MOUSE, "Toggle mouse input"},
{VIRTKEY_TOGGLE_TOUCH_CONTROLS, "Toggle touch controls"},
{VIRTKEY_AXIS_RIGHT_Y_MAX, "RightAn.Up"},
{VIRTKEY_AXIS_RIGHT_Y_MIN, "RightAn.Down"},
{VIRTKEY_AXIS_RIGHT_X_MIN, "RightAn.Left"},
{VIRTKEY_AXIS_RIGHT_X_MAX, "RightAn.Right"},
{CTRL_HOME, "Home"},
{CTRL_HOLD, "Hold"},
{CTRL_WLAN, "Wlan"},
{CTRL_REMOTE_HOLD, "Remote hold"},
{CTRL_VOL_UP, "Vol +"},
{CTRL_VOL_DOWN, "Vol -"},
{CTRL_SCREEN, "Screen"},
{CTRL_NOTE, "Note"},
{CTRL_L2, "Dev-kit L2"},
{CTRL_L3, "Dev-kit L3"},
{CTRL_R2, "Dev-kit R2"},
{CTRL_R3, "Dev-kit R3"},
};
// key here can be other things than InputKeyCode.
static std::string FindName(int key, const KeyMap_IntStrPair list[], size_t size) {
for (size_t i = 0; i < size; i++) {
if (list[i].key == key)
return list[i].name;
}
return StringFromFormat("%02x?", key);
}
std::string GetKeyName(InputKeyCode keyCode) {
return FindName(keyCode, key_names, ARRAY_SIZE(key_names));
}
std::string GetKeyOrAxisName(const InputMapping &mapping) {
if (mapping.IsAxis()) {
int direction;
int axis = mapping.Axis(&direction);
std::string temp = GetAxisName(axis);
if (direction == 1)
temp += "+";
else if (direction == -1)
temp += "-";
return temp;
} else {
return FindName(mapping.keyCode, key_names, ARRAY_SIZE(key_names));
}
}
std::string GetAxisName(int axisId) {
return FindName(axisId, axis_names, ARRAY_SIZE(axis_names));
}
std::string GetPspButtonName(int btn) {
return FindName(btn, psp_button_names, ARRAY_SIZE(psp_button_names));
}
const char* GetPspButtonNameCharPointer(int btn) {
for (size_t i = 0; i < ARRAY_SIZE(psp_button_names); i++)
if (psp_button_names[i].key == btn)
return psp_button_names[i].name;
return nullptr;
}
const KeyMap::KeyMap_IntStrPair *GetMappableKeys(size_t *count) {
*count = ARRAY_SIZE(psp_button_names);
return psp_button_names;
}
bool InputMappingToPspButton(const InputMapping &mapping, std::vector<int> *pspButtons) {
bool found = false;
for (auto iter = g_controllerMap.begin(); iter != g_controllerMap.end(); ++iter) {
for (auto iter2 = iter->second.begin(); iter2 != iter->second.end(); ++iter2) {
if (iter2->EqualsSingleMapping(mapping)) {
if (pspButtons)
pspButtons->push_back(iter->first);
found = true;
}
}
}
return found;
}
bool InputMappingsFromPspButton(int btn, std::vector<MultiInputMapping> *mappings, bool ignoreMouse) {
auto iter = g_controllerMap.find(btn);
if (iter == g_controllerMap.end()) {
return false;
}
bool mapped = false;
if (mappings) {
mappings->clear();
}
for (auto &iter2 : iter->second) {
bool ignore = ignoreMouse && iter2.HasMouse();
if (!ignore) {
mapped = true;
if (mappings) {
mappings->push_back(iter2);
}
}
}
return mapped;
}
bool PspButtonHasMappings(int btn) {
auto iter = g_controllerMap.find(btn);
if (iter == g_controllerMap.end()) {
return false;
}
return !iter->second.empty();
}
MappedAnalogAxes MappedAxesForDevice(InputDeviceID deviceId) {
// Find the axisId mapped for a specific virtual button.
auto findAxisId = [&](int btn) -> MappedAnalogAxis {
MappedAnalogAxis info{ -1 };
for (const auto &key : g_controllerMap[btn]) {
// Only consider single mappings, combos don't make much sense for these.
if (key.mappings.empty()) continue;
auto &mapping = key.mappings[0];
if (mapping.deviceId == deviceId) {
info.axisId = TranslateKeyCodeToAxis(mapping.keyCode, &info.direction);
return info;
}
}
return info;
};
// Find the axisId of a pair of opposing buttons.
auto findAxisIdPair = [&](int minBtn, int maxBtn) -> MappedAnalogAxis {
MappedAnalogAxis foundMin = findAxisId(minBtn);
MappedAnalogAxis foundMax = findAxisId(maxBtn);
if (foundMin.axisId == foundMax.axisId) {
return foundMax;
}
return MappedAnalogAxis{ -1 };
};
MappedAnalogAxes result;
result.leftX = findAxisIdPair(VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX);
result.leftY = findAxisIdPair(VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_Y_MAX);
result.rightX = findAxisIdPair(VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX);
result.rightY = findAxisIdPair(VIRTKEY_AXIS_RIGHT_Y_MIN, VIRTKEY_AXIS_RIGHT_Y_MAX);
return result;
}
void RemoveButtonMapping(int btn) {
for (auto iter = g_controllerMap.begin(); iter != g_controllerMap.end(); ++iter) {
if (iter->first == btn) {
g_controllerMap.erase(iter);
return;
}
}
}
bool IsKeyMapped(InputDeviceID device, int key) {
for (auto &iter : g_controllerMap) {
for (auto &mappedKey : iter.second) {
if (mappedKey.mappings.contains(InputMapping(device, key))) {
return true;
}
}
}
return false;
}
bool ReplaceSingleKeyMapping(int btn, int index, const MultiInputMapping &key) {
// Check for duplicate
for (int i = 0; i < (int)g_controllerMap[btn].size(); ++i) {
if (i != index && g_controllerMap[btn][i] == key) {
g_controllerMap[btn].erase(g_controllerMap[btn].begin()+index);
g_controllerMapGeneration++;
UpdateNativeMenuKeys();
return false;
}
}
if (key.empty()) {
return false;
}
KeyMap::g_controllerMap[btn][index] = key;
g_controllerMapGeneration++;
for (auto &mapping : key.mappings) {
g_seenDeviceIds.insert(mapping.deviceId);
}
UpdateNativeMenuKeys();
return true;
}
void DeleteNthMapping(int key, int number) {
auto iter = g_controllerMap.find(key);
if (iter != g_controllerMap.end()) {
if (number < iter->second.size()) {
iter->second.erase(iter->second.begin() + number);
g_controllerMapGeneration++;
}
}
}
void SetInputMapping(int btn, const MultiInputMapping &key, bool replace) {
if (key.empty()) {
g_controllerMap.erase(btn);
return;
}
if (replace) {
RemoveButtonMapping(btn);
g_controllerMap[btn].clear();
g_controllerMap[btn].push_back(key);
} else {
for (auto iter = g_controllerMap[btn].begin(); iter != g_controllerMap[btn].end(); ++iter) {
if (*iter == key)
return;
}
g_controllerMap[btn].push_back(key);
}
g_controllerMapGeneration++;
for (auto &mapping : key.mappings) {
g_seenDeviceIds.insert(mapping.deviceId);
}
}
void RestoreDefault() {
g_controllerMap.clear();
g_controllerMapGeneration++;
if (IsVREnabled()) {
SetDefaultKeyMap(DEFAULT_MAPPING_VR_HEADSET, false);
return;
}
#if PPSSPP_PLATFORM(WINDOWS)
SetDefaultKeyMap(DEFAULT_MAPPING_KEYBOARD, true);
SetDefaultKeyMap(DEFAULT_MAPPING_XINPUT, false);
SetDefaultKeyMap(DEFAULT_MAPPING_PAD, false);
#elif PPSSPP_PLATFORM(ANDROID)
// Autodetect a few common (and less common) devices
// Note that here we check the device name, not the controller name. We don't get
// the controller name until a button has been pressed so can't use it to set defaults.
std::string name = System_GetProperty(SYSPROP_NAME);
if (IsNvidiaShield(name)) {
SetDefaultKeyMap(DEFAULT_MAPPING_SHIELD, false);
} else if (IsXperiaPlay(name)) {
SetDefaultKeyMap(DEFAULT_MAPPING_XPERIA_PLAY, false);
} else if (IsMOQII7S(name)) {
SetDefaultKeyMap(DEFAULT_MAPPING_MOQI_I7S, false);
} else if (IsRetroid(name)) {
SetDefaultKeyMap(DEFAULT_MAPPING_RETROID_CONTROLLER, false);
} else {
SetDefaultKeyMap(DEFAULT_MAPPING_ANDROID_PAD, false);
}
#elif PPSSPP_PLATFORM(IOS)
SetDefaultKeyMap(DEFAULT_MAPPING_IOS_PAD, false);
#else
SetDefaultKeyMap(DEFAULT_MAPPING_KEYBOARD, true);
SetDefaultKeyMap(DEFAULT_MAPPING_PAD, false);
#endif
}
// TODO: Make the ini format nicer.
void LoadFromIni(IniFile &file) {
RestoreDefault();
if (!file.HasSection("ControlMapping")) {
return;
}
Section *controls = file.GetOrCreateSection("ControlMapping");
for (size_t i = 0; i < ARRAY_SIZE(psp_button_names); i++) {
std::string value;
controls->Get(psp_button_names[i].name, &value);
// Erase default mapping
g_controllerMap.erase(psp_button_names[i].key);
if (value.empty())
continue;
std::vector<std::string> mappings;
SplitString(value, ',', mappings);
for (size_t j = 0; j < mappings.size(); j++) {
MultiInputMapping input = MultiInputMapping::FromConfigString(mappings[j]);
if (input.empty()) {
continue; // eat empty mappings, however they arose, so they can't keep haunting us.
}
SetInputMapping(psp_button_names[i].key, input, false);
for (auto mapping : input.mappings) {
g_seenDeviceIds.insert(mapping.deviceId);
}
}
}
UpdateNativeMenuKeys();
}
void SaveToIni(IniFile &file) {
Section *controls = file.GetOrCreateSection("ControlMapping");
for (size_t i = 0; i < ARRAY_SIZE(psp_button_names); i++) {
std::vector<MultiInputMapping> keys;
InputMappingsFromPspButton(psp_button_names[i].key, &keys, false);
std::string value;
for (size_t j = 0; j < keys.size(); j++) {
value += keys[j].ToConfigString();
if (j != keys.size() - 1)
value += ",";
}
controls->Set(psp_button_names[i].name, value, "");
}
}
void ClearAllMappings() {
g_controllerMap.clear();
g_controllerMapGeneration++;
}
bool IsNvidiaShield(std::string_view name) {
return name == "NVIDIA:SHIELD";
}
bool IsRetroid(std::string_view name) {
// TODO: Not sure if there are differences between different Retroid devices.
// The one I have is a "Retroid Pocket 2+".
return startsWith(name, "Retroid:");
}
bool IsNvidiaShieldTV(std::string_view name) {
return name == "NVIDIA:SHIELD Android TV";
}
bool IsXperiaPlay(std::string_view name) {
return name == "Sony Ericsson:R800a" || name == "Sony Ericsson:R800i" || name == "Sony Ericsson:R800x" || name == "Sony Ericsson:R800at" || name == "Sony Ericsson:SO-01D" || name == "Sony Ericsson:zeus";
}
bool IsMOQII7S(std::string_view name) {
return name == "MOQI:I7S";
}
bool HasBuiltinController(std::string_view name) {
return IsXperiaPlay(name) || IsNvidiaShield(name) || IsMOQII7S(name) || IsRetroid(name);
}
void NotifyPadConnected(InputDeviceID deviceId, std::string_view name) {
{
g_seenPads.insert(std::string(name));
g_padNames[deviceId] = name;
// Don't notify within the first 5 seconds, to avoid notification spam on startup.
// Also for some reason we get some strange things on Android... "Virtual"?
if (time_now_d() >= 5.0 && name != "Virtual") {
auto co = GetI18NCategory(I18NCat::CONTROLS);
g_OSD.Show(OSDType::MESSAGE_SUCCESS, ApplySafeSubstitutions("%1: %2", co->T("Game controller connected"), name), "", "I_CONTROLLER", 2.0f, "controller_connected");
}
}
System_Notify(SystemNotification::PAD_STATE_CHANGED);
}
void NotifyPadDisconnected(InputDeviceID deviceId) {
{
auto iter = g_padNames.find(deviceId);
if (iter != g_padNames.end()) {
auto co = GetI18NCategory(I18NCat::CONTROLS);
g_OSD.Show(OSDType::MESSAGE_WARNING, ApplySafeSubstitutions("%1: %2", co->T("Game controller disconnected"), iter->second), "", "I_CONTROLLER", 2.0f, "controller_connected");
g_seenPads.erase(iter->second);
}
g_padNames.erase(deviceId);
}
System_Notify(SystemNotification::PAD_STATE_CHANGED);
}
void ClearControlsWithDeviceId(InputDeviceID deviceId) {
bool modified = false;
for (auto iter = g_controllerMap.begin(); iter != g_controllerMap.end(); ++iter) {
auto &mappings = iter->second;
for (auto mapIter = mappings.begin(); mapIter != mappings.end(); ) {
bool found = false;
for (auto &mapping : mapIter->mappings) {
if (mapping.deviceId == deviceId) {
found = true;
break;
}
}
if (found) {
mapIter = mappings.erase(mapIter);
modified = true;
} else {
++mapIter;
}
}
}
if (modified) {
g_controllerMapGeneration++;
}
}
void AutoConfForPad(std::string_view name) {
InputDeviceID deviceId = DEVICE_ID_PAD_0;
for (auto [padDeviceId, padName] : g_padNames) {
if (padName == name) {
// Already configured.
deviceId = padDeviceId;
}
}
ClearControlsWithDeviceId(deviceId);
#if PPSSPP_PLATFORM(ANDROID)
if (name.find("Xbox") != std::string::npos) {
SetDefaultKeyMap(DEFAULT_MAPPING_ANDROID_XBOX, false);
} else if (name == "Retro Station Controller") {
SetDefaultKeyMap(DEFAULT_MAPPING_RETROID_CONTROLLER, false);
} else {
SetDefaultKeyMap(DEFAULT_MAPPING_ANDROID_PAD, false);
}
#else
#if PPSSPP_PLATFORM(WINDOWS)
const bool platformSupportsXinput = true;
#else
const bool platformSupportsXinput = false;
#endif
if (platformSupportsXinput && name.find("Xbox") != std::string::npos) {
SetDefaultKeyMap(DEFAULT_MAPPING_XINPUT, false);
} else {
SetDefaultKeyMap(DEFAULT_MAPPING_PAD, false);
}
#endif
// Add a couple of convenient keyboard mappings by default, too.
#if !defined(MOBILE_DEVICE)
g_controllerMap[VIRTKEY_PAUSE].push_back(MultiInputMapping(InputMapping(DEVICE_ID_KEYBOARD, NKCODE_ESCAPE)));
g_controllerMap[VIRTKEY_FASTFORWARD].push_back(MultiInputMapping(InputMapping(DEVICE_ID_KEYBOARD, NKCODE_TAB)));
#endif
g_controllerMapGeneration++;
}
const std::set<std::string> &GetSeenPads() {
return g_seenPads;
}
std::string PadName(InputDeviceID deviceId) {
auto it = g_padNames.find(deviceId);
if (it != g_padNames.end())
return it->second;
return "";
}
bool HasChanged(int &prevGeneration) {
if (prevGeneration != g_controllerMapGeneration) {
prevGeneration = g_controllerMapGeneration;
return true;
}
return false;
}
MultiInputMapping MultiInputMapping::FromConfigString(std::string_view str) {
MultiInputMapping out;
std::vector<std::string_view> parts;
SplitString(str, ':', parts);
for (auto iter : parts) {
out.mappings.push_back(InputMapping::FromConfigString(iter));
}
return out;
}
std::string MultiInputMapping::ToConfigString() const {
std::string out;
for (auto iter : mappings) {
out += iter.ToConfigString() + ":";
}
out.pop_back(); // remove the last ':'
return out;
}
std::string MultiInputMapping::ToVisualString() const {
std::string out;
for (auto iter : mappings) {
out += std::string(GetDeviceName(iter.deviceId)) + "." + GetKeyOrAxisName(iter) + " + ";
}
if (!out.empty()) {
// remove the last ' + '
out.pop_back();
out.pop_back();
out.pop_back();
}
return out;
}
} // KeyMap