Implemented Controller Hotkeys (#111)

Implemented Controller Hotkeys

Added controller hotkeys
Added guide button support for XInput and winkey

The hotkey configurations can be found in HID -> Display controller hotkeys
If the Xbox Gamebar overlay is enabled then use the Back button instead of the Guide button.

- Fixed hotkey thread destruction
- Fixed XINPUT_STATE by padding 4 bytes
- Added hotkey vibration for user feedback
- Replaced MessageBoxA with ImGuiDialog::ShowMessageBox

Co-authored-by: Margen67 <Margen67@users.noreply.github.com>
This commit is contained in:
Adrian
2023-01-13 08:17:43 +00:00
committed by GitHub
parent ea1003c6bf
commit 459497f0b6
12 changed files with 455 additions and 50 deletions

View File

@@ -31,6 +31,7 @@
#include "xenia/cpu/processor.h"
#include "xenia/emulator.h"
#include "xenia/gpu/command_processor.h"
#include "xenia/gpu/d3d12/d3d12_command_processor.h"
#include "xenia/gpu/graphics_system.h"
#include "xenia/hid/input_system.h"
#include "xenia/ui/file_picker.h"
@@ -47,6 +48,14 @@
DECLARE_bool(debug);
DECLARE_string(hid);
DECLARE_bool(guide_button);
DECLARE_bool(d3d12_readback_resolve);
DECLARE_bool(d3d12_clear_memory_page_state);
DEFINE_bool(fullscreen, false, "Whether to launch the emulator in fullscreen.",
"Display");
@@ -134,6 +143,9 @@ using xe::ui::KeyEvent;
using xe::ui::MenuItem;
using xe::ui::UIEvent;
using namespace xe::hid;
using namespace xe::gpu;
const std::string kBaseTitle = "Xenia-canary";
EmulatorWindow::EmulatorWindow(Emulator* emulator,
@@ -230,6 +242,15 @@ void EmulatorWindow::OnEmulatorInitialized() {
if (cvars::fullscreen) {
SetFullscreen(true);
}
if (IsUseNexusForGameBarEnabled()) {
XELOGE("Xbox Gamebar Enabled, using BACK button instead of GUIDE!!!");
}
// Create a thread to listen for controller hotkeys.
Gamepad_HotKeys_Listener =
threading::Thread::Create({}, [&] { GamepadHotKeys(); });
Gamepad_HotKeys_Listener->set_name("Gamepad HotKeys Listener");
}
void EmulatorWindow::EmulatorWindowListener::OnClosing(ui::UIEvent& e) {
@@ -608,6 +629,9 @@ bool EmulatorWindow::Initialize() {
hid_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&Toggle controller vibration", "",
std::bind(&EmulatorWindow::ToggleControllerVibration, this)));
hid_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&Display controller hotkeys", "",
std::bind(&EmulatorWindow::DisplayHotKeysConfig, this)));
}
main_menu->AddChild(std::move(hid_menu));
@@ -888,10 +912,10 @@ void EmulatorWindow::InstallContent() {
if (result != X_STATUS_SUCCESS) {
XELOGE("Failed to install content! Error code: {:08X}", result);
MessageBoxA(nullptr,
"Failed to install content!\n\nCheck xenia.log for technical "
"details.",
"Failed to install content!", MB_ICONERROR);
xe::ui::ImGuiDialog::ShowMessageBox(
imgui_drawer_.get(), "Failed to install content!",
"Failed to install content!\n\nCheck xenia.log for technical "
"details.");
}
}
}
@@ -1076,26 +1100,308 @@ void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {
UpdateTitle();
}
void EmulatorWindow::RunPreviouslyPlayedTitle() {
if (recently_launched_titles_.size() >= 1) {
RunTitle(recently_launched_titles_[0].path_to_file);
// Notes:
// Assumes titles do not use the guide button.
// For titles that do such as dashboards these titles could be excluded based on
// their title ID.
//
// If the Xbox Gamebar overlay is enabled Windows will consume the guide
// button's input, this can be seen using hid-demo.
//
// Workaround: Detect if the Xbox Gamebar overlay is enabled then use the BACK
// button instead of the GUIDE button. Therefore BACK and GUIDE are reserved
// buttons for hotkeys.
//
// This is not an issue with DualShock controllers because Windows will not
// open the gamebar overlay using the PlayStation menu button.
//
// Steam:
// If guide button focus is enabled steam will open.
// Steam uses BACK + GUIDE to open an On-Screen keyboard, however this is not a
// problem since both these buttons are reserved.
const std::map<int, EmulatorWindow::ControllerHotKey> controller_hotkey_map = {
// Must use the Guide Button for all pass through hotkeys
{X_INPUT_GAMEPAD_A | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::ReadbackResolve,
"A + Guide = Toggle Readback Resolve", true)},
{X_INPUT_GAMEPAD_B | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::CloseWindow,
"B + Guide = Close Window")},
{X_INPUT_GAMEPAD_Y | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::ToggleFullscreen,
"Y + Guide = Toggle Fullscreen", true)},
{X_INPUT_GAMEPAD_X | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::ClearMemoryPageState,
"X + Guide = Toggle Clear Memory Page State", true)},
{X_INPUT_GAMEPAD_RIGHT_SHOULDER | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::ToggleControllerVibration,
"Right Shoulder + Guide = Toggle Controller Vibration", true)},
// CPU Time Scalar with no rumble feedback
{X_INPUT_GAMEPAD_DPAD_DOWN | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::CpuTimeScalarSetHalf,
"D-PAD Down + Guide = Half CPU Scalar")},
{X_INPUT_GAMEPAD_DPAD_UP | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::CpuTimeScalarSetDouble,
"D-PAD Up + Guide = Double CPU Scalar")},
{X_INPUT_GAMEPAD_DPAD_RIGHT | X_INPUT_GAMEPAD_GUIDE,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::CpuTimeScalarReset,
"D-PAD Right + Guide = Reset CPU Scalar")},
// non-pass through hotkeys
{X_INPUT_GAMEPAD_Y, EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::ToggleFullscreen,
"Y = Toggle Fullscreen", true, false)},
{X_INPUT_GAMEPAD_START,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::RunPreviouslyPlayedTitle,
"Start = Run previously played title", false, false)},
{X_INPUT_GAMEPAD_BACK | X_INPUT_GAMEPAD_START,
EmulatorWindow::ControllerHotKey(
EmulatorWindow::ButtonFunctions::CloseWindow,
"Back + Start = Close Window", false, false)}};
EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey(
int buttons) {
// Default return value
EmulatorWindow::ControllerHotKey Unknown_hotkey = {};
if (buttons == 0) return Unknown_hotkey;
// Hotkey cool-down to prevent toggling too fast
const std::chrono::milliseconds delay(75);
// If the Xbox Gamebar is enabled or the Guide button is disabled then
// replace the Guide button with the Back button without redeclaring the key
// mappings
if (IsUseNexusForGameBarEnabled() || !cvars::guide_button) {
if ((buttons & X_INPUT_GAMEPAD_BACK) == X_INPUT_GAMEPAD_BACK) {
buttons &= ~X_INPUT_GAMEPAD_BACK;
buttons |= X_INPUT_GAMEPAD_GUIDE;
}
}
auto it = controller_hotkey_map.find(buttons);
if (it == controller_hotkey_map.end()) {
return Unknown_hotkey;
}
// Do not activate hotkeys that are not intended for activation during
// gameplay
if (emulator_->is_title_open() && !it->second.title_passthru) {
return Unknown_hotkey;
}
EmulatorWindow::ControllerHotKey button_combination = it->second;
switch (button_combination.function) {
case ButtonFunctions::ToggleFullscreen:
ToggleFullscreen();
// Extra Sleep
xe::threading::Sleep(delay);
break;
case ButtonFunctions::RunPreviouslyPlayedTitle:
RunPreviouslyPlayedTitle();
break;
case ButtonFunctions::ClearMemoryPageState:
ToggleGPUSetting(gpu_cvar::ClearMemoryPageState);
// Assume the user wants ClearCaches as well
if (cvars::d3d12_clear_memory_page_state) {
GpuClearCaches();
}
// Extra Sleep
xe::threading::Sleep(delay);
break;
case ButtonFunctions::ReadbackResolve:
ToggleGPUSetting(gpu_cvar::ReadbackResolve);
// Extra Sleep
xe::threading::Sleep(delay);
break;
case ButtonFunctions::CpuTimeScalarSetHalf:
CpuTimeScalarSetHalf();
break;
case ButtonFunctions::CpuTimeScalarSetDouble:
CpuTimeScalarSetDouble();
break;
case ButtonFunctions::CpuTimeScalarReset:
CpuTimeScalarReset();
break;
case ButtonFunctions::ToggleControllerVibration:
ToggleControllerVibration();
// Extra Sleep
xe::threading::Sleep(delay);
break;
case ButtonFunctions::CloseWindow:
window_->RequestClose();
break;
case ButtonFunctions::Unknown:
default:
break;
}
xe::threading::Sleep(delay);
return it->second;
}
void EmulatorWindow::VibrateController(xe::hid::InputSystem* input_sys,
bool toggle_rumble) {
const std::chrono::milliseconds rumble_duration(100);
// Hold lock while sleeping this thread for the duration of the rumble,
// otherwise the rumble may fail.
auto input_lock = input_sys->lock();
X_INPUT_VIBRATION vibration;
vibration.left_motor_speed = toggle_rumble ? UINT16_MAX : 0;
vibration.right_motor_speed = toggle_rumble ? UINT16_MAX : 0;
input_sys->SetState(0, &vibration);
// Vibration duration
if (toggle_rumble) {
xe::threading::Sleep(rumble_duration);
}
}
void EmulatorWindow::GamepadHotKeys() {
X_INPUT_STATE state;
const std::chrono::milliseconds thread_delay(75);
auto input_sys = emulator_->input_system();
if (input_sys) {
// SDL and XInput both support the guide button
while (true) {
// Block scope surrounding input_lock used to release the lock
{
auto input_lock = input_sys->lock();
for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) {
input_sys->GetState(user_index, &state);
}
}
if (ProcessControllerHotkey(state.gamepad.buttons).rumble) {
// Enable Vibration
VibrateController(input_sys, true);
// Disable Vibration
VibrateController(input_sys, false);
}
xe::threading::Sleep(thread_delay);
}
}
}
void EmulatorWindow::ToggleGPUSetting(gpu_cvar value) {
switch (value) {
case gpu_cvar::ClearMemoryPageState:
D3D12SaveGPUSetting(D3D12GPUSetting::ClearMemoryPageState,
!cvars::d3d12_clear_memory_page_state);
break;
case gpu_cvar::ReadbackResolve:
D3D12SaveGPUSetting(D3D12GPUSetting::ReadbackResolve,
!cvars::d3d12_readback_resolve);
break;
}
}
// Determine if the Xbox Gamebar is enabled via the Windows registry
bool EmulatorWindow::IsUseNexusForGameBarEnabled() {
#ifdef _WIN32
const LPWSTR reg_path = L"SOFTWARE\\Microsoft\\GameBar";
const LPWSTR key = L"UseNexusForGameBarEnabled";
DWORD value = 0;
DWORD dataSize = sizeof(value);
RegGetValue(HKEY_CURRENT_USER, reg_path, key, RRF_RT_DWORD, nullptr, &value,
&dataSize);
return (bool)value;
#else
return false;
#endif
}
void EmulatorWindow::DisplayHotKeysConfig() {
std::string msg = "";
std::string msg_passthru = "";
bool guide_enabled = !IsUseNexusForGameBarEnabled() && cvars::guide_button;
for (auto const& [key, val] : controller_hotkey_map) {
std::string pretty_text = val.pretty;
if (!guide_enabled) {
pretty_text = std::regex_replace(
pretty_text,
std::regex("Guide", std::regex_constants::syntax_option_type::icase),
"Back");
}
if (emulator_->is_title_open() && !val.title_passthru) {
pretty_text += " (Disabled)";
}
if (val.title_passthru) {
msg += pretty_text + "\n";
} else {
msg_passthru += pretty_text + "\n";
}
}
// Add Title
msg.insert(0, "Gameplay Hotkeys\n");
// Prepend non-passthru hotkeys
msg_passthru += "\n";
msg.insert(0, msg_passthru);
msg += "\n";
msg += "Readback Resolve: " +
std::string(cvars::d3d12_readback_resolve ? "true\n" : "false\n");
msg +=
"Clear Memory Page State: " +
std::string(cvars::d3d12_clear_memory_page_state ? "true\n" : "false\n");
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Controller Hotkeys",
msg);
}
xe::X_STATUS EmulatorWindow::RunTitle(std::filesystem::path path) {
bool titleExists = !std::filesystem::exists(path);
if (path.empty() || titleExists) {
char* log_msg = path.empty() ? "Failed to launch title path is empty"
: "Failed to launch title path is invalid";
char* log_msg = path.empty() ? "Failed to launch title path is empty."
: "Failed to launch title path is invalid.";
XELOGE(log_msg);
MessageBoxA(nullptr, log_msg, "Title Launch Failed!", MB_ICONERROR);
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(),
"Title Launch Failed!", log_msg);
return X_STATUS_NO_SUCH_FILE;
}
if (emulator_->is_title_open()) {
// Terminate the current title and start a new title.
// if (emulator_->TerminateTitle() == X_STATUS_SUCCESS) {
@@ -1113,10 +1419,9 @@ xe::X_STATUS EmulatorWindow::RunTitle(std::filesystem::path path) {
if (result) {
XELOGE("Failed to launch target: {:08X}", result);
MessageBoxA(
nullptr,
"Failed to launch title.\n\nCheck xenia.log for technical details.",
"Title Launch Failed!", MB_ICONERROR);
xe::ui::ImGuiDialog::ShowMessageBox(
imgui_drawer_.get(), "Title Launch Failed!",
"Failed to launch title.\n\nCheck xenia.log for technical details.");
} else {
AddRecentlyLaunchedTitle(path, emulator_->title_name());
}
@@ -1124,6 +1429,12 @@ xe::X_STATUS EmulatorWindow::RunTitle(std::filesystem::path path) {
return result;
}
void EmulatorWindow::RunPreviouslyPlayedTitle() {
if (recently_launched_titles_.size() >= 1) {
RunTitle(recently_launched_titles_[0].path_to_file);
}
}
void EmulatorWindow::FillRecentlyLaunchedTitlesMenu(
xe::ui::MenuItem* recent_menu) {
unsigned int index = 0;

View File

@@ -25,6 +25,8 @@
#include "xenia/ui/windowed_app_context.h"
#include "xenia/xbox.h"
#define MAX_USERS 4
namespace xe {
namespace app {
@@ -53,6 +55,8 @@ class EmulatorWindow {
static std::unique_ptr<EmulatorWindow> Create(
Emulator* emulator, ui::WindowedAppContext& app_context);
std::unique_ptr<xe::threading::Thread> Gamepad_HotKeys_Listener;
Emulator* emulator() const { return emulator_; }
ui::WindowedAppContext& app_context() const { return app_context_; }
ui::Window* window() const { return window_.get(); }
@@ -69,6 +73,47 @@ class EmulatorWindow {
void ToggleFullscreen();
void SetInitializingShaderStorage(bool initializing);
// Types of button functions for hotkeys.
enum class ButtonFunctions {
ToggleFullscreen,
RunPreviouslyPlayedTitle,
CpuTimeScalarSetHalf,
CpuTimeScalarSetDouble,
CpuTimeScalarReset,
ToggleControllerVibration,
ClearMemoryPageState,
ReadbackResolve,
CloseWindow,
Unknown
};
enum class gpu_cvar {
ClearMemoryPageState,
ReadbackResolve,
};
class ControllerHotKey {
public:
// If true the hotkey can be activated while a title is running, otherwise
// false.
bool title_passthru;
// If true vibrate the controller after activating the hotkey, otherwise
// false.
bool rumble;
std::string pretty;
ButtonFunctions function;
ControllerHotKey(ButtonFunctions fn = ButtonFunctions::Unknown,
std::string pretty = "", bool rumble = false,
bool active = true) {
function = fn;
this->pretty = pretty;
title_passthru = active;
this->rumble = rumble;
}
};
private:
class EmulatorWindowListener final : public ui::WindowListener,
public ui::WindowInputListener {
@@ -152,6 +197,13 @@ class EmulatorWindow {
void ShowFAQ();
void ShowBuildCommit();
EmulatorWindow::ControllerHotKey ProcessControllerHotkey(int buttons);
void VibrateController(xe::hid::InputSystem* input_sys, bool vibrate = true);
void GamepadHotKeys();
void ToggleGPUSetting(gpu_cvar index);
bool IsUseNexusForGameBarEnabled();
void DisplayHotKeysConfig();
xe::X_STATUS RunTitle(std::filesystem::path path);
void RunPreviouslyPlayedTitle();
void FillRecentlyLaunchedTitlesMenu(xe::ui::MenuItem* recent_menu);