Compare commits

...

10 Commits

Author SHA1 Message Date
6e4cc779d7 It builds 2023-05-14 11:45:00 +03:00
2a4f9b5dd4 Qt: Fix mouse scaling
Since yuzu-emu/yuzu#4949 any user that had a window scale different from 100% will have wrong mouse coordinates. This PR fixes the issue by removing the workaround for scaling since now it should be 1:1 at any scale.

This PR also splits mouse input into 3 different devices. A physical mouse, touch and finally one for controllers. This solves the issue Zeikken85 had where having emulated mouse enabled had a weird behavior on stick input. As a final protection against setting invalid configs a warning message will show up if emulated mouse is enabled.

Co-Authored-By: Narr the Reg <5944268+german77@users.noreply.github.com>
2023-05-14 01:10:14 +03:00
0382b275c0 input_common: Simplify stick from button 2023-05-14 01:10:14 +03:00
b714114ab0 yuzu: config: Avoid reading deleted object 2023-05-14 01:10:14 +03:00
79e505cb6f fix input profiles 2023-05-14 01:10:14 +03:00
a2a1f6c14c narr don't look 2023-05-14 01:10:13 +03:00
dfc359affb Stash "TODO" 2023-05-14 01:10:13 +03:00
b2e04bf8f3 Stash "DONE"
# Conflicts:
#	src/citra_qt/bootmanager.cpp
#	src/citra_qt/configuration/configure_motion_touch.ui
#	src/citra_qt/main.cpp
#	src/common/settings.cpp
#	src/core/CMakeLists.txt
#	src/core/core.h
2023-05-14 01:10:13 +03:00
a24cac6308 add common changes 2023-05-14 01:10:13 +03:00
e44ec20e57 add input common changes 2023-05-14 01:10:09 +03:00
111 changed files with 12978 additions and 5449 deletions

View File

@ -358,18 +358,18 @@ int main(int argc, char** argv) {
// Register frontend applets
Frontend::RegisterDefaultApplets();
EmuWindow_SDL2::InitializeSDL2();
InputCommon::InputSubsystem input_subsystem{};
const auto create_emu_window = [](bool fullscreen,
const auto create_emu_window = [&](bool fullscreen,
bool is_secondary) -> std::unique_ptr<EmuWindow_SDL2> {
switch (Settings::values.graphics_api.GetValue()) {
case Settings::GraphicsAPI::OpenGL:
return std::make_unique<EmuWindow_SDL2_GL>(fullscreen, is_secondary);
return std::make_unique<EmuWindow_SDL2_GL>(&input_subsystem, fullscreen, is_secondary);
case Settings::GraphicsAPI::Software:
return std::make_unique<EmuWindow_SDL2_SW>(fullscreen, is_secondary);
return std::make_unique<EmuWindow_SDL2_SW>(&input_subsystem, fullscreen, is_secondary);
}
LOG_ERROR(Frontend, "Invalid Graphics API, using OpenGL");
return std::make_unique<EmuWindow_SDL2_GL>(fullscreen, is_secondary);
return std::make_unique<EmuWindow_SDL2_GL>(&input_subsystem, fullscreen, is_secondary);
};
const auto emu_window{create_emu_window(fullscreen, false)};
@ -496,7 +496,6 @@ int main(int argc, char** argv) {
}
Network::Shutdown();
InputCommon::Shutdown();
system.Shutdown();

View File

@ -15,8 +15,8 @@
#include "common/param_package.h"
#include "common/settings.h"
#include "core/hle/service/service.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/main.h"
#include "input_common/udp/client.h"
#include "network/network_settings.h"
Config::Config() {
@ -97,8 +97,7 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<Type, range
void Config::ReadValues() {
// Controls
// TODO: add multiple input profile support
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
/*for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
Settings::values.current_input_profile.buttons[i] =
sdl2_config->GetString("Controls", Settings::NativeButton::mapping[i], default_param);
@ -125,7 +124,7 @@ void Config::ReadValues() {
"Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR);
Settings::values.current_input_profile.udp_input_port =
static_cast<u16>(sdl2_config->GetInteger("Controls", "udp_input_port",
InputCommon::CemuhookUDP::DEFAULT_PORT));
InputCommon::CemuhookUDP::DEFAULT_PORT));*/
// Core
ReadSetting("Core", Settings::values.use_cpu_jit);

View File

@ -2,131 +2,38 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <SDL.h>
#include <algorithm>
#include <cstdlib>
#include <string>
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include "citra/emu_window/emu_window_sdl2.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include "core/core.h"
#include "input_common/keyboard.h"
#include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/main.h"
#include "input_common/motion_emu.h"
#include "network/network.h"
void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) {
TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0));
InputCommon::GetMotionEmu()->Tilt(x, y);
}
void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) {
if (button == SDL_BUTTON_LEFT) {
if (state == SDL_PRESSED) {
TouchPressed((unsigned)std::max(x, 0), (unsigned)std::max(y, 0));
} else {
TouchReleased();
}
} else if (button == SDL_BUTTON_RIGHT) {
if (state == SDL_PRESSED) {
InputCommon::GetMotionEmu()->BeginTilt(x, y);
} else {
InputCommon::GetMotionEmu()->EndTilt();
}
}
}
std::pair<unsigned, unsigned> EmuWindow_SDL2::TouchToPixelPos(float touch_x, float touch_y) const {
int w, h;
SDL_GetWindowSize(render_window, &w, &h);
touch_x *= w;
touch_y *= h;
return {static_cast<unsigned>(std::max(std::round(touch_x), 0.0f)),
static_cast<unsigned>(std::max(std::round(touch_y), 0.0f))};
}
void EmuWindow_SDL2::OnFingerDown(float x, float y) {
// TODO(NeatNit): keep track of multitouch using the fingerID and a dictionary of some kind
// This isn't critical because the best we can do when we have that is to average them, like the
// 3DS does
const auto [px, py] = TouchToPixelPos(x, y);
TouchPressed(px, py);
}
void EmuWindow_SDL2::OnFingerMotion(float x, float y) {
const auto [px, py] = TouchToPixelPos(x, y);
TouchMoved(px, py);
}
void EmuWindow_SDL2::OnFingerUp() {
TouchReleased();
}
void EmuWindow_SDL2::OnKeyEvent(int key, u8 state) {
if (state == SDL_PRESSED) {
InputCommon::GetKeyboard()->PressKey(key);
} else if (state == SDL_RELEASED) {
InputCommon::GetKeyboard()->ReleaseKey(key);
}
}
bool EmuWindow_SDL2::IsOpen() const {
return is_open;
}
void EmuWindow_SDL2::RequestClose() {
is_open = false;
}
void EmuWindow_SDL2::OnResize() {
int width, height;
SDL_GL_GetDrawableSize(render_window, &width, &height);
UpdateCurrentFramebufferLayout(width, height);
}
void EmuWindow_SDL2::Fullscreen() {
if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN) == 0) {
return;
}
LOG_ERROR(Frontend, "Fullscreening failed: {}", SDL_GetError());
// Try a different fullscreening method
LOG_INFO(Frontend, "Attempting to use borderless fullscreen...");
if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN_DESKTOP) == 0) {
return;
}
LOG_ERROR(Frontend, "Borderless fullscreening failed: {}", SDL_GetError());
// Fallback algorithm: Maximise window.
// Works on all systems (unless something is seriously wrong), so no fallback for this one.
LOG_INFO(Frontend, "Falling back on a maximised window...");
SDL_MaximizeWindow(render_window);
}
EmuWindow_SDL2::EmuWindow_SDL2(bool is_secondary) : EmuWindow(is_secondary) {}
EmuWindow_SDL2::~EmuWindow_SDL2() {
SDL_Quit();
}
void EmuWindow_SDL2::InitializeSDL2() {
EmuWindow_SDL2::EmuWindow_SDL2(InputCommon::InputSubsystem* input_subsystem_, bool is_secondary)
: EmuWindow(is_secondary), input_subsystem{input_subsystem_} {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
LOG_CRITICAL(Frontend, "Failed to initialize SDL2: {}! Exiting...", SDL_GetError());
exit(1);
}
InputCommon::Init();
input_subsystem->Initialize();
Network::Init();
SDL_SetMainReady();
}
EmuWindow_SDL2::~EmuWindow_SDL2() {
SDL_Quit();
}
void EmuWindow_SDL2::PollEvents() {
SDL_Event event;
std::vector<SDL_Event> other_window_events;
@ -170,10 +77,12 @@ void EmuWindow_SDL2::PollEvents() {
}
break;
case SDL_FINGERDOWN:
OnFingerDown(event.tfinger.x, event.tfinger.y);
OnFingerDown(event.tfinger.x, event.tfinger.y,
static_cast<std::size_t>(event.tfinger.touchId));
break;
case SDL_FINGERMOTION:
OnFingerMotion(event.tfinger.x, event.tfinger.y);
OnFingerMotion(event.tfinger.x, event.tfinger.y,
static_cast<std::size_t>(event.tfinger.touchId));
break;
case SDL_FINGERUP:
OnFingerUp();
@ -195,6 +104,106 @@ void EmuWindow_SDL2::PollEvents() {
}
}
void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) {
const auto [touch_x, touch_y] = MouseToTouchPos(x, y);
input_subsystem->GetMouse()->Move(x, y, 0, 0);
input_subsystem->GetMouse()->MouseMove(touch_x, touch_y);
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
}
InputCommon::MouseButton EmuWindow_SDL2::SDLButtonToMouseButton(u32 button) const {
switch (button) {
case SDL_BUTTON_LEFT:
return InputCommon::MouseButton::Left;
case SDL_BUTTON_RIGHT:
return InputCommon::MouseButton::Right;
case SDL_BUTTON_MIDDLE:
return InputCommon::MouseButton::Wheel;
case SDL_BUTTON_X1:
return InputCommon::MouseButton::Backward;
case SDL_BUTTON_X2:
return InputCommon::MouseButton::Forward;
default:
return InputCommon::MouseButton::Undefined;
}
}
std::pair<float, float> EmuWindow_SDL2::MouseToTouchPos(s32 touch_x, s32 touch_y) const {
int w, h;
SDL_GetWindowSize(render_window, &w, &h);
const float fx = static_cast<float>(touch_x) / w;
const float fy = static_cast<float>(touch_y) / h;
return {std::clamp<float>(fx, 0.0f, 1.0f), std::clamp<float>(fy, 0.0f, 1.0f)};
}
void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) {
const auto mouse_button = SDLButtonToMouseButton(button);
if (state == SDL_PRESSED) {
const auto [touch_x, touch_y] = MouseToTouchPos(x, y);
input_subsystem->GetMouse()->PressButton(x, y, mouse_button);
input_subsystem->GetMouse()->PressMouseButton(mouse_button);
input_subsystem->GetMouse()->PressTouchButton(touch_x, touch_y, mouse_button);
} else {
input_subsystem->GetMouse()->ReleaseButton(mouse_button);
}
}
void EmuWindow_SDL2::OnFingerDown(float x, float y, std::size_t id) {
input_subsystem->GetTouchScreen()->TouchPressed(x, y, id);
}
void EmuWindow_SDL2::OnFingerMotion(float x, float y, std::size_t id) {
input_subsystem->GetTouchScreen()->TouchMoved(x, y, id);
}
void EmuWindow_SDL2::OnFingerUp() {
input_subsystem->GetTouchScreen()->ReleaseAllTouch();
}
void EmuWindow_SDL2::OnKeyEvent(int key, u8 state) {
if (state == SDL_PRESSED) {
input_subsystem->GetKeyboard()->PressKey(static_cast<std::size_t>(key));
} else if (state == SDL_RELEASED) {
input_subsystem->GetKeyboard()->ReleaseKey(static_cast<std::size_t>(key));
}
}
bool EmuWindow_SDL2::IsOpen() const {
return is_open;
}
void EmuWindow_SDL2::RequestClose() {
is_open = false;
}
void EmuWindow_SDL2::OnResize() {
int width, height;
SDL_GL_GetDrawableSize(render_window, &width, &height);
UpdateCurrentFramebufferLayout(width, height);
}
void EmuWindow_SDL2::Fullscreen() {
if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN) == 0) {
return;
}
LOG_ERROR(Frontend, "Fullscreening failed: {}", SDL_GetError());
// Try a different fullscreening method
LOG_INFO(Frontend, "Attempting to use borderless fullscreen...");
if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN_DESKTOP) == 0) {
return;
}
LOG_ERROR(Frontend, "Borderless fullscreening failed: {}", SDL_GetError());
// Fallback algorithm: Maximise window.
// Works on all systems (unless something is seriously wrong), so no fallback for this one.
LOG_INFO(Frontend, "Falling back on a maximised window...");
SDL_MaximizeWindow(render_window);
}
void EmuWindow_SDL2::OnMinimalClientAreaChangeRequest(std::pair<u32, u32> minimal_size) {
SDL_SetWindowMinimumSize(render_window, minimal_size.first, minimal_size.second);
}

View File

@ -10,14 +10,16 @@
struct SDL_Window;
namespace InputCommon {
class InputSubsystem;
enum class MouseButton;
} // namespace InputCommon
class EmuWindow_SDL2 : public Frontend::EmuWindow {
public:
explicit EmuWindow_SDL2(bool is_secondary);
explicit EmuWindow_SDL2(InputCommon::InputSubsystem* input_subsystem, bool is_secondary);
~EmuWindow_SDL2();
/// Initializes SDL2
static void InitializeSDL2();
/// Presents the most recent frame from the video backend
virtual void Present() {}
@ -37,17 +39,20 @@ protected:
/// Called by PollEvents when the mouse moves.
void OnMouseMotion(s32 x, s32 y);
/// Converts a SDL mouse button into MouseInput mouse button
InputCommon::MouseButton SDLButtonToMouseButton(u32 button) const;
/// Translates pixel position to float position
std::pair<float, float> MouseToTouchPos(s32 touch_x, s32 touch_y) const;
/// Called by PollEvents when a mouse button is pressed or released
void OnMouseButton(u32 button, u8 state, s32 x, s32 y);
/// Translates pixel position (0..1) to pixel positions
std::pair<unsigned, unsigned> TouchToPixelPos(float touch_x, float touch_y) const;
/// Called by PollEvents when a finger starts touching the touchscreen
void OnFingerDown(float x, float y);
void OnFingerDown(float x, float y, std::size_t id);
/// Called by PollEvents when a finger moves while touching the touchscreen
void OnFingerMotion(float x, float y);
void OnFingerMotion(float x, float y, std::size_t id);
/// Called by PollEvents when a finger stops touching the touchscreen
void OnFingerUp();
@ -78,4 +83,7 @@ protected:
/// Keeps track of how often to update the title bar during gameplay
u32 last_time = 0;
/// Input subsystem to use with this window.
InputCommon::InputSubsystem* input_subsystem;
};

View File

@ -42,8 +42,8 @@ private:
SDL_GLContext context;
};
EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(bool fullscreen, bool is_secondary)
: EmuWindow_SDL2{is_secondary} {
EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(InputCommon::InputSubsystem* input_subsystem, bool fullscreen, bool is_secondary)
: EmuWindow_SDL2{input_subsystem, is_secondary} {
// Initialize the window
if (Settings::values.use_gles) {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);

View File

@ -11,7 +11,7 @@ struct SDL_Window;
class EmuWindow_SDL2_GL : public EmuWindow_SDL2 {
public:
explicit EmuWindow_SDL2_GL(bool fullscreen, bool is_secondary);
explicit EmuWindow_SDL2_GL(InputCommon::InputSubsystem* input_subsystem, bool fullscreen, bool is_secondary);
~EmuWindow_SDL2_GL();
void Present() override;

View File

@ -19,8 +19,8 @@
class DummyContext : public Frontend::GraphicsContext {};
EmuWindow_SDL2_SW::EmuWindow_SDL2_SW(bool fullscreen, bool is_secondary)
: EmuWindow_SDL2{is_secondary} {
EmuWindow_SDL2_SW::EmuWindow_SDL2_SW(InputCommon::InputSubsystem* input_subsystem, bool fullscreen, bool is_secondary)
: EmuWindow_SDL2{input_subsystem, is_secondary} {
std::string window_title = fmt::format("Citra {} | {}-{}", Common::g_build_fullname,
Common::g_scm_branch, Common::g_scm_desc);
render_window =

View File

@ -12,7 +12,7 @@ struct SDL_Surface;
class EmuWindow_SDL2_SW : public EmuWindow_SDL2 {
public:
explicit EmuWindow_SDL2_SW(bool fullscreen, bool is_secondary);
explicit EmuWindow_SDL2_SW(InputCommon::InputSubsystem* input_subsystem, bool fullscreen, bool is_secondary);
~EmuWindow_SDL2_SW();
void Present() override;

View File

@ -88,6 +88,8 @@ add_executable(citra-qt
configuration/configure_cheats.cpp
configuration/configure_cheats.h
configuration/configure_cheats.ui
configuration/input_profiles.cpp
configuration/input_profiles.h
debugger/console.h
debugger/console.cpp
debugger/graphics/graphics.cpp

View File

@ -20,9 +20,11 @@
#include "core/core.h"
#include "core/frontend/framebuffer_layout.h"
#include "core/perf_stats.h"
#include "input_common/keyboard.h"
#include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/main.h"
#include "input_common/motion_emu.h"
#include "network/network.h"
#include "video_core/custom_textures/custom_tex_manager.h"
#include "video_core/renderer_base.h"
#include "video_core/video_core.h"
@ -402,8 +404,11 @@ static Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow* window
std::unique_ptr<Frontend::GraphicsContext> GRenderWindow::main_context;
GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread, bool is_secondary_)
: QWidget(parent_), EmuWindow(is_secondary_), emu_thread(emu_thread) {
GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread,
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem_,
bool is_secondary_)
: QWidget(parent_), EmuWindow(is_secondary_),
emu_thread(emu_thread), input_subsystem{std::move(input_subsystem_)} {
setWindowTitle(QStringLiteral("Citra %1 | %2-%3")
.arg(QString::fromUtf8(Common::g_build_name),
@ -481,90 +486,117 @@ qreal GRenderWindow::windowPixelRatio() const {
return devicePixelRatioF();
}
std::pair<u32, u32> GRenderWindow::ScaleTouch(const QPointF pos) const {
const qreal pixel_ratio = windowPixelRatio();
return {static_cast<u32>(std::max(std::round(pos.x() * pixel_ratio), qreal{0.0})),
static_cast<u32>(std::max(std::round(pos.y() * pixel_ratio), qreal{0.0}))};
}
void GRenderWindow::closeEvent(QCloseEvent* event) {
emit Closed();
QWidget::closeEvent(event);
}
void GRenderWindow::keyPressEvent(QKeyEvent* event) {
InputCommon::GetKeyboard()->PressKey(event->key());
if (!event->isAutoRepeat()) {
input_subsystem->GetKeyboard()->PressKey(event->key());
}
}
void GRenderWindow::keyReleaseEvent(QKeyEvent* event) {
InputCommon::GetKeyboard()->ReleaseKey(event->key());
if (!event->isAutoRepeat()) {
input_subsystem->GetKeyboard()->ReleaseKey(event->key());
}
}
InputCommon::MouseButton GRenderWindow::QtButtonToMouseButton(Qt::MouseButton button) {
switch (button) {
case Qt::LeftButton:
return InputCommon::MouseButton::Left;
case Qt::RightButton:
return InputCommon::MouseButton::Right;
case Qt::MiddleButton:
return InputCommon::MouseButton::Wheel;
case Qt::BackButton:
return InputCommon::MouseButton::Backward;
case Qt::ForwardButton:
return InputCommon::MouseButton::Forward;
case Qt::TaskButton:
return InputCommon::MouseButton::Task;
default:
return InputCommon::MouseButton::Extra;
}
}
void GRenderWindow::mousePressEvent(QMouseEvent* event) {
// Touch input is handled in TouchBeginEvent
if (event->source() == Qt::MouseEventSynthesizedBySystem) {
return; // touch input is handled in TouchBeginEvent
return;
}
// Qt sometimes returns the parent coordinates. To avoid this we read the global mouse
// coordinates and map them to the current render area
const auto pos = mapFromGlobal(QCursor::pos());
const auto [touch_x, touch_y] = MapToTouchScreen(pos.x(), pos.y());
const auto button = QtButtonToMouseButton(event->button());
input_subsystem->GetMouse()->PressMouseButton(button);
input_subsystem->GetMouse()->PressButton(pos.x(), pos.y(), button);
input_subsystem->GetMouse()->PressTouchButton(touch_x, touch_y, button);
auto pos = event->pos();
if (event->button() == Qt::LeftButton) {
const auto [x, y] = ScaleTouch(pos);
this->TouchPressed(x, y);
} else if (event->button() == Qt::RightButton) {
InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y());
}
emit MouseActivity();
}
void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
// Touch input is handled in TouchUpdateEvent
if (event->source() == Qt::MouseEventSynthesizedBySystem) {
return; // touch input is handled in TouchUpdateEvent
return;
}
// Qt sometimes returns the parent coordinates. To avoid this we read the global mouse
// coordinates and map them to the current render area
const auto pos = mapFromGlobal(QCursor::pos());
const auto [touch_x, touch_y] = MapToTouchScreen(pos.x(), pos.y());
const int center_x = width() / 2;
const int center_y = height() / 2;
input_subsystem->GetMouse()->MouseMove(touch_x, touch_y);
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
input_subsystem->GetMouse()->Move(pos.x(), pos.y(), center_x, center_y);
auto pos = event->pos();
const auto [x, y] = ScaleTouch(pos);
this->TouchMoved(x, y);
InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y());
emit MouseActivity();
}
void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
// Touch input is handled in TouchEndEvent
if (event->source() == Qt::MouseEventSynthesizedBySystem) {
return; // touch input is handled in TouchEndEvent
return;
}
if (event->button() == Qt::LeftButton)
this->TouchReleased();
else if (event->button() == Qt::RightButton)
InputCommon::GetMotionEmu()->EndTilt();
emit MouseActivity();
const auto button = QtButtonToMouseButton(event->button());
input_subsystem->GetMouse()->ReleaseButton(button);
}
void GRenderWindow::wheelEvent(QWheelEvent* event) {
const int x = event->angleDelta().x();
const int y = event->angleDelta().y();
input_subsystem->GetMouse()->MouseWheelChange(x, y);
}
void GRenderWindow::TouchBeginEvent(const QTouchEvent* event) {
// TouchBegin always has exactly one touch point, so take the .first()
const auto [x, y] = ScaleTouch(event->points().first().position());
this->TouchPressed(x, y);
QList<QTouchEvent::TouchPoint> touch_points = event->touchPoints();
for (const auto& touch_point : touch_points) {
const auto pos = touch_point.pos();
const auto [touch_x, touch_y] = MapToTouchScreen(pos.x(), pos.y());
input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, touch_point.id());
}
}
void GRenderWindow::TouchUpdateEvent(const QTouchEvent* event) {
QPointF pos;
int active_points = 0;
// average all active touch points
for (const auto& tp : event->points()) {
if (tp.state() & (Qt::TouchPointPressed | Qt::TouchPointMoved | Qt::TouchPointStationary)) {
active_points++;
pos += tp.position();
}
QList<QTouchEvent::TouchPoint> touch_points = event->touchPoints();
input_subsystem->GetTouchScreen()->ClearActiveFlag();
for (const auto& touch_point : touch_points) {
const auto pos = touch_point.pos();
const auto [touch_x, touch_y] = MapToTouchScreen(pos.x(), pos.y());
input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, touch_point.id());
}
pos /= active_points;
const auto [x, y] = ScaleTouch(pos);
this->TouchMoved(x, y);
input_subsystem->GetTouchScreen()->ReleaseInactiveTouch();
}
void GRenderWindow::TouchEndEvent() {
this->TouchReleased();
input_subsystem->GetTouchScreen()->ReleaseAllTouch();
}
bool GRenderWindow::event(QEvent* event) {
@ -588,7 +620,9 @@ bool GRenderWindow::event(QEvent* event) {
void GRenderWindow::focusOutEvent(QFocusEvent* event) {
QWidget::focusOutEvent(event);
InputCommon::GetKeyboard()->ReleaseAllKeys();
input_subsystem->GetKeyboard()->ReleaseAllKeys();
input_subsystem->GetMouse()->ReleaseAllButtons();
input_subsystem->GetTouchScreen()->ReleaseAllTouch();
has_focus = false;
}

View File

@ -18,6 +18,11 @@ class QTouchEvent;
class GRenderWindow;
namespace InputCommon {
class InputSubsystem;
enum class MouseButton;
} // namespace InputCommon
namespace VideoCore {
enum class LoadCallbackStage;
}
@ -112,7 +117,8 @@ class GRenderWindow : public QWidget, public Frontend::EmuWindow {
Q_OBJECT
public:
GRenderWindow(QWidget* parent, EmuThread* emu_thread, bool is_secondary);
GRenderWindow(QWidget* parent, EmuThread* emu_thread,
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem_, bool is_secondary);
~GRenderWindow() override;
// EmuWindow implementation.
@ -135,9 +141,13 @@ public:
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
/// Converts a Qt mouse button into MouseInput mouse button
static InputCommon::MouseButton QtButtonToMouseButton(Qt::MouseButton button);
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
bool event(QEvent* event) override;
@ -188,6 +198,7 @@ private:
QWidget* child_widget = nullptr;
EmuThread* emu_thread;
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
/// Main context that will be shared with all other contexts that are requested.
/// If this is used in a shared context setting, then this should not be used directly, but

View File

@ -10,8 +10,8 @@
#include "common/file_util.h"
#include "common/settings.h"
#include "core/hle/service/service.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/main.h"
#include "input_common/udp/client.h"
#include "network/network.h"
#include "network/network_settings.h"
@ -27,17 +27,21 @@ Config::~Config() {
}
const std::array<int, Settings::NativeButton::NumButtons> Config::default_buttons = {
Qt::Key_A, Qt::Key_S, Qt::Key_Z, Qt::Key_X, Qt::Key_T, Qt::Key_G,
Qt::Key_F, Qt::Key_H, Qt::Key_Q, Qt::Key_W, Qt::Key_M, Qt::Key_N,
Qt::Key_O, Qt::Key_P, Qt::Key_1, Qt::Key_2, Qt::Key_B,
Qt::Key_C, Qt::Key_X, Qt::Key_V, Qt::Key_Z, Qt::Key_Up, Qt::Key_Down,
Qt::Key_Left, Qt::Key_Right, Qt::Key_Q, Qt::Key_E, Qt::Key_M, Qt::Key_N,
Qt::Key_O, Qt::Key_P, Qt::Key_R, Qt::Key_T, Qt::Key_B,
};
const std::array<int, Settings::NativeMotion::NumMotions> Config::default_motions = {
Qt::Key_7,
Qt::Key_8,
};
const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> Config::default_analogs{{
{
Qt::Key_Up,
Qt::Key_Down,
Qt::Key_Left,
Qt::Key_Right,
Qt::Key_W,
Qt::Key_S,
Qt::Key_A,
Qt::Key_D,
},
{
@ -45,10 +49,14 @@ const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> Config:
Qt::Key_K,
Qt::Key_J,
Qt::Key_L,
Qt::Key_D,
},
}};
const std::array<int, 2> Config::default_stick_mod = {
Qt::Key_Shift,
0,
};
// This shouldn't have anything except static initializers (no functions). So
// QKeySequence(...).toString() is NOT ALLOWED HERE.
// This must be in alphabetical order according to action name as it must have the same order as
@ -97,12 +105,21 @@ void Config::Initialize(const std::string& config_name) {
case ConfigType::PerGameConfig:
qt_config_loc = fmt::format("{}/custom/{}", fs_config_loc, config_file);
break;
case ConfigType::InputProfile:
qt_config_loc = fmt::format("{}/input/{}", fs_config_loc, config_file);
break;
}
FileUtil::CreateFullPath(qt_config_loc);
qt_config =
std::make_unique<QSettings>(QString::fromStdString(qt_config_loc), QSettings::IniFormat);
Reload();
if (type != ConfigType::InputProfile) {
Reload();
}
}
bool Config::IsCustomConfig() {
return type == ConfigType::PerGameConfig;
}
/* {Read,Write}BasicSetting and WriteGlobalSetting templates must be defined here before their
@ -317,9 +334,78 @@ void Config::ReadCameraValues() {
qt_config->endGroup();
}
void Config::ReadControlValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
void Config::ReadPlayerValues() {
const QString player_prefix = [this] {
if (type == ConfigType::InputProfile) {
return QString{};
} else {
return QStringLiteral("player_");
}
}();
auto& player = Settings::values.players.GetValue();
if (IsCustomConfig()) {
const auto profile_name =
qt_config->value(QStringLiteral("%1profile_name").arg(player_prefix), QString{})
.toString()
.toStdString();
if (profile_name.empty()) {
// Use the global input config
player = Settings::values.players.GetValue(true);
return;
}
player.profile_name = profile_name;
}
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
auto& player_buttons = player.buttons[i];
player_buttons = qt_config
->value(QStringLiteral("%1").arg(player_prefix) +
QString::fromUtf8(Settings::NativeButton::mapping[i]),
QString::fromStdString(default_param))
.toString()
.toStdString();
if (player_buttons.empty()) {
player_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
auto& player_analogs = player.analogs[i];
player_analogs = qt_config
->value(QStringLiteral("%1").arg(player_prefix) +
QString::fromUtf8(Settings::NativeAnalog::mapping[i]),
QString::fromStdString(default_param))
.toString()
.toStdString();
if (player_analogs.empty()) {
player_analogs = default_param;
}
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
auto& player_motions = player.motions[i];
player_motions = qt_config
->value(QStringLiteral("%1").arg(player_prefix) +
QString::fromUtf8(Settings::NativeMotion::mapping[i]),
QString::fromStdString(default_param))
.toString()
.toStdString();
if (player_motions.empty()) {
player_motions = default_param;
}
}
}
void Config::ReadMotionTouchValues() {
int num_touch_from_button_maps =
qt_config->beginReadArray(QStringLiteral("touch_from_button_maps"));
@ -352,82 +438,26 @@ void Config::ReadControlValues() {
}
qt_config->endArray();
Settings::values.current_input_profile_index =
ReadSetting(QStringLiteral("profile"), 0).toInt();
ReadBasicSetting(Settings::values.touch_device);
ReadBasicSetting(Settings::values.touch_from_button_map_index);
Settings::values.touch_from_button_map_index = std::clamp(
Settings::values.touch_from_button_map_index.GetValue(), 0, num_touch_from_button_maps - 1);
const auto append_profile = [this, num_touch_from_button_maps] {
Settings::InputProfile profile;
profile.name =
ReadSetting(QStringLiteral("name"), QStringLiteral("default")).toString().toStdString();
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
profile.buttons[i] = ReadSetting(QString::fromUtf8(Settings::NativeButton::mapping[i]),
QString::fromStdString(default_param))
.toString()
.toStdString();
if (profile.buttons[i].empty())
profile.buttons[i] = default_param;
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_analogs[i][4], 0.5f);
profile.analogs[i] = ReadSetting(QString::fromUtf8(Settings::NativeAnalog::mapping[i]),
QString::fromStdString(default_param))
.toString()
.toStdString();
if (profile.analogs[i].empty())
profile.analogs[i] = default_param;
}
profile.motion_device =
ReadSetting(QStringLiteral("motion_device"),
QStringLiteral(
"engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"))
.toString()
.toStdString();
profile.touch_device =
ReadSetting(QStringLiteral("touch_device"), QStringLiteral("engine:emu_window"))
.toString()
.toStdString();
profile.use_touch_from_button =
ReadSetting(QStringLiteral("use_touch_from_button"), false).toBool();
profile.touch_from_button_map_index =
ReadSetting(QStringLiteral("touch_from_button_map"), 0).toInt();
profile.touch_from_button_map_index =
std::clamp(profile.touch_from_button_map_index, 0, num_touch_from_button_maps - 1);
profile.udp_input_address =
ReadSetting(QStringLiteral("udp_input_address"),
QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR))
.toString()
.toStdString();
profile.udp_input_port = static_cast<u16>(
ReadSetting(QStringLiteral("udp_input_port"), InputCommon::CemuhookUDP::DEFAULT_PORT)
.toInt());
profile.udp_pad_index =
static_cast<u8>(ReadSetting(QStringLiteral("udp_pad_index"), 0).toUInt());
Settings::values.input_profiles.emplace_back(std::move(profile));
};
ReadBasicSetting(Settings::values.udp_input_address);
ReadBasicSetting(Settings::values.udp_input_port);
ReadBasicSetting(Settings::values.udp_pad_index);
}
int num_input_profiles = qt_config->beginReadArray(QStringLiteral("profiles"));
void Config::ReadControlValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
for (int i = 0; i < num_input_profiles; ++i) {
qt_config->setArrayIndex(i);
append_profile();
Settings::values.players.SetGlobal(!IsCustomConfig());
ReadPlayerValues();
if (IsCustomConfig()) {
qt_config->endGroup();
return;
}
qt_config->endArray();
// create a input profile if no input profiles exist, with the default or old settings
if (num_input_profiles == 0) {
append_profile();
num_input_profiles = 1;
}
// ensure that the current input profile index is valid.
Settings::values.current_input_profile_index =
std::clamp(Settings::values.current_input_profile_index, 0, num_input_profiles - 1);
Settings::LoadProfile(Settings::values.current_input_profile_index);
ReadMotionTouchValues();
qt_config->endGroup();
}
@ -884,61 +914,87 @@ void Config::SaveCameraValues() {
qt_config->endGroup();
}
void Config::SaveControlValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
void Config::SavePlayerValues() {
const QString player_prefix = [this] {
if (type == ConfigType::InputProfile) {
return QString{};
} else {
return QStringLiteral("player_");
}
}();
WriteSetting(QStringLiteral("profile"), Settings::values.current_input_profile_index, 0);
qt_config->beginWriteArray(QStringLiteral("profiles"));
for (std::size_t p = 0; p < Settings::values.input_profiles.size(); ++p) {
qt_config->setArrayIndex(static_cast<int>(p));
const auto& profile = Settings::values.input_profiles[p];
WriteSetting(QStringLiteral("name"), QString::fromStdString(profile.name),
QStringLiteral("default"));
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
WriteSetting(QString::fromStdString(Settings::NativeButton::mapping[i]),
QString::fromStdString(profile.buttons[i]),
QString::fromStdString(default_param));
const auto& player = Settings::values.players.GetValue();
if (IsCustomConfig()) {
if (player.profile_name.empty()) {
// No custom profile selected
return;
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_analogs[i][4], 0.5f);
WriteSetting(QString::fromStdString(Settings::NativeAnalog::mapping[i]),
QString::fromStdString(profile.analogs[i]),
QString::fromStdString(default_param));
}
WriteSetting(
QStringLiteral("motion_device"), QString::fromStdString(profile.motion_device),
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"));
WriteSetting(QStringLiteral("touch_device"), QString::fromStdString(profile.touch_device),
QStringLiteral("engine:emu_window"));
WriteSetting(QStringLiteral("use_touch_from_button"), profile.use_touch_from_button, false);
WriteSetting(QStringLiteral("touch_from_button_map"), profile.touch_from_button_map_index,
0);
WriteSetting(QStringLiteral("udp_input_address"),
QString::fromStdString(profile.udp_input_address),
QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR));
WriteSetting(QStringLiteral("udp_input_port"), profile.udp_input_port,
InputCommon::CemuhookUDP::DEFAULT_PORT);
WriteSetting(QStringLiteral("udp_pad_index"), profile.udp_pad_index, 0);
WriteSetting(QStringLiteral("%1profile_name").arg(player_prefix),
QString::fromStdString(player.profile_name), QString{});
}
qt_config->endArray();
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
WriteSetting(QStringLiteral("%1").arg(player_prefix) +
QString::fromStdString(Settings::NativeButton::mapping[i]),
QString::fromStdString(player.buttons[i]),
QString::fromStdString(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
WriteSetting(QStringLiteral("%1").arg(player_prefix) +
QString::fromStdString(Settings::NativeAnalog::mapping[i]),
QString::fromStdString(player.analogs[i]),
QString::fromStdString(default_param));
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
WriteSetting(QStringLiteral("%1").arg(player_prefix) +
QString::fromStdString(Settings::NativeMotion::mapping[i]),
QString::fromStdString(player.motions[i]),
QString::fromStdString(default_param));
}
}
void Config::SaveMotionTouchValues() {
WriteBasicSetting(Settings::values.touch_device);
WriteBasicSetting(Settings::values.touch_from_button_map_index);
WriteBasicSetting(Settings::values.udp_input_address);
WriteBasicSetting(Settings::values.udp_input_port);
WriteBasicSetting(Settings::values.udp_pad_index);
qt_config->beginWriteArray(QStringLiteral("touch_from_button_maps"));
for (std::size_t p = 0; p < Settings::values.touch_from_button_maps.size(); ++p) {
qt_config->setArrayIndex(static_cast<int>(p));
const auto& map = Settings::values.touch_from_button_maps[p];
WriteSetting(QStringLiteral("name"), QString::fromStdString(map.name),
WriteSetting(QStringLiteral("name"),
QString::fromStdString(Settings::values.touch_from_button_maps[p].name),
QStringLiteral("default"));
qt_config->beginWriteArray(QStringLiteral("entries"));
for (std::size_t q = 0; q < map.buttons.size(); ++q) {
for (std::size_t q = 0; q < Settings::values.touch_from_button_maps[p].buttons.size();
++q) {
qt_config->setArrayIndex(static_cast<int>(q));
WriteSetting(QStringLiteral("bind"), QString::fromStdString(map.buttons[q]));
WriteSetting(
QStringLiteral("bind"),
QString::fromStdString(Settings::values.touch_from_button_maps[p].buttons[q]));
}
qt_config->endArray();
}
qt_config->endArray();
}
void Config::SaveControlValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
Settings::values.players.SetGlobal(!IsCustomConfig());
SavePlayerValues();
if (IsCustomConfig()) {
qt_config->endGroup();
return;
}
SaveMotionTouchValues();
qt_config->endGroup();
}
@ -1302,3 +1358,26 @@ void Config::Reload() {
void Config::Save() {
SaveValues();
}
void Config::ReadControlPlayerValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
ReadPlayerValues();
qt_config->endGroup();
}
void Config::SaveControlPlayerValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
SavePlayerValues();
qt_config->endGroup();
}
void Config::ClearControlPlayerValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
// If key is an empty string, all keys in the current group() are removed.
qt_config->remove(QString{});
qt_config->endGroup();
}
const std::string& Config::GetConfigFilePath() const {
return qt_config_loc;
}

View File

@ -15,7 +15,11 @@ class QSettings;
class Config {
public:
enum class ConfigType : u32 { GlobalConfig, PerGameConfig };
enum class ConfigType : u32 {
GlobalConfig,
PerGameConfig,
InputProfile,
};
explicit Config(const std::string& config_name = "qt-config",
ConfigType config_type = ConfigType::GlobalConfig);
@ -24,17 +28,28 @@ public:
void Reload();
void Save();
// Used for input profiles
void ReadControlPlayerValues();
void SaveControlPlayerValues();
void ClearControlPlayerValues();
const std::string& GetConfigFilePath() const;
static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
static const std::array<int, Settings::NativeMotion::NumMotions> default_motions;
static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs;
static const std::array<UISettings::Shortcut, 28> default_hotkeys;
static const std::array<int, 2> default_stick_mod;
private:
void Initialize(const std::string& config_name);
bool IsCustomConfig();
void ReadValues();
void ReadPlayerValues();
void ReadMotionTouchValues();
void ReadControlValues();
void ReadAudioValues();
void ReadCameraValues();
void ReadControlValues();
void ReadCoreValues();
void ReadDataStorageValues();
void ReadDebuggingValues();
@ -54,9 +69,11 @@ private:
void ReadVideoDumpingValues();
void SaveValues();
void SavePlayerValues();
void SaveMotionTouchValues();
void SaveControlValues();
void SaveAudioValues();
void SaveCameraValues();
void SaveControlValues();
void SaveCoreValues();
void SaveDataStorageValues();
void SaveDebuggingValues();

View File

@ -10,11 +10,14 @@
#include "common/settings.h"
#include "ui_configure.h"
ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry, bool enable_web_config)
ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry,
InputCommon::InputSubsystem* input_subsystem,
bool enable_web_config)
: QDialog(parent), ui(std::make_unique<Ui::ConfigureDialog>()), registry(registry) {
Settings::SetConfiguringGlobal(true);
ui->setupUi(this);
ui->inputTab->Initialize(input_subsystem);
ui->hotkeysTab->Populate(registry);
ui->webTab->SetWebServiceConfigEnabled(enable_web_config);
@ -58,7 +61,6 @@ void ConfigureDialog::ApplyConfiguration() {
ui->generalTab->ApplyConfiguration();
ui->systemTab->ApplyConfiguration();
ui->inputTab->ApplyConfiguration();
ui->inputTab->ApplyProfile();
ui->hotkeysTab->ApplyConfiguration(registry);
ui->graphicsTab->ApplyConfiguration();
ui->enhancementsTab->ApplyConfiguration();

View File

@ -13,11 +13,16 @@ namespace Ui {
class ConfigureDialog;
}
namespace InputCommon {
class InputSubsystem;
}
class ConfigureDialog : public QDialog {
Q_OBJECT
public:
explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry,
InputCommon::InputSubsystem* input_subsystem,
bool enable_web_config = true);
~ConfigureDialog() override;

File diff suppressed because it is too large Load Diff

View File

@ -9,22 +9,42 @@
#include <memory>
#include <optional>
#include <string>
#include <QKeySequence>
#include <vector>
#include <QWidget>
#include "common/param_package.h"
#include "common/settings.h"
#include "input_common/main.h"
#include "ui_configure_input.h"
class QCheckBox;
class QKeyEvent;
class QLabel;
class QPushButton;
class QSlider;
class QSpinBox;
class QString;
class QTimer;
class QWidget;
class InputProfiles;
namespace InputCommon {
class InputSubsystem;
}
namespace InputCommon::Polling {
enum class InputType;
} // namespace InputCommon::Polling
namespace Ui {
class ConfigureInput;
}
} // namespace Ui
namespace Core::HID {
class HIDCore;
class EmulatedController;
} // namespace Core::HID
class ConfigureInput : public QWidget {
Q_OBJECT
@ -33,16 +53,32 @@ public:
explicit ConfigureInput(QWidget* parent = nullptr);
~ConfigureInput() override;
/// Save all button configurations to settings file
void ApplyConfiguration();
void RetranslateUI();
/// Initializes the ConfigureInput dialog
void Initialize(InputCommon::InputSubsystem* input_subsystem_);
/// Load configuration settings.
void LoadConfiguration();
/// Save all button configurations to settings file.
void ApplyConfiguration();
/// Update the input devices combobox.
void UpdateInputDeviceCombobox();
/// Updates the list of controller profiles.
void UpdateInputProfiles();
/// Restore all buttons to their default values.
void RestoreDefaults();
/// Clear all input configuration.
void ClearAll();
/// Updates UI with selected language translations
void RetranslateUI();
void EmitInputKeysChanged();
/// Save the current input profile index
void ApplyProfile();
public slots:
void OnHotkeysChanged(QList<QKeySequence> new_key_list);
@ -50,39 +86,97 @@ signals:
void InputKeysChanged(QList<QKeySequence> new_key_list);
private:
/// Generates list of all used keys
QList<QKeySequence> GetUsedKeyboardKeys();
QString ButtonToText(const Common::ParamPackage& param);
QString AnalogToText(const Common::ParamPackage& param, const std::string& dir);
void changeEvent(QEvent* event) override;
/// Called when the button was pressed.
void HandleClick(QPushButton* button, std::size_t button_id,
std::function<void(const Common::ParamPackage&)> new_input_setter,
InputCommon::Polling::InputType type);
/// Finish polling and configure input using the input_setter.
void SetPollingResult(const Common::ParamPackage& params, bool abort);
/// Checks whether a given input can be accepted.
bool IsInputAcceptable(const Common::ParamPackage& params) const;
/// Handle mouse button press events.
void mousePressEvent(QMouseEvent* event) override;
/// Handle mouse wheel move events.
void wheelEvent(QWheelEvent* event) override;
/// Handle key press events.
void keyPressEvent(QKeyEvent* event) override;
/// Handle combobox list refresh
bool eventFilter(QObject* object, QEvent* event) override;
/// Update UI to reflect current configuration.
void UpdateUI();
/// Update the available input devices.
void UpdateInputDevices();
/// Gets the default controller mapping for this device and auto configures the input to match.
void UpdateMappingWithDefaults();
/// Creates a controller profile.
void CreateProfile();
/// Deletes the selected controller profile.
void DeleteProfile();
/// Loads the selected controller profile.
void LoadProfile();
/// Saves the current controller configuration into a selected controller profile.
void SaveProfile();
std::unique_ptr<Ui::ConfigureInput> ui;
InputCommon::InputSubsystem* input_subsystem;
std::unique_ptr<InputProfiles> profiles;
std::unique_ptr<QTimer> timeout_timer;
std::unique_ptr<QTimer> poll_timer;
/// This will be the the setting function when an input is awaiting configuration.
std::optional<std::function<void(const Common::ParamPackage&)>> input_setter;
std::array<Common::ParamPackage, Settings::NativeButton::NumButtons> buttons_param;
std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs> analogs_param;
Core::HID::EmulatedController* emulated_controller;
static constexpr int ANALOG_SUB_BUTTONS_NUM = 5;
static constexpr int ANALOG_SUB_BUTTONS_NUM = 4;
/// Each button input is represented by a QPushButton.
std::array<QPushButton*, Settings::NativeButton::NumButtons> button_map;
/// A group of five QPushButtons represent one analog input. The buttons each represent up,
/// down, left, right, and modifier, respectively.
/// A group of four QPushButtons represent one analog input. The buttons each represent up,
/// down, left, right, respectively.
std::array<std::array<QPushButton*, ANALOG_SUB_BUTTONS_NUM>, Settings::NativeAnalog::NumAnalogs>
analog_map_buttons;
/// Analog inputs are also represented each with a single button, used to configure with an
/// actual analog stick
std::array<QPushButton*, Settings::NativeAnalog::NumAnalogs> analog_map_stick;
std::array<QSlider*, Settings::NativeAnalog::NumAnalogs>
analog_map_deadzone_and_modifier_slider;
std::array<QLabel*, Settings::NativeAnalog::NumAnalogs>
analog_map_deadzone_and_modifier_slider_label;
/// Each motion input is represented by a QPushButton.
std::array<QPushButton*, Settings::NativeMotion::NumMotions> motion_map;
std::array<QLabel*, Settings::NativeAnalog::NumAnalogs> analog_map_deadzone_label;
std::array<QSlider*, Settings::NativeAnalog::NumAnalogs> analog_map_deadzone_slider;
std::array<QGroupBox*, Settings::NativeAnalog::NumAnalogs> analog_map_modifier_groupbox;
std::array<QPushButton*, Settings::NativeAnalog::NumAnalogs> analog_map_modifier_button;
std::array<QLabel*, Settings::NativeAnalog::NumAnalogs> analog_map_modifier_label;
std::array<QSlider*, Settings::NativeAnalog::NumAnalogs> analog_map_modifier_slider;
std::array<QGroupBox*, Settings::NativeAnalog::NumAnalogs> analog_map_range_groupbox;
std::array<QSpinBox*, Settings::NativeAnalog::NumAnalogs> analog_map_range_spinbox;
static const std::array<std::string, ANALOG_SUB_BUTTONS_NUM> analog_sub_buttons;
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> device_pollers;
/**
* List of keys currently registered to hotkeys.
* These can't be bound to any input key.
@ -90,43 +184,10 @@ private:
*/
QList<QKeySequence> hotkey_list;
/// A flag to indicate if keyboard keys are okay when configuring an input. If this is false,
/// keyboard events are ignored.
bool want_keyboard_keys = false;
/// A flag to indicate that the "Map Analog Stick" pop-up has been shown and accepted once.
bool map_analog_stick_accepted{};
/// Generates list of all used keys
QList<QKeySequence> GetUsedKeyboardKeys();
void MapFromButton(const Common::ParamPackage& params);
void AutoMap();
/// Restore all buttons to their default values.
void RestoreDefaults();
/// Clear all input configuration
void ClearAll();
/// Update UI to reflect current configuration.
void UpdateButtonLabels();
/// Called when the button was pressed.
void HandleClick(QPushButton* button,
std::function<void(const Common::ParamPackage&)> new_input_setter,
InputCommon::Polling::DeviceType type);
/// The key code of the previous state of the key being currently bound.
int previous_key_code;
/// Finish polling and configure input using the input_setter
void SetPollingResult(const Common::ParamPackage& params, bool abort);
/// Handle key press events.
void keyPressEvent(QKeyEvent* event) override;
/// input profiles
void NewProfile();
void DeleteProfile();
void RenameProfile();
bool IsProfileNameDuplicate(const QString& name) const;
void WarnProposedProfileNameIsDuplicate();
/// List of physical devices users can map with. If a SDL backed device is selected, then you
/// can use this device to get a default mapping.
std::vector<Common::ParamPackage> input_devices;
};

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,14 @@
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QStringListModel>
#include <QTimer>
#include <QVBoxLayout>
#include "citra_qt/configuration/configure_motion_touch.h"
#include "citra_qt/configuration/configure_touch_from_button.h"
#include "common/logging/log.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/helpers/udp_protocol.h"
#include "input_common/main.h"
#include "ui_configure_motion_touch.h"
@ -34,26 +37,27 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent,
using namespace InputCommon::CemuhookUDP;
job = std::make_unique<CalibrationConfigurationJob>(
host, port, pad_index, client_id,
host, port, pad_index,
[this](CalibrationConfigurationJob::Status status) {
QString text;
switch (status) {
case CalibrationConfigurationJob::Status::Ready:
text = tr("Touch the top left corner <br>of your touchpad.");
break;
case CalibrationConfigurationJob::Status::Stage1Completed:
text = tr("Now touch the bottom right corner <br>of your touchpad.");
break;
case CalibrationConfigurationJob::Status::Completed:
text = tr("Configuration completed!");
break;
default:
LOG_ERROR(Frontend, "Unknown calibration status {}", status);
break;
}
QMetaObject::invokeMethod(this, "UpdateLabelText", Q_ARG(QString, text));
QMetaObject::invokeMethod(this, [status, this] {
QString text;
switch (status) {
case CalibrationConfigurationJob::Status::Ready:
text = tr("Touch the top left corner <br>of your touchpad.");
break;
case CalibrationConfigurationJob::Status::Stage1Completed:
text = tr("Now touch the bottom right corner <br>of your touchpad.");
break;
case CalibrationConfigurationJob::Status::Completed:
text = tr("Configuration completed!");
break;
default:
break;
}
UpdateLabelText(text);
});
if (status == CalibrationConfigurationJob::Status::Completed) {
QMetaObject::invokeMethod(this, "UpdateButtonText", Q_ARG(QString, tr("OK")));
QMetaObject::invokeMethod(this, [this] { UpdateButtonText(tr("OK")); });
}
},
[this](u16 min_x_, u16 min_y_, u16 max_x_, u16 max_y_) {
@ -86,10 +90,13 @@ constexpr std::array<std::pair<const char*, const char*>, 2> TouchProviders = {{
{"cemuhookudp", QT_TRANSLATE_NOOP("ConfigureMotionTouch", "CemuhookUDP")},
}};
ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent)
: QDialog(parent), ui(std::make_unique<Ui::ConfigureMotionTouch>()),
timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent,
InputCommon::InputSubsystem* input_subsystem_)
: QDialog(parent), input_subsystem{input_subsystem_},
ui(std::make_unique<Ui::ConfigureMotionTouch>()), timeout_timer(std::make_unique<QTimer>()),
poll_timer(std::make_unique<QTimer>()) {
ui->setupUi(this);
for (const auto& [provider, name] : MotionProviders) {
ui->motion_provider->addItem(tr(name), QString::fromUtf8(provider));
}
@ -104,22 +111,7 @@ ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent)
"using-a-controller-or-android-phone-for-motion-or-touch-input'><span "
"style=\"text-decoration: underline; color:#039be5;\">Learn More</span></a>"));
timeout_timer->setSingleShot(true);
connect(timeout_timer.get(), &QTimer::timeout, this, [this]() { SetPollingResult({}, true); });
connect(poll_timer.get(), &QTimer::timeout, this, [this]() {
Common::ParamPackage params;
for (auto& poller : device_pollers) {
params = poller->GetNextInput();
// We want all the input systems to be in a "polling" state, but we only care about the
// input from SDL.
if (params.Has("engine") && params.Get("engine", "") == "sdl") {
SetPollingResult(params, false);
return;
}
}
});
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
SetConfiguration();
UpdateUiDisplay();
ConnectEvents();
@ -128,37 +120,23 @@ ConfigureMotionTouch::ConfigureMotionTouch(QWidget* parent)
ConfigureMotionTouch::~ConfigureMotionTouch() = default;
void ConfigureMotionTouch::SetConfiguration() {
const Common::ParamPackage motion_param(Settings::values.current_input_profile.motion_device);
const Common::ParamPackage touch_param(Settings::values.current_input_profile.touch_device);
const std::string motion_engine = motion_param.Get("engine", "motion_emu");
const std::string touch_engine = touch_param.Get("engine", "emu_window");
const Common::ParamPackage touch_param(Settings::values.touch_device.GetValue());
ui->motion_provider->setCurrentIndex(
ui->motion_provider->findData(QString::fromStdString(motion_engine)));
ui->touch_provider->setCurrentIndex(
ui->touch_provider->findData(QString::fromStdString(touch_engine)));
ui->touch_from_button_checkbox->setChecked(
Settings::values.current_input_profile.use_touch_from_button);
touch_from_button_maps = Settings::values.touch_from_button_maps;
for (const auto& touch_map : touch_from_button_maps) {
ui->touch_from_button_map->addItem(QString::fromStdString(touch_map.name));
}
ui->touch_from_button_map->setCurrentIndex(
Settings::values.current_input_profile.touch_from_button_map_index);
ui->motion_sensitivity->setValue(motion_param.Get("sensitivity", 0.01f));
guid = motion_param.Get("guid", "0");
port = motion_param.Get("port", 0);
Settings::values.touch_from_button_map_index.GetValue());
min_x = touch_param.Get("min_x", 100);
min_y = touch_param.Get("min_y", 50);
max_x = touch_param.Get("max_x", 1800);
max_y = touch_param.Get("max_y", 850);
ui->udp_server->setText(
QString::fromStdString(Settings::values.current_input_profile.udp_input_address));
ui->udp_port->setText(QString::number(Settings::values.current_input_profile.udp_input_port));
ui->udp_pad_index->setCurrentIndex(Settings::values.current_input_profile.udp_pad_index);
ui->udp_server->setText(QString::fromStdString(Settings::values.udp_input_address.GetValue()));
ui->udp_port->setText(QString::number(Settings::values.udp_input_port.GetValue()));
ui->udp_pad_index->setCurrentIndex(Settings::values.udp_pad_index.GetValue());
}
void ConfigureMotionTouch::UpdateUiDisplay() {
@ -205,30 +183,6 @@ void ConfigureMotionTouch::ConnectEvents() {
[this]([[maybe_unused]] int index) { UpdateUiDisplay(); });
connect(ui->touch_provider, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this]([[maybe_unused]] int index) { UpdateUiDisplay(); });
connect(ui->motion_controller_button, &QPushButton::clicked, this, [this]() {
if (QMessageBox::information(this, tr("Information"),
tr("After pressing OK, press a button on the controller whose "
"motion you want to track."),
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) {
ui->motion_controller_button->setText(tr("[press button]"));
ui->motion_controller_button->setFocus();
input_setter = [this](const Common::ParamPackage& params) {
guid = params.Get("guid", "0");
port = params.Get("port", 0);
};
device_pollers =
InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button);
for (auto& poller : device_pollers) {
poller->Start();
}
timeout_timer->start(5000); // Cancel after 5 seconds
poll_timer->start(200); // Check for new inputs every 200ms
}
});
connect(ui->udp_test, &QPushButton::clicked, this, &ConfigureMotionTouch::OnCemuhookUDPTest);
connect(ui->touch_calibration_config, &QPushButton::clicked, this,
&ConfigureMotionTouch::OnConfigureTouchCalibration);
@ -243,28 +197,13 @@ void ConfigureMotionTouch::ConnectEvents() {
});
}
void ConfigureMotionTouch::SetPollingResult(const Common::ParamPackage& params, bool abort) {
timeout_timer->stop();
poll_timer->stop();
for (auto& poller : device_pollers) {
poller->Stop();
}
if (!abort && input_setter) {
(*input_setter)(params);
}
ui->motion_controller_button->setText(tr("Configure"));
input_setter.reset();
}
void ConfigureMotionTouch::OnCemuhookUDPTest() {
ui->udp_test->setEnabled(false);
ui->udp_test->setText(tr("Testing"));
udp_test_in_progress = true;
InputCommon::CemuhookUDP::TestCommunication(
ui->udp_server->text().toStdString(), static_cast<u16>(ui->udp_port->text().toInt()),
static_cast<u8>(ui->udp_pad_index->currentIndex()), 24872,
static_cast<u8>(ui->udp_pad_index->currentIndex()),
[this] {
LOG_INFO(Frontend, "UDP input test success");
QMetaObject::invokeMethod(this, "ShowUDPTestResult", Q_ARG(bool, true));
@ -322,7 +261,7 @@ void ConfigureMotionTouch::ShowUDPTestResult(bool result) {
}
void ConfigureMotionTouch::OnConfigureTouchFromButton() {
ConfigureTouchFromButton dialog{this, touch_from_button_maps,
ConfigureTouchFromButton dialog{this, touch_from_button_maps, input_subsystem,
ui->touch_from_button_map->currentIndex()};
if (dialog.exec() != QDialog::Accepted) {
return;
@ -374,20 +313,13 @@ void ConfigureMotionTouch::ApplyConfiguration() {
touch_param.Set("max_y", max_y);
}
Settings::values.current_input_profile.motion_device = motion_param.Serialize();
Settings::values.current_input_profile.touch_device = touch_param.Serialize();
Settings::values.current_input_profile.use_touch_from_button =
ui->touch_from_button_checkbox->isChecked();
Settings::values.current_input_profile.touch_from_button_map_index =
ui->touch_from_button_map->currentIndex();
Settings::values.touch_device = touch_param.Serialize();
Settings::values.touch_from_button_map_index = ui->touch_from_button_map->currentIndex();
Settings::values.touch_from_button_maps = touch_from_button_maps;
Settings::values.current_input_profile.udp_input_address = ui->udp_server->text().toStdString();
Settings::values.current_input_profile.udp_input_port =
static_cast<u16>(ui->udp_port->text().toInt());
Settings::values.current_input_profile.udp_pad_index =
static_cast<u8>(ui->udp_pad_index->currentIndex());
Settings::SaveProfile(Settings::values.current_input_profile_index);
InputCommon::ReloadInputDevices();
Settings::values.udp_input_address = ui->udp_server->text().toStdString();
Settings::values.udp_input_port = static_cast<u16>(ui->udp_port->text().toInt());
Settings::values.udp_pad_index = static_cast<u8>(ui->udp_pad_index->currentIndex());
input_subsystem->ReloadInputDevices();
accept();
}

View File

@ -6,15 +6,21 @@
#include <memory>
#include <QDialog>
#include "common/param_package.h"
#include "common/settings.h"
#include "input_common/main.h"
#include "input_common/udp/udp.h"
class QVBoxLayout;
class QLabel;
class QPushButton;
class QTimer;
class QStringListModel;
namespace InputCommon {
class InputSubsystem;
}
namespace InputCommon::CemuhookUDP {
class CalibrationConfigurationJob;
}
namespace Ui {
class ConfigureMotionTouch;
@ -51,7 +57,7 @@ class ConfigureMotionTouch : public QDialog {
Q_OBJECT
public:
explicit ConfigureMotionTouch(QWidget* parent = nullptr);
explicit ConfigureMotionTouch(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_);
~ConfigureMotionTouch() override;
public slots:
@ -68,9 +74,10 @@ private:
void SetConfiguration();
void UpdateUiDisplay();
void ConnectEvents();
void SetPollingResult(const Common::ParamPackage& params, bool abort);
bool CanCloseDialog();
InputCommon::InputSubsystem* input_subsystem;
std::unique_ptr<Ui::ConfigureMotionTouch> ui;
// Used for SDL input polling
@ -78,10 +85,6 @@ private:
int port;
std::unique_ptr<QTimer> timeout_timer;
std::unique_ptr<QTimer> poll_timer;
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> device_pollers;
/// This will be the the setting function when an input is awaiting configuration.
std::optional<std::function<void(const Common::ParamPackage&)>> input_setter;
// Coordinate system of the CemuhookUDP touch provider
int min_x{};

View File

@ -2,17 +2,17 @@
<ui version="4.0">
<class>ConfigureMotionTouch</class>
<widget class="QDialog" name="ConfigureMotionTouch">
<property name="windowTitle">
<string>Configure Motion / Touch</string>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>450</height>
<height>580</height>
</rect>
</property>
<property name="windowTitle">
<string>Configure Motion / Touch</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox" name="motion_group_box">
@ -324,4 +324,25 @@
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ConfigureMotionTouch</receiver>
<slot>ApplyConfiguration()</slot>
<hints>
<hint type="sourcelabel">
<x>220</x>
<y>380</y>
</hint>
<hint type="destinationlabel">
<x>220</x>
<y>200</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>ApplyConfiguration()</slot>
</slots>
</ui>

View File

@ -12,7 +12,9 @@
#include "citra_qt/configuration/configure_touch_from_button.h"
#include "citra_qt/configuration/configure_touch_widget.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "core/3ds.h"
#include "core/frontend/framebuffer_layout.h"
#include "input_common/main.h"
#include "ui_configure_touch_from_button.h"
@ -68,11 +70,11 @@ static QString ButtonToText(const Common::ParamPackage& param) {
}
ConfigureTouchFromButton::ConfigureTouchFromButton(
QWidget* parent, const std::vector<Settings::TouchFromButtonMap>& touch_maps,
const int default_index)
: QDialog(parent), ui(std::make_unique<Ui::ConfigureTouchFromButton>()), touch_maps(touch_maps),
selected_index(default_index), timeout_timer(std::make_unique<QTimer>()),
poll_timer(std::make_unique<QTimer>()) {
QWidget* parent, const std::vector<Settings::TouchFromButtonMap>& touch_maps_,
InputCommon::InputSubsystem* input_subsystem_, const int default_index)
: QDialog(parent), ui(std::make_unique<Ui::ConfigureTouchFromButton>()),
touch_maps{touch_maps_}, input_subsystem{input_subsystem_}, selected_index{default_index},
timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
ui->setupUi(this);
binding_list_model = new QStandardItemModel(0, 3, this);
binding_list_model->setHorizontalHeaderLabels(
@ -162,13 +164,10 @@ void ConfigureTouchFromButton::ConnectEvents() {
connect(timeout_timer.get(), &QTimer::timeout, [this]() { SetPollingResult({}, true); });
connect(poll_timer.get(), &QTimer::timeout, [this]() {
Common::ParamPackage params;
for (auto& poller : device_pollers) {
params = poller->GetNextInput();
if (params.Has("engine")) {
SetPollingResult(params, false);
return;
}
const auto& params = input_subsystem->GetNextInput();
if (params.Has("engine")) {
SetPollingResult(params, false);
return;
}
});
}
@ -229,6 +228,9 @@ void ConfigureTouchFromButton::RenameMapping() {
}
void ConfigureTouchFromButton::GetButtonInput(const int row_index, const bool is_new) {
if (timeout_timer->isActive()) {
return;
}
binding_list_model->item(row_index, 0)->setText(tr("[press key]"));
input_setter = [this, row_index, is_new](const Common::ParamPackage& params,
@ -247,11 +249,7 @@ void ConfigureTouchFromButton::GetButtonInput(const int row_index, const bool is
}
};
device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button);
for (auto& poller : device_pollers) {
poller->Start();
}
input_subsystem->BeginMapping(InputCommon::Polling::InputType::Button);
grabKeyboard();
grabMouse();
@ -363,14 +361,14 @@ void ConfigureTouchFromButton::SetCoordinates(const int dot_id, const QPoint& po
void ConfigureTouchFromButton::SetPollingResult(const Common::ParamPackage& params,
const bool cancel) {
timeout_timer->stop();
poll_timer->stop();
input_subsystem->StopMapping();
releaseKeyboard();
releaseMouse();
qApp->restoreOverrideCursor();
timeout_timer->stop();
poll_timer->stop();
for (auto& poller : device_pollers) {
poller->Stop();
}
if (input_setter) {
(*input_setter)(params, cancel);
input_setter.reset();
@ -597,7 +595,6 @@ std::optional<QPoint> TouchScreenPreview::MapToDeviceCoords(const int screen_x,
(Core::kScreenBottomHeight - 1) / (contentsRect().height() - 1);
if (t_x >= 0.5f && t_x < Core::kScreenBottomWidth && t_y >= 0.5f &&
t_y < Core::kScreenBottomHeight) {
return QPoint{static_cast<int>(t_x), static_cast<int>(t_y)};
}
return std::nullopt;

View File

@ -1,6 +1,5 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -9,7 +8,6 @@
#include <optional>
#include <vector>
#include <QDialog>
#include "common/settings.h"
class QItemSelection;
class QModelIndex;
@ -22,10 +20,12 @@ class ParamPackage;
}
namespace InputCommon {
namespace Polling {
class DevicePoller;
class InputSubsystem;
}
namespace Settings {
struct TouchFromButtonMap;
}
} // namespace InputCommon
namespace Ui {
class ConfigureTouchFromButton;
@ -36,7 +36,8 @@ class ConfigureTouchFromButton : public QDialog {
public:
explicit ConfigureTouchFromButton(QWidget* parent,
const std::vector<Settings::TouchFromButtonMap>& touch_maps,
const std::vector<Settings::TouchFromButtonMap>& touch_maps_,
InputCommon::InputSubsystem* input_subsystem_,
int default_index = 0);
~ConfigureTouchFromButton() override;
@ -72,13 +73,13 @@ private:
void SaveCurrentMapping();
std::unique_ptr<Ui::ConfigureTouchFromButton> ui;
QStandardItemModel* binding_list_model;
std::vector<Settings::TouchFromButtonMap> touch_maps;
QStandardItemModel* binding_list_model;
InputCommon::InputSubsystem* input_subsystem;
int selected_index;
std::unique_ptr<QTimer> timeout_timer;
std::unique_ptr<QTimer> poll_timer;
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> device_pollers;
std::optional<std::function<void(const Common::ParamPackage&, bool)>> input_setter;
static constexpr int DataRoleDot = Qt::ItemDataRole::UserRole + 2;

View File

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/format.h>
#include "citra_qt/configuration/config.h"
#include "citra_qt/configuration/input_profiles.h"
#include "common/common_paths.h"
#include "common/file_util.h"
namespace {
bool ProfileExistsInFilesystem(std::string_view profile_name) {
return FileUtil::Exists(FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "input" +
DIR_SEP + fmt::format("{}.ini", profile_name));
}
bool IsINI(const std::string& filename) {
return filename.ends_with(".ini");
}
std::string GetNameWithoutExtension(const std::string& filename) {
const auto last_dot = filename.find_last_of(".");
if (last_dot == std::string::npos) {
return filename;
}
return filename.substr(0, last_dot);
}
} // namespace
InputProfiles::InputProfiles() {
const auto input_profile_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "input";
if (!FileUtil::IsDirectory(input_profile_loc)) {
return;
}
FileUtil::ForeachDirectoryEntry(
nullptr, input_profile_loc,
[this](u64* num_entries_out, const std::string& directory, const std::string& filename) {
const auto name_without_ext = GetNameWithoutExtension(filename);
if (IsINI(filename) && IsProfileNameValid(name_without_ext)) {
map_profiles.insert_or_assign(
name_without_ext,
std::make_unique<Config>(name_without_ext, Config::ConfigType::InputProfile));
}
return true;
});
}
InputProfiles::~InputProfiles() = default;
std::vector<std::string> InputProfiles::GetInputProfileNames() {
std::vector<std::string> profile_names;
profile_names.reserve(map_profiles.size());
auto it = map_profiles.cbegin();
while (it != map_profiles.cend()) {
const auto& [profile_name, config] = *it;
if (!ProfileExistsInFilesystem(profile_name)) {
it = map_profiles.erase(it);
continue;
}
profile_names.push_back(profile_name);
++it;
}
std::stable_sort(profile_names.begin(), profile_names.end());
return profile_names;
}
bool InputProfiles::IsProfileNameValid(std::string_view profile_name) {
return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
}
bool InputProfiles::CreateProfile(const std::string& profile_name) {
if (ProfileExistsInMap(profile_name)) {
return false;
}
map_profiles.insert_or_assign(
profile_name, std::make_unique<Config>(profile_name, Config::ConfigType::InputProfile));
return SaveProfile(profile_name);
}
bool InputProfiles::DeleteProfile(const std::string& profile_name) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
if (!ProfileExistsInFilesystem(profile_name) ||
FileUtil::Delete(map_profiles[profile_name]->GetConfigFilePath())) {
map_profiles.erase(profile_name);
}
return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
}
bool InputProfiles::LoadProfile(const std::string& profile_name) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
if (!ProfileExistsInFilesystem(profile_name)) {
map_profiles.erase(profile_name);
return false;
}
map_profiles[profile_name]->ReadControlPlayerValues();
return true;
}
bool InputProfiles::SaveProfile(const std::string& profile_name) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
map_profiles[profile_name]->SaveControlPlayerValues();
return true;
}
bool InputProfiles::ProfileExistsInMap(const std::string& profile_name) const {
return map_profiles.find(profile_name) != map_profiles.end();
}

View File

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include <unordered_map>
namespace Core {
class System;
}
class Config;
class InputProfiles {
public:
explicit InputProfiles();
virtual ~InputProfiles();
std::vector<std::string> GetInputProfileNames();
static bool IsProfileNameValid(std::string_view profile_name);
bool CreateProfile(const std::string& profile_name);
bool DeleteProfile(const std::string& profile_name);
bool LoadProfile(const std::string& profile_name);
bool SaveProfile(const std::string& profile_name);
private:
bool ProfileExistsInMap(const std::string& profile_name) const;
std::unordered_map<std::string, std::unique_ptr<Config>> map_profiles;
};

View File

@ -123,6 +123,7 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
#endif
constexpr int default_mouse_timeout = 2500;
constexpr int default_input_update_timeout = 1;
/**
* "Callouts" are one-time instructional messages shown to the user. In the config settings, there
@ -189,8 +190,9 @@ static QString PrettyProductName() {
}
GMainWindow::GMainWindow()
: ui{std::make_unique<Ui::MainWindow>()}, config{std::make_unique<Config>()}, emu_thread{
nullptr} {
: ui{std::make_unique<Ui::MainWindow>()},
input_subsystem{std::make_shared<InputCommon::InputSubsystem>()},
config{std::make_unique<Config>()}, emu_thread{nullptr} {
InitializeLogging();
Debugger::ToggleConsole();
Settings::LogSettings();
@ -236,6 +238,8 @@ GMainWindow::GMainWindow()
ConnectMenuEvents();
ConnectWidgetEvents();
Core::System::GetInstance().HIDCore().ReloadInputDevices();
LOG_INFO(Frontend, "Citra Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch,
Common::g_scm_desc);
#if CITRA_ARCH(x86_64)
@ -273,6 +277,10 @@ GMainWindow::GMainWindow()
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity);
update_input_timer.setInterval(default_input_update_timeout);
connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
update_input_timer.start();
if (UISettings::values.check_for_update_on_start) {
CheckForUpdates();
}
@ -296,8 +304,9 @@ void GMainWindow::InitializeWidgets() {
#ifdef CITRA_ENABLE_COMPATIBILITY_REPORTING
ui->action_Report_Compatibility->setVisible(true);
#endif
render_window = new GRenderWindow(this, emu_thread.get(), false);
secondary_window = new GRenderWindow(this, emu_thread.get(), true);
input_subsystem->Initialize();
render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, false);
secondary_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, true);
render_window->hide();
secondary_window->hide();
secondary_window->setParent(nullptr);
@ -320,7 +329,6 @@ void GMainWindow::InitializeWidgets() {
}
});
InputCommon::Init();
multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room,
ui->action_Show_Room);
multiplayer_state->setVisible(false);
@ -1142,6 +1150,7 @@ void GMainWindow::BootGame(const QString& filename) {
const std::string config_file_name =
title_id == 0 ? name : fmt::format("{:016X}", title_id);
Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig);
Core::System::GetInstance().HIDCore().ReloadInputDevices();
Settings::Apply();
LOG_INFO(Frontend, "Using per game config file for title id {}", config_file_name);
@ -1772,6 +1781,7 @@ void GMainWindow::OnPauseContinueGame() {
void GMainWindow::OnStopGame() {
ShutdownGame();
Settings::RestoreGlobalState(false);
Core::System::GetInstance().HIDCore().ReloadInputDevices();
}
void GMainWindow::OnLoadComplete() {
@ -1956,17 +1966,17 @@ void GMainWindow::OnLoadState() {
}
void GMainWindow::OnConfigure() {
const auto old_theme = UISettings::values.theme;
const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps;
game_list->SetDirectoryWatcherEnabled(false);
Settings::SetConfiguringGlobal(true);
ConfigureDialog configureDialog(this, hotkey_registry,
ConfigureDialog configureDialog(this, hotkey_registry, input_subsystem.get(),
!multiplayer_state->IsHostingPublicRoom());
connect(&configureDialog, &ConfigureDialog::LanguageChanged, this,
&GMainWindow::OnLanguageChanged);
auto old_theme = UISettings::values.theme;
const int old_input_profile_index = Settings::values.current_input_profile_index;
const auto old_input_profiles = Settings::values.input_profiles;
const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps;
const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
auto result = configureDialog.exec();
game_list->SetDirectoryWatcherEnabled(true);
if (result == QDialog::Accepted) {
@ -1991,9 +2001,7 @@ void GMainWindow::OnConfigure() {
UpdateSecondaryWindowVisibility();
UpdateBootHomeMenuState();
} else {
Settings::values.input_profiles = old_input_profiles;
Settings::values.touch_from_button_maps = old_touch_from_button_maps;
Settings::LoadProfile(old_input_profile_index);
}
}
@ -2294,6 +2302,13 @@ void GMainWindow::UpdateBootHomeMenuState() {
}
}
void GMainWindow::UpdateInputDrivers() {
if (!input_subsystem) {
return;
}
input_subsystem->PumpEvents();
}
void GMainWindow::HideMouseCursor() {
if (emu_thread == nullptr || !UISettings::values.hide_mouse.GetValue()) {
mouse_hide_timer.stop();
@ -2432,7 +2447,10 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
render_window->close();
secondary_window->close();
multiplayer_state->Close();
InputCommon::Shutdown();
Core::System::GetInstance().HIDCore().UnloadInputDevices();
update_input_timer.stop();
input_subsystem->Shutdown();
QWidget::closeEvent(event);
}
@ -2605,6 +2623,7 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_nam
// Do not cause the global config to write local settings into the config file
const bool is_powered_on = system.IsPoweredOn();
Settings::RestoreGlobalState(system.IsPoweredOn());
Core::System::GetInstance().HIDCore().ReloadInputDevices();
if (!is_powered_on) {
config->Save();

View File

@ -57,6 +57,10 @@ namespace DiscordRPC {
class DiscordInterface;
}
namespace InputCommon {
class InputSubsystem;
}
namespace Ui {
class MainWindow;
}
@ -255,11 +259,13 @@ private:
void UpdateUISettings();
void RetranslateStatusBar();
void InstallCIA(QStringList filepaths);
void UpdateInputDrivers();
void HideMouseCursor();
void ShowMouseCursor();
void OpenPerGameConfiguration(u64 title_id, const QString& file_name);
std::unique_ptr<Ui::MainWindow> ui;
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
GRenderWindow* render_window;
GRenderWindow* secondary_window;
@ -289,6 +295,7 @@ private:
bool auto_paused = false;
QTimer mouse_hide_timer;
QTimer update_input_timer;
// Movie
bool movie_record_on_start = false;

View File

@ -70,6 +70,7 @@ add_library(citra_common STATIC
file_util.cpp
file_util.h
hash.h
input.h
linear_disk_cache.h
literals.h
logging/backend.cpp
@ -100,6 +101,7 @@ add_library(citra_common STATIC
scope_exit.h
settings.cpp
settings.h
settings_input.h
slot_vector.h
serialization/atomic.h
serialization/boost_discrete_interval.hpp
@ -119,9 +121,12 @@ add_library(citra_common STATIC
thread_queue_list.h
thread_worker.h
threadsafe_queue.h
tiny_mt.h
timer.cpp
timer.h
unique_function.h
uuid.cpp
uuid.h
vector_math.h
web_result.h
x64/cpu_detect.cpp

View File

@ -112,7 +112,7 @@ bool CreateDir(const std::string& filename);
bool CreateFullPath(const std::string& fullPath);
// Deletes a given filename, return true on success
// Doesn't supports deleting a directory
// Doesn't support deleting a directory
bool Delete(const std::string& filename);
// Deletes a directory filename, returns true on success

375
src/common/input.h Normal file
View File

@ -0,0 +1,375 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/uuid.h"
namespace Common::Input {
// Type of data that is expected to recieve or send
enum class InputType {
None,
Button,
Stick,
Analog,
Motion,
Touch,
};
enum class PollingMode {
// Constant polling of buttons, analogs and motion data
Active,
// Only update on button change, digital analogs
Pasive,
};
// Polling mode reply from the controller
enum class PollingError {
None,
NotSupported,
Unknown,
};
// Different results that can happen from a device request
enum class DriverResult {
Success,
WrongReply,
Timeout,
UnsupportedControllerType,
HandleInUse,
ErrorReadingData,
ErrorWritingData,
NoDeviceDetected,
InvalidHandle,
NotSupported,
Disabled,
Unknown,
};
// Hint for amplification curve to be used
enum class VibrationAmplificationType {
Linear,
Exponential,
};
// Analog properties for calibration
struct AnalogProperties {
// Anything below this value will be detected as zero
float deadzone{};
// Anyting above this values will be detected as one
float range{1.0f};
// Minimum value to be detected as active
float threshold{0.5f};
// Drift correction applied to the raw data
float offset{};
// Invert direction of the sensor data
bool inverted{};
// Press once to activate, press again to release
bool toggle{};
};
// Single analog sensor data
struct AnalogStatus {
float value{};
float raw_value{};
AnalogProperties properties{};
};
// Button data
struct ButtonStatus {
Common::UUID uuid{};
bool value{};
// Invert value of the button
bool inverted{};
// Press once to activate, press again to release
bool toggle{};
// Internal lock for the toggle status
bool locked{};
};
// Analog and digital joystick data
struct StickStatus {
Common::UUID uuid{};
AnalogStatus x{};
AnalogStatus y{};
bool left{};
bool right{};
bool up{};
bool down{};
};
// Analog and digital trigger data
struct TriggerStatus {
Common::UUID uuid{};
AnalogStatus analog{};
ButtonStatus pressed{};
};
// 3D vector representing motion input
struct MotionSensor {
AnalogStatus x{};
AnalogStatus y{};
AnalogStatus z{};
};
// Motion data used to calculate controller orientation
struct MotionStatus {
// Gyroscope vector measurement in radians/s.
MotionSensor gyro{};
// Acceleration vector measurement in G force
MotionSensor accel{};
// Time since last measurement in microseconds
u64 delta_timestamp{};
// Request to update after reading the value
bool force_update{};
};
// Data of a single point on a touch screen
struct TouchStatus {
ButtonStatus pressed{};
AnalogStatus x{};
AnalogStatus y{};
int id{};
};
// HD rumble data
struct VibrationStatus {
f32 low_amplitude{};
f32 low_frequency{};
f32 high_amplitude{};
f32 high_frequency{};
VibrationAmplificationType type;
};
// List of buttons to be passed to Qt that can be translated
enum class ButtonNames {
Undefined,
Invalid,
// This will display the engine name instead of the button name
Engine,
// This will display the button by value instead of the button name
Value,
ButtonLeft,
ButtonRight,
ButtonDown,
ButtonUp,
TriggerZ,
TriggerR,
TriggerL,
ButtonA,
ButtonB,
ButtonX,
ButtonY,
ButtonStart,
// DS4 button names
L1,
L2,
L3,
R1,
R2,
R3,
Circle,
Cross,
Square,
Triangle,
Share,
Options,
Home,
Touch,
// Mouse buttons
ButtonMouseWheel,
ButtonBackward,
ButtonForward,
ButtonTask,
ButtonExtra,
};
// Callback data consisting of an input type and the equivalent data status
struct CallbackStatus {
InputType type{InputType::None};
ButtonStatus button_status{};
StickStatus stick_status{};
AnalogStatus analog_status{};
MotionStatus motion_status{};
TouchStatus touch_status{};
std::vector<u8> raw_data{};
};
// Triggered once every input change
struct InputCallback {
std::function<void(const CallbackStatus&)> on_change;
};
/// An abstract class template for an input device (a button, an analog input, etc.).
class InputDevice {
public:
virtual ~InputDevice() = default;
// Request input device to update if necessary
virtual void SoftUpdate() {}
// Force input device to update data regardless of the current state
virtual void ForceUpdate() {}
// Sets the function to be triggered when input changes
void SetCallback(InputCallback callback_) {
callback = std::move(callback_);
}
// Triggers the function set in the callback
void TriggerOnChange(const CallbackStatus& status) {
if (callback.on_change) {
callback.on_change(status);
}
}
private:
InputCallback callback;
};
/// An abstract class template for an output device (rumble, LED pattern, polling mode).
class OutputDevice {
public:
virtual ~OutputDevice() = default;
virtual PollingError SetPollingMode([[maybe_unused]] PollingMode polling_mode) {
return PollingError::NotSupported;
}
};
/// An abstract class template for a factory that can create input devices.
template <typename InputDeviceType>
class Factory {
public:
virtual ~Factory() = default;
virtual std::unique_ptr<InputDeviceType> Create(const Common::ParamPackage&) = 0;
};
namespace Impl {
template <typename InputDeviceType>
using FactoryListType = std::unordered_map<std::string, std::shared_ptr<Factory<InputDeviceType>>>;
template <typename InputDeviceType>
struct FactoryList {
static FactoryListType<InputDeviceType> list;
};
template <typename InputDeviceType>
FactoryListType<InputDeviceType> FactoryList<InputDeviceType>::list;
} // namespace Impl
/**
* Registers an input device factory.
* @tparam InputDeviceType the type of input devices the factory can create
* @param name the name of the factory. Will be used to match the "engine" parameter when creating
* a device
* @param factory the factory object to register
*/
template <typename InputDeviceType>
void RegisterFactory(const std::string& name, std::shared_ptr<Factory<InputDeviceType>> factory) {
auto pair = std::make_pair(name, std::move(factory));
if (!Impl::FactoryList<InputDeviceType>::list.insert(std::move(pair)).second) {
LOG_ERROR(Input, "Factory '{}' already registered", name);
}
}
inline void RegisterInputFactory(const std::string& name,
std::shared_ptr<Factory<InputDevice>> factory) {
RegisterFactory<InputDevice>(name, std::move(factory));
}
inline void RegisterOutputFactory(const std::string& name,
std::shared_ptr<Factory<OutputDevice>> factory) {
RegisterFactory<OutputDevice>(name, std::move(factory));
}
/**
* Unregisters an input device factory.
* @tparam InputDeviceType the type of input devices the factory can create
* @param name the name of the factory to unregister
*/
template <typename InputDeviceType>
void UnregisterFactory(const std::string& name) {
if (Impl::FactoryList<InputDeviceType>::list.erase(name) == 0) {
LOG_ERROR(Input, "Factory '{}' not registered", name);
}
}
inline void UnregisterInputFactory(const std::string& name) {
UnregisterFactory<InputDevice>(name);
}
inline void UnregisterOutputFactory(const std::string& name) {
UnregisterFactory<OutputDevice>(name);
}
/**
* Create an input device from given paramters.
* @tparam InputDeviceType the type of input devices to create
* @param params a serialized ParamPackage string that contains all parameters for creating the
* device
*/
template <typename InputDeviceType>
std::unique_ptr<InputDeviceType> CreateDeviceFromString(const std::string& params) {
const Common::ParamPackage package(params);
const std::string engine = package.Get("engine", "null");
const auto& factory_list = Impl::FactoryList<InputDeviceType>::list;
const auto pair = factory_list.find(engine);
if (pair == factory_list.end()) {
if (engine != "null") {
LOG_ERROR(Input, "Unknown engine name: {}", engine);
}
return std::make_unique<InputDeviceType>();
}
return pair->second->Create(package);
}
inline std::unique_ptr<InputDevice> CreateInputDeviceFromString(const std::string& params) {
return CreateDeviceFromString<InputDevice>(params);
}
inline std::unique_ptr<OutputDevice> CreateOutputDeviceFromString(const std::string& params) {
return CreateDeviceFromString<OutputDevice>(params);
}
/**
* Create an input device from given parameters.
* @tparam InputDeviceType the type of input devices to create
* @param package A ParamPackage that contains all parameters for creating the device
*/
template <typename InputDeviceType>
std::unique_ptr<InputDeviceType> CreateDevice(const ParamPackage& package) {
const std::string engine = package.Get("engine", "null");
const auto& factory_list = Impl::FactoryList<InputDeviceType>::list;
const auto pair = factory_list.find(engine);
if (pair == factory_list.end()) {
if (engine != "null") {
LOG_ERROR(Input, "Unknown engine name: {}", engine);
}
return std::make_unique<InputDeviceType>();
}
return pair->second->Create(package);
}
inline std::unique_ptr<InputDevice> CreateInputDevice(const ParamPackage& package) {
return CreateDevice<InputDevice>(package);
}
inline std::unique_ptr<OutputDevice> CreateOutputDevice(const ParamPackage& package) {
return CreateDevice<OutputDevice>(package);
}
} // namespace Common::Input

View File

@ -36,6 +36,36 @@ public:
T length = std::sqrt(xyz.Length2() + w * w);
return {xyz / length, w / length};
}
[[nodiscard]] std::array<decltype(-T{}), 16> ToMatrix() const {
const T x2 = xyz[0] * xyz[0];
const T y2 = xyz[1] * xyz[1];
const T z2 = xyz[2] * xyz[2];
const T xy = xyz[0] * xyz[1];
const T wz = w * xyz[2];
const T xz = xyz[0] * xyz[2];
const T wy = w * xyz[1];
const T yz = xyz[1] * xyz[2];
const T wx = w * xyz[0];
return {1.0f - 2.0f * (y2 + z2),
2.0f * (xy + wz),
2.0f * (xz - wy),
0.0f,
2.0f * (xy - wz),
1.0f - 2.0f * (x2 + z2),
2.0f * (yz + wx),
0.0f,
2.0f * (xz + wy),
2.0f * (yz - wx),
1.0f - 2.0f * (x2 + y2),
0.0f,
0.0f,
0.0f,
0.0f,
1.0f};
}
};
template <typename T>

View File

@ -101,24 +101,6 @@ void Apply() {
Core::DSP().SetSink(values.output_type.GetValue(), values.output_device.GetValue());
Core::DSP().EnableStretching(values.enable_audio_stretching.GetValue());
auto hid = Service::HID::GetModule(system);
if (hid) {
hid->ReloadInputDevices();
}
auto apt = Service::APT::GetModule(system);
if (apt) {
apt->GetAppletManager()->ReloadInputDevices();
}
auto sm = system.ServiceManager();
auto ir_user = sm.GetService<Service::IR::IR_USER>("ir:USER");
if (ir_user)
ir_user->ReloadInputDevices();
auto ir_rst = sm.GetService<Service::IR::IR_RST>("ir:rst");
if (ir_rst)
ir_rst->ReloadInputDevices();
auto cam = Service::CAM::GetModule(system);
if (cam) {
cam->ReloadCameraDevices();
@ -252,31 +234,4 @@ void RestoreGlobalState(bool is_powered_on) {
values.preload_textures.SetGlobal(true);
}
void LoadProfile(int index) {
Settings::values.current_input_profile = Settings::values.input_profiles[index];
Settings::values.current_input_profile_index = index;
}
void SaveProfile(int index) {
Settings::values.input_profiles[index] = Settings::values.current_input_profile;
}
void CreateProfile(std::string name) {
Settings::InputProfile profile = values.current_input_profile;
profile.name = std::move(name);
Settings::values.input_profiles.push_back(std::move(profile));
Settings::values.current_input_profile_index =
static_cast<int>(Settings::values.input_profiles.size()) - 1;
Settings::LoadProfile(Settings::values.current_input_profile_index);
}
void DeleteProfile(int index) {
Settings::values.input_profiles.erase(Settings::values.input_profiles.begin() + index);
Settings::LoadProfile(0);
}
void RenameCurrentProfile(std::string new_name) {
Settings::values.current_input_profile.name = std::move(new_name);
}
} // namespace Settings

View File

@ -13,6 +13,7 @@
#include "audio_core/input_details.h"
#include "audio_core/sink_details.h"
#include "common/common_types.h"
#include "common/settings_input.h"
#include "core/hle/service/cam/cam_params.h"
namespace Settings {
@ -53,8 +54,8 @@ enum class StereoRenderOption : u32 {
CardboardVR = 5
};
// Which eye to render when 3d is off. 800px wide mode could be added here in the future, when
// implemented
// Which eye to render when 3d is off. 800px wide mode
// could be added here in the future, when implemented.
enum class MonoRenderOption : u32 {
LeftEye = 0,
RightEye = 1,
@ -75,79 +76,6 @@ enum class TextureFilter : u32 {
xBRZ = 5,
};
namespace NativeButton {
enum Values {
A,
B,
X,
Y,
Up,
Down,
Left,
Right,
L,
R,
Start,
Select,
Debug,
Gpio14,
ZL,
ZR,
Home,
NumButtons,
};
constexpr int BUTTON_HID_BEGIN = A;
constexpr int BUTTON_IR_BEGIN = ZL;
constexpr int BUTTON_NS_BEGIN = Home;
constexpr int BUTTON_HID_END = BUTTON_IR_BEGIN;
constexpr int BUTTON_IR_END = BUTTON_NS_BEGIN;
constexpr int BUTTON_NS_END = NumButtons;
constexpr int NUM_BUTTONS_HID = BUTTON_HID_END - BUTTON_HID_BEGIN;
constexpr int NUM_BUTTONS_IR = BUTTON_IR_END - BUTTON_IR_BEGIN;
constexpr int NUM_BUTTONS_NS = BUTTON_NS_END - BUTTON_NS_BEGIN;
static const std::array<const char*, NumButtons> mapping = {{
"button_a",
"button_b",
"button_x",
"button_y",
"button_up",
"button_down",
"button_left",
"button_right",
"button_l",
"button_r",
"button_start",
"button_select",
"button_debug",
"button_gpio14",
"button_zl",
"button_zr",
"button_home",
}};
} // namespace NativeButton
namespace NativeAnalog {
enum Values {
CirclePad,
CStick,
NumAnalogs,
};
constexpr std::array<const char*, NumAnalogs> mapping = {{
"circle_pad",
"c_stick",
}};
} // namespace NativeAnalog
/** The Setting class is a simple resource manager. It defines a label and default value alongside
* the actual value of the setting for simpler and less-error prone use with frontend
* configurations. Specifying a default value and label is required. A minimum and maximum range can
@ -378,22 +306,36 @@ protected:
Type custom{}; ///< The custom value of the setting
};
struct InputProfile {
std::string name;
std::array<std::string, NativeButton::NumButtons> buttons;
std::array<std::string, NativeAnalog::NumAnalogs> analogs;
std::string motion_device;
std::string touch_device;
bool use_touch_from_button;
int touch_from_button_map_index;
std::string udp_input_address;
u16 udp_input_port;
u8 udp_pad_index;
};
/**
* The InputSetting class allows for getting a reference to either the global or custom members.
* This is required as we cannot easily modify the values of user-defined types within containers
* using the SetValue() member function found in the Setting class. The primary purpose of this
* class is to store an array of 10 PlayerInput structs for both the global and custom setting and
* allows for easily accessing and modifying both settings.
*/
template <typename Type>
class InputSetting final {
public:
InputSetting() = default;
explicit InputSetting(Type val) : Setting<Type>(val) {}
~InputSetting() = default;
void SetGlobal(bool to_global) {
use_global = to_global;
}
[[nodiscard]] bool UsingGlobal() const {
return use_global;
}
[[nodiscard]] Type& GetValue(bool need_global = false) {
if (use_global || need_global) {
return global;
}
return custom;
}
struct TouchFromButtonMap {
std::string name;
std::vector<std::string> buttons;
private:
bool use_global{true}; ///< The setting's global state
Type global{}; ///< The setting
Type custom{}; ///< The custom setting value
};
/// A special region value indicating that citra will automatically select a region
@ -402,11 +344,19 @@ static constexpr s32 REGION_VALUE_AUTO_SELECT = -1;
struct Values {
// Controls
InputProfile current_input_profile; ///< The current input profile
int current_input_profile_index; ///< The current input profile index
std::vector<InputProfile> input_profiles; ///< The list of input profiles
InputSetting<PlayerInput> players;
Setting<std::string> touch_device{"min_x:100,min_y:50,max_x:1800,max_y:850", "touch_device"};
Setting<int> touch_from_button_map_index{0, "touch_from_button_map"};
std::vector<TouchFromButtonMap> touch_from_button_maps;
Setting<std::string> udp_input_address{"127.0.0.1", "udp_input_address"};
Setting<u16> udp_input_port{26760, "udp_input_port"};
Setting<u8> udp_pad_index{0, "udp_pad_index"};
Setting<bool> emulate_analog_keyboard{false, "emulate_analog_keyboard"};
Setting<bool> keyboard_enabled{false, "keyboard_enabled"};
// Core
Setting<bool> use_cpu_jit{true, "use_cpu_jit"};
SwitchableSetting<s32, true> cpu_clock_percentage{100, 5, 400, "cpu_clock_percentage"};
@ -525,11 +475,4 @@ void LogSettings();
// Restore the global state of all applicable settings in the Values struct
void RestoreGlobalState(bool is_powered_on);
// Input profiles
void LoadProfile(int index);
void SaveProfile(int index);
void CreateProfile(std::string name);
void DeleteProfile(int index);
void RenameCurrentProfile(std::string new_name);
} // namespace Settings

119
src/common/settings_input.h Normal file
View File

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <string>
#include "common/common_types.h"
namespace Settings {
namespace NativeButton {
enum Values : int {
A,
B,
X,
Y,
DUp,
DDown,
DLeft,
DRight,
L,
R,
Start,
Select,
Debug,
Gpio14,
ZL,
ZR,
Home,
NumButtons,
};
constexpr int BUTTON_HID_BEGIN = A;
constexpr int BUTTON_IR_BEGIN = ZL;
constexpr int BUTTON_NS_BEGIN = Home;
constexpr int BUTTON_HID_END = BUTTON_IR_BEGIN;
constexpr int BUTTON_IR_END = BUTTON_NS_BEGIN;
constexpr int BUTTON_NS_END = NumButtons;
constexpr int NUM_BUTTONS_HID = BUTTON_HID_END - BUTTON_HID_BEGIN;
constexpr int NUM_BUTTONS_IR = BUTTON_IR_END - BUTTON_IR_BEGIN;
constexpr int NUM_BUTTONS_NS = BUTTON_NS_END - BUTTON_NS_BEGIN;
constexpr std::array<const char*, NumButtons> mapping = {{
"button_a",
"button_b",
"button_x",
"button_y",
"button_up",
"button_down",
"button_left",
"button_right",
"button_l",
"button_r",
"button_start",
"button_select",
"button_debug",
"button_gpio14",
"button_zl",
"button_zr",
"button_home",
}};
} // namespace NativeButton
namespace NativeAnalog {
enum Values : int {
CirclePad,
CStick,
NumAnalogs,
};
constexpr std::array<const char*, NumAnalogs> mapping = {{
"circle_pad",
"c_stick",
}};
} // namespace NativeAnalog
namespace NativeMotion {
enum Values : int {
MotionLeft,
MotionRight,
NumMotions,
};
constexpr int MOTION_HID_BEGIN = MotionLeft;
constexpr int MOTION_HID_END = NumMotions;
constexpr int NUM_MOTIONS_HID = NumMotions;
constexpr std::array<const char*, NumMotions> mapping = {{
"motionleft",
"motionright",
}};
} // namespace NativeMotion
using AnalogsRaw = std::array<std::string, NativeAnalog::NumAnalogs>;
using ButtonsRaw = std::array<std::string, NativeButton::NumButtons>;
using MotionsRaw = std::array<std::string, NativeMotion::NumMotions>;
struct PlayerInput {
ButtonsRaw buttons;
AnalogsRaw analogs;
MotionsRaw motions;
std::string profile_name;
};
struct TouchFromButtonMap {
std::string name;
std::vector<std::string> buttons;
};
} // namespace Settings

249
src/common/tiny_mt.h Normal file
View File

@ -0,0 +1,249 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include "common/alignment.h"
#include "common/common_types.h"
namespace Common {
// Implementation of TinyMT (mersenne twister RNG).
// Like Nintendo, we will use the sample parameters.
class TinyMT {
public:
static constexpr std::size_t NumStateWords = 4;
struct State {
std::array<u32, NumStateWords> data{};
};
private:
static constexpr u32 ParamMat1 = 0x8F7011EE;
static constexpr u32 ParamMat2 = 0xFC78FF1F;
static constexpr u32 ParamTmat = 0x3793FDFF;
static constexpr u32 ParamMult = 0x6C078965;
static constexpr u32 ParamPlus = 0x0019660D;
static constexpr u32 ParamXor = 0x5D588B65;
static constexpr u32 TopBitmask = 0x7FFFFFFF;
static constexpr int MinimumInitIterations = 8;
static constexpr int NumDiscardedInitOutputs = 8;
static constexpr u32 XorByShifted27(u32 value) {
return value ^ (value >> 27);
}
static constexpr u32 XorByShifted30(u32 value) {
return value ^ (value >> 30);
}
private:
State state{};
private:
// Internal API.
void FinalizeInitialization() {
const u32 state0 = this->state.data[0] & TopBitmask;
const u32 state1 = this->state.data[1];
const u32 state2 = this->state.data[2];
const u32 state3 = this->state.data[3];
if (state0 == 0 && state1 == 0 && state2 == 0 && state3 == 0) {
this->state.data[0] = 'T';
this->state.data[1] = 'I';
this->state.data[2] = 'N';
this->state.data[3] = 'Y';
}
for (int i = 0; i < NumDiscardedInitOutputs; i++) {
this->GenerateRandomU32();
}
}
u32 GenerateRandomU24() {
return (this->GenerateRandomU32() >> 8);
}
static void GenerateInitialValuePlus(TinyMT::State* state, int index, u32 value) {
u32& state0 = state->data[(index + 0) % NumStateWords];
u32& state1 = state->data[(index + 1) % NumStateWords];
u32& state2 = state->data[(index + 2) % NumStateWords];
u32& state3 = state->data[(index + 3) % NumStateWords];
const u32 x = XorByShifted27(state0 ^ state1 ^ state3) * ParamPlus;
const u32 y = x + index + value;
state0 = y;
state1 += x;
state2 += y;
}
static void GenerateInitialValueXor(TinyMT::State* state, int index) {
u32& state0 = state->data[(index + 0) % NumStateWords];
u32& state1 = state->data[(index + 1) % NumStateWords];
u32& state2 = state->data[(index + 2) % NumStateWords];
u32& state3 = state->data[(index + 3) % NumStateWords];
const u32 x = XorByShifted27(state0 + state1 + state3) * ParamXor;
const u32 y = x - index;
state0 = y;
state1 ^= x;
state2 ^= y;
}
public:
constexpr TinyMT() = default;
// Public API.
// Initialization.
void Initialize(u32 seed) {
this->state.data[0] = seed;
this->state.data[1] = ParamMat1;
this->state.data[2] = ParamMat2;
this->state.data[3] = ParamTmat;
for (int i = 1; i < MinimumInitIterations; i++) {
const u32 mixed = XorByShifted30(this->state.data[(i - 1) % NumStateWords]);
this->state.data[i % NumStateWords] ^= mixed * ParamMult + i;
}
this->FinalizeInitialization();
}
void Initialize(const u32* seed, int seed_count) {
this->state.data[0] = 0;
this->state.data[1] = ParamMat1;
this->state.data[2] = ParamMat2;
this->state.data[3] = ParamTmat;
{
const int num_init_iterations = std::max(seed_count + 1, MinimumInitIterations) - 1;
GenerateInitialValuePlus(&this->state, 0, seed_count);
for (int i = 0; i < num_init_iterations; i++) {
GenerateInitialValuePlus(&this->state, (i + 1) % NumStateWords,
(i < seed_count) ? seed[i] : 0);
}
for (int i = 0; i < static_cast<int>(NumStateWords); i++) {
GenerateInitialValueXor(&this->state,
(i + 1 + num_init_iterations) % NumStateWords);
}
}
this->FinalizeInitialization();
}
// State management.
void GetState(TinyMT::State& out) const {
out.data = this->state.data;
}
void SetState(const TinyMT::State& state_) {
this->state.data = state_.data;
}
// Random generation.
void GenerateRandomBytes(void* dst, std::size_t size) {
const uintptr_t start = reinterpret_cast<uintptr_t>(dst);
const uintptr_t end = start + size;
const uintptr_t aligned_start = Common::AlignUp(start, 4);
const uintptr_t aligned_end = Common::AlignDown(end, 4);
// Make sure we're aligned.
if (start < aligned_start) {
const u32 rnd = this->GenerateRandomU32();
std::memcpy(dst, &rnd, aligned_start - start);
}
// Write as many aligned u32s as we can.
{
u32* cur_dst = reinterpret_cast<u32*>(aligned_start);
u32* const end_dst = reinterpret_cast<u32*>(aligned_end);
while (cur_dst < end_dst) {
*(cur_dst++) = this->GenerateRandomU32();
}
}
// Handle any leftover unaligned data.
if (aligned_end < end) {
const u32 rnd = this->GenerateRandomU32();
std::memcpy(reinterpret_cast<void*>(aligned_end), &rnd, end - aligned_end);
}
}
u32 GenerateRandomU32() {
// Advance state.
const u32 x0 =
(this->state.data[0] & TopBitmask) ^ this->state.data[1] ^ this->state.data[2];
const u32 y0 = this->state.data[3];
const u32 x1 = x0 ^ (x0 << 1);
const u32 y1 = y0 ^ (y0 >> 1) ^ x1;
const u32 state0 = this->state.data[1];
u32 state1 = this->state.data[2];
u32 state2 = x1 ^ (y1 << 10);
const u32 state3 = y1;
if ((y1 & 1) != 0) {
state1 ^= ParamMat1;
state2 ^= ParamMat2;
}
this->state.data[0] = state0;
this->state.data[1] = state1;
this->state.data[2] = state2;
this->state.data[3] = state3;
// Temper.
const u32 t1 = state0 + (state2 >> 8);
u32 t0 = state3 ^ t1;
if ((t1 & 1) != 0) {
t0 ^= ParamTmat;
}
return t0;
}
u64 GenerateRandomU64() {
const u32 lo = this->GenerateRandomU32();
const u32 hi = this->GenerateRandomU32();
return (u64{hi} << 32) | u64{lo};
}
float GenerateRandomF32() {
// Floats have 24 bits of mantissa.
constexpr u32 MantissaBits = 24;
return static_cast<float>(GenerateRandomU24()) * (1.0f / (1U << MantissaBits));
}
double GenerateRandomF64() {
// Doubles have 53 bits of mantissa.
// The smart way to generate 53 bits of random would be to use 32 bits
// from the first rnd32() call, and then 21 from the second.
// Nintendo does not. They use (32 - 5) = 27 bits from the first rnd32()
// call, and (32 - 6) bits from the second. We'll do what they do, but
// There's not a clear reason why.
constexpr u32 MantissaBits = 53;
constexpr u32 Shift1st = (64 - MantissaBits) / 2;
constexpr u32 Shift2nd = (64 - MantissaBits) - Shift1st;
const u32 first = (this->GenerateRandomU32() >> Shift1st);
const u32 second = (this->GenerateRandomU32() >> Shift2nd);
return (1.0 * first * (u64{1} << (32 - Shift2nd)) + second) *
(1.0 / (u64{1} << MantissaBits));
}
};
} // namespace Common

212
src/common/uuid.cpp Normal file
View File

@ -0,0 +1,212 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <bit>
#include <optional>
#include <random>
#include <fmt/format.h>
#include "common/assert.h"
#include "common/tiny_mt.h"
#include "common/uuid.h"
namespace Common {
namespace {
constexpr size_t RawStringSize = sizeof(UUID) * 2;
constexpr size_t FormattedStringSize = RawStringSize + 4;
std::optional<u8> HexCharToByte(char c) {
if (c >= '0' && c <= '9') {
return static_cast<u8>(c - '0');
}
if (c >= 'a' && c <= 'f') {
return static_cast<u8>(c - 'a' + 10);
}
if (c >= 'A' && c <= 'F') {
return static_cast<u8>(c - 'A' + 10);
}
ASSERT_MSG(false, "{} is not a hexadecimal digit!", c);
return std::nullopt;
}
std::array<u8, 0x10> ConstructFromRawString(std::string_view raw_string) {
std::array<u8, 0x10> uuid;
for (size_t i = 0; i < RawStringSize; i += 2) {
const auto upper = HexCharToByte(raw_string[i]);
const auto lower = HexCharToByte(raw_string[i + 1]);
if (!upper || !lower) {
return {};
}
uuid[i / 2] = static_cast<u8>((*upper << 4) | *lower);
}
return uuid;
}
std::array<u8, 0x10> ConstructFromFormattedString(std::string_view formatted_string) {
std::array<u8, 0x10> uuid;
size_t i = 0;
// Process the first 8 characters.
const auto* str = formatted_string.data();
for (; i < 4; ++i) {
const auto upper = HexCharToByte(*(str++));
const auto lower = HexCharToByte(*(str++));
if (!upper || !lower) {
return {};
}
uuid[i] = static_cast<u8>((*upper << 4) | *lower);
}
// Process the next 4 characters.
++str;
for (; i < 6; ++i) {
const auto upper = HexCharToByte(*(str++));
const auto lower = HexCharToByte(*(str++));
if (!upper || !lower) {
return {};
}
uuid[i] = static_cast<u8>((*upper << 4) | *lower);
}
// Process the next 4 characters.
++str;
for (; i < 8; ++i) {
const auto upper = HexCharToByte(*(str++));
const auto lower = HexCharToByte(*(str++));
if (!upper || !lower) {
return {};
}
uuid[i] = static_cast<u8>((*upper << 4) | *lower);
}
// Process the next 4 characters.
++str;
for (; i < 10; ++i) {
const auto upper = HexCharToByte(*(str++));
const auto lower = HexCharToByte(*(str++));
if (!upper || !lower) {
return {};
}
uuid[i] = static_cast<u8>((*upper << 4) | *lower);
}
// Process the last 12 characters.
++str;
for (; i < 16; ++i) {
const auto upper = HexCharToByte(*(str++));
const auto lower = HexCharToByte(*(str++));
if (!upper || !lower) {
return {};
}
uuid[i] = static_cast<u8>((*upper << 4) | *lower);
}
return uuid;
}
std::array<u8, 0x10> ConstructUUID(std::string_view uuid_string) {
const auto length = uuid_string.length();
if (length == 0) {
return {};
}
// Check if the input string contains 32 hexadecimal characters.
if (length == RawStringSize) {
return ConstructFromRawString(uuid_string);
}
// Check if the input string has the length of a RFC 4122 formatted UUID string.
if (length == FormattedStringSize) {
return ConstructFromFormattedString(uuid_string);
}
ASSERT_MSG(false, "UUID string has an invalid length of {} characters!", length);
return {};
}
} // Anonymous namespace
UUID::UUID(std::string_view uuid_string) : uuid{ConstructUUID(uuid_string)} {}
std::string UUID::RawString() const {
return fmt::format("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}"
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7],
uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14],
uuid[15]);
}
std::string UUID::FormattedString() const {
return fmt::format("{:02x}{:02x}{:02x}{:02x}"
"-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-"
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7],
uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14],
uuid[15]);
}
size_t UUID::Hash() const noexcept {
u64 upper_hash;
u64 lower_hash;
std::memcpy(&upper_hash, uuid.data(), sizeof(u64));
std::memcpy(&lower_hash, uuid.data() + sizeof(u64), sizeof(u64));
return upper_hash ^ std::rotl(lower_hash, 1);
}
u128 UUID::AsU128() const {
u128 uuid_old;
std::memcpy(&uuid_old, uuid.data(), sizeof(UUID));
return uuid_old;
}
UUID UUID::MakeRandom() {
std::random_device device;
return MakeRandomWithSeed(device());
}
UUID UUID::MakeRandomWithSeed(u32 seed) {
// Create and initialize our RNG.
TinyMT rng;
rng.Initialize(seed);
UUID uuid;
// Populate the UUID with random bytes.
rng.GenerateRandomBytes(uuid.uuid.data(), sizeof(UUID));
return uuid;
}
UUID UUID::MakeRandomRFC4122V4() {
auto uuid = MakeRandom();
// According to Proposed Standard RFC 4122 Section 4.4, we must:
// 1. Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively.
uuid.uuid[8] = 0x80 | (uuid.uuid[8] & 0x3F);
// 2. Set the four most significant bits (bits 12 through 15) of the
// time_hi_and_version field to the 4-bit version number from Section 4.1.3.
uuid.uuid[6] = 0x40 | (uuid.uuid[6] & 0xF);
return uuid;
}
} // namespace Common

140
src/common/uuid.h Normal file
View File

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <functional>
#include <string>
#include "common/common_types.h"
namespace Common {
struct UUID {
std::array<u8, 0x10> uuid{};
/// Constructs an invalid UUID.
constexpr UUID() = default;
/// Constructs a UUID from a reference to a 128 bit array.
constexpr explicit UUID(const std::array<u8, 16>& uuid_) : uuid{uuid_} {}
/**
* Constructs a UUID from either:
* 1. A 32 hexadecimal character string representing the bytes of the UUID
* 2. A RFC 4122 formatted UUID string, in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
*
* The input string may contain uppercase or lowercase characters, but they must:
* 1. Contain valid hexadecimal characters (0-9, a-f, A-F)
* 2. Not contain the "0x" hexadecimal prefix
*
* Should the input string not meet the above requirements,
* an assert will be triggered and an invalid UUID is set instead.
*/
explicit UUID(std::string_view uuid_string);
~UUID() = default;
constexpr UUID(const UUID&) noexcept = default;
constexpr UUID(UUID&&) noexcept = default;
constexpr UUID& operator=(const UUID&) noexcept = default;
constexpr UUID& operator=(UUID&&) noexcept = default;
/**
* Returns whether the stored UUID is valid or not.
*
* @returns True if the stored UUID is valid, false otherwise.
*/
constexpr bool IsValid() const {
return uuid != std::array<u8, 0x10>{};
}
/**
* Returns whether the stored UUID is invalid or not.
*
* @returns True if the stored UUID is invalid, false otherwise.
*/
constexpr bool IsInvalid() const {
return !IsValid();
}
/**
* Returns a 32 hexadecimal character string representing the bytes of the UUID.
*
* @returns A 32 hexadecimal character string of the UUID.
*/
std::string RawString() const;
/**
* Returns a RFC 4122 formatted UUID string in the format
* xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
*
* @returns A RFC 4122 formatted UUID string.
*/
std::string FormattedString() const;
/**
* Returns a 64-bit hash of the UUID for use in hash table data structures.
*
* @returns A 64-bit hash of the UUID.
*/
size_t Hash() const noexcept;
/// DO NOT USE. Copies the contents of the UUID into a u128.
u128 AsU128() const;
/**
* Creates a default UUID "yuzu Default UID".
*
* @returns A UUID with its bytes set to the ASCII values of "yuzu Default UID".
*/
static constexpr UUID MakeDefault() {
return UUID{
{'y', 'u', 'z', 'u', ' ', 'D', 'e', 'f', 'a', 'u', 'l', 't', ' ', 'U', 'I', 'D'},
};
}
/**
* Creates a random UUID.
*
* @returns A random UUID.
*/
static UUID MakeRandom();
/**
* Creates a random UUID with a seed.
*
* @param seed A seed to initialize the Mersenne-Twister RNG
*
* @returns A random UUID.
*/
static UUID MakeRandomWithSeed(u32 seed);
/**
* Creates a random UUID. The generated UUID is RFC 4122 Version 4 compliant.
*
* @returns A random UUID that is RFC 4122 Version 4 compliant.
*/
static UUID MakeRandomRFC4122V4();
friend constexpr bool operator==(const UUID& lhs, const UUID& rhs) = default;
};
static_assert(sizeof(UUID) == 0x10, "UUID has incorrect size.");
/// An invalid UUID. This UUID has all its bytes set to 0.
constexpr UUID InvalidUUID = {};
} // namespace Common
namespace std {
template <>
struct hash<Common::UUID> {
size_t operator()(const Common::UUID& uuid) const noexcept {
return uuid.Hash();
}
};
} // namespace std

View File

@ -109,11 +109,23 @@ add_library(citra_core STATIC
frontend/framebuffer_layout.h
frontend/image_interface.cpp
frontend/image_interface.h
frontend/input.h
gdbstub/gdbstub.cpp
gdbstub/gdbstub.h
gdbstub/hio.cpp
gdbstub/hio.h
gdbstub/gdbstub.cpp
gdbstub/gdbstub.h
hid/emulated_console.cpp
hid/emulated_console.h
hid/emulated_controller.cpp
hid/emulated_controller.h
hid/hid_core.cpp
hid/hid_core.h
hid/hid_types.h
hid/input_converter.cpp
hid/input_converter.h
hid/motion_input.cpp
hid/motion_input.h
hle/applets/applet.cpp
hle/applets/applet.h
hle/applets/erreula.cpp

View File

@ -280,7 +280,7 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
auto n3ds_mode = app_loader->LoadKernelN3dsMode();
ASSERT(n3ds_mode.first);
u32 num_cores = 2;
if (Settings::values.is_new_3ds) {
if (Settings::values.is_new_3ds.GetValue()) {
num_cores = 4;
}
ResultStatus init_result{
@ -464,6 +464,14 @@ const Service::FS::ArchiveManager& System::ArchiveManager() const {
return *archive_manager;
}
HID::HIDCore& System::HIDCore() {
return hid_core;
}
const HID::HIDCore& System::HIDCore() const {
return hid_core;
}
Kernel::KernelSystem& System::Kernel() {
return *kernel;
}

View File

@ -12,6 +12,7 @@
#include "common/common_types.h"
#include "core/frontend/applets/mii_selector.h"
#include "core/frontend/applets/swkbd.h"
#include "core/hid/hid_core.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#include "core/perf_stats.h"
@ -225,6 +226,12 @@ public:
/// Gets a const reference to the archive manager
[[nodiscard]] const Service::FS::ArchiveManager& ArchiveManager() const;
/// Gets a mutable reference to the HID interface.
[[nodiscard]] HID::HIDCore& HIDCore();
/// Gets an immutable reference to the HID interface.
[[nodiscard]] const HID::HIDCore& HIDCore() const;
/// Gets a reference to the kernel
[[nodiscard]] Kernel::KernelSystem& Kernel();
@ -389,6 +396,8 @@ private:
std::unique_ptr<Core::ExclusiveMonitor> exclusive_monitor;
HID::HIDCore hid_core{};
private:
static System s_instance;

View File

@ -4,87 +4,31 @@
#include <cmath>
#include <mutex>
#include "common/input.h"
#include "common/settings.h"
#include "core/3ds.h"
#include "core/frontend/emu_window.h"
#include "core/frontend/input.h"
namespace Frontend {
/// We need a global touch state that is shared across the different window instances
static std::weak_ptr<EmuWindow::TouchState> global_touch_state;
GraphicsContext::~GraphicsContext() = default;
class EmuWindow::TouchState : public Input::Factory<Input::TouchDevice>,
public std::enable_shared_from_this<TouchState> {
public:
std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage&) override {
return std::make_unique<Device>(shared_from_this());
}
EmuWindow::EmuWindow(){};
std::mutex mutex;
bool touch_pressed = false; ///< True if touchpad area is currently pressed, otherwise false
float touch_x = 0.0f; ///< Touchpad X-position
float touch_y = 0.0f; ///< Touchpad Y-position
private:
class Device : public Input::TouchDevice {
public:
explicit Device(std::weak_ptr<TouchState>&& touch_state) : touch_state(touch_state) {}
std::tuple<float, float, bool> GetStatus() const override {
if (auto state = touch_state.lock()) {
std::lock_guard guard{state->mutex};
return std::make_tuple(state->touch_x, state->touch_y, state->touch_pressed);
}
return std::make_tuple(0.0f, 0.0f, false);
}
private:
std::weak_ptr<TouchState> touch_state;
};
};
EmuWindow::EmuWindow() {
CreateTouchState();
};
EmuWindow::EmuWindow(bool is_secondary_) : is_secondary{is_secondary_} {
CreateTouchState();
}
EmuWindow::EmuWindow(bool is_secondary_) : is_secondary{is_secondary_} {}
EmuWindow::~EmuWindow() = default;
/**
* Check if the given x/y coordinates are within the touchpad specified by the framebuffer layout
* @param layout FramebufferLayout object describing the framebuffer size and screen positions
* @param framebuffer_x Framebuffer x-coordinate to check
* @param framebuffer_y Framebuffer y-coordinate to check
* @return True if the coordinates are within the touchpad, otherwise false
*/
static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x,
unsigned framebuffer_y) {
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::SideBySide) {
return (framebuffer_y >= layout.bottom_screen.top &&
framebuffer_y < layout.bottom_screen.bottom &&
((framebuffer_x >= layout.bottom_screen.left / 2 &&
framebuffer_x < layout.bottom_screen.right / 2) ||
(framebuffer_x >= (layout.bottom_screen.left / 2) + (layout.width / 2) &&
framebuffer_x < (layout.bottom_screen.right / 2) + (layout.width / 2))));
} else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) {
return (framebuffer_y >= layout.bottom_screen.top &&
framebuffer_y < layout.bottom_screen.bottom &&
((framebuffer_x >= layout.bottom_screen.left &&
framebuffer_x < layout.bottom_screen.right) ||
(framebuffer_x >= layout.cardboard.bottom_screen_right_eye + (layout.width / 2) &&
framebuffer_x < layout.cardboard.bottom_screen_right_eye +
layout.bottom_screen.GetWidth() + (layout.width / 2))));
} else {
return (framebuffer_y >= layout.bottom_screen.top &&
framebuffer_y < layout.bottom_screen.bottom &&
framebuffer_x >= layout.bottom_screen.left &&
framebuffer_x < layout.bottom_screen.right);
}
std::pair<f32, f32> EmuWindow::MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const {
std::tie(framebuffer_x, framebuffer_y) = ClipToTouchScreen(framebuffer_x, framebuffer_y);
const float x = static_cast<float>(framebuffer_x - framebuffer_layout.bottom_screen.left) /
static_cast<float>(framebuffer_layout.bottom_screen.right -
framebuffer_layout.bottom_screen.left);
const float y = static_cast<float>(framebuffer_y - framebuffer_layout.bottom_screen.top) /
static_cast<float>(framebuffer_layout.bottom_screen.bottom -
framebuffer_layout.bottom_screen.top);
return std::make_pair(x, y);
}
std::tuple<unsigned, unsigned> EmuWindow::ClipToTouchScreen(unsigned new_x, unsigned new_y) const {
@ -109,67 +53,6 @@ std::tuple<unsigned, unsigned> EmuWindow::ClipToTouchScreen(unsigned new_x, unsi
return std::make_tuple(new_x, new_y);
}
void EmuWindow::CreateTouchState() {
if (touch_state = global_touch_state.lock()) {
return;
}
touch_state = std::make_shared<TouchState>();
Input::RegisterFactory<Input::TouchDevice>("emu_window", touch_state);
global_touch_state = touch_state;
}
bool EmuWindow::TouchPressed(unsigned framebuffer_x, unsigned framebuffer_y) {
if (!IsWithinTouchscreen(framebuffer_layout, framebuffer_x, framebuffer_y))
return false;
if (framebuffer_x >= framebuffer_layout.width / 2) {
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::SideBySide)
framebuffer_x -= framebuffer_layout.width / 2;
else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR)
framebuffer_x -=
(framebuffer_layout.width / 2) - (framebuffer_layout.cardboard.user_x_shift * 2);
}
std::lock_guard guard(touch_state->mutex);
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::SideBySide) {
touch_state->touch_x =
static_cast<float>(framebuffer_x - framebuffer_layout.bottom_screen.left / 2) /
(framebuffer_layout.bottom_screen.right / 2 -
framebuffer_layout.bottom_screen.left / 2);
} else {
touch_state->touch_x =
static_cast<float>(framebuffer_x - framebuffer_layout.bottom_screen.left) /
(framebuffer_layout.bottom_screen.right - framebuffer_layout.bottom_screen.left);
}
touch_state->touch_y =
static_cast<float>(framebuffer_y - framebuffer_layout.bottom_screen.top) /
(framebuffer_layout.bottom_screen.bottom - framebuffer_layout.bottom_screen.top);
if (!framebuffer_layout.is_rotated) {
std::swap(touch_state->touch_x, touch_state->touch_y);
touch_state->touch_x = 1.f - touch_state->touch_x;
}
touch_state->touch_pressed = true;
return true;
}
void EmuWindow::TouchReleased() {
std::lock_guard guard{touch_state->mutex};
touch_state->touch_pressed = false;
touch_state->touch_x = 0;
touch_state->touch_y = 0;
}
void EmuWindow::TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y) {
if (!touch_state->touch_pressed)
return;
if (!IsWithinTouchscreen(framebuffer_layout, framebuffer_x, framebuffer_y))
std::tie(framebuffer_x, framebuffer_y) = ClipToTouchScreen(framebuffer_x, framebuffer_y);
TouchPressed(framebuffer_x, framebuffer_y);
}
void EmuWindow::UpdateCurrentFramebufferLayout(unsigned width, unsigned height,
bool is_portrait_mode) {
Layout::FramebufferLayout layout;

View File

@ -122,8 +122,6 @@ public:
*/
class EmuWindow : public GraphicsContext {
public:
class TouchState;
/// Data structure to store emuwindow configuration
struct WindowConfig {
bool fullscreen = false;
@ -176,24 +174,6 @@ public:
*/
virtual void RestoreContext(){};
/**
* Signal that a touch pressed event has occurred (e.g. mouse click pressed)
* @param framebuffer_x Framebuffer x-coordinate that was pressed
* @param framebuffer_y Framebuffer y-coordinate that was pressed
* @returns True if the coordinates are within the touchpad, otherwise false
*/
bool TouchPressed(unsigned framebuffer_x, unsigned framebuffer_y);
/// Signal that a touch released event has occurred (e.g. mouse click released)
void TouchReleased();
/**
* Signal that a touch movement event has occurred (e.g. mouse was moved over the emu window)
* @param framebuffer_x Framebuffer x-coordinate
* @param framebuffer_y Framebuffer y-coordinate
*/
void TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y);
/**
* Returns currently active configuration.
* @note Accesses to the returned object need not be consistent because it may be modified in
@ -271,6 +251,11 @@ protected:
framebuffer_layout = layout;
}
/**
* Converts a screen postion into the equivalent touchscreen position.
*/
std::pair<f32, f32> MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const;
bool is_secondary{};
bool strict_context_required{};
WindowSystemInfo window_info;
@ -286,16 +271,12 @@ private:
// By default, ignore this request and do nothing.
}
void CreateTouchState();
Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout
WindowConfig config{}; ///< Internal configuration (changes pending for being applied in
/// ProcessConfigurationChanges)
WindowConfig active_config{}; ///< Internal active configuration
std::shared_ptr<TouchState> touch_state;
/**
* Clip the provided coordinates to be inside the touchscreen area.
*/

View File

@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/settings.h"
#include "core/hid/emulated_console.h"
#include "core/hid/input_converter.h"
namespace Core::HID {
EmulatedConsole::EmulatedConsole() = default;
EmulatedConsole::~EmulatedConsole() = default;
void EmulatedConsole::ReloadFromSettings() {
// Using first motion device from player 1. No need to assign any unique config at the moment
const auto& player = Settings::values.players.GetValue();
motion_params = Common::ParamPackage(player.motions[0]);
ReloadInput();
}
void EmulatedConsole::SetTouchParams() {
std::size_t index = 0;
touch_params[index++] = Common::ParamPackage{"engine:mouse,axis_x:10,axis_y:11,button:0"};
touch_params[index++] =
Common::ParamPackage{"engine:cemuhookudp,axis_x:17,axis_y:18,button:65536"};
touch_params[index++] =
Common::ParamPackage{"engine:cemuhookudp,axis_x:19,axis_y:20,button:131072"};
for (int i = 0; i < static_cast<int>(MaxActiveTouchInputs); i++) {
Common::ParamPackage touchscreen_param{};
touchscreen_param.Set("engine", "touch");
touchscreen_param.Set("axis_x", i * 2);
touchscreen_param.Set("axis_y", (i * 2) + 1);
touchscreen_param.Set("button", i);
touch_params[index++] = std::move(touchscreen_param);
}
if (Settings::values.touch_from_button_maps.empty()) {
LOG_WARNING(Input, "touch_from_button_maps is unset by frontend config");
return;
}
const auto button_index =
static_cast<u64>(Settings::values.touch_from_button_map_index.GetValue());
const auto& touch_buttons = Settings::values.touch_from_button_maps[button_index].buttons;
// Map the rest of the fingers from touch from button configuration
for (const auto& config_entry : touch_buttons) {
if (index >= MaxTouchDevices) {
continue;
}
Common::ParamPackage params{config_entry};
Common::ParamPackage touch_button_params;
const int x = params.Get("x", 0);
const int y = params.Get("y", 0);
params.Erase("x");
params.Erase("y");
touch_button_params.Set("engine", "touch_from_button");
touch_button_params.Set("button", params.Serialize());
touch_button_params.Set("x", x);
touch_button_params.Set("y", y);
touch_params[index] = std::move(touch_button_params);
index++;
}
}
void EmulatedConsole::ReloadInput() {
// If you load any device here add the equivalent to the UnloadInput() function
SetTouchParams();
motion_devices = Common::Input::CreateInputDevice(motion_params);
if (motion_devices) {
motion_devices->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback) { SetMotion(callback); },
});
}
// Unique index for identifying touch device source
std::size_t index = 0;
for (auto& touch_device : touch_devices) {
touch_device = Common::Input::CreateInputDevice(touch_params[index]);
if (!touch_device) {
continue;
}
touch_device->SetCallback({
.on_change =
[this, index](const Common::Input::CallbackStatus& callback) {
SetTouch(callback, index);
},
});
index++;
}
}
void EmulatedConsole::UnloadInput() {
motion_devices.reset();
for (auto& touch : touch_devices) {
touch.reset();
}
}
void EmulatedConsole::EnableConfiguration() {
is_configuring = true;
SaveCurrentConfig();
}
void EmulatedConsole::DisableConfiguration() {
is_configuring = false;
}
bool EmulatedConsole::IsConfiguring() const {
return is_configuring;
}
void EmulatedConsole::SaveCurrentConfig() {
if (!is_configuring) {
return;
}
}
void EmulatedConsole::RestoreConfig() {
if (!is_configuring) {
return;
}
ReloadFromSettings();
}
Common::ParamPackage EmulatedConsole::GetMotionParam() const {
return motion_params;
}
void EmulatedConsole::SetMotionParam(Common::ParamPackage param) {
motion_params = std::move(param);
ReloadInput();
}
void EmulatedConsole::SetMotion(const Common::Input::CallbackStatus& callback) {
std::unique_lock lock{mutex};
auto& raw_status = console.motion_values.raw_status;
auto& emulated = console.motion_values.emulated;
raw_status = TransformToMotion(callback);
emulated.SetAcceleration(Common::Vec3f{
raw_status.accel.x.value,
raw_status.accel.y.value,
raw_status.accel.z.value,
});
emulated.SetGyroscope(Common::Vec3f{
raw_status.gyro.x.value,
raw_status.gyro.y.value,
raw_status.gyro.z.value,
});
emulated.UpdateRotation(raw_status.delta_timestamp);
emulated.UpdateOrientation(raw_status.delta_timestamp);
if (is_configuring) {
lock.unlock();
TriggerOnChange(ConsoleTriggerType::Motion);
return;
}
auto& motion = console.motion_state;
motion.accel = emulated.GetAcceleration();
motion.gyro = emulated.GetGyroscope();
motion.rotation = emulated.GetRotations();
motion.orientation = emulated.GetOrientation();
motion.quaternion = emulated.GetQuaternion();
motion.gyro_bias = emulated.GetGyroBias();
motion.is_at_rest = !emulated.IsMoving(motion_sensitivity);
// Find what is this value
motion.verticalization_error = 0.0f;
lock.unlock();
TriggerOnChange(ConsoleTriggerType::Motion);
}
void EmulatedConsole::SetTouch(const Common::Input::CallbackStatus& callback, std::size_t index) {
if (index >= MaxTouchDevices) {
return;
}
std::unique_lock lock{mutex};
const auto touch_input = TransformToTouch(callback);
auto touch_index = GetIndexFromFingerId(index);
bool is_new_input = false;
if (!touch_index.has_value() && touch_input.pressed.value) {
touch_index = GetNextFreeIndex();
is_new_input = true;
}
// No free entries or invalid state. Ignore input
if (!touch_index.has_value()) {
return;
}
auto& touch_value = console.touch_values[touch_index.value()];
if (is_new_input) {
touch_value.pressed.value = true;
touch_value.id = static_cast<int>(index);
}
touch_value.x = touch_input.x;
touch_value.y = touch_input.y;
if (!touch_input.pressed.value) {
touch_value.pressed.value = false;
}
if (is_configuring) {
lock.unlock();
TriggerOnChange(ConsoleTriggerType::Touch);
return;
}
// Touch outside allowed range. Ignore input
if (touch_index.value() >= MaxActiveTouchInputs) {
return;
}
console.touch_state[touch_index.value()] = {
.position_x = touch_value.x.value,
.position_y = touch_value.y.value,
.id = static_cast<u32>(touch_index.value()),
.pressed = touch_input.pressed.value,
};
lock.unlock();
TriggerOnChange(ConsoleTriggerType::Touch);
}
ConsoleMotionValues EmulatedConsole::GetMotionValues() const {
std::scoped_lock lock{mutex};
return console.motion_values;
}
TouchValues EmulatedConsole::GetTouchValues() const {
std::scoped_lock lock{mutex};
return console.touch_values;
}
ConsoleMotion EmulatedConsole::GetMotion() const {
std::scoped_lock lock{mutex};
return console.motion_state;
}
TouchFingerState EmulatedConsole::GetTouch() const {
std::scoped_lock lock{mutex};
return console.touch_state;
}
std::optional<std::size_t> EmulatedConsole::GetIndexFromFingerId(std::size_t finger_id) const {
for (std::size_t index = 0; index < MaxTouchDevices; ++index) {
const auto& finger = console.touch_values[index];
if (!finger.pressed.value) {
continue;
}
if (finger.id == static_cast<int>(finger_id)) {
return index;
}
}
return std::nullopt;
}
std::optional<std::size_t> EmulatedConsole::GetNextFreeIndex() const {
for (std::size_t index = 0; index < MaxTouchDevices; ++index) {
if (!console.touch_values[index].pressed.value) {
return index;
}
}
return std::nullopt;
}
void EmulatedConsole::TriggerOnChange(ConsoleTriggerType type) {
std::scoped_lock lock{callback_mutex};
for (const auto& poller_pair : callback_list) {
const ConsoleUpdateCallback& poller = poller_pair.second;
if (poller.on_change) {
poller.on_change(type);
}
}
}
int EmulatedConsole::SetCallback(ConsoleUpdateCallback update_callback) {
std::scoped_lock lock{callback_mutex};
callback_list.insert_or_assign(last_callback_key, std::move(update_callback));
return last_callback_key++;
}
void EmulatedConsole::DeleteCallback(int key) {
std::scoped_lock lock{callback_mutex};
const auto& iterator = callback_list.find(key);
if (iterator == callback_list.end()) {
LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
return;
}
callback_list.erase(iterator);
}
} // namespace Core::HID

View File

@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <unordered_map>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/input.h"
#include "common/param_package.h"
#include "common/quaternion.h"
#include "common/vector_math.h"
#include "core/hid/hid_types.h"
#include "core/hid/motion_input.h"
namespace Core::HID {
static constexpr std::size_t MaxTouchDevices = 32;
static constexpr std::size_t MaxActiveTouchInputs = 16;
struct ConsoleMotionInfo {
Common::Input::MotionStatus raw_status{};
MotionInput emulated{};
};
using ConsoleMotionDevices = std::unique_ptr<Common::Input::InputDevice>;
using TouchDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, MaxTouchDevices>;
using ConsoleMotionParams = Common::ParamPackage;
using TouchParams = std::array<Common::ParamPackage, MaxTouchDevices>;
using ConsoleMotionValues = ConsoleMotionInfo;
using TouchValues = std::array<Common::Input::TouchStatus, MaxTouchDevices>;
struct TouchFinger {
u64 last_touch{};
float position_x{};
float position_y{};
u32 id{};
bool pressed{};
};
// Contains all motion related data that is used on the services
struct ConsoleMotion {
Common::Vec3f accel{};
Common::Vec3f gyro{};
Common::Vec3f rotation{};
std::array<Common::Vec3f, 3> orientation{};
Common::Quaternion<f32> quaternion{};
Common::Vec3f gyro_bias{};
f32 verticalization_error{};
bool is_at_rest{};
};
using TouchFingerState = std::array<TouchFinger, MaxActiveTouchInputs>;
struct ConsoleStatus {
// Data from input_common
ConsoleMotionValues motion_values{};
TouchValues touch_values{};
// Data for HID services
ConsoleMotion motion_state{};
TouchFingerState touch_state{};
};
enum class ConsoleTriggerType {
Motion,
Touch,
All,
};
struct ConsoleUpdateCallback {
std::function<void(ConsoleTriggerType)> on_change;
};
class EmulatedConsole {
public:
/**
* Contains all input data within the emulated switch console tablet such as touch and motion
*/
explicit EmulatedConsole();
~EmulatedConsole();
/// Removes all callbacks created from input devices
void UnloadInput();
/**
* Sets the emulated console into configuring mode
* This prevents the modification of the HID state of the emulated console by input commands
*/
void EnableConfiguration();
/// Returns the emulated console into normal mode, allowing the modification of the HID state
void DisableConfiguration();
/// Returns true if the emulated console is in configuring mode
bool IsConfiguring() const;
/// Reload all input devices
void ReloadInput();
/// Overrides current mapped devices with the stored configuration and reloads all input devices
void ReloadFromSettings();
/// Saves the current mapped configuration
void SaveCurrentConfig();
/// Reverts any mapped changes made that weren't saved
void RestoreConfig();
// Returns the current mapped motion device
Common::ParamPackage GetMotionParam() const;
/**
* Updates the current mapped motion device
* @param param ParamPackage with controller data to be mapped
*/
void SetMotionParam(Common::ParamPackage param);
/// Returns the latest status of motion input from the console with parameters
ConsoleMotionValues GetMotionValues() const;
/// Returns the latest status of touch input from the console with parameters
TouchValues GetTouchValues() const;
/// Returns the latest status of motion input from the console
ConsoleMotion GetMotion() const;
/// Returns the latest status of touch input from the console
TouchFingerState GetTouch() const;
/**
* Adds a callback to the list of events
* @param update_callback A ConsoleUpdateCallback that will be triggered
* @return an unique key corresponding to the callback index in the list
*/
int SetCallback(ConsoleUpdateCallback update_callback);
/**
* Removes a callback from the list stopping any future events to this object
* @param key Key corresponding to the callback index in the list
*/
void DeleteCallback(int key);
private:
/// Creates and stores the touch params
void SetTouchParams();
/**
* Updates the motion status of the console
* @param callback A CallbackStatus containing gyro and accelerometer data
*/
void SetMotion(const Common::Input::CallbackStatus& callback);
/**
* Updates the touch status of the console
* @param callback A CallbackStatus containing the touch position
* @param index Finger ID to be updated
*/
void SetTouch(const Common::Input::CallbackStatus& callback, std::size_t index);
std::optional<std::size_t> GetIndexFromFingerId(std::size_t finger_id) const;
std::optional<std::size_t> GetNextFreeIndex() const;
/**
* Triggers a callback that something has changed on the console status
* @param type Input type of the event to trigger
*/
void TriggerOnChange(ConsoleTriggerType type);
bool is_configuring{false};
f32 motion_sensitivity{0.01f};
ConsoleMotionParams motion_params;
TouchParams touch_params;
ConsoleMotionDevices motion_devices;
TouchDevices touch_devices;
mutable std::mutex mutex;
mutable std::mutex callback_mutex;
std::unordered_map<int, ConsoleUpdateCallback> callback_list;
int last_callback_key = 0;
// Stores the current status of all console input
ConsoleStatus console;
};
} // namespace Core::HID

View File

@ -0,0 +1,518 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include "core/hid/emulated_controller.h"
#include "core/hid/input_converter.h"
namespace Core::HID {
// Use a common UUID for Virtual Gamepad
constexpr Common::UUID VIRTUAL_UUID =
Common::UUID{{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xFF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}};
// xperia64: 0x9A seems to be the calibrated limit of the circle pad
// Verified by using Input Redirector with very large-value digital inputs
// on the circle pad and calibrating using the system settings application
constexpr int MAX_CIRCLEPAD_POS = 0x9A; // Max value for a circle pad position
EmulatedController::EmulatedController() {}
EmulatedController::~EmulatedController() = default;
void EmulatedController::LoadDevices() {
LoadVirtualGamepadParams();
std::ranges::transform(button_params, button_devices.begin(), Common::Input::CreateInputDevice);
std::ranges::transform(stick_params, stick_devices.begin(), Common::Input::CreateInputDevice);
std::ranges::transform(motion_params, motion_devices.begin(), Common::Input::CreateInputDevice);
// Initialize virtual gamepad devices
std::ranges::transform(virtual_button_params, virtual_button_devices.begin(),
Common::Input::CreateInputDevice);
std::ranges::transform(virtual_stick_params, virtual_stick_devices.begin(),
Common::Input::CreateInputDevice);
}
void EmulatedController::LoadVirtualGamepadParams() {
Common::ParamPackage common_params{};
common_params.Set("engine", "virtual_gamepad");
common_params.Set("port", 0);
for (auto& param : virtual_button_params) {
param = common_params;
}
for (auto& param : virtual_stick_params) {
param = common_params;
}
// TODO(german77): Replace this with an input profile or something better
virtual_button_params[Settings::NativeButton::A].Set("button", 0);
virtual_button_params[Settings::NativeButton::B].Set("button", 1);
virtual_button_params[Settings::NativeButton::X].Set("button", 2);
virtual_button_params[Settings::NativeButton::Y].Set("button", 3);
virtual_button_params[Settings::NativeButton::DUp].Set("button", 4);
virtual_button_params[Settings::NativeButton::DDown].Set("button", 5);
virtual_button_params[Settings::NativeButton::DLeft].Set("button", 6);
virtual_button_params[Settings::NativeButton::DRight].Set("button", 7);
virtual_button_params[Settings::NativeButton::L].Set("button", 8);
virtual_button_params[Settings::NativeButton::R].Set("button", 9);
virtual_button_params[Settings::NativeButton::Start].Set("button", 10);
virtual_button_params[Settings::NativeButton::Select].Set("button", 11);
virtual_button_params[Settings::NativeButton::Debug].Set("button", 12);
virtual_button_params[Settings::NativeButton::Gpio14].Set("button", 13);
virtual_button_params[Settings::NativeButton::ZL].Set("button", 14);
virtual_button_params[Settings::NativeButton::ZR].Set("button", 15);
virtual_button_params[Settings::NativeButton::Home].Set("button", 16);
virtual_stick_params[Settings::NativeAnalog::CirclePad].Set("axis_x", 0);
virtual_stick_params[Settings::NativeAnalog::CirclePad].Set("axis_y", 1);
virtual_stick_params[Settings::NativeAnalog::CStick].Set("axis_x", 2);
virtual_stick_params[Settings::NativeAnalog::CStick].Set("axis_y", 3);
}
void EmulatedController::UnloadInput() {
for (auto& button : button_devices) {
button.reset();
}
for (auto& stick : stick_devices) {
stick.reset();
}
for (auto& motion : motion_devices) {
motion.reset();
}
for (auto& button : virtual_button_devices) {
button.reset();
}
for (auto& stick : virtual_stick_devices) {
stick.reset();
}
}
void EmulatedController::EnableConfiguration() {
is_configuring = true;
}
void EmulatedController::DisableConfiguration() {
is_configuring = false;
}
void EmulatedController::ReloadInput() {
// If you load any device here add the equivalent to the UnloadInput() function
LoadDevices();
for (std::size_t index = 0; index < button_devices.size(); ++index) {
if (!button_devices[index]) {
continue;
}
const auto uuid = Common::UUID{button_params[index].Get("guid", "")};
button_devices[index]->SetCallback({
.on_change =
[this, index, uuid](const Common::Input::CallbackStatus& callback) {
SetButton(callback, index, uuid);
},
});
button_devices[index]->ForceUpdate();
}
for (std::size_t index = 0; index < stick_devices.size(); ++index) {
if (!stick_devices[index]) {
continue;
}
const auto uuid = Common::UUID{stick_params[index].Get("guid", "")};
stick_devices[index]->SetCallback({
.on_change =
[this, index, uuid](const Common::Input::CallbackStatus& callback) {
SetStick(callback, index, uuid);
},
});
stick_devices[index]->ForceUpdate();
}
for (std::size_t index = 0; index < motion_devices.size(); ++index) {
if (!motion_devices[index]) {
continue;
}
motion_devices[index]->SetCallback({
.on_change =
[/*this, index*/](const Common::Input::CallbackStatus& callback) {
// SetMotion(callback, index);
},
});
motion_devices[index]->ForceUpdate();
}
// Register virtual devices. No need to force update
for (std::size_t index = 0; index < virtual_button_devices.size(); ++index) {
if (!virtual_button_devices[index]) {
continue;
}
virtual_button_devices[index]->SetCallback({
.on_change =
[this, index](const Common::Input::CallbackStatus& callback) {
SetButton(callback, index, VIRTUAL_UUID);
},
});
}
for (std::size_t index = 0; index < virtual_stick_devices.size(); ++index) {
if (!virtual_stick_devices[index]) {
continue;
}
virtual_stick_devices[index]->SetCallback({
.on_change =
[this, index](const Common::Input::CallbackStatus& callback) {
SetStick(callback, index, VIRTUAL_UUID);
},
});
}
}
void EmulatedController::ReloadFromSettings() {
const auto& player = Settings::values.players.GetValue();
for (std::size_t i = 0; i < player.buttons.size(); i++) {
button_params[i] = Common::ParamPackage(player.buttons[i]);
}
for (std::size_t i = 0; i < player.analogs.size(); i++) {
stick_params[i] = Common::ParamPackage(player.analogs[i]);
}
for (std::size_t index = 0; index < player.motions.size(); ++index) {
motion_params[index] = Common::ParamPackage(player.motions[index]);
}
ReloadInput();
}
void EmulatedController::SaveCurrentConfig() {
auto& player = Settings::values.players.GetValue();
for (std::size_t index = 0; index < player.buttons.size(); ++index) {
player.buttons[index] = button_params[index].Serialize();
}
for (std::size_t index = 0; index < player.analogs.size(); ++index) {
player.analogs[index] = stick_params[index].Serialize();
}
for (std::size_t index = 0; index < player.motions.size(); ++index) {
player.motions[index] = motion_params[index].Serialize();
}
}
std::vector<Common::ParamPackage> EmulatedController::GetMappedDevices() const {
std::vector<Common::ParamPackage> devices;
for (const auto& param : button_params) {
if (!param.Has("engine")) {
continue;
}
const auto devices_it = std::find_if(
devices.begin(), devices.end(), [&param](const Common::ParamPackage& param_) {
return param.Get("engine", "") == param_.Get("engine", "") &&
param.Get("guid", "") == param_.Get("guid", "") &&
param.Get("port", 0) == param_.Get("port", 0) &&
param.Get("pad", 0) == param_.Get("pad", 0);
});
if (devices_it != devices.end()) {
continue;
}
auto& device = devices.emplace_back();
device.Set("engine", param.Get("engine", ""));
device.Set("guid", param.Get("guid", ""));
device.Set("port", param.Get("port", 0));
device.Set("pad", param.Get("pad", 0));
}
for (const auto& param : stick_params) {
if (!param.Has("engine")) {
continue;
}
if (param.Get("engine", "") == "analog_from_button") {
continue;
}
const auto devices_it = std::find_if(
devices.begin(), devices.end(), [&param](const Common::ParamPackage& param_) {
return param.Get("engine", "") == param_.Get("engine", "") &&
param.Get("guid", "") == param_.Get("guid", "") &&
param.Get("port", 0) == param_.Get("port", 0) &&
param.Get("pad", 0) == param_.Get("pad", 0);
});
if (devices_it != devices.end()) {
continue;
}
auto& device = devices.emplace_back();
device.Set("engine", param.Get("engine", ""));
device.Set("guid", param.Get("guid", ""));
device.Set("port", param.Get("port", 0));
device.Set("pad", param.Get("pad", 0));
}
return devices;
}
Common::ParamPackage EmulatedController::GetButtonParam(std::size_t index) const {
if (index >= button_params.size()) {
return {};
}
return button_params[index];
}
Common::ParamPackage EmulatedController::GetStickParam(std::size_t index) const {
if (index >= stick_params.size()) {
return {};
}
return stick_params[index];
}
Common::ParamPackage EmulatedController::GetMotionParam(std::size_t index) const {
if (index >= motion_params.size()) {
return {};
}
return motion_params[index];
}
void EmulatedController::SetButtonParam(std::size_t index, Common::ParamPackage param) {
if (index >= button_params.size()) {
return;
}
button_params[index] = std::move(param);
ReloadInput();
}
void EmulatedController::SetStickParam(std::size_t index, Common::ParamPackage param) {
if (index >= stick_params.size()) {
return;
}
stick_params[index] = std::move(param);
ReloadInput();
}
void EmulatedController::SetMotionParam(std::size_t index, Common::ParamPackage param) {
if (index >= motion_params.size()) {
return;
}
motion_params[index] = std::move(param);
ReloadInput();
}
ButtonValues EmulatedController::GetButtonsValues() const {
std::scoped_lock lock{mutex};
return controller.button_values;
}
SticksValues EmulatedController::GetSticksValues() const {
std::scoped_lock lock{mutex};
return controller.stick_values;
}
PadState EmulatedController::GetPadState() const {
std::scoped_lock lock{mutex};
if (is_configuring) {
return {};
}
return controller.pad_state;
}
ExtraState EmulatedController::GetExtraState() const {
std::scoped_lock lock{mutex};
if (is_configuring) {
return {};
}
return controller.extra_state;
}
AnalogSticks EmulatedController::GetSticks() const {
std::unique_lock lock{mutex};
if (is_configuring) {
return {};
}
return controller.analog_stick_state;
}
void EmulatedController::SetButton(const Common::Input::CallbackStatus& callback, std::size_t index,
Common::UUID uuid) {
if (index >= controller.button_values.size()) {
return;
}
std::unique_lock lock{mutex};
bool value_changed = false;
const auto new_status = TransformToButton(callback);
auto& current_status = controller.button_values[index];
// Only read button values that have the same uuid or are pressed once
if (current_status.uuid != uuid) {
if (!new_status.value) {
return;
}
}
current_status.toggle = new_status.toggle;
current_status.uuid = uuid;
// Update button status with current
if (!current_status.toggle) {
current_status.locked = false;
if (current_status.value != new_status.value) {
current_status.value = new_status.value;
value_changed = true;
}
} else {
// Toggle button and lock status
if (new_status.value && !current_status.locked) {
current_status.locked = true;
current_status.value = !current_status.value;
value_changed = true;
}
// Unlock button ready for next press
if (!new_status.value && current_status.locked) {
current_status.locked = false;
}
}
if (!value_changed) {
return;
}
if (is_configuring) {
controller.pad_state.hex = 0;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Button, false);
return;
}
switch (index) {
case Settings::NativeButton::A:
controller.pad_state.a.Assign(current_status.value);
break;
case Settings::NativeButton::B:
controller.pad_state.b.Assign(current_status.value);
break;
case Settings::NativeButton::X:
controller.pad_state.x.Assign(current_status.value);
break;
case Settings::NativeButton::Y:
controller.pad_state.y.Assign(current_status.value);
break;
case Settings::NativeButton::L:
controller.pad_state.l.Assign(current_status.value);
break;
case Settings::NativeButton::R:
controller.pad_state.r.Assign(current_status.value);
break;
case Settings::NativeButton::ZL:
controller.extra_state.zl = current_status.value;
break;
case Settings::NativeButton::ZR:
controller.extra_state.zr = current_status.value;
break;
case Settings::NativeButton::Start:
controller.pad_state.start.Assign(current_status.value);
break;
case Settings::NativeButton::Select:
controller.pad_state.select.Assign(current_status.value);
break;
case Settings::NativeButton::DLeft:
controller.pad_state.left.Assign(current_status.value);
break;
case Settings::NativeButton::DUp:
controller.pad_state.up.Assign(current_status.value);
break;
case Settings::NativeButton::DRight:
controller.pad_state.right.Assign(current_status.value);
break;
case Settings::NativeButton::DDown:
controller.pad_state.down.Assign(current_status.value);
break;
case Settings::NativeButton::Debug:
controller.pad_state.debug.Assign(current_status.value);
break;
case Settings::NativeButton::Gpio14:
controller.pad_state.gpio14.Assign(current_status.value);
break;
}
lock.unlock();
TriggerOnChange(ControllerTriggerType::Button, true);
}
void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback, std::size_t index,
Common::UUID uuid) {
if (index >= controller.stick_values.size()) {
return;
}
std::unique_lock lock{mutex};
const auto stick_value = TransformToStick(callback);
// Only read stick values that have the same uuid or are over the threshold to avoid flapping
if (controller.stick_values[index].uuid != uuid) {
if (!stick_value.down && !stick_value.up && !stick_value.left && !stick_value.right) {
return;
}
}
controller.stick_values[index] = stick_value;
controller.stick_values[index].uuid = uuid;
if (is_configuring) {
controller.analog_stick_state.circle_pad = {};
controller.analog_stick_state.c_stick = {};
lock.unlock();
TriggerOnChange(ControllerTriggerType::Stick, false);
return;
}
const AnalogStickState stick{
.x = static_cast<s16>(
std::roundf(controller.stick_values[index].x.value * MAX_CIRCLEPAD_POS)),
.y = static_cast<s16>(
std::roundf(controller.stick_values[index].y.value * MAX_CIRCLEPAD_POS)),
};
switch (index) {
case Settings::NativeAnalog::CirclePad:
controller.analog_stick_state.circle_pad = stick;
controller.pad_state.circle_left.Assign(controller.stick_values[index].left);
controller.pad_state.circle_up.Assign(controller.stick_values[index].up);
controller.pad_state.circle_right.Assign(controller.stick_values[index].right);
controller.pad_state.circle_down.Assign(controller.stick_values[index].down);
break;
case Settings::NativeAnalog::CStick:
controller.analog_stick_state.c_stick = stick;
controller.extra_state.c_stick_left = controller.stick_values[index].left;
controller.extra_state.c_stick_up = controller.stick_values[index].up;
controller.extra_state.c_stick_right = controller.stick_values[index].right;
controller.extra_state.c_stick_down = controller.stick_values[index].down;
break;
}
lock.unlock();
TriggerOnChange(ControllerTriggerType::Stick, true);
}
void EmulatedController::TriggerOnChange(ControllerTriggerType type, bool is_hid_service_update) {
std::scoped_lock lock{callback_mutex};
for (const auto& poller_pair : callback_list) {
const ControllerUpdateCallback& poller = poller_pair.second;
if (!is_hid_service_update && poller.is_hid_service) {
continue;
}
if (poller.on_change) {
poller.on_change(type);
}
}
}
int EmulatedController::SetCallback(ControllerUpdateCallback update_callback) {
std::scoped_lock lock{callback_mutex};
callback_list.insert_or_assign(last_callback_key, std::move(update_callback));
return last_callback_key++;
}
void EmulatedController::DeleteCallback(int key) {
std::scoped_lock lock{callback_mutex};
const auto& iterator = callback_list.find(key);
if (iterator == callback_list.end()) {
LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
return;
}
callback_list.erase(iterator);
}
} // namespace Core::HID

View File

@ -0,0 +1,226 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <vector>
#include "common/bit_field.h"
#include "common/common_types.h"
#include "common/input.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "common/vector_math.h"
#include "core/hid/hid_types.h"
namespace Core::HID {
using ButtonDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeButton::NumButtons>;
using StickDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeAnalog::NumAnalogs>;
using ControllerMotionDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeMotion::NumMotions>;
using ButtonParams = std::array<Common::ParamPackage, Settings::NativeButton::NumButtons>;
using StickParams = std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs>;
using ControllerMotionParams = std::array<Common::ParamPackage, Settings::NativeMotion::NumMotions>;
using ButtonValues = std::array<Common::Input::ButtonStatus, Settings::NativeButton::NumButtons>;
using SticksValues = std::array<Common::Input::StickStatus, Settings::NativeAnalog::NumAnalogs>;
struct AnalogStickState {
s16 x{};
s16 y{};
};
struct AnalogSticks {
AnalogStickState circle_pad{};
AnalogStickState c_stick{};
};
struct ExtraState {
bool zr{};
bool zl{};
bool c_stick_right{};
bool c_stick_left{};
bool c_stick_up{};
bool c_stick_down{};
};
struct ControllerStatus {
// Data from input_common
ButtonValues button_values{};
SticksValues stick_values{};
// Data for HID services
PadState pad_state{};
AnalogSticks analog_stick_state{};
ExtraState extra_state{};
};
enum class ControllerTriggerType {
Button,
Stick,
Motion,
Connected,
Disconnected,
Type,
All,
};
struct ControllerUpdateCallback {
std::function<void(ControllerTriggerType)> on_change;
bool is_hid_service;
};
class EmulatedController {
public:
/**
* Contains all input data (buttons, joysticks, and motion) within this controller.
*/
explicit EmulatedController();
~EmulatedController();
/// Removes all callbacks created from input devices
void UnloadInput();
/**
* Sets the emulated controller into configuring mode
* This prevents the modification of the HID state of the emulated controller by input commands
*/
void EnableConfiguration();
/// Returns the emulated controller into normal mode, allowing the modification of the HID state
void DisableConfiguration();
/// Reload all input devices
void ReloadInput();
/// Overrides current mapped devices with the stored configuration and reloads all input devices
void ReloadFromSettings();
/// Saves the current mapped configuration
void SaveCurrentConfig();
/// Returns a vector of mapped devices from the mapped button and stick parameters
std::vector<Common::ParamPackage> GetMappedDevices() const;
// Returns the current mapped button device
Common::ParamPackage GetButtonParam(std::size_t index) const;
// Returns the current mapped stick device
Common::ParamPackage GetStickParam(std::size_t index) const;
// Returns the current mapped motion device
Common::ParamPackage GetMotionParam(std::size_t index) const;
/**
* Updates the current mapped button device
* @param param ParamPackage with controller data to be mapped
*/
void SetButtonParam(std::size_t index, Common::ParamPackage param);
/**
* Updates the current mapped stick device
* @param param ParamPackage with controller data to be mapped
*/
void SetStickParam(std::size_t index, Common::ParamPackage param);
/**
* Updates the current mapped motion device
* @param param ParamPackage with controller data to be mapped
*/
void SetMotionParam(std::size_t index, Common::ParamPackage param);
/// Returns the latest button status from the controller with parameters
ButtonValues GetButtonsValues() const;
/// Returns the latest analog stick status from the controller with parameters
SticksValues GetSticksValues() const;
/// Returns the latest status of button input for the hid service
PadState GetPadState() const;
/// Returns the latest status of extra button input for the hid service
ExtraState GetExtraState() const;
/// Returns the latest status of stick input from the mouse
AnalogSticks GetSticks() const;
/**
* Adds a callback to the list of events
* @param update_callback A ConsoleUpdateCallback that will be triggered
* @return an unique key corresponding to the callback index in the list
*/
int SetCallback(ControllerUpdateCallback update_callback);
/**
* Removes a callback from the list stopping any future events to this object
* @param key Key corresponding to the callback index in the list
*/
void DeleteCallback(int key);
private:
/// creates input devices from params
void LoadDevices();
/// Set the params for virtual pad devices
void LoadVirtualGamepadParams();
/**
* Updates the button status of the controller
* @param callback A CallbackStatus containing the button status
* @param index Button ID of the to be updated
*/
void SetButton(const Common::Input::CallbackStatus& callback, std::size_t index,
Common::UUID uuid);
/**
* Updates the analog stick status of the controller
* @param callback A CallbackStatus containing the analog stick status
* @param index stick ID of the to be updated
*/
void SetStick(const Common::Input::CallbackStatus& callback, std::size_t index,
Common::UUID uuid);
/**
* Triggers a callback that something has changed on the controller status
* @param type Input type of the event to trigger
* @param is_service_update indicates if this event should only be sent to HID services
*/
void TriggerOnChange(ControllerTriggerType type, bool is_hid_service_update);
bool is_connected{false};
bool is_configuring{false};
ButtonParams button_params;
StickParams stick_params;
ControllerMotionParams motion_params;
ButtonDevices button_devices;
StickDevices stick_devices;
ControllerMotionDevices motion_devices;
// Virtual gamepad related variables
ButtonParams virtual_button_params;
StickParams virtual_stick_params;
ButtonDevices virtual_button_devices;
StickDevices virtual_stick_devices;
mutable std::mutex mutex;
mutable std::mutex callback_mutex;
std::unordered_map<int, ControllerUpdateCallback> callback_list;
int last_callback_key = 0;
// Stores the current status of all controller input
ControllerStatus controller;
};
} // namespace Core::HID

51
src/core/hid/hid_core.cpp Normal file
View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/assert.h"
#include "core/hid/emulated_console.h"
#include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h"
namespace Core::HID {
HIDCore::HIDCore()
: controller{std::make_unique<EmulatedController>()},
console{std::make_unique<EmulatedConsole>()} {}
HIDCore::~HIDCore() = default;
EmulatedController* HIDCore::GetEmulatedController() {
return controller.get();
}
const EmulatedController* HIDCore::GetEmulatedController() const {
return controller.get();
}
EmulatedConsole* HIDCore::GetEmulatedConsole() {
return console.get();
}
const EmulatedConsole* HIDCore::GetEmulatedConsole() const {
return console.get();
}
void HIDCore::EnableAllControllerConfiguration() {
controller->EnableConfiguration();
}
void HIDCore::DisableAllControllerConfiguration() {
controller->DisableConfiguration();
}
void HIDCore::ReloadInputDevices() {
controller->ReloadFromSettings();
console->ReloadFromSettings();
}
void HIDCore::UnloadInputDevices() {
controller->UnloadInput();
console->UnloadInput();
}
} // namespace Core::HID

45
src/core/hid/hid_core.h Normal file
View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include "common/common_funcs.h"
namespace Core::HID {
class EmulatedConsole;
class EmulatedController;
} // namespace Core::HID
namespace Core::HID {
class HIDCore {
public:
explicit HIDCore();
~HIDCore();
EmulatedController* GetEmulatedController();
const EmulatedController* GetEmulatedController() const;
EmulatedConsole* GetEmulatedConsole();
const EmulatedConsole* GetEmulatedConsole() const;
/// Sets all emulated controllers into configuring mode.
void EnableAllControllerConfiguration();
/// Sets all emulated controllers into normal mode.
void DisableAllControllerConfiguration();
/// Reloads all input devices from settings
void ReloadInputDevices();
/// Removes all callbacks from input common
void UnloadInputDevices();
private:
std::unique_ptr<EmulatedController> controller;
std::unique_ptr<EmulatedConsole> console;
};
} // namespace Core::HID

40
src/core/hid/hid_types.h Normal file
View File

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/bit_field.h"
#include "common/common_types.h"
namespace Core::HID {
/**
* Structure of a Pad controller state.
*/
struct PadState {
union {
u32 hex{};
BitField<0, 1, u32> a;
BitField<1, 1, u32> b;
BitField<2, 1, u32> select;
BitField<3, 1, u32> start;
BitField<4, 1, u32> right;
BitField<5, 1, u32> left;
BitField<6, 1, u32> up;
BitField<7, 1, u32> down;
BitField<8, 1, u32> r;
BitField<9, 1, u32> l;
BitField<10, 1, u32> x;
BitField<11, 1, u32> y;
BitField<12, 1, u32> debug;
BitField<13, 1, u32> gpio14;
BitField<28, 1, u32> circle_right;
BitField<29, 1, u32> circle_left;
BitField<30, 1, u32> circle_up;
BitField<31, 1, u32> circle_down;
};
};
} // namespace Core::HID

View File

@ -0,0 +1,340 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <random>
#include "common/input.h"
#include "core/hid/input_converter.h"
namespace Core::HID {
Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatus& callback) {
Common::Input::ButtonStatus status{};
switch (callback.type) {
case Common::Input::InputType::Analog:
status.value = TransformToTrigger(callback).pressed.value;
status.toggle = callback.analog_status.properties.toggle;
break;
case Common::Input::InputType::Button:
status = callback.button_status;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to button not implemented", callback.type);
break;
}
if (status.inverted) {
status.value = !status.value;
}
return status;
}
Common::Input::MotionStatus TransformToMotion(const Common::Input::CallbackStatus& callback) {
Common::Input::MotionStatus status{};
switch (callback.type) {
case Common::Input::InputType::Button: {
Common::Input::AnalogProperties properties{
.deadzone = 0.0f,
.range = 1.0f,
.offset = 0.0f,
};
status.delta_timestamp = 5000;
status.force_update = true;
status.accel.x = {
.value = 0.0f,
.raw_value = 0.0f,
.properties = properties,
};
status.accel.y = {
.value = 0.0f,
.raw_value = 0.0f,
.properties = properties,
};
status.accel.z = {
.value = 0.0f,
.raw_value = -1.0f,
.properties = properties,
};
status.gyro.x = {
.value = 0.0f,
.raw_value = 0.0f,
.properties = properties,
};
status.gyro.y = {
.value = 0.0f,
.raw_value = 0.0f,
.properties = properties,
};
status.gyro.z = {
.value = 0.0f,
.raw_value = 0.0f,
.properties = properties,
};
if (TransformToButton(callback).value) {
std::random_device device;
std::mt19937 gen(device());
std::uniform_int_distribution<s16> distribution(-5000, 5000);
status.accel.x.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
status.accel.y.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
status.accel.z.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
status.gyro.x.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
status.gyro.y.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
status.gyro.z.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
}
break;
}
case Common::Input::InputType::Motion:
status = callback.motion_status;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to motion not implemented", callback.type);
break;
}
SanitizeAnalog(status.accel.x, false);
SanitizeAnalog(status.accel.y, false);
SanitizeAnalog(status.accel.z, false);
SanitizeAnalog(status.gyro.x, false);
SanitizeAnalog(status.gyro.y, false);
SanitizeAnalog(status.gyro.z, false);
return status;
}
Common::Input::StickStatus TransformToStick(const Common::Input::CallbackStatus& callback) {
Common::Input::StickStatus status{};
switch (callback.type) {
case Common::Input::InputType::Stick:
status = callback.stick_status;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to stick not implemented", callback.type);
break;
}
SanitizeStick(status.x, status.y, true);
const auto& properties_x = status.x.properties;
const auto& properties_y = status.y.properties;
const float x = status.x.value;
const float y = status.y.value;
// Set directional buttons
status.right = x > properties_x.threshold;
status.left = x < -properties_x.threshold;
status.up = y > properties_y.threshold;
status.down = y < -properties_y.threshold;
return status;
}
Common::Input::TouchStatus TransformToTouch(const Common::Input::CallbackStatus& callback) {
Common::Input::TouchStatus status{};
switch (callback.type) {
case Common::Input::InputType::Touch:
status = callback.touch_status;
break;
case Common::Input::InputType::Stick:
status.x = callback.stick_status.x;
status.y = callback.stick_status.y;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to touch not implemented", callback.type);
break;
}
SanitizeAnalog(status.x, true);
SanitizeAnalog(status.y, true);
float& x = status.x.value;
float& y = status.y.value;
// Adjust if value is inverted
x = status.x.properties.inverted ? 1.0f + x : x;
y = status.y.properties.inverted ? 1.0f + y : y;
// clamp value
x = std::clamp(x, 0.0f, 1.0f);
y = std::clamp(y, 0.0f, 1.0f);
if (status.pressed.inverted) {
status.pressed.value = !status.pressed.value;
}
return status;
}
Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackStatus& callback) {
Common::Input::TriggerStatus status{};
float& raw_value = status.analog.raw_value;
bool calculate_button_value = true;
switch (callback.type) {
case Common::Input::InputType::Analog:
status.analog.properties = callback.analog_status.properties;
raw_value = callback.analog_status.raw_value;
break;
case Common::Input::InputType::Button:
status.analog.properties.range = 1.0f;
status.analog.properties.inverted = callback.button_status.inverted;
raw_value = callback.button_status.value ? 1.0f : 0.0f;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to trigger not implemented", callback.type);
break;
}
SanitizeAnalog(status.analog, true);
const auto& properties = status.analog.properties;
float& value = status.analog.value;
// Set button status
if (calculate_button_value) {
status.pressed.value = value > properties.threshold;
}
// Adjust if value is inverted
value = properties.inverted ? 1.0f + value : value;
// clamp value
value = std::clamp(value, 0.0f, 1.0f);
return status;
}
Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback) {
Common::Input::AnalogStatus status{};
switch (callback.type) {
case Common::Input::InputType::Analog:
status.properties = callback.analog_status.properties;
status.raw_value = callback.analog_status.raw_value;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to analog not implemented", callback.type);
break;
}
SanitizeAnalog(status, false);
// Adjust if value is inverted
status.value = status.properties.inverted ? -status.value : status.value;
return status;
}
void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) {
const auto& properties = analog.properties;
float& raw_value = analog.raw_value;
float& value = analog.value;
if (!std::isnormal(raw_value)) {
raw_value = 0;
}
// Apply center offset
raw_value -= properties.offset;
// Set initial values to be formated
value = raw_value;
// Calculate vector size
const float r = std::abs(value);
// Return zero if value is smaller than the deadzone
if (r <= properties.deadzone || properties.deadzone == 1.0f) {
analog.value = 0;
return;
}
// Adjust range of value
const float deadzone_factor =
1.0f / r * (r - properties.deadzone) / (1.0f - properties.deadzone);
value = value * deadzone_factor / properties.range;
// Invert direction if needed
if (properties.inverted) {
value = -value;
}
// Clamp value
if (clamp_value) {
value = std::clamp(value, -1.0f, 1.0f);
}
}
void SanitizeStick(Common::Input::AnalogStatus& analog_x, Common::Input::AnalogStatus& analog_y,
bool clamp_value) {
const auto& properties_x = analog_x.properties;
const auto& properties_y = analog_y.properties;
float& raw_x = analog_x.raw_value;
float& raw_y = analog_y.raw_value;
float& x = analog_x.value;
float& y = analog_y.value;
if (!std::isnormal(raw_x)) {
raw_x = 0;
}
if (!std::isnormal(raw_y)) {
raw_y = 0;
}
// Apply center offset
raw_x += properties_x.offset;
raw_y += properties_y.offset;
// Apply X scale correction from offset
if (std::abs(properties_x.offset) < 0.75f) {
if (raw_x > 0) {
raw_x /= 1 + properties_x.offset;
} else {
raw_x /= 1 - properties_x.offset;
}
}
// Apply Y scale correction from offset
if (std::abs(properties_y.offset) < 0.75f) {
if (raw_y > 0) {
raw_y /= 1 + properties_y.offset;
} else {
raw_y /= 1 - properties_y.offset;
}
}
// Invert direction if needed
raw_x = properties_x.inverted ? -raw_x : raw_x;
raw_y = properties_y.inverted ? -raw_y : raw_y;
// Set initial values to be formated
x = raw_x;
y = raw_y;
// Calculate vector size
float r = x * x + y * y;
r = std::sqrt(r);
// TODO(German77): Use deadzone and range of both axis
// Return zero if values are smaller than the deadzone
if (r <= properties_x.deadzone || properties_x.deadzone >= 1.0f) {
x = 0;
y = 0;
return;
}
// Adjust range of joystick
const float deadzone_factor =
1.0f / r * (r - properties_x.deadzone) / (1.0f - properties_x.deadzone);
x = x * deadzone_factor / properties_x.range;
y = y * deadzone_factor / properties_x.range;
r = r * deadzone_factor / properties_x.range;
// Normalize joystick
if (clamp_value && r > 1.0f) {
x /= r;
y /= r;
}
}
} // namespace Core::HID

View File

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Common::Input {
struct CallbackStatus;
struct AnalogStatus;
struct ButtonStatus;
struct MotionStatus;
struct StickStatus;
struct TouchStatus;
struct TriggerStatus;
}; // namespace Common::Input
namespace Core::HID {
/**
* Converts raw input data into a valid button status. Applies invert properties to the output.
*
* @param callback Supported callbacks: Analog, Button, Trigger.
* @return A valid TouchStatus object.
*/
Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatus& callback);
/**
* Converts raw input data into a valid motion status.
*
* @param callback Supported callbacks: Motion.
* @return A valid TouchStatus object.
*/
Common::Input::MotionStatus TransformToMotion(const Common::Input::CallbackStatus& callback);
/**
* Converts raw input data into a valid stick status. Applies offset, deadzone, range and invert
* properties to the output.
*
* @param callback Supported callbacks: Stick.
* @return A valid StickStatus object.
*/
Common::Input::StickStatus TransformToStick(const Common::Input::CallbackStatus& callback);
/**
* Converts raw input data into a valid touch status.
*
* @param callback Supported callbacks: Touch.
* @return A valid TouchStatus object.
*/
Common::Input::TouchStatus TransformToTouch(const Common::Input::CallbackStatus& callback);
/**
* Converts raw input data into a valid trigger status. Applies offset, deadzone, range and
* invert properties to the output. Button status uses the threshold property if necessary.
*
* @param callback Supported callbacks: Analog, Button, Trigger.
* @return A valid TriggerStatus object.
*/
Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackStatus& callback);
/**
* Converts raw input data into a valid analog status. Applies offset, deadzone, range and
* invert properties to the output.
*
* @param callback Supported callbacks: Analog.
* @return A valid AnalogStatus object.
*/
Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback);
/**
* Converts raw analog data into a valid analog value
* @param analog An analog object containing raw data and properties
* @param clamp_value determines if the value needs to be clamped between -1.0f and 1.0f.
*/
void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value);
/**
* Converts raw stick data into a valid stick value
* @param analog_x raw analog data and properties for the x-axis
* @param analog_y raw analog data and properties for the y-axis
* @param clamp_value bool that determines if the value needs to be clamped into the unit circle.
*/
void SanitizeStick(Common::Input::AnalogStatus& analog_x, Common::Input::AnalogStatus& analog_y,
bool clamp_value);
} // namespace Core::HID

View File

@ -0,0 +1,284 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/math_util.h"
#include "core/hid/motion_input.h"
namespace Core::HID {
MotionInput::MotionInput() {
// Initialize PID constants with default values
SetPID(0.3f, 0.005f, 0.0f);
SetGyroThreshold(0.007f);
}
void MotionInput::SetPID(f32 new_kp, f32 new_ki, f32 new_kd) {
kp = new_kp;
ki = new_ki;
kd = new_kd;
}
void MotionInput::SetAcceleration(const Common::Vec3f& acceleration) {
accel = acceleration;
}
void MotionInput::SetGyroscope(const Common::Vec3f& gyroscope) {
gyro = gyroscope - gyro_bias;
// Auto adjust drift to minimize drift
if (!IsMoving(0.1f)) {
gyro_bias = (gyro_bias * 0.9999f) + (gyroscope * 0.0001f);
}
if (gyro.Length() < gyro_threshold) {
gyro = {};
} else {
only_accelerometer = false;
}
}
void MotionInput::SetQuaternion(const Common::Quaternion<f32>& quaternion) {
quat = quaternion;
}
void MotionInput::SetGyroBias(const Common::Vec3f& bias) {
gyro_bias = bias;
}
void MotionInput::SetGyroThreshold(f32 threshold) {
gyro_threshold = threshold;
}
void MotionInput::EnableReset(bool reset) {
reset_enabled = reset;
}
void MotionInput::ResetRotations() {
rotations = {};
}
bool MotionInput::IsMoving(f32 sensitivity) const {
return gyro.Length() >= sensitivity || accel.Length() <= 0.9f || accel.Length() >= 1.1f;
}
bool MotionInput::IsCalibrated(f32 sensitivity) const {
return real_error.Length() < sensitivity;
}
void MotionInput::UpdateRotation(u64 elapsed_time) {
const auto sample_period = static_cast<f32>(elapsed_time) / 1000000.0f;
if (sample_period > 0.1f) {
return;
}
rotations += gyro * sample_period;
}
// Based on Madgwick's implementation of Mayhony's AHRS algorithm.
// https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs
void MotionInput::UpdateOrientation(u64 elapsed_time) {
if (!IsCalibrated(0.1f)) {
ResetOrientation();
}
// Short name local variable for readability
f32 q1 = quat.w;
f32 q2 = quat.xyz[0];
f32 q3 = quat.xyz[1];
f32 q4 = quat.xyz[2];
const auto sample_period = static_cast<f32>(elapsed_time) / 1000000.0f;
// Ignore invalid elapsed time
if (sample_period > 0.1f) {
return;
}
const auto normal_accel = accel.Normalized();
auto rad_gyro = gyro * Common::PI * 2;
const f32 swap = rad_gyro.x;
rad_gyro.x = rad_gyro.y;
rad_gyro.y = -swap;
rad_gyro.z = -rad_gyro.z;
// Clear gyro values if there is no gyro present
if (only_accelerometer) {
rad_gyro.x = 0;
rad_gyro.y = 0;
rad_gyro.z = 0;
}
// Ignore drift correction if acceleration is not reliable
if (accel.Length() >= 0.75f && accel.Length() <= 1.25f) {
const f32 ax = -normal_accel.x;
const f32 ay = normal_accel.y;
const f32 az = -normal_accel.z;
// Estimated direction of gravity
const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
const f32 vy = 2.0f * (q1 * q2 + q3 * q4);
const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
// Error is cross product between estimated direction and measured direction of gravity
const Common::Vec3f new_real_error = {
az * vx - ax * vz,
ay * vz - az * vy,
ax * vy - ay * vx,
};
derivative_error = new_real_error - real_error;
real_error = new_real_error;
// Prevent integral windup
if (ki != 0.0f && !IsCalibrated(0.05f)) {
integral_error += real_error;
} else {
integral_error = {};
}
// Apply feedback terms
if (!only_accelerometer) {
rad_gyro += kp * real_error;
rad_gyro += ki * integral_error;
rad_gyro += kd * derivative_error;
} else {
// Give more weight to accelerometer values to compensate for the lack of gyro
rad_gyro += 35.0f * kp * real_error;
rad_gyro += 10.0f * ki * integral_error;
rad_gyro += 10.0f * kd * derivative_error;
// Emulate gyro values for games that need them
gyro.x = -rad_gyro.y;
gyro.y = rad_gyro.x;
gyro.z = -rad_gyro.z;
UpdateRotation(elapsed_time);
}
}
const f32 gx = rad_gyro.y;
const f32 gy = rad_gyro.x;
const f32 gz = rad_gyro.z;
// Integrate rate of change of quaternion
const f32 pa = q2;
const f32 pb = q3;
const f32 pc = q4;
q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period);
q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period);
q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
quat.w = q1;
quat.xyz[0] = q2;
quat.xyz[1] = q3;
quat.xyz[2] = q4;
quat = quat.Normalized();
}
std::array<Common::Vec3f, 3> MotionInput::GetOrientation() const {
const Common::Quaternion<float> quad{
.xyz = {-quat.xyz[1], -quat.xyz[0], -quat.w},
.w = -quat.xyz[2],
};
const std::array<float, 16> matrix4x4 = quad.ToMatrix();
return {Common::Vec3f(matrix4x4[0], matrix4x4[1], -matrix4x4[2]),
Common::Vec3f(matrix4x4[4], matrix4x4[5], -matrix4x4[6]),
Common::Vec3f(-matrix4x4[8], -matrix4x4[9], matrix4x4[10])};
}
Common::Vec3f MotionInput::GetAcceleration() const {
return accel;
}
Common::Vec3f MotionInput::GetGyroscope() const {
return gyro;
}
Common::Vec3f MotionInput::GetGyroBias() const {
return gyro_bias;
}
Common::Quaternion<f32> MotionInput::GetQuaternion() const {
return quat;
}
Common::Vec3f MotionInput::GetRotations() const {
return rotations;
}
void MotionInput::ResetOrientation() {
if (!reset_enabled || only_accelerometer) {
return;
}
if (!IsMoving(0.5f) && accel.z <= -0.9f) {
++reset_counter;
if (reset_counter > 900) {
quat.w = 0;
quat.xyz[0] = 0;
quat.xyz[1] = 0;
quat.xyz[2] = -1;
SetOrientationFromAccelerometer();
integral_error = {};
reset_counter = 0;
}
} else {
reset_counter = 0;
}
}
void MotionInput::SetOrientationFromAccelerometer() {
int iterations = 0;
const f32 sample_period = 0.015f;
const auto normal_accel = accel.Normalized();
while (!IsCalibrated(0.01f) && ++iterations < 100) {
// Short name local variable for readability
f32 q1 = quat.w;
f32 q2 = quat.xyz[0];
f32 q3 = quat.xyz[1];
f32 q4 = quat.xyz[2];
Common::Vec3f rad_gyro;
const f32 ax = -normal_accel.x;
const f32 ay = normal_accel.y;
const f32 az = -normal_accel.z;
// Estimated direction of gravity
const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
const f32 vy = 2.0f * (q1 * q2 + q3 * q4);
const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
// Error is cross product between estimated direction and measured direction of gravity
const Common::Vec3f new_real_error = {
az * vx - ax * vz,
ay * vz - az * vy,
ax * vy - ay * vx,
};
derivative_error = new_real_error - real_error;
real_error = new_real_error;
rad_gyro += 10.0f * kp * real_error;
rad_gyro += 5.0f * ki * integral_error;
rad_gyro += 10.0f * kd * derivative_error;
const f32 gx = rad_gyro.y;
const f32 gy = rad_gyro.x;
const f32 gz = rad_gyro.z;
// Integrate rate of change of quaternion
const f32 pa = q2;
const f32 pb = q3;
const f32 pc = q4;
q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period);
q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period);
q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
quat.w = q1;
quat.xyz[0] = q2;
quat.xyz[1] = q3;
quat.xyz[2] = q4;
quat = quat.Normalized();
}
}
} // namespace Core::HID

View File

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
#include "common/quaternion.h"
#include "common/vector_math.h"
namespace Core::HID {
class MotionInput {
public:
explicit MotionInput();
MotionInput(const MotionInput&) = default;
MotionInput& operator=(const MotionInput&) = default;
MotionInput(MotionInput&&) = default;
MotionInput& operator=(MotionInput&&) = default;
void SetPID(f32 new_kp, f32 new_ki, f32 new_kd);
void SetAcceleration(const Common::Vec3f& acceleration);
void SetGyroscope(const Common::Vec3f& gyroscope);
void SetQuaternion(const Common::Quaternion<f32>& quaternion);
void SetGyroBias(const Common::Vec3f& bias);
void SetGyroThreshold(f32 threshold);
void EnableReset(bool reset);
void ResetRotations();
void UpdateRotation(u64 elapsed_time);
void UpdateOrientation(u64 elapsed_time);
[[nodiscard]] std::array<Common::Vec3f, 3> GetOrientation() const;
[[nodiscard]] Common::Vec3f GetAcceleration() const;
[[nodiscard]] Common::Vec3f GetGyroscope() const;
[[nodiscard]] Common::Vec3f GetGyroBias() const;
[[nodiscard]] Common::Vec3f GetRotations() const;
[[nodiscard]] Common::Quaternion<f32> GetQuaternion() const;
[[nodiscard]] bool IsMoving(f32 sensitivity) const;
[[nodiscard]] bool IsCalibrated(f32 sensitivity) const;
private:
void ResetOrientation();
void SetOrientationFromAccelerometer();
// PID constants
f32 kp;
f32 ki;
f32 kd;
// PID errors
Common::Vec3f real_error;
Common::Vec3f integral_error;
Common::Vec3f derivative_error;
// Quaternion containing the device orientation
Common::Quaternion<f32> quat{{0.0f, 0.0f, -1.0f}, 0.0f};
// Number of full rotations in each axis
Common::Vec3f rotations;
// Acceleration vector measurement in G force
Common::Vec3f accel;
// Gyroscope vector measurement in radians/s.
Common::Vec3f gyro;
// Vector to be substracted from gyro measurements
Common::Vec3f gyro_bias;
// Minimum gyro amplitude to detect if the device is moving
f32 gyro_threshold = 0.0f;
// Number of invalid sequential data
u32 reset_counter = 0;
// If the provided data is invalid the device will be autocalibrated
bool reset_enabled = true;
// Use accelerometer values to calculate position
bool only_accelerometer = true;
};
} // namespace Core::HID

View File

@ -4,7 +4,8 @@
#include "common/settings.h"
#include "core/core.h"
#include "core/frontend/input.h"
#include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h"
#include "core/hle/applets/applet.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/apt/applet_manager.h"
@ -1230,33 +1231,25 @@ void AppletManager::CaptureFrameBuffers() {
}
}
void AppletManager::LoadInputDevices() {
home_button = Input::CreateDevice<Input::ButtonDevice>(
Settings::values.current_input_profile.buttons[Settings::NativeButton::Home]);
}
void AppletManager::HomeButtonUpdateEvent(std::uintptr_t user_data, s64 cycles_late) {
if (is_device_reload_pending.exchange(false)) {
LoadInputDevices();
}
const bool state = home_button->GetStatus();
//const auto& controller = hid_core.GetEmulatedController();
//const bool state = home_button->GetStatus();
// NOTE: We technically do support loading and jumping to home menu even if it isn't
// initially registered. However since the home menu suspend is not bug-free, we don't
// want normal users who didn't launch the home menu accidentally pressing the home
// button binding and freezing their game, so for now, gate it to only environments
// where the home menu was already loaded by the user (last condition).
if (state && !last_home_button_state && GetAppletSlot(AppletSlot::HomeMenu)->registered) {
if (/*state &&*/ !last_home_button_state && GetAppletSlot(AppletSlot::HomeMenu)->registered) {
SendNotification(Notification::HomeButtonSingle);
}
last_home_button_state = state;
//last_home_button_state = state;
// Reschedule recurrent event
Core::System::GetInstance().CoreTiming().ScheduleEvent(
system.CoreTiming().ScheduleEvent(
usToCycles(home_button_update_interval_us) - cycles_late, home_button_update_event);
}
AppletManager::AppletManager(Core::System& system) : system(system) {
AppletManager::AppletManager(Core::System& system) : system(system), hid_core(system.HIDCore()) {
lock = system.Kernel().CreateMutex(false, "APT_U:Lock");
for (std::size_t slot = 0; slot < applet_slots.size(); ++slot) {
auto& slot_data = applet_slots[slot];
@ -1283,8 +1276,4 @@ AppletManager::~AppletManager() {
HLE::Applets::Shutdown();
}
void AppletManager::ReloadInputDevices() {
is_device_reload_pending.store(true);
}
} // namespace Service::APT

View File

@ -23,6 +23,10 @@ namespace Core {
class System;
}
namespace Core::HID {
class HIDCore;
};
namespace Service::APT {
/// Signals used by APT functions
@ -228,8 +232,6 @@ public:
explicit AppletManager(Core::System& system);
~AppletManager();
void ReloadInputDevices();
/**
* Clears any existing parameter and places a new one. This function is currently only used by
* HLE Applets and should be likely removed in the future
@ -428,11 +430,10 @@ private:
AppletSlot application_close_target = AppletSlot::Error;
Core::TimingEventType* home_button_update_event;
std::atomic<bool> is_device_reload_pending{true};
std::unique_ptr<Input::ButtonDevice> home_button;
bool last_home_button_state = false;
Core::System& system;
Core::HID::HIDCore& hid_core;
AppletSlotData* GetAppletSlot(AppletSlot slot) {
return &applet_slots[static_cast<std::size_t>(slot)];
@ -454,7 +455,6 @@ private:
void CaptureFrameBuffers();
void LoadInputDevices();
void HomeButtonUpdateEvent(std::uintptr_t user_data, s64 cycles_late);
template <class Archive>
@ -479,10 +479,6 @@ private:
}
ar& applet_slots;
ar& library_applet_closing_command;
if (Archive::is_loading::value) {
LoadInputDevices();
}
}
friend class boost::serialization::access;
};

View File

@ -12,6 +12,9 @@
#include "common/logging/log.h"
#include "core/3ds.h"
#include "core/core.h"
#include "core/hid/emulated_console.h"
#include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h"
#include "core/hle/ipc_helpers.h"
#include "core/hle/kernel/event.h"
#include "core/hle/kernel/handle_table.h"
@ -43,9 +46,6 @@ void Module::serialize(Archive& ar, const unsigned int file_version) {
ar& next_gyroscope_index;
ar& enable_accelerometer_count;
ar& enable_gyroscope_count;
if (Archive::is_loading::value) {
LoadInputDevices();
}
if (file_version >= 1) {
ar& state.hex;
}
@ -86,65 +86,23 @@ DirectionState GetStickDirectionState(s16 circle_pad_x, s16 circle_pad_y) {
return state;
}
void Module::LoadInputDevices() {
std::transform(Settings::values.current_input_profile.buttons.begin() +
Settings::NativeButton::BUTTON_HID_BEGIN,
Settings::values.current_input_profile.buttons.begin() +
Settings::NativeButton::BUTTON_HID_END,
buttons.begin(), Input::CreateDevice<Input::ButtonDevice>);
circle_pad = Input::CreateDevice<Input::AnalogDevice>(
Settings::values.current_input_profile.analogs[Settings::NativeAnalog::CirclePad]);
motion_device = Input::CreateDevice<Input::MotionDevice>(
Settings::values.current_input_profile.motion_device);
touch_device = Input::CreateDevice<Input::TouchDevice>(
Settings::values.current_input_profile.touch_device);
if (Settings::values.current_input_profile.use_touch_from_button) {
touch_btn_device = Input::CreateDevice<Input::TouchDevice>("engine:touch_from_button");
} else {
touch_btn_device.reset();
}
}
void Module::UpdatePadCallback(std::uintptr_t user_data, s64 cycles_late) {
SharedMem* mem = reinterpret_cast<SharedMem*>(shared_mem->GetPointer());
if (is_device_reload_pending.exchange(false))
LoadInputDevices();
using namespace Settings::NativeButton;
state.a.Assign(buttons[A - BUTTON_HID_BEGIN]->GetStatus());
state.b.Assign(buttons[B - BUTTON_HID_BEGIN]->GetStatus());
state.x.Assign(buttons[X - BUTTON_HID_BEGIN]->GetStatus());
state.y.Assign(buttons[Y - BUTTON_HID_BEGIN]->GetStatus());
state.right.Assign(buttons[Right - BUTTON_HID_BEGIN]->GetStatus());
state.left.Assign(buttons[Left - BUTTON_HID_BEGIN]->GetStatus());
state.up.Assign(buttons[Up - BUTTON_HID_BEGIN]->GetStatus());
state.down.Assign(buttons[Down - BUTTON_HID_BEGIN]->GetStatus());
state.l.Assign(buttons[L - BUTTON_HID_BEGIN]->GetStatus());
state.r.Assign(buttons[R - BUTTON_HID_BEGIN]->GetStatus());
state.start.Assign(buttons[Start - BUTTON_HID_BEGIN]->GetStatus());
state.select.Assign(buttons[Select - BUTTON_HID_BEGIN]->GetStatus());
state.debug.Assign(buttons[Debug - BUTTON_HID_BEGIN]->GetStatus());
state.gpio14.Assign(buttons[Gpio14 - BUTTON_HID_BEGIN]->GetStatus());
const auto& controller = hid_core.GetEmulatedController();
state = controller->GetPadState();
// Get current circle pad position and update circle pad direction
float circle_pad_x_f, circle_pad_y_f;
std::tie(circle_pad_x_f, circle_pad_y_f) = circle_pad->GetStatus();
const s16 circle_pad_new_x = controller->GetSticks().circle_pad.x;
const s16 circle_pad_new_y = controller->GetSticks().circle_pad.y;
// xperia64: 0x9A seems to be the calibrated limit of the circle pad
// Verified by using Input Redirector with very large-value digital inputs
// on the circle pad and calibrating using the system settings application
constexpr int MAX_CIRCLEPAD_POS = 0x9A; // Max value for a circle pad position
// These are rounded rather than truncated on actual hardware
s16 circle_pad_new_x = static_cast<s16>(std::roundf(circle_pad_x_f * MAX_CIRCLEPAD_POS));
s16 circle_pad_new_y = static_cast<s16>(std::roundf(circle_pad_y_f * MAX_CIRCLEPAD_POS));
s16 circle_pad_x =
(circle_pad_new_x + std::accumulate(circle_pad_old_x.begin(), circle_pad_old_x.end(), 0)) /
CIRCLE_PAD_AVERAGING;
s16 circle_pad_y =
(circle_pad_new_y + std::accumulate(circle_pad_old_y.begin(), circle_pad_old_y.end(), 0)) /
CIRCLE_PAD_AVERAGING;
circle_pad_old_x.erase(circle_pad_old_x.begin());
circle_pad_old_x.push_back(circle_pad_new_x);
circle_pad_old_y.erase(circle_pad_old_y.begin());
@ -190,12 +148,10 @@ void Module::UpdatePadCallback(std::uintptr_t user_data, s64 cycles_late) {
// Get the current touch entry
TouchDataEntry& touch_entry = mem->touch.entries[mem->touch.index];
bool pressed = false;
float x, y;
std::tie(x, y, pressed) = touch_device->GetStatus();
if (!pressed && touch_btn_device) {
std::tie(x, y, pressed) = touch_btn_device->GetStatus();
}
const auto& touch_state = hid_core.GetEmulatedConsole()->GetTouch()[0];
float x = touch_state.position_x;
float y = touch_state.position_y;
bool pressed = touch_state.pressed;
touch_entry.x = static_cast<u16>(x * Core::kScreenBottomWidth);
touch_entry.y = static_cast<u16>(y * Core::kScreenBottomHeight);
touch_entry.valid.Assign(pressed ? 1 : 0);
@ -232,8 +188,8 @@ void Module::UpdateAccelerometerCallback(std::uintptr_t user_data, s64 cycles_la
mem->accelerometer.index = next_accelerometer_index;
next_accelerometer_index = (next_accelerometer_index + 1) % mem->accelerometer.entries.size();
Common::Vec3<float> accel;
std::tie(accel, std::ignore) = motion_device->GetStatus();
const auto& motion_state = hid_core.GetEmulatedConsole()->GetMotion();
Common::Vec3<float> accel = motion_state.gyro;
accel *= accelerometer_coef;
// TODO(wwylele): do a time stretch like the one in UpdateGyroscopeCallback
// The time stretch formula should be like
@ -279,8 +235,8 @@ void Module::UpdateGyroscopeCallback(std::uintptr_t user_data, s64 cycles_late)
GyroscopeDataEntry& gyroscope_entry = mem->gyroscope.entries[mem->gyroscope.index];
Common::Vec3<float> gyro;
std::tie(std::ignore, gyro) = motion_device->GetStatus();
const auto& motion_state = hid_core.GetEmulatedConsole()->GetMotion();
Common::Vec3<float> gyro = motion_state.gyro;
double stretch = system.perf_stats->GetLastFrameTimeScale();
gyro *= gyroscope_coef * static_cast<float>(stretch);
gyroscope_entry.x = static_cast<s16>(gyro.x);
@ -421,7 +377,7 @@ std::shared_ptr<Module> Module::Interface::GetModule() const {
return hid;
}
Module::Module(Core::System& system) : system(system) {
Module::Module(Core::System& system) : system(system), hid_core{system.HIDCore()} {
using namespace Kernel;
shared_mem =
@ -455,10 +411,6 @@ Module::Module(Core::System& system) : system(system) {
timing.ScheduleEvent(pad_update_ticks, pad_update_event);
}
void Module::ReloadInputDevices() {
is_device_reload_pending.store(true);
}
const PadState& Module::GetState() const {
return state;
}

View File

@ -13,9 +13,10 @@
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/input.h"
#include "common/settings.h"
#include "core/core_timing.h"
#include "core/frontend/input.h"
#include "core/hid/hid_types.h"
#include "core/hle/service/service.h"
namespace Core {
@ -31,36 +32,13 @@ namespace Core {
struct TimingEventType;
};
namespace Core::HID {
class HIDCore;
};
namespace Service::HID {
/**
* Structure of a Pad controller state.
*/
struct PadState {
union {
u32 hex{};
BitField<0, 1, u32> a;
BitField<1, 1, u32> b;
BitField<2, 1, u32> select;
BitField<3, 1, u32> start;
BitField<4, 1, u32> right;
BitField<5, 1, u32> left;
BitField<6, 1, u32> up;
BitField<7, 1, u32> down;
BitField<8, 1, u32> r;
BitField<9, 1, u32> l;
BitField<10, 1, u32> x;
BitField<11, 1, u32> y;
BitField<12, 1, u32> debug;
BitField<13, 1, u32> gpio14;
BitField<28, 1, u32> circle_right;
BitField<29, 1, u32> circle_left;
BitField<30, 1, u32> circle_up;
BitField<31, 1, u32> circle_down;
};
};
using Core::HID::PadState;
/**
* Structure of a single entry of Pad state history within HID shared memory
@ -296,8 +274,6 @@ public:
std::shared_ptr<Module> hid;
};
void ReloadInputDevices();
const PadState& GetState() const;
// Updating period for each HID device. These empirical values are measured from a 11.2 3DS.
@ -306,7 +282,6 @@ public:
static constexpr u64 gyroscope_update_ticks = BASE_CLOCK_RATE_ARM11 / 101;
private:
void LoadInputDevices();
void UpdatePadCallback(std::uintptr_t user_data, s64 cycles_late);
void UpdateAccelerometerCallback(std::uintptr_t user_data, s64 cycles_late);
void UpdateGyroscopeCallback(std::uintptr_t user_data, s64 cycles_late);
@ -347,13 +322,7 @@ private:
Core::TimingEventType* accelerometer_update_event;
Core::TimingEventType* gyroscope_update_event;
std::atomic<bool> is_device_reload_pending{true};
std::array<std::unique_ptr<Input::ButtonDevice>, Settings::NativeButton::NUM_BUTTONS_HID>
buttons;
std::unique_ptr<Input::AnalogDevice> circle_pad;
std::unique_ptr<Input::MotionDevice> motion_device;
std::unique_ptr<Input::TouchDevice> touch_device;
std::unique_ptr<Input::TouchDevice> touch_btn_device;
Core::HID::HIDCore& hid_core;
template <class Archive>
void serialize(Archive& ar, const unsigned int);

View File

@ -7,6 +7,8 @@
#include "common/string_util.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h"
#include "core/hle/service/ir/extra_hid.h"
#include "core/movie.h"
@ -64,9 +66,8 @@ enum class ResponseID : u8 {
ReadCalibrationData = 0x11,
};
ExtraHID::ExtraHID(SendFunc send_func, Core::Timing& timing) : IRDevice(send_func), timing(timing) {
LoadInputDevices();
ExtraHID::ExtraHID(SendFunc send_func, Core::System& system)
: IRDevice(send_func), timing(system.CoreTiming()), hid_core{system.HIDCore()} {
// The data below was retrieved from a New 3DS
// TODO(wwylele): this data is probably writable (via request 3?) and thus should be saved to
// and loaded from somewhere.
@ -228,24 +229,23 @@ void ExtraHID::OnReceive(const std::vector<u8>& data) {
}
void ExtraHID::SendHIDStatus() {
if (is_device_reload_pending.exchange(false))
LoadInputDevices();
constexpr int C_STICK_CENTER = 0x800;
// TODO(wwylele): this value is not accurately measured. We currently assume that the axis can
// take values in the whole range of a 12-bit integer.
constexpr int C_STICK_RADIUS = 0x7FF;
float x, y;
std::tie(x, y) = c_stick->GetStatus();
// TODO: Correct this!!!!!
const auto& controller = hid_core.GetEmulatedController();
float x = controller->GetSticks().c_stick.x;
float y = controller->GetSticks().c_stick.y;
ExtraHIDResponse response;
response.c_stick.header.Assign(static_cast<u8>(ResponseID::PollHID));
response.c_stick.c_stick_x.Assign(static_cast<u32>(C_STICK_CENTER + C_STICK_RADIUS * x));
response.c_stick.c_stick_y.Assign(static_cast<u32>(C_STICK_CENTER + C_STICK_RADIUS * y));
response.buttons.battery_level.Assign(0x1F);
response.buttons.zl_not_held.Assign(!zl->GetStatus());
response.buttons.zr_not_held.Assign(!zr->GetStatus());
response.buttons.zl_not_held.Assign(!controller->GetExtraState().zl);
response.buttons.zr_not_held.Assign(!controller->GetExtraState().zr);
response.buttons.r_not_held.Assign(1);
response.unknown = 0;
@ -256,17 +256,4 @@ void ExtraHID::SendHIDStatus() {
Send(response_buffer);
}
void ExtraHID::RequestInputDevicesReload() {
is_device_reload_pending.store(true);
}
void ExtraHID::LoadInputDevices() {
zl = Input::CreateDevice<Input::ButtonDevice>(
Settings::values.current_input_profile.buttons[Settings::NativeButton::ZL]);
zr = Input::CreateDevice<Input::ButtonDevice>(
Settings::values.current_input_profile.buttons[Settings::NativeButton::ZR]);
c_stick = Input::CreateDevice<Input::AnalogDevice>(
Settings::values.current_input_profile.analogs[Settings::NativeAnalog::CStick]);
}
} // namespace Service::IR

View File

@ -8,8 +8,8 @@
#include <atomic>
#include <boost/serialization/array.hpp>
#include "common/bit_field.h"
#include "common/input.h"
#include "common/swap.h"
#include "core/frontend/input.h"
#include "core/hle/service/ir/ir_user.h"
namespace Core {
@ -17,6 +17,10 @@ struct TimingEventType;
class Timing;
} // namespace Core
namespace Core::HID {
class HIDCore;
};
namespace Service::IR {
struct ExtraHIDResponse {
@ -42,38 +46,28 @@ static_assert(sizeof(ExtraHIDResponse) == 6, "HID status response has wrong size
*/
class ExtraHID final : public IRDevice {
public:
explicit ExtraHID(SendFunc send_func, Core::Timing& timing);
explicit ExtraHID(SendFunc send_func, Core::System& system);
~ExtraHID();
void OnConnect() override;
void OnDisconnect() override;
void OnReceive(const std::vector<u8>& data) override;
/// Requests input devices reload from current settings. Called when the input settings change.
void RequestInputDevicesReload();
private:
void SendHIDStatus();
void HandleConfigureHIDPollingRequest(const std::vector<u8>& request);
void HandleReadCalibrationDataRequest(const std::vector<u8>& request);
void LoadInputDevices();
Core::Timing& timing;
u8 hid_period;
Core::TimingEventType* hid_polling_callback_id;
std::array<u8, 0x40> calibration_data;
std::unique_ptr<Input::ButtonDevice> zl;
std::unique_ptr<Input::ButtonDevice> zr;
std::unique_ptr<Input::AnalogDevice> c_stick;
std::atomic<bool> is_device_reload_pending;
Core::HID::HIDCore& hid_core;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar& hid_period;
ar& calibration_data; // This isn't writeable for now, but might be in future
if (Archive::is_loading::value) {
LoadInputDevices(); // zl, zr, c_stick are loaded here
}
}
friend class boost::serialization::access;
};

View File

@ -8,6 +8,7 @@
#include "common/settings.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hid/emulated_controller.h"
#include "core/hle/ipc_helpers.h"
#include "core/hle/kernel/event.h"
#include "core/hle/kernel/shared_memory.h"
@ -29,7 +30,6 @@ void IR_RST::serialize(Archive& ar, const unsigned int) {
ar& raw_c_stick;
ar& update_period;
// update_callback_id and input devices are set separately
ReloadInputDevices();
}
struct PadDataEntry {
@ -51,34 +51,19 @@ struct SharedMem {
static_assert(sizeof(SharedMem) == 0x98, "SharedMem has wrong size!");
void IR_RST::LoadInputDevices() {
zl_button = Input::CreateDevice<Input::ButtonDevice>(
Settings::values.current_input_profile.buttons[Settings::NativeButton::ZL]);
zr_button = Input::CreateDevice<Input::ButtonDevice>(
Settings::values.current_input_profile.buttons[Settings::NativeButton::ZR]);
c_stick = Input::CreateDevice<Input::AnalogDevice>(
Settings::values.current_input_profile.analogs[Settings::NativeAnalog::CStick]);
}
void IR_RST::UnloadInputDevices() {
zl_button = nullptr;
zr_button = nullptr;
c_stick = nullptr;
}
void IR_RST::UpdateCallback(std::uintptr_t user_data, s64 cycles_late) {
SharedMem* mem = reinterpret_cast<SharedMem*>(shared_memory->GetPointer());
if (is_device_reload_pending.exchange(false))
LoadInputDevices();
const auto& controller = hid_core.GetEmulatedController();
PadState state;
state.zl.Assign(zl_button->GetStatus());
state.zr.Assign(zr_button->GetStatus());
state.zl.Assign(controller->GetExtraState().zl);
state.zr.Assign(controller->GetExtraState().zr);
// Get current c-stick position and update c-stick direction
float c_stick_x_f, c_stick_y_f;
std::tie(c_stick_x_f, c_stick_y_f) = c_stick->GetStatus();
// TODO: FIX!!!! (same as in extra_hid)
float c_stick_x_f = controller->GetSticks().c_stick.x;
float c_stick_y_f = controller->GetSticks().c_stick.y;
constexpr int MAX_CSTICK_RADIUS = 0x9C; // Max value for a c-stick radius
s16 c_stick_x = static_cast<s16>(c_stick_x_f * MAX_CSTICK_RADIUS);
s16 c_stick_y = static_cast<s16>(c_stick_y_f * MAX_CSTICK_RADIUS);
@ -143,7 +128,6 @@ void IR_RST::Initialize(Kernel::HLERequestContext& ctx) {
LOG_ERROR(Service_IR, "raw C-stick data is not implemented!");
next_pad_index = 0;
is_device_reload_pending.store(true);
system.CoreTiming().ScheduleEvent(msToCycles(update_period), update_callback_id);
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -156,14 +140,14 @@ void IR_RST::Shutdown(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx, 0x03, 0, 0);
system.CoreTiming().UnscheduleEvent(update_callback_id, 0);
UnloadInputDevices();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(RESULT_SUCCESS);
LOG_DEBUG(Service_IR, "called");
}
IR_RST::IR_RST(Core::System& system) : ServiceFramework("ir:rst", 1), system(system) {
IR_RST::IR_RST(Core::System& system)
: ServiceFramework("ir:rst", 1), system(system), hid_core{system.HIDCore()} {
using namespace Kernel;
// Note: these two kernel objects are even available before Initialize service function is
// called.
@ -192,8 +176,4 @@ IR_RST::IR_RST(Core::System& system) : ServiceFramework("ir:rst", 1), system(sys
IR_RST::~IR_RST() = default;
void IR_RST::ReloadInputDevices() {
is_device_reload_pending.store(true);
}
} // namespace Service::IR

View File

@ -8,8 +8,8 @@
#include <memory>
#include "common/bit_field.h"
#include "common/common_types.h"
#include "common/input.h"
#include "common/swap.h"
#include "core/frontend/input.h"
#include "core/hle/service/service.h"
namespace Kernel {
@ -21,6 +21,10 @@ namespace Core {
struct TimingEventType;
};
namespace Core::HID {
class HIDCore;
};
namespace Service::IR {
union PadState {
@ -40,7 +44,6 @@ class IR_RST final : public ServiceFramework<IR_RST> {
public:
explicit IR_RST(Core::System& system);
~IR_RST();
void ReloadInputDevices();
private:
/**
@ -72,8 +75,6 @@ private:
*/
void Shutdown(Kernel::HLERequestContext& ctx);
void LoadInputDevices();
void UnloadInputDevices();
void UpdateCallback(std::uintptr_t user_data, s64 cycles_late);
Core::System& system;
@ -81,12 +82,9 @@ private:
std::shared_ptr<Kernel::SharedMemory> shared_memory;
u32 next_pad_index{0};
Core::TimingEventType* update_callback_id;
std::unique_ptr<Input::ButtonDevice> zl_button;
std::unique_ptr<Input::ButtonDevice> zr_button;
std::unique_ptr<Input::AnalogDevice> c_stick;
std::atomic<bool> is_device_reload_pending{false};
bool raw_c_stick{false};
int update_period{0};
Core::HID::HIDCore& hid_core;
template <class Archive>
void serialize(Archive& ar, const unsigned int);

View File

@ -464,7 +464,7 @@ IR_USER::IR_USER(Core::System& system) : ServiceFramework("ir:USER", 1) {
receive_event = system.Kernel().CreateEvent(ResetType::OneShot, "IR:ReceiveEvent");
extra_hid = std::make_unique<ExtraHID>(
[this](const std::vector<u8>& data) { PutToReceive(data); }, system.CoreTiming());
[this](const std::vector<u8>& data) { PutToReceive(data); }, system);
}
IR_USER::~IR_USER() {
@ -473,10 +473,6 @@ IR_USER::~IR_USER() {
}
}
void IR_USER::ReloadInputDevices() {
extra_hid->RequestInputDevicesReload();
}
IRDevice::IRDevice(SendFunc send_func_) : send_func(send_func_) {}
IRDevice::~IRDevice() = default;

View File

@ -55,8 +55,6 @@ public:
explicit IR_USER(Core::System& system);
~IR_USER();
void ReloadInputDevices();
private:
/**
* InitializeIrNopShared service function

View File

@ -1,44 +1,62 @@
add_library(input_common STATIC
analog_from_button.cpp
analog_from_button.h
keyboard.cpp
keyboard.h
drivers/keyboard.cpp
drivers/keyboard.h
drivers/mouse.cpp
drivers/mouse.h
drivers/touch_screen.cpp
drivers/touch_screen.h
drivers/udp_client.cpp
drivers/udp_client.h
drivers/virtual_gamepad.cpp
drivers/virtual_gamepad.h
helpers/stick_from_buttons.cpp
helpers/stick_from_buttons.h
helpers/touch_from_buttons.cpp
helpers/touch_from_buttons.h
helpers/udp_protocol.cpp
helpers/udp_protocol.h
input_engine.cpp
input_engine.h
input_mapping.cpp
input_mapping.h
input_poller.cpp
input_poller.h
main.cpp
main.h
motion_emu.cpp
motion_emu.h
precompiled_headers.h
touch_from_button.cpp
touch_from_button.h
sdl/sdl.cpp
sdl/sdl.h
udp/client.cpp
udp/client.h
udp/protocol.cpp
udp/protocol.h
udp/udp.cpp
udp/udp.h
)
if(ENABLE_SDL2)
if (MSVC)
target_compile_options(input_common PRIVATE
/W4
/we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data
/we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data
/we4800 # Implicit conversion from 'type' to bool. Possible information loss
)
else()
target_compile_options(input_common PRIVATE
-Werror=conversion
)
endif()
if (ENABLE_SDL2)
target_sources(input_common PRIVATE
sdl/sdl_impl.cpp
sdl/sdl_impl.h
drivers/sdl_driver.cpp
drivers/sdl_driver.h
)
target_link_libraries(input_common PRIVATE SDL2::SDL2)
target_compile_definitions(input_common PRIVATE HAVE_SDL2)
endif()
if(ENABLE_LIBUSB)
if (ENABLE_LIBUSB)
target_sources(input_common PRIVATE
gcadapter/gc_adapter.cpp
gcadapter/gc_adapter.h
gcadapter/gc_poller.cpp
gcadapter/gc_poller.h
drivers/gc_adapter.cpp
drivers/gc_adapter.h
)
target_include_directories(input_common PRIVATE ${LIBUSB_INCLUDE_DIR})
target_link_libraries(input_common PRIVATE ${LIBUSB_LIBRARIES})
add_definitions(-DENABLE_GCADAPTER)
target_compile_definitions(input_common PRIVATE HAVE_LIBUSB)
endif()
create_target_directory_groups(input_common)

View File

@ -1,58 +0,0 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "input_common/analog_from_button.h"
namespace InputCommon {
class Analog final : public Input::AnalogDevice {
public:
using Button = std::unique_ptr<Input::ButtonDevice>;
Analog(Button up_, Button down_, Button left_, Button right_, Button modifier_,
float modifier_scale_)
: up(std::move(up_)), down(std::move(down_)), left(std::move(left_)),
right(std::move(right_)), modifier(std::move(modifier_)),
modifier_scale(modifier_scale_) {}
std::tuple<float, float> GetStatus() const override {
constexpr float SQRT_HALF = 0.707106781f;
int x = 0, y = 0;
if (right->GetStatus())
++x;
if (left->GetStatus())
--x;
if (up->GetStatus())
++y;
if (down->GetStatus())
--y;
float coef = modifier->GetStatus() ? modifier_scale : 1.0f;
return std::make_tuple(x * coef * (y == 0 ? 1.0f : SQRT_HALF),
y * coef * (x == 0 ? 1.0f : SQRT_HALF));
}
private:
Button up;
Button down;
Button left;
Button right;
Button modifier;
float modifier_scale;
};
std::unique_ptr<Input::AnalogDevice> AnalogFromButton::Create(const Common::ParamPackage& params) {
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
auto up = Input::CreateDevice<Input::ButtonDevice>(params.Get("up", null_engine));
auto down = Input::CreateDevice<Input::ButtonDevice>(params.Get("down", null_engine));
auto left = Input::CreateDevice<Input::ButtonDevice>(params.Get("left", null_engine));
auto right = Input::CreateDevice<Input::ButtonDevice>(params.Get("right", null_engine));
auto modifier = Input::CreateDevice<Input::ButtonDevice>(params.Get("modifier", null_engine));
auto modifier_scale = params.Get("modifier_scale", 0.5f);
return std::make_unique<Analog>(std::move(up), std::move(down), std::move(left),
std::move(right), std::move(modifier), modifier_scale);
}
} // namespace InputCommon

View File

@ -0,0 +1,546 @@
// SPDX-FileCopyrightText: 2014 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/format.h>
#include <libusb.h>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/settings_input.h"
#include "common/thread.h"
#include "input_common/drivers/gc_adapter.h"
namespace InputCommon {
class LibUSBContext {
public:
explicit LibUSBContext() {
init_result = libusb_init_context(&ctx, nullptr, 0);
}
~LibUSBContext() {
libusb_exit(ctx);
}
LibUSBContext& operator=(const LibUSBContext&) = delete;
LibUSBContext(const LibUSBContext&) = delete;
LibUSBContext& operator=(LibUSBContext&&) noexcept = delete;
LibUSBContext(LibUSBContext&&) noexcept = delete;
[[nodiscard]] int InitResult() const noexcept {
return init_result;
}
[[nodiscard]] libusb_context* get() noexcept {
return ctx;
}
private:
libusb_context* ctx;
int init_result{};
};
class LibUSBDeviceHandle {
public:
explicit LibUSBDeviceHandle(libusb_context* ctx, uint16_t vid, uint16_t pid) noexcept {
handle = libusb_open_device_with_vid_pid(ctx, vid, pid);
}
~LibUSBDeviceHandle() noexcept {
if (handle) {
libusb_release_interface(handle, 1);
libusb_close(handle);
}
}
LibUSBDeviceHandle& operator=(const LibUSBDeviceHandle&) = delete;
LibUSBDeviceHandle(const LibUSBDeviceHandle&) = delete;
LibUSBDeviceHandle& operator=(LibUSBDeviceHandle&&) noexcept = delete;
LibUSBDeviceHandle(LibUSBDeviceHandle&&) noexcept = delete;
[[nodiscard]] libusb_device_handle* get() noexcept {
return handle;
}
private:
libusb_device_handle* handle{};
};
GCAdapter::GCAdapter(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
if (usb_adapter_handle) {
return;
}
LOG_DEBUG(Input, "Initialization started");
libusb_ctx = std::make_unique<LibUSBContext>();
const int init_res = libusb_ctx->InitResult();
if (init_res == LIBUSB_SUCCESS) {
adapter_scan_thread =
std::jthread([this](std::stop_token stop_token) { AdapterScanThread(stop_token); });
} else {
LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res);
}
}
GCAdapter::~GCAdapter() {
Reset();
}
void GCAdapter::AdapterInputThread(std::stop_token stop_token) {
LOG_DEBUG(Input, "Input thread started");
Common::SetCurrentThreadName("GCAdapter");
s32 payload_size{};
AdapterPayload adapter_payload{};
adapter_scan_thread = {};
while (!stop_token.stop_requested()) {
libusb_interrupt_transfer(usb_adapter_handle->get(), input_endpoint, adapter_payload.data(),
static_cast<s32>(adapter_payload.size()), &payload_size, 16);
if (IsPayloadCorrect(adapter_payload, payload_size)) {
UpdateControllers(adapter_payload);
UpdateVibrations();
}
std::this_thread::yield();
}
if (restart_scan_thread) {
adapter_scan_thread =
std::jthread([this](std::stop_token token) { AdapterScanThread(token); });
restart_scan_thread = false;
}
}
bool GCAdapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size) {
if (payload_size != static_cast<s32>(adapter_payload.size()) ||
adapter_payload[0] != LIBUSB_DT_HID) {
LOG_DEBUG(Input, "Error reading payload (size: {}, type: {:02x})", payload_size,
adapter_payload[0]);
if (input_error_counter++ > 20) {
LOG_ERROR(Input, "Timeout, Is the adapter connected?");
adapter_input_thread.request_stop();
restart_scan_thread = true;
}
return false;
}
input_error_counter = 0;
return true;
}
void GCAdapter::UpdateControllers(const AdapterPayload& adapter_payload) {
for (std::size_t port = 0; port < pads.size(); ++port) {
const std::size_t offset = 1 + (9 * port);
const auto type = static_cast<ControllerTypes>(adapter_payload[offset] >> 4);
UpdatePadType(port, type);
if (DeviceConnected(port)) {
const u8 b1 = adapter_payload[offset + 1];
const u8 b2 = adapter_payload[offset + 2];
UpdateStateButtons(port, b1, b2);
UpdateStateAxes(port, adapter_payload);
}
}
}
void GCAdapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) {
if (pads[port].type == pad_type) {
return;
}
// Device changed reset device and set new type
pads[port].axis_origin = {};
pads[port].reset_origin_counter = {};
pads[port].enable_vibration = {};
pads[port].rumble_amplitude = {};
pads[port].type = pad_type;
}
void GCAdapter::UpdateStateButtons(std::size_t port, [[maybe_unused]] u8 b1,
[[maybe_unused]] u8 b2) {
if (port >= pads.size()) {
return;
}
static constexpr std::array<PadButton, 8> b1_buttons{
PadButton::ButtonA, PadButton::ButtonB, PadButton::ButtonX, PadButton::ButtonY,
PadButton::ButtonLeft, PadButton::ButtonRight, PadButton::ButtonDown, PadButton::ButtonUp,
};
static constexpr std::array<PadButton, 4> b2_buttons{
PadButton::ButtonStart,
PadButton::TriggerZ,
PadButton::TriggerR,
PadButton::TriggerL,
};
for (std::size_t i = 0; i < b1_buttons.size(); ++i) {
const bool button_status = (b1 & (1U << i)) != 0;
const int button = static_cast<int>(b1_buttons[i]);
SetButton(pads[port].identifier, button, button_status);
}
for (std::size_t j = 0; j < b2_buttons.size(); ++j) {
const bool button_status = (b2 & (1U << j)) != 0;
const int button = static_cast<int>(b2_buttons[j]);
SetButton(pads[port].identifier, button, button_status);
}
}
void GCAdapter::UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload) {
if (port >= pads.size()) {
return;
}
const std::size_t offset = 1 + (9 * port);
static constexpr std::array<PadAxes, 6> axes{
PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX,
PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight,
};
for (const PadAxes axis : axes) {
const auto index = static_cast<std::size_t>(axis);
const u8 axis_value = adapter_payload[offset + 3 + index];
if (pads[port].reset_origin_counter <= 18) {
if (pads[port].axis_origin[index] != axis_value) {
pads[port].reset_origin_counter = 0;
}
pads[port].axis_origin[index] = axis_value;
pads[port].reset_origin_counter++;
}
const f32 axis_status = (axis_value - pads[port].axis_origin[index]) / 100.0f;
SetAxis(pads[port].identifier, static_cast<int>(index), axis_status);
}
}
void GCAdapter::AdapterScanThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("ScanGCAdapter");
usb_adapter_handle = nullptr;
pads = {};
while (!stop_token.stop_requested() && !Setup()) {
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
bool GCAdapter::Setup() {
constexpr u16 nintendo_vid = 0x057e;
constexpr u16 gc_adapter_pid = 0x0337;
usb_adapter_handle =
std::make_unique<LibUSBDeviceHandle>(libusb_ctx->get(), nintendo_vid, gc_adapter_pid);
if (!usb_adapter_handle->get()) {
return false;
}
if (!CheckDeviceAccess()) {
usb_adapter_handle = nullptr;
return false;
}
libusb_device* const device = libusb_get_device(usb_adapter_handle->get());
LOG_INFO(Input, "GC adapter is now connected");
// GC Adapter found and accessible, registering it
if (GetGCEndpoint(device)) {
rumble_enabled = true;
input_error_counter = 0;
output_error_counter = 0;
std::size_t port = 0;
for (GCController& pad : pads) {
pad.identifier = {
.guid = Common::UUID{},
.port = port++,
.pad = 0,
};
PreSetController(pad.identifier);
}
adapter_input_thread =
std::jthread([this](std::stop_token stop_token) { AdapterInputThread(stop_token); });
return true;
}
return false;
}
bool GCAdapter::CheckDeviceAccess() {
s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle->get(), 0);
if (kernel_driver_error == 1) {
kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle->get(), 0);
if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}",
kernel_driver_error);
}
}
if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
usb_adapter_handle = nullptr;
return false;
}
const int interface_claim_error = libusb_claim_interface(usb_adapter_handle->get(), 0);
if (interface_claim_error) {
LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error);
usb_adapter_handle = nullptr;
return false;
}
// This fixes payload problems from offbrand GCAdapters
const s32 control_transfer_error =
libusb_control_transfer(usb_adapter_handle->get(), 0x21, 11, 0x0001, 0, nullptr, 0, 1000);
if (control_transfer_error < 0) {
LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error);
}
return true;
}
bool GCAdapter::GetGCEndpoint(libusb_device* device) {
libusb_config_descriptor* config = nullptr;
const int config_descriptor_return = libusb_get_config_descriptor(device, 0, &config);
if (config_descriptor_return != LIBUSB_SUCCESS) {
LOG_ERROR(Input, "libusb_get_config_descriptor failed with error = {}",
config_descriptor_return);
return false;
}
for (u8 ic = 0; ic < config->bNumInterfaces; ic++) {
const libusb_interface* interfaceContainer = &config->interface[ic];
for (int i = 0; i < interfaceContainer->num_altsetting; i++) {
const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i];
for (u8 e = 0; e < interface->bNumEndpoints; e++) {
const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e];
if ((endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0) {
input_endpoint = endpoint->bEndpointAddress;
} else {
output_endpoint = endpoint->bEndpointAddress;
}
}
}
}
// This transfer seems to be responsible for clearing the state of the adapter
// Used to clear the "busy" state of when the device is unexpectedly unplugged
unsigned char clear_payload = 0x13;
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, &clear_payload,
sizeof(clear_payload), nullptr, 16);
return true;
}
Common::Input::DriverResult GCAdapter::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f;
const auto processed_amplitude =
static_cast<u8>((mean_amplitude + std::pow(mean_amplitude, 0.3f)) * 0.5f * 0x8);
pads[identifier.port].rumble_amplitude = processed_amplitude;
if (!rumble_enabled) {
return Common::Input::DriverResult::Disabled;
}
return Common::Input::DriverResult::Success;
}
bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
return rumble_enabled;
}
void GCAdapter::UpdateVibrations() {
// Use 8 states to keep the switching between on/off fast enough for
// a human to feel different vibration strenght
// More states == more rumble strengths == slower update time
constexpr u8 vibration_states = 8;
vibration_counter = (vibration_counter + 1) % vibration_states;
for (GCController& pad : pads) {
const bool vibrate = pad.rumble_amplitude > vibration_counter;
vibration_changed |= vibrate != pad.enable_vibration;
pad.enable_vibration = vibrate;
}
SendVibrations();
}
void GCAdapter::SendVibrations() {
if (!rumble_enabled || !vibration_changed) {
return;
}
s32 size{};
constexpr u8 rumble_command = 0x11;
const u8 p1 = pads[0].enable_vibration;
const u8 p2 = pads[1].enable_vibration;
const u8 p3 = pads[2].enable_vibration;
const u8 p4 = pads[3].enable_vibration;
std::array<u8, 5> payload = {rumble_command, p1, p2, p3, p4};
const int err =
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, payload.data(),
static_cast<s32>(payload.size()), &size, 16);
if (err) {
LOG_DEBUG(Input, "Libusb write failed: {}", libusb_error_name(err));
if (output_error_counter++ > 5) {
LOG_ERROR(Input, "Output timeout, Rumble disabled");
rumble_enabled = false;
}
return;
}
output_error_counter = 0;
vibration_changed = false;
}
bool GCAdapter::DeviceConnected(std::size_t port) const {
return pads[port].type != ControllerTypes::None;
}
void GCAdapter::Reset() {
adapter_scan_thread = {};
adapter_input_thread = {};
usb_adapter_handle = nullptr;
pads = {};
libusb_ctx = nullptr;
}
std::vector<Common::ParamPackage> GCAdapter::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
for (std::size_t port = 0; port < pads.size(); ++port) {
if (!DeviceConnected(port)) {
continue;
}
Common::ParamPackage identifier{};
identifier.Set("engine", GetEngineName());
identifier.Set("display", fmt::format("Gamecube Controller {}", port + 1));
identifier.Set("port", static_cast<int>(port));
devices.emplace_back(identifier);
}
return devices;
}
ButtonMapping GCAdapter::GetButtonMappingForDevice(const Common::ParamPackage& params) {
// This list is missing ZL/ZR since those are not considered buttons.
// We will add those afterwards
// This list also excludes any button that can't be really mapped
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 12>
switch_to_gcadapter_button = {
std::pair{Settings::NativeButton::A, PadButton::ButtonA},
{Settings::NativeButton::B, PadButton::ButtonB},
{Settings::NativeButton::X, PadButton::ButtonX},
{Settings::NativeButton::Y, PadButton::ButtonY},
{Settings::NativeButton::Start, PadButton::ButtonStart},
{Settings::NativeButton::Select, PadButton::ButtonSelect},
{Settings::NativeButton::DLeft, PadButton::ButtonLeft},
{Settings::NativeButton::DUp, PadButton::ButtonUp},
{Settings::NativeButton::DRight, PadButton::ButtonRight},
{Settings::NativeButton::DDown, PadButton::ButtonDown},
{Settings::NativeButton::L, PadButton::TriggerL},
{Settings::NativeButton::R, PadButton::TriggerR},
};
if (!params.Has("port")) {
return {};
}
ButtonMapping mapping{};
for (const auto& [switch_button, gcadapter_button] : switch_to_gcadapter_button) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("button", static_cast<int>(gcadapter_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
// Add the missing bindings for ZL/ZR
static constexpr std::array<std::tuple<Settings::NativeButton::Values, PadButton, PadAxes>, 2>
switch_to_gcadapter_axis = {
std::tuple{Settings::NativeButton::ZL, PadButton::TriggerL, PadAxes::TriggerLeft},
{Settings::NativeButton::ZR, PadButton::TriggerR, PadAxes::TriggerRight},
};
for (const auto& [switch_button, gcadapter_buton, gcadapter_axis] : switch_to_gcadapter_axis) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("button", static_cast<s32>(gcadapter_buton));
button_params.Set("axis", static_cast<s32>(gcadapter_axis));
button_params.Set("threshold", 0.5f);
button_params.Set("range", 1.9f);
button_params.Set("direction", "+");
mapping.insert_or_assign(switch_button, std::move(button_params));
}
return mapping;
}
AnalogMapping GCAdapter::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return {};
}
AnalogMapping mapping = {};
Common::ParamPackage left_analog_params;
left_analog_params.Set("engine", GetEngineName());
left_analog_params.Set("port", params.Get("port", 0));
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::StickX));
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::StickY));
mapping.insert_or_assign(Settings::NativeAnalog::CirclePad, std::move(left_analog_params));
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("port", params.Get("port", 0));
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::SubstickX));
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::SubstickY));
mapping.insert_or_assign(Settings::NativeAnalog::CStick, std::move(right_analog_params));
return mapping;
}
Common::Input::ButtonNames GCAdapter::GetUIButtonName(const Common::ParamPackage& params) const {
PadButton button = static_cast<PadButton>(params.Get("button", 0));
switch (button) {
case PadButton::ButtonLeft:
return Common::Input::ButtonNames::ButtonLeft;
case PadButton::ButtonRight:
return Common::Input::ButtonNames::ButtonRight;
case PadButton::ButtonDown:
return Common::Input::ButtonNames::ButtonDown;
case PadButton::ButtonUp:
return Common::Input::ButtonNames::ButtonUp;
case PadButton::TriggerZ:
return Common::Input::ButtonNames::TriggerZ;
case PadButton::TriggerR:
return Common::Input::ButtonNames::TriggerR;
case PadButton::TriggerL:
return Common::Input::ButtonNames::TriggerL;
case PadButton::ButtonA:
return Common::Input::ButtonNames::ButtonA;
case PadButton::ButtonB:
return Common::Input::ButtonNames::ButtonB;
case PadButton::ButtonX:
return Common::Input::ButtonNames::ButtonX;
case PadButton::ButtonY:
return Common::Input::ButtonNames::ButtonY;
case PadButton::ButtonStart:
return Common::Input::ButtonNames::ButtonStart;
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames GCAdapter::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
return Common::Input::ButtonNames::Invalid;
}
bool GCAdapter::IsStickInverted(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return false;
}
const auto x_axis = static_cast<PadAxes>(params.Get("axis_x", 0));
const auto y_axis = static_cast<PadAxes>(params.Get("axis_y", 0));
if (x_axis != PadAxes::StickY && x_axis != PadAxes::SubstickY) {
return false;
}
if (y_axis != PadAxes::StickX && y_axis != PadAxes::SubstickX) {
return false;
}
return true;
}
} // namespace InputCommon

View File

@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: 2014 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <string>
#include <thread>
#include "common/polyfill_thread.h"
#include "input_common/input_engine.h"
struct libusb_context;
struct libusb_device;
struct libusb_device_handle;
namespace InputCommon {
class LibUSBContext;
class LibUSBDeviceHandle;
class GCAdapter : public InputEngine {
public:
explicit GCAdapter(std::string input_engine_);
~GCAdapter() override;
Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
bool IsStickInverted(const Common::ParamPackage& params) override;
private:
enum class PadButton {
Undefined = 0x0000,
ButtonLeft = 0x0001,
ButtonRight = 0x0002,
ButtonDown = 0x0004,
ButtonUp = 0x0008,
TriggerZ = 0x0010,
TriggerR = 0x0020,
TriggerL = 0x0040,
ButtonA = 0x0100,
ButtonB = 0x0200,
ButtonX = 0x0400,
ButtonY = 0x0800,
ButtonStart = 0x1000,
ButtonSelect = 0x2000,
};
enum class PadAxes : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
TriggerLeft,
TriggerRight,
Undefined,
};
enum class ControllerTypes {
None,
Wired,
Wireless,
};
struct GCController {
ControllerTypes type = ControllerTypes::None;
PadIdentifier identifier{};
bool enable_vibration = false;
u8 rumble_amplitude{};
std::array<u8, 6> axis_origin{};
u8 reset_origin_counter{};
};
using AdapterPayload = std::array<u8, 37>;
void UpdatePadType(std::size_t port, ControllerTypes pad_type);
void UpdateControllers(const AdapterPayload& adapter_payload);
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2);
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload);
void AdapterInputThread(std::stop_token stop_token);
void AdapterScanThread(std::stop_token stop_token);
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size);
/// For use in initialization, querying devices to find the adapter
bool Setup();
/// Returns true if we successfully gain access to GC Adapter
bool CheckDeviceAccess();
/// Captures GC Adapter endpoint address
/// Returns true if the endpoint was set correctly
bool GetGCEndpoint(libusb_device* device);
/// Returns true if there is a device connected to port
bool DeviceConnected(std::size_t port) const;
/// For shutting down, clear all data, join all threads, release usb
void Reset();
void UpdateVibrations();
/// Updates vibration state of all controllers
void SendVibrations();
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle;
std::array<GCController, 4> pads;
std::jthread adapter_input_thread;
std::jthread adapter_scan_thread;
bool restart_scan_thread{};
std::unique_ptr<LibUSBContext> libusb_ctx;
u8 input_endpoint{0};
u8 output_endpoint{0};
u8 input_error_counter{0};
u8 output_error_counter{0};
int vibration_counter{0};
bool rumble_enabled{true};
bool vibration_changed{true};
};
} // namespace InputCommon

View File

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/param_package.h"
#include "input_common/drivers/keyboard.h"
namespace InputCommon {
constexpr PadIdentifier key_identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
Keyboard::Keyboard(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(key_identifier);
}
void Keyboard::PressKey(int key_code) {
SetButton(key_identifier, key_code, true);
}
void Keyboard::ReleaseKey(int key_code) {
SetButton(key_identifier, key_code, false);
}
void Keyboard::ReleaseAllKeys() {
ResetButtonState();
}
std::vector<Common::ParamPackage> Keyboard::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", "Keyboard Only"},
});
return devices;
}
} // namespace InputCommon

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Keyboard final : public InputEngine {
public:
explicit Keyboard(std::string input_engine_);
/**
* Sets the status of all buttons bound with the key to pressed
* @param key_code the code of the key to press
*/
void PressKey(int key_code);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseKey(int key_code);
/// Sets all keys to the non pressed state
void ReleaseAllKeys();
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
};
} // namespace InputCommon

View File

@ -0,0 +1,183 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include <fmt/format.h>
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "input_common/drivers/mouse.h"
namespace InputCommon {
constexpr int mouse_axis_x = 0;
constexpr int mouse_axis_y = 1;
constexpr int wheel_axis_x = 2;
constexpr int wheel_axis_y = 3;
constexpr int motion_wheel_y = 4;
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
constexpr PadIdentifier real_mouse_identifier = {
.guid = Common::UUID{},
.port = 1,
.pad = 0,
};
constexpr PadIdentifier touch_identifier = {
.guid = Common::UUID{},
.port = 2,
.pad = 0,
};
Mouse::Mouse(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
PreSetAxis(identifier, mouse_axis_x);
PreSetAxis(identifier, mouse_axis_y);
PreSetAxis(identifier, wheel_axis_x);
PreSetAxis(identifier, wheel_axis_y);
PreSetAxis(identifier, motion_wheel_y);
PreSetAxis(touch_identifier, mouse_axis_x);
PreSetAxis(touch_identifier, mouse_axis_y);
update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
}
void Mouse::UpdateThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("Mouse");
constexpr int update_time = 10;
while (!stop_token.stop_requested()) {
SetAxis(identifier, motion_wheel_y, 0.0f);
std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
}
}
void Mouse::Move(int x, int y, int, int) {
if (button_pressed) {
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
const float sensitivity = /*Settings::values.mouse_panning_sensitivity.GetValue()*/50 * 0.0012f;
SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * sensitivity);
SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * sensitivity);
last_motion_change = {
static_cast<float>(-mouse_move.y) / 50.0f,
static_cast<float>(-mouse_move.x) / 50.0f,
last_motion_change.z,
};
}
}
void Mouse::MouseMove(f32 touch_x, f32 touch_y) {
SetAxis(real_mouse_identifier, mouse_axis_x, touch_x);
SetAxis(real_mouse_identifier, mouse_axis_y, touch_y);
}
void Mouse::TouchMove(f32 touch_x, f32 touch_y) {
SetAxis(touch_identifier, mouse_axis_x, touch_x);
SetAxis(touch_identifier, mouse_axis_y, touch_y);
}
void Mouse::PressButton(int x, int y, MouseButton button) {
SetButton(identifier, static_cast<int>(button), true);
// Set initial analog parameters
mouse_origin = {x, y};
last_mouse_position = {x, y};
button_pressed = true;
}
void Mouse::PressMouseButton(MouseButton button) {
SetButton(real_mouse_identifier, static_cast<int>(button), true);
}
void Mouse::PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button) {
SetAxis(touch_identifier, mouse_axis_x, touch_x);
SetAxis(touch_identifier, mouse_axis_y, touch_y);
SetButton(touch_identifier, static_cast<int>(button), true);
}
void Mouse::ReleaseButton(MouseButton button) {
SetButton(identifier, static_cast<int>(button), false);
SetButton(real_mouse_identifier, static_cast<int>(button), false);
SetButton(touch_identifier, static_cast<int>(button), false);
button_pressed = false;
}
void Mouse::MouseWheelChange(int x, int y) {
wheel_position.x += x;
wheel_position.y += y;
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
SetAxis(identifier, motion_wheel_y, static_cast<f32>(y) / 100.0f);
}
void Mouse::ReleaseAllButtons() {
ResetButtonState();
button_pressed = false;
}
std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", "Keyboard/Mouse"},
});
return devices;
}
AnalogMapping Mouse::GetAnalogMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
// Only overwrite different buttons from default
AnalogMapping mapping = {};
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("axis_x", 0);
right_analog_params.Set("axis_y", 1);
right_analog_params.Set("threshold", 0.5f);
right_analog_params.Set("range", 1.0f);
right_analog_params.Set("deadzone", 0.0f);
mapping.insert_or_assign(Settings::NativeAnalog::CStick, std::move(right_analog_params));
return mapping;
}
Common::Input::ButtonNames Mouse::GetUIButtonName(const Common::ParamPackage& params) const {
const auto button = static_cast<MouseButton>(params.Get("button", 0));
switch (button) {
case MouseButton::Left:
return Common::Input::ButtonNames::ButtonLeft;
case MouseButton::Right:
return Common::Input::ButtonNames::ButtonRight;
case MouseButton::Wheel:
return Common::Input::ButtonNames::ButtonMouseWheel;
case MouseButton::Backward:
return Common::Input::ButtonNames::ButtonBackward;
case MouseButton::Forward:
return Common::Input::ButtonNames::ButtonForward;
case MouseButton::Task:
return Common::Input::ButtonNames::ButtonTask;
case MouseButton::Extra:
return Common::Input::ButtonNames::ButtonExtra;
case MouseButton::Undefined:
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames Mouse::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
} // namespace InputCommon

View File

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <thread>
#include "common/vector_math.h"
#include "input_common/input_engine.h"
namespace InputCommon {
enum class MouseButton {
Left,
Right,
Wheel,
Backward,
Forward,
Task,
Extra,
Undefined,
};
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Mouse final : public InputEngine {
public:
explicit Mouse(std::string input_engine_);
/**
* Signals that mouse has moved.
* @param x the x-coordinate of the cursor
* @param y the y-coordinate of the cursor
* @param center_x the x-coordinate of the middle of the screen
* @param center_y the y-coordinate of the middle of the screen
*/
void Move(int x, int y, int center_x, int center_y);
/**
* Signals that real mouse has moved.
* @param x the absolute position on the touchscreen of the cursor
* @param y the absolute position on the touchscreen of the cursor
*/
void MouseMove(f32 touch_x, f32 touch_y);
/**
* Signals that touch finger has moved.
* @param x the absolute position on the touchscreen of the cursor
* @param y the absolute position on the touchscreen of the cursor
*/
void TouchMove(f32 touch_x, f32 touch_y);
/**
* Sets the status of a button to pressed
* @param x the x-coordinate of the cursor
* @param y the y-coordinate of the cursor
* @param button the id of the button to press
*/
void PressButton(int x, int y, MouseButton button);
/**
* Sets the status of a mouse button to pressed
* @param button the id of the button to press
*/
void PressMouseButton(MouseButton button);
/**
* Sets the status of touch finger to pressed
* @param x the absolute position on the touchscreen of the cursor
* @param y the absolute position on the touchscreen of the cursor
* @param button the id of the button to press
*/
void PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseButton(MouseButton button);
/**
* Sets the status of the mouse wheel
* @param x delta movement in the x direction
* @param y delta movement in the y direction
*/
void MouseWheelChange(int x, int y);
void ReleaseAllButtons();
std::vector<Common::ParamPackage> GetInputDevices() const override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
private:
void UpdateThread(std::stop_token stop_token);
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
Common::Vec2<int> mouse_origin;
Common::Vec2<int> last_mouse_position;
Common::Vec2<float> last_mouse_change;
Common::Vec3<float> last_motion_change;
Common::Vec2<int> wheel_position;
bool button_pressed;
std::jthread update_thread;
};
} // namespace InputCommon

View File

@ -0,0 +1,818 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "common/math_util.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "common/vector_math.h"
#include "input_common/drivers/sdl_driver.h"
namespace InputCommon {
namespace {
Common::UUID GetGUID(SDL_Joystick* joystick) {
const SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
std::array<u8, 16> data{};
std::memcpy(data.data(), guid.data, sizeof(data));
// Clear controller name crc
std::memset(data.data() + 2, 0, sizeof(u16));
return Common::UUID{data};
}
} // Anonymous namespace
static int SDLEventWatcher(void* user_data, SDL_Event* event) {
auto* const sdl_state = static_cast<SDLDriver*>(user_data);
sdl_state->HandleGameControllerEvent(*event);
return 0;
}
class SDLJoystick {
public:
SDLJoystick(Common::UUID guid_, int port_, SDL_Joystick* joystick,
SDL_GameController* game_controller)
: guid{guid_}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose},
sdl_controller{game_controller, &SDL_GameControllerClose} {
EnableMotion();
}
void EnableMotion() {
if (sdl_controller) {
SDL_GameController* controller = sdl_controller.get();
has_accel = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE;
has_gyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE;
if (has_accel) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
}
if (has_gyro) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
}
}
}
bool HasGyro() const {
return has_gyro;
}
bool HasAccel() const {
return has_accel;
}
bool UpdateMotion(SDL_ControllerSensorEvent event) {
constexpr float gravity_constant = 9.80665f;
std::scoped_lock lock{mutex};
const u64 time_difference = event.timestamp - last_motion_update;
last_motion_update = event.timestamp;
switch (event.sensor) {
case SDL_SENSOR_ACCEL: {
motion.accel_x = -event.data[0] / gravity_constant;
motion.accel_y = event.data[2] / gravity_constant;
motion.accel_z = -event.data[1] / gravity_constant;
break;
}
case SDL_SENSOR_GYRO: {
motion.gyro_x = event.data[0] / (Common::PI * 2);
motion.gyro_y = -event.data[2] / (Common::PI * 2);
motion.gyro_z = event.data[1] / (Common::PI * 2);
break;
}
}
// Ignore duplicated timestamps
if (time_difference == 0) {
return false;
}
motion.delta_timestamp = time_difference * 1000;
return true;
}
const BasicMotion& GetMotion() const {
return motion;
}
/**
* The Pad identifier of the joystick
*/
const PadIdentifier GetPadIdentifier() const {
return {
.guid = guid,
.port = static_cast<std::size_t>(port),
.pad = 0,
};
}
/**
* The guid of the joystick
*/
const Common::UUID& GetGUID() const {
return guid;
}
/**
* The number of joystick from the same type that were connected before this joystick
*/
int GetPort() const {
return port;
}
SDL_Joystick* GetSDLJoystick() const {
return sdl_joystick.get();
}
SDL_GameController* GetSDLGameController() const {
return sdl_controller.get();
}
void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) {
sdl_joystick.reset(joystick);
sdl_controller.reset(controller);
}
std::string GetControllerName() const {
if (sdl_controller) {
switch (SDL_GameControllerGetType(sdl_controller.get())) {
case SDL_CONTROLLER_TYPE_XBOX360:
return "Xbox 360 Controller";
case SDL_CONTROLLER_TYPE_XBOXONE:
return "Xbox One Controller";
case SDL_CONTROLLER_TYPE_PS3:
return "DualShock 3 Controller";
case SDL_CONTROLLER_TYPE_PS4:
return "DualShock 4 Controller";
case SDL_CONTROLLER_TYPE_PS5:
return "DualSense Controller";
default:
break;
}
const auto name = SDL_GameControllerName(sdl_controller.get());
if (name) {
return name;
}
}
if (sdl_joystick) {
const auto name = SDL_JoystickName(sdl_joystick.get());
if (name) {
return name;
}
}
return "Unknown";
}
private:
Common::UUID guid;
int port;
std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)> sdl_joystick;
std::unique_ptr<SDL_GameController, decltype(&SDL_GameControllerClose)> sdl_controller;
mutable std::mutex mutex;
u64 last_motion_update{};
bool has_gyro{false};
bool has_accel{false};
BasicMotion motion;
};
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGUID(const Common::UUID& guid, int port) {
std::scoped_lock lock{joystick_map_mutex};
const auto it = joystick_map.find(guid);
if (it != joystick_map.end()) {
while (it->second.size() <= static_cast<std::size_t>(port)) {
auto joystick = std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()),
nullptr, nullptr);
it->second.emplace_back(std::move(joystick));
}
return it->second[static_cast<std::size_t>(port)];
}
auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr, nullptr);
return joystick_map[guid].emplace_back(std::move(joystick));
}
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGUID(const std::string& guid, int port) {
return GetSDLJoystickByGUID(Common::UUID{guid}, port);
}
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) {
auto sdl_joystick = SDL_JoystickFromInstanceID(sdl_id);
const auto guid = GetGUID(sdl_joystick);
std::scoped_lock lock{joystick_map_mutex};
const auto map_it = joystick_map.find(guid);
if (map_it == joystick_map.end()) {
return nullptr;
}
const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(),
[&sdl_joystick](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick;
});
if (vec_it == map_it->second.end()) {
return nullptr;
}
return *vec_it;
}
void SDLDriver::InitJoystick(int joystick_index) {
SDL_Joystick* sdl_joystick = SDL_JoystickOpen(joystick_index);
SDL_GameController* sdl_gamecontroller = nullptr;
if (SDL_IsGameController(joystick_index)) {
sdl_gamecontroller = SDL_GameControllerOpen(joystick_index);
}
if (!sdl_joystick) {
LOG_ERROR(Input, "Failed to open joystick {}", joystick_index);
return;
}
const auto guid = GetGUID(sdl_joystick);
std::scoped_lock lock{joystick_map_mutex};
if (joystick_map.find(guid) == joystick_map.end()) {
auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
joystick->EnableMotion();
joystick_map[guid].emplace_back(std::move(joystick));
return;
}
auto& joystick_guid_list = joystick_map[guid];
const auto joystick_it =
std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
[](const auto& joystick) { return !joystick->GetSDLJoystick(); });
if (joystick_it != joystick_guid_list.end()) {
(*joystick_it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller);
(*joystick_it)->EnableMotion();
return;
}
const int port = static_cast<int>(joystick_guid_list.size());
auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
joystick->EnableMotion();
joystick_guid_list.emplace_back(std::move(joystick));
}
void SDLDriver::CloseJoystick(SDL_Joystick* sdl_joystick) {
const auto guid = GetGUID(sdl_joystick);
std::scoped_lock lock{joystick_map_mutex};
// This call to guid is safe since the joystick is guaranteed to be in the map
const auto& joystick_guid_list = joystick_map[guid];
const auto joystick_it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
[&sdl_joystick](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick;
});
if (joystick_it != joystick_guid_list.end()) {
(*joystick_it)->SetSDLJoystick(nullptr, nullptr);
}
}
void SDLDriver::PumpEvents() const {
if (initialized) {
SDL_PumpEvents();
}
}
void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
switch (event.type) {
case SDL_JOYBUTTONUP: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, false);
}
break;
}
case SDL_JOYBUTTONDOWN: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, true);
}
break;
}
case SDL_JOYHATMOTION: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jhat.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetHatButton(identifier, event.jhat.hat, event.jhat.value);
}
break;
}
case SDL_JOYAXISMOTION: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jaxis.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetAxis(identifier, event.jaxis.axis, event.jaxis.value / 32767.0f);
}
break;
}
case SDL_CONTROLLERSENSORUPDATE: {
if (auto joystick = GetSDLJoystickBySDLID(event.csensor.which)) {
if (joystick->UpdateMotion(event.csensor)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetMotion(identifier, 0, joystick->GetMotion());
}
}
break;
}
case SDL_JOYDEVICEREMOVED:
LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which);
CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which));
break;
case SDL_JOYDEVICEADDED:
LOG_DEBUG(Input, "Controller connected with device index {}", event.jdevice.which);
InitJoystick(event.jdevice.which);
break;
}
}
void SDLDriver::CloseJoysticks() {
std::scoped_lock lock{joystick_map_mutex};
joystick_map.clear();
}
SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
// Prevent SDL from adding undesired axis
SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
// Enable HIDAPI rumble. This prevents SDL from disabling motion on PS4 and PS5 controllers
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// Use hidapi driver for joycons. This will allow joycons to be detected as a GameController and
// not a generic one
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
// Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native
// driver on Linux.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
// If the frontend is going to manage the event loop, then we don't start one here
start_thread = SDL_WasInit(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) == 0;
if (start_thread && SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) {
LOG_CRITICAL(Input, "SDL_Init failed with: {}", SDL_GetError());
return;
}
SDL_AddEventWatch(&SDLEventWatcher, this);
initialized = true;
// Because the events for joystick connection happens before we have our event watcher added, we
// can just open all the joysticks right here
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
InitJoystick(i);
}
}
SDLDriver::~SDLDriver() {
CloseJoysticks();
SDL_DelEventWatch(&SDLEventWatcher, this);
initialized = false;
if (start_thread) {
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
}
}
std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
std::unordered_map<int, std::shared_ptr<SDLJoystick>> joycon_pairs;
for (const auto& [key, value] : joystick_map) {
for (const auto& joystick : value) {
if (!joystick->GetSDLJoystick()) {
continue;
}
const std::string name =
fmt::format("{} {}", joystick->GetControllerName(), joystick->GetPort());
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", joystick->GetGUID().RawString()},
{"port", std::to_string(joystick->GetPort())},
});
}
}
return devices;
}
Common::ParamPackage SDLDriver::BuildAnalogParamPackageForButton(int port, const Common::UUID& guid,
s32 axis, float value) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", guid.RawString());
params.Set("axis", axis);
params.Set("threshold", "0.5");
params.Set("invert", value < 0 ? "-" : "+");
return params;
}
Common::ParamPackage SDLDriver::BuildButtonParamPackageForButton(int port, const Common::UUID& guid,
s32 button) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", guid.RawString());
params.Set("button", button);
return params;
}
Common::ParamPackage SDLDriver::BuildHatParamPackageForButton(int port, const Common::UUID& guid,
s32 hat, u8 value) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", guid.RawString());
params.Set("hat", hat);
params.Set("direction", GetHatButtonName(value));
return params;
}
Common::ParamPackage SDLDriver::BuildMotionParam(int port, const Common::UUID& guid) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("motion", 0);
params.Set("port", port);
params.Set("guid", guid.RawString());
return params;
}
Common::ParamPackage SDLDriver::BuildParamPackageForBinding(
int port, const Common::UUID& guid, const SDL_GameControllerButtonBind& binding) const {
switch (binding.bindType) {
case SDL_CONTROLLER_BINDTYPE_NONE:
break;
case SDL_CONTROLLER_BINDTYPE_AXIS:
return BuildAnalogParamPackageForButton(port, guid, binding.value.axis);
case SDL_CONTROLLER_BINDTYPE_BUTTON:
return BuildButtonParamPackageForButton(port, guid, binding.value.button);
case SDL_CONTROLLER_BINDTYPE_HAT:
return BuildHatParamPackageForButton(port, guid, binding.value.hat.hat,
static_cast<u8>(binding.value.hat.hat_mask));
}
return {};
}
Common::ParamPackage SDLDriver::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y, float offset_x,
float offset_y) const {
Common::ParamPackage params;
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("axis_x", axis_x);
params.Set("axis_y", axis_y);
params.Set("offset_x", offset_x);
params.Set("offset_y", offset_y);
params.Set("invert_x", "+");
params.Set("invert_y", "+");
return params;
}
ButtonMapping SDLDriver::GetButtonMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
// This list is missing ZL/ZR since those are not considered buttons in SDL GameController.
// We will add those afterwards
// This list also excludes Screenshot since theres not really a mapping for that
ButtonBindings switch_to_sdl_button;
if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) {
switch_to_sdl_button = GetNintendoButtonBinding(joystick);
} else {
switch_to_sdl_button = GetDefaultButtonBinding();
}
// Add the missing bindings for ZL/ZR
static constexpr ZButtonBindings switch_to_sdl_axis{{
{Settings::NativeButton::ZL, SDL_CONTROLLER_AXIS_TRIGGERLEFT},
{Settings::NativeButton::ZR, SDL_CONTROLLER_AXIS_TRIGGERRIGHT},
}};
// Parameters contain two joysticks return dual
if (params.Has("guid2")) {
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
if (joystick2->GetSDLGameController() != nullptr) {
return GetDualControllerMapping(joystick, joystick2, switch_to_sdl_button,
switch_to_sdl_axis);
}
}
return GetSingleControllerMapping(joystick, switch_to_sdl_button, switch_to_sdl_axis);
}
ButtonBindings SDLDriver::GetDefaultButtonBinding() const {
return {
std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_B},
{Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_A},
{Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_Y},
{Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_X},
{Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::R, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Start, SDL_CONTROLLER_BUTTON_START},
{Settings::NativeButton::Select, SDL_CONTROLLER_BUTTON_BACK},
{Settings::NativeButton::DLeft, SDL_CONTROLLER_BUTTON_DPAD_LEFT},
{Settings::NativeButton::DUp, SDL_CONTROLLER_BUTTON_DPAD_UP},
{Settings::NativeButton::DRight, SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
{Settings::NativeButton::DDown, SDL_CONTROLLER_BUTTON_DPAD_DOWN},
{Settings::NativeButton::ZL, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::ZR, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
};
}
ButtonBindings SDLDriver::GetNintendoButtonBinding(
const std::shared_ptr<SDLJoystick>& joystick) const {
return {
std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_A},
{Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_B},
{Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_X},
{Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_Y},
{Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::R, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Start, SDL_CONTROLLER_BUTTON_START},
{Settings::NativeButton::Select, SDL_CONTROLLER_BUTTON_BACK},
{Settings::NativeButton::DLeft, SDL_CONTROLLER_BUTTON_DPAD_LEFT},
{Settings::NativeButton::DUp, SDL_CONTROLLER_BUTTON_DPAD_UP},
{Settings::NativeButton::DRight, SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
{Settings::NativeButton::DDown, SDL_CONTROLLER_BUTTON_DPAD_DOWN},
{Settings::NativeButton::ZL, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::ZR, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
};
}
ButtonMapping SDLDriver::GetSingleControllerMapping(
const std::shared_ptr<SDLJoystick>& joystick, const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const {
ButtonMapping mapping;
mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
auto* controller = joystick->GetSDLGameController();
for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
ButtonMapping SDLDriver::GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const std::shared_ptr<SDLJoystick>& joystick2,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const {
ButtonMapping mapping;
mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
auto* controller = joystick->GetSDLGameController();
auto* controller2 = joystick2->GetSDLGameController();
for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
if (IsButtonOnLeftSide(switch_button)) {
const auto& binding = SDL_GameControllerGetBindForButton(controller2, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
continue;
}
const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
if (IsButtonOnLeftSide(switch_button)) {
const auto& binding = SDL_GameControllerGetBindForAxis(controller2, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
continue;
}
const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
bool SDLDriver::IsButtonOnLeftSide(Settings::NativeButton::Values button) const {
switch (button) {
case Settings::NativeButton::DDown:
case Settings::NativeButton::DLeft:
case Settings::NativeButton::DRight:
case Settings::NativeButton::DUp:
case Settings::NativeButton::L:
case Settings::NativeButton::Start:
case Settings::NativeButton::ZL:
return true;
default:
return false;
}
}
AnalogMapping SDLDriver::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
AnalogMapping mapping = {};
const auto& binding_left_x =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX);
const auto& binding_left_y =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
if (params.Has("guid2")) {
const auto identifier = joystick2->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_left_x.value.axis);
PreSetAxis(identifier, binding_left_y.value.axis);
const auto left_offset_x = -GetAxis(identifier, binding_left_x.value.axis);
const auto left_offset_y = GetAxis(identifier, binding_left_y.value.axis);
mapping.insert_or_assign(Settings::NativeAnalog::CirclePad,
BuildParamPackageForAnalog(identifier, binding_left_x.value.axis,
binding_left_y.value.axis,
left_offset_x, left_offset_y));
} else {
const auto identifier = joystick->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_left_x.value.axis);
PreSetAxis(identifier, binding_left_y.value.axis);
const auto left_offset_x = -GetAxis(identifier, binding_left_x.value.axis);
const auto left_offset_y = GetAxis(identifier, binding_left_y.value.axis);
mapping.insert_or_assign(Settings::NativeAnalog::CirclePad,
BuildParamPackageForAnalog(identifier, binding_left_x.value.axis,
binding_left_y.value.axis,
left_offset_x, left_offset_y));
}
const auto& binding_right_x =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
const auto& binding_right_y =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
const auto identifier = joystick->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_right_x.value.axis);
PreSetAxis(identifier, binding_right_y.value.axis);
const auto right_offset_x = -GetAxis(identifier, binding_right_x.value.axis);
const auto right_offset_y = GetAxis(identifier, binding_right_y.value.axis);
mapping.insert_or_assign(Settings::NativeAnalog::CStick,
BuildParamPackageForAnalog(identifier, binding_right_x.value.axis,
binding_right_y.value.axis, right_offset_x,
right_offset_y));
return mapping;
}
MotionMapping SDLDriver::GetMotionMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
MotionMapping mapping = {};
joystick->EnableMotion();
if (joystick->HasGyro() || joystick->HasAccel()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionRight,
BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
}
if (params.Has("guid2")) {
joystick2->EnableMotion();
if (joystick2->HasGyro() || joystick2->HasAccel()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
BuildMotionParam(joystick2->GetPort(), joystick2->GetGUID()));
}
} else {
if (joystick->HasGyro() || joystick->HasAccel()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
}
}
return mapping;
}
Common::Input::ButtonNames SDLDriver::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
// TODO(German77): Find how to substitue the values for real button names
return Common::Input::ButtonNames::Value;
}
if (params.Has("hat")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
std::string SDLDriver::GetHatButtonName(u8 direction_value) const {
switch (direction_value) {
case SDL_HAT_UP:
return "up";
case SDL_HAT_DOWN:
return "down";
case SDL_HAT_LEFT:
return "left";
case SDL_HAT_RIGHT:
return "right";
default:
return {};
}
}
u8 SDLDriver::GetHatButtonId(const std::string& direction_name) const {
Uint8 direction;
if (direction_name == "up") {
direction = SDL_HAT_UP;
} else if (direction_name == "down") {
direction = SDL_HAT_DOWN;
} else if (direction_name == "left") {
direction = SDL_HAT_LEFT;
} else if (direction_name == "right") {
direction = SDL_HAT_RIGHT;
} else {
direction = 0;
}
return direction;
}
bool SDLDriver::IsStickInverted(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return false;
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
if (joystick == nullptr) {
return false;
}
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return false;
}
const auto& axis_x = params.Get("axis_x", 0);
const auto& axis_y = params.Get("axis_y", 0);
const auto& binding_left_x =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX);
const auto& binding_right_x =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
const auto& binding_left_y =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
const auto& binding_right_y =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
if (axis_x != binding_left_y.value.axis && axis_x != binding_right_y.value.axis) {
return false;
}
if (axis_y != binding_left_x.value.axis && axis_y != binding_right_x.value.axis) {
return false;
}
return true;
}
} // namespace InputCommon

View File

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <SDL.h>
#include "common/common_types.h"
#include "common/threadsafe_queue.h"
#include "input_common/input_engine.h"
union SDL_Event;
using SDL_GameController = struct _SDL_GameController;
using SDL_Joystick = struct _SDL_Joystick;
using SDL_JoystickID = s32;
namespace InputCommon {
class SDLJoystick;
using ButtonBindings =
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 18>;
using ZButtonBindings =
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
class SDLDriver : public InputEngine {
public:
/// Initializes and registers SDL device factories
explicit SDLDriver(std::string input_engine_);
/// Unregisters SDL device factories and shut them down.
~SDLDriver() override;
void PumpEvents() const;
/// Handle SDL_Events for joysticks from SDL_PollEvent
void HandleGameControllerEvent(const SDL_Event& event);
/// Get the nth joystick with the corresponding GUID
std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
/**
* Check how many identical joysticks (by guid) were connected before the one with sdl_id and so
* tie it to a SDLJoystick with the same guid and that port
*/
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const Common::UUID& guid, int port);
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const std::string& guid, int port);
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
std::string GetHatButtonName(u8 direction_value) const override;
u8 GetHatButtonId(const std::string& direction_name) const override;
bool IsStickInverted(const Common::ParamPackage& params) override;
private:
void InitJoystick(int joystick_index);
void CloseJoystick(SDL_Joystick* sdl_joystick);
/// Needs to be called before SDL_QuitSubSystem.
void CloseJoysticks();
Common::ParamPackage BuildAnalogParamPackageForButton(int port, const Common::UUID& guid,
s32 axis, float value = 0.1f) const;
Common::ParamPackage BuildButtonParamPackageForButton(int port, const Common::UUID& guid,
s32 button) const;
Common::ParamPackage BuildHatParamPackageForButton(int port, const Common::UUID& guid, s32 hat,
u8 value) const;
Common::ParamPackage BuildMotionParam(int port, const Common::UUID& guid) const;
Common::ParamPackage BuildParamPackageForBinding(
int port, const Common::UUID& guid, const SDL_GameControllerButtonBind& binding) const;
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y, float offset_x,
float offset_y) const;
/// Returns the default button bindings list for generic controllers
ButtonBindings GetDefaultButtonBinding() const;
/// Returns the default button bindings list for nintendo controllers
ButtonBindings GetNintendoButtonBinding(const std::shared_ptr<SDLJoystick>& joystick) const;
/// Returns the button mappings from a single controller
ButtonMapping GetSingleControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const;
/// Returns the button mappings from two different controllers
ButtonMapping GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const std::shared_ptr<SDLJoystick>& joystick2,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const;
/// Returns true if the button is on the left joycon
bool IsButtonOnLeftSide(Settings::NativeButton::Values button) const;
/// Map of GUID of a list of corresponding virtual Joysticks
std::unordered_map<Common::UUID, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map;
std::mutex joystick_map_mutex;
bool start_thread = false;
std::atomic<bool> initialized = false;
};
} // namespace InputCommon

View File

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/param_package.h"
#include "input_common/drivers/touch_screen.h"
namespace InputCommon {
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
TouchScreen::TouchScreen(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
ReleaseAllTouch();
}
void TouchScreen::TouchMoved(float x, float y, std::size_t finger_id) {
const auto index = GetIndexFromFingerId(finger_id);
if (!index) {
// Touch doesn't exist handle it as a new one
TouchPressed(x, y, finger_id);
return;
}
const auto i = index.value();
fingers[i].is_active = true;
SetButton(identifier, static_cast<int>(i), true);
SetAxis(identifier, static_cast<int>(i * 2), x);
SetAxis(identifier, static_cast<int>(i * 2 + 1), y);
}
void TouchScreen::TouchPressed(float x, float y, std::size_t finger_id) {
if (GetIndexFromFingerId(finger_id)) {
// Touch already exist. Just update the data
TouchMoved(x, y, finger_id);
return;
}
const auto index = GetNextFreeIndex();
if (!index) {
// No free entries. Ignore input
return;
}
const auto i = index.value();
fingers[i].is_enabled = true;
fingers[i].finger_id = finger_id;
TouchMoved(x, y, finger_id);
}
void TouchScreen::TouchReleased(std::size_t finger_id) {
const auto index = GetIndexFromFingerId(finger_id);
if (!index) {
return;
}
const auto i = index.value();
fingers[i].is_enabled = false;
SetButton(identifier, static_cast<int>(i), false);
SetAxis(identifier, static_cast<int>(i * 2), 0.0f);
SetAxis(identifier, static_cast<int>(i * 2 + 1), 0.0f);
}
std::optional<std::size_t> TouchScreen::GetIndexFromFingerId(std::size_t finger_id) const {
for (std::size_t index = 0; index < MAX_FINGER_COUNT; ++index) {
const auto& finger = fingers[index];
if (!finger.is_enabled) {
continue;
}
if (finger.finger_id == finger_id) {
return index;
}
}
return std::nullopt;
}
std::optional<std::size_t> TouchScreen::GetNextFreeIndex() const {
for (std::size_t index = 0; index < MAX_FINGER_COUNT; ++index) {
if (!fingers[index].is_enabled) {
return index;
}
}
return std::nullopt;
}
void TouchScreen::ClearActiveFlag() {
for (auto& finger : fingers) {
finger.is_active = false;
}
}
void TouchScreen::ReleaseInactiveTouch() {
for (const auto& finger : fingers) {
if (!finger.is_active) {
TouchReleased(finger.finger_id);
}
}
}
void TouchScreen::ReleaseAllTouch() {
for (const auto& finger : fingers) {
if (finger.is_enabled) {
TouchReleased(finger.finger_id);
}
}
}
} // namespace InputCommon

View File

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A touch device factory representing a touch screen. It receives touch events and forward them
* to all touch devices it created.
*/
class TouchScreen final : public InputEngine {
public:
explicit TouchScreen(std::string input_engine_);
/**
* Signals that touch has moved and marks this touch point as active
* @param x new horizontal position
* @param y new vertical position
* @param finger_id of the touch point to be updated
*/
void TouchMoved(float x, float y, std::size_t finger_id);
/**
* Signals and creates a new touch point with this finger id
* @param x starting horizontal position
* @param y starting vertical position
* @param finger_id to be assigned to the new touch point
*/
void TouchPressed(float x, float y, std::size_t finger_id);
/**
* Signals and resets the touch point related to the this finger id
* @param finger_id to be released
*/
void TouchReleased(std::size_t finger_id);
/// Resets the active flag for each touch point
void ClearActiveFlag();
/// Releases all touch that haven't been marked as active
void ReleaseInactiveTouch();
/// Resets all inputs to their initial value
void ReleaseAllTouch();
private:
static constexpr std::size_t MAX_FINGER_COUNT = 16;
struct TouchStatus {
std::size_t finger_id{};
bool is_enabled{};
bool is_active{};
};
std::optional<std::size_t> GetIndexFromFingerId(std::size_t finger_id) const;
std::optional<std::size_t> GetNextFreeIndex() const;
std::array<TouchStatus, MAX_FINGER_COUNT> fingers{};
};
} // namespace InputCommon

View File

@ -0,0 +1,588 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <random>
#include <boost/asio.hpp>
#include <fmt/format.h>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/helpers/udp_protocol.h"
using boost::asio::ip::udp;
namespace InputCommon::CemuhookUDP {
struct SocketCallback {
std::function<void(Response::Version)> version;
std::function<void(Response::PortInfo)> port_info;
std::function<void(Response::PadData)> pad_data;
};
class Socket {
public:
using clock = std::chrono::system_clock;
explicit Socket(const std::string& host, u16 port, u8 pad_index, SocketCallback callback_)
: callback(std::move(callback_)), timer(io_service),
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()),
pad_index(pad_index) {
boost::system::error_code ec{};
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
if (ec.value() != boost::system::errc::success) {
LOG_ERROR(Input, "Invalid IPv4 address \"{}\" provided to socket", host);
ipv4 = boost::asio::ip::address_v4{};
}
send_endpoint = {udp::endpoint(ipv4, port)};
}
void Stop() {
io_service.stop();
}
void Loop() {
io_service.run();
}
void StartSend(const clock::time_point& from) {
timer.expires_at(from + std::chrono::seconds(3));
timer.async_wait([this](const boost::system::error_code& error) { HandleSend(error); });
}
void StartReceive() {
socket.async_receive_from(
boost::asio::buffer(receive_buffer), receive_endpoint,
[this](const boost::system::error_code& error, std::size_t bytes_transferred) {
HandleReceive(error, bytes_transferred);
});
}
private:
u32 GenerateRandomClientId() const {
std::random_device device;
return device();
}
void HandleReceive(const boost::system::error_code&, std::size_t bytes_transferred) {
if (auto type = Response::Validate(receive_buffer.data(), bytes_transferred)) {
switch (*type) {
case Type::Version: {
Response::Version version;
std::memcpy(&version, &receive_buffer[sizeof(Header)], sizeof(Response::Version));
callback.version(std::move(version));
break;
}
case Type::PortInfo: {
Response::PortInfo port_info;
std::memcpy(&port_info, &receive_buffer[sizeof(Header)],
sizeof(Response::PortInfo));
callback.port_info(std::move(port_info));
break;
}
case Type::PadData: {
Response::PadData pad_data;
std::memcpy(&pad_data, &receive_buffer[sizeof(Header)], sizeof(Response::PadData));
callback.pad_data(std::move(pad_data));
break;
}
}
}
StartReceive();
}
void HandleSend(const boost::system::error_code&) {
boost::system::error_code _ignored{};
// Send a request for getting port info for the pad
const Request::PortInfo port_info{1, {pad_index, 0, 0, 0}};
const auto port_message = Request::Create(port_info, client_id);
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint, {}, _ignored);
// Send a request for getting pad data for the pad
const Request::PadData pad_data{Request::RegisterFlags::PadID, pad_index, EMPTY_MAC_ADDRESS};
const auto pad_message = Request::Create(pad_data, client_id);
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint, {}, _ignored);
StartSend(timer.expiry());
}
SocketCallback callback;
boost::asio::io_service io_service;
boost::asio::basic_waitable_timer<clock> timer;
udp::socket socket;
const u32 client_id;
const u8 pad_index;
static constexpr std::size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>);
static constexpr std::size_t PAD_DATA_SIZE = sizeof(Message<Request::PadData>);
std::array<u8, PORT_INFO_SIZE> send_buffer1;
std::array<u8, PAD_DATA_SIZE> send_buffer2;
udp::endpoint send_endpoint;
std::array<u8, MAX_PACKET_SIZE> receive_buffer;
udp::endpoint receive_endpoint;
};
static void SocketLoop(Socket* socket) {
socket->StartReceive();
socket->StartSend(Socket::clock::now());
socket->Loop();
}
UDPClient::UDPClient(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
LOG_INFO(Input, "Udp Initialization started");
ReloadSockets();
}
UDPClient::~UDPClient() {
Reset();
}
UDPClient::ClientConnection::ClientConnection() = default;
UDPClient::ClientConnection::~ClientConnection() = default;
void UDPClient::ReloadSockets() {
Reset();
const std::string& host = Settings::values.udp_input_address.GetValue();
const u16 udp_input_port = Settings::values.udp_input_port.GetValue();
// TODO: Hook it up again!!!!!!!!!
const u8 pad_index = Settings::values.udp_pad_index.GetValue();
const std::size_t client_number = GetClientNumber(host, udp_input_port);
if (client_number != MAX_UDP_CLIENTS) {
LOG_ERROR(Input, "Duplicated UDP servers found");
return;
}
StartCommunication(0, host, udp_input_port, pad_index);
}
std::size_t UDPClient::GetClientNumber(std::string_view host, u16 port) const {
for (std::size_t client = 0; client < clients.size(); client++) {
if (clients[client].active == -1) {
continue;
}
if (clients[client].host == host && clients[client].port == port) {
return client;
}
}
return MAX_UDP_CLIENTS;
}
void UDPClient::OnVersion([[maybe_unused]] Response::Version data) {
LOG_TRACE(Input, "Version packet received: {}", data.version);
}
void UDPClient::OnPortInfo([[maybe_unused]] Response::PortInfo data) {
LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
}
void UDPClient::OnPadData(Response::PadData data, std::size_t client) {
const std::size_t pad_index = (client * PADS_PER_CLIENT) + data.info.id;
if (pad_index >= pads.size()) {
LOG_ERROR(Input, "Invalid pad id {}", data.info.id);
return;
}
LOG_TRACE(Input, "PadData packet received");
if (data.packet_counter == pads[pad_index].packet_sequence) {
LOG_WARNING(
Input,
"PadData packet dropped because its stale info. Current count: {} Packet count: {}",
pads[pad_index].packet_sequence, data.packet_counter);
pads[pad_index].connected = false;
return;
}
clients[client].active = 1;
pads[pad_index].connected = true;
pads[pad_index].packet_sequence = data.packet_counter;
const auto now = std::chrono::steady_clock::now();
const auto time_difference = static_cast<u64>(
std::chrono::duration_cast<std::chrono::microseconds>(now - pads[pad_index].last_update)
.count());
pads[pad_index].last_update = now;
// Gyroscope values are not it the correct scale from better joy.
// Dividing by 312 allows us to make one full turn = 1 turn
// This must be a configurable valued called sensitivity
const float gyro_scale = 1.0f / 312.0f;
const BasicMotion motion{
.gyro_x = data.gyro.pitch * gyro_scale,
.gyro_y = data.gyro.roll * gyro_scale,
.gyro_z = -data.gyro.yaw * gyro_scale,
.accel_x = data.accel.x,
.accel_y = -data.accel.z,
.accel_z = data.accel.y,
.delta_timestamp = time_difference,
};
const PadIdentifier identifier = GetPadIdentifier(pad_index);
SetMotion(identifier, 0, motion);
for (std::size_t id = 0; id < data.touch.size(); ++id) {
const auto touch_pad = data.touch[id];
const auto touch_axis_x_id =
static_cast<int>(id == 0 ? PadAxes::Touch1X : PadAxes::Touch2X);
const auto touch_axis_y_id =
static_cast<int>(id == 0 ? PadAxes::Touch1Y : PadAxes::Touch2Y);
const auto touch_button_id =
static_cast<int>(id == 0 ? PadButton::Touch1 : PadButton::Touch2);
// TODO: Use custom calibration per device
const Common::ParamPackage touch_param(Settings::values.touch_device.GetValue());
const u16 min_x = static_cast<u16>(touch_param.Get("min_x", 100));
const u16 min_y = static_cast<u16>(touch_param.Get("min_y", 50));
const u16 max_x = static_cast<u16>(touch_param.Get("max_x", 1800));
const u16 max_y = static_cast<u16>(touch_param.Get("max_y", 850));
const f32 x =
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.x), min_x, max_x) - min_x) /
static_cast<f32>(max_x - min_x);
const f32 y =
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.y), min_y, max_y) - min_y) /
static_cast<f32>(max_y - min_y);
if (touch_pad.is_active) {
SetAxis(identifier, touch_axis_x_id, x);
SetAxis(identifier, touch_axis_y_id, y);
SetButton(identifier, touch_button_id, true);
continue;
}
SetAxis(identifier, touch_axis_x_id, 0);
SetAxis(identifier, touch_axis_y_id, 0);
SetButton(identifier, touch_button_id, false);
}
SetAxis(identifier, static_cast<int>(PadAxes::LeftStickX),
(data.left_stick_x - 127.0f) / 127.0f);
SetAxis(identifier, static_cast<int>(PadAxes::LeftStickY),
(data.left_stick_y - 127.0f) / 127.0f);
SetAxis(identifier, static_cast<int>(PadAxes::RightStickX),
(data.right_stick_x - 127.0f) / 127.0f);
SetAxis(identifier, static_cast<int>(PadAxes::RightStickY),
(data.right_stick_y - 127.0f) / 127.0f);
static constexpr std::array<PadButton, 16> buttons{
PadButton::Share, PadButton::L3, PadButton::R3, PadButton::Options,
PadButton::Up, PadButton::Right, PadButton::Down, PadButton::Left,
PadButton::L2, PadButton::R2, PadButton::L1, PadButton::R1,
PadButton::Triangle, PadButton::Circle, PadButton::Cross, PadButton::Square};
for (std::size_t i = 0; i < buttons.size(); ++i) {
const bool button_status = (data.digital_button & (1U << i)) != 0;
const int button = static_cast<int>(buttons[i]);
SetButton(identifier, button, button_status);
}
SetButton(identifier, static_cast<int>(PadButton::Home), data.home != 0);
SetButton(identifier, static_cast<int>(PadButton::TouchHardPress), data.touch_hard_press != 0);
}
void UDPClient::StartCommunication(std::size_t client, const std::string& host, u16 port, u8 pad_index) {
SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
[this](Response::PortInfo info) { OnPortInfo(info); },
[this, client](Response::PadData data) { OnPadData(data, client); }};
LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
clients[client].uuid = GetHostUUID(host);
clients[client].host = host;
clients[client].port = port;
clients[client].active = 0;
clients[client].socket = std::make_unique<Socket>(host, port, pad_index, callback);
clients[client].thread = std::thread{SocketLoop, clients[client].socket.get()};
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
const PadIdentifier identifier = GetPadIdentifier(client * PADS_PER_CLIENT + index);
PreSetController(identifier);
}
}
PadIdentifier UDPClient::GetPadIdentifier(std::size_t pad_index) const {
const std::size_t client = pad_index / PADS_PER_CLIENT;
return {
.guid = clients[client].uuid,
.port = static_cast<std::size_t>(clients[client].port),
.pad = pad_index,
};
}
Common::UUID UDPClient::GetHostUUID(const std::string& host) const {
const auto ip = boost::asio::ip::make_address_v4(host);
const auto hex_host = fmt::format("00000000-0000-0000-0000-0000{:06x}", ip.to_uint());
return Common::UUID{hex_host};
}
void UDPClient::Reset() {
for (auto& client : clients) {
if (client.thread.joinable()) {
client.active = -1;
client.socket->Stop();
client.thread.join();
}
}
}
std::vector<Common::ParamPackage> UDPClient::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
return devices;
}
ButtonMapping UDPClient::GetButtonMappingForDevice(const Common::ParamPackage& params) {
// This list excludes any button that can't be really mapped
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 20>
switch_to_dsu_button = {
std::pair{Settings::NativeButton::A, PadButton::Circle},
{Settings::NativeButton::B, PadButton::Cross},
{Settings::NativeButton::X, PadButton::Triangle},
{Settings::NativeButton::Y, PadButton::Square},
{Settings::NativeButton::Start, PadButton::Options},
{Settings::NativeButton::Select, PadButton::Share},
{Settings::NativeButton::DLeft, PadButton::Left},
{Settings::NativeButton::DUp, PadButton::Up},
{Settings::NativeButton::DRight, PadButton::Right},
{Settings::NativeButton::DDown, PadButton::Down},
{Settings::NativeButton::L, PadButton::L1},
{Settings::NativeButton::R, PadButton::R1},
{Settings::NativeButton::ZL, PadButton::L2},
{Settings::NativeButton::ZR, PadButton::R2},
{Settings::NativeButton::ZL, PadButton::L2},
{Settings::NativeButton::ZR, PadButton::R2},
{Settings::NativeButton::Home, PadButton::Home},
};
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return {};
}
ButtonMapping mapping{};
for (const auto& [switch_button, dsu_button] : switch_to_dsu_button) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("guid", params.Get("guid", ""));
button_params.Set("port", params.Get("port", 0));
button_params.Set("pad", params.Get("pad", 0));
button_params.Set("button", static_cast<int>(dsu_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
return mapping;
}
AnalogMapping UDPClient::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return {};
}
AnalogMapping mapping = {};
Common::ParamPackage left_analog_params;
left_analog_params.Set("engine", GetEngineName());
left_analog_params.Set("guid", params.Get("guid", ""));
left_analog_params.Set("port", params.Get("port", 0));
left_analog_params.Set("pad", params.Get("pad", 0));
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::LeftStickX));
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::LeftStickY));
mapping.insert_or_assign(Settings::NativeAnalog::CirclePad, std::move(left_analog_params));
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("guid", params.Get("guid", ""));
right_analog_params.Set("port", params.Get("port", 0));
right_analog_params.Set("pad", params.Get("pad", 0));
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::RightStickX));
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::RightStickY));
mapping.insert_or_assign(Settings::NativeAnalog::CStick, std::move(right_analog_params));
return mapping;
}
MotionMapping UDPClient::GetMotionMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return {};
}
MotionMapping mapping = {};
Common::ParamPackage left_motion_params;
left_motion_params.Set("engine", GetEngineName());
left_motion_params.Set("guid", params.Get("guid", ""));
left_motion_params.Set("port", params.Get("port", 0));
left_motion_params.Set("pad", params.Get("pad", 0));
left_motion_params.Set("motion", 0);
Common::ParamPackage right_motion_params;
right_motion_params.Set("engine", GetEngineName());
right_motion_params.Set("guid", params.Get("guid", ""));
right_motion_params.Set("port", params.Get("port", 0));
right_motion_params.Set("pad", params.Get("pad", 0));
right_motion_params.Set("motion", 0);
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params));
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_motion_params));
return mapping;
}
Common::Input::ButtonNames UDPClient::GetUIButtonName(const Common::ParamPackage& params) const {
PadButton button = static_cast<PadButton>(params.Get("button", 0));
switch (button) {
case PadButton::Left:
return Common::Input::ButtonNames::ButtonLeft;
case PadButton::Right:
return Common::Input::ButtonNames::ButtonRight;
case PadButton::Down:
return Common::Input::ButtonNames::ButtonDown;
case PadButton::Up:
return Common::Input::ButtonNames::ButtonUp;
case PadButton::L1:
return Common::Input::ButtonNames::L1;
case PadButton::L2:
return Common::Input::ButtonNames::L2;
case PadButton::L3:
return Common::Input::ButtonNames::L3;
case PadButton::R1:
return Common::Input::ButtonNames::R1;
case PadButton::R2:
return Common::Input::ButtonNames::R2;
case PadButton::R3:
return Common::Input::ButtonNames::R3;
case PadButton::Circle:
return Common::Input::ButtonNames::Circle;
case PadButton::Cross:
return Common::Input::ButtonNames::Cross;
case PadButton::Square:
return Common::Input::ButtonNames::Square;
case PadButton::Triangle:
return Common::Input::ButtonNames::Triangle;
case PadButton::Share:
return Common::Input::ButtonNames::Share;
case PadButton::Options:
return Common::Input::ButtonNames::Options;
case PadButton::Home:
return Common::Input::ButtonNames::Home;
case PadButton::Touch1:
case PadButton::Touch2:
case PadButton::TouchHardPress:
return Common::Input::ButtonNames::Touch;
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames UDPClient::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
bool UDPClient::IsStickInverted(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return false;
}
const auto x_axis = static_cast<PadAxes>(params.Get("axis_x", 0));
const auto y_axis = static_cast<PadAxes>(params.Get("axis_y", 0));
if (x_axis != PadAxes::LeftStickY && x_axis != PadAxes::RightStickY) {
return false;
}
if (y_axis != PadAxes::LeftStickX && y_axis != PadAxes::RightStickX) {
return false;
}
return true;
}
void TestCommunication(const std::string& host, u16 port, u8 pad_index,
const std::function<void()>& success_callback,
const std::function<void()>& failure_callback) {
std::thread([=] {
Common::Event success_event;
SocketCallback callback{
.version = [](Response::Version) {},
.port_info = [](Response::PortInfo) {},
.pad_data = [&](Response::PadData) { success_event.Set(); },
};
Socket socket{host, port, pad_index, std::move(callback)};
std::thread worker_thread{SocketLoop, &socket};
const bool result =
success_event.WaitUntil(std::chrono::steady_clock::now() + std::chrono::seconds(10));
socket.Stop();
worker_thread.join();
if (result) {
success_callback();
} else {
failure_callback();
}
}).detach();
}
CalibrationConfigurationJob::CalibrationConfigurationJob(
const std::string& host, u16 port, u8 pad_index, std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback) {
std::thread([=, this] {
u16 min_x{UINT16_MAX};
u16 min_y{UINT16_MAX};
u16 max_x{};
u16 max_y{};
Status current_status{Status::Initialized};
SocketCallback callback{[](Response::Version) {}, [](Response::PortInfo) {},
[&](Response::PadData data) {
constexpr u16 CALIBRATION_THRESHOLD = 100;
if (current_status == Status::Initialized) {
// Receiving data means the communication is ready now
current_status = Status::Ready;
status_callback(current_status);
}
if (data.touch[0].is_active == 0) {
return;
}
LOG_DEBUG(Input, "Current touch: {} {}", data.touch[0].x,
data.touch[0].y);
min_x = std::min(min_x, static_cast<u16>(data.touch[0].x));
min_y = std::min(min_y, static_cast<u16>(data.touch[0].y));
if (current_status == Status::Ready) {
// First touch - min data (min_x/min_y)
current_status = Status::Stage1Completed;
status_callback(current_status);
}
if (data.touch[0].x - min_x > CALIBRATION_THRESHOLD &&
data.touch[0].y - min_y > CALIBRATION_THRESHOLD) {
// Set the current position as max value and finishes
// configuration
max_x = data.touch[0].x;
max_y = data.touch[0].y;
current_status = Status::Completed;
data_callback(min_x, min_y, max_x, max_y);
status_callback(current_status);
complete_event.Set();
}
}};
Socket socket{host, port, pad_index, std::move(callback)};
std::thread worker_thread{SocketLoop, &socket};
complete_event.Wait();
socket.Stop();
worker_thread.join();
}).detach();
}
CalibrationConfigurationJob::~CalibrationConfigurationJob() {
Stop();
}
void CalibrationConfigurationJob::Stop() {
complete_event.Set();
}
} // namespace InputCommon::CemuhookUDP

View File

@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include "common/common_types.h"
#include "common/thread.h"
#include "input_common/input_engine.h"
namespace InputCommon::CemuhookUDP {
class Socket;
namespace Response {
enum class Battery : u8;
struct PadData;
struct PortInfo;
struct TouchPad;
struct Version;
} // namespace Response
enum class PadTouch {
Click,
Undefined,
};
struct UDPPadStatus {
std::string host{"127.0.0.1"};
u16 port{26760};
std::size_t pad_index{};
};
struct DeviceStatus {
std::mutex update_mutex;
// calibration data for scaling the device's touch area to 3ds
struct CalibrationData {
u16 min_x{};
u16 min_y{};
u16 max_x{};
u16 max_y{};
};
std::optional<CalibrationData> touch_calibration;
};
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class UDPClient final : public InputEngine {
public:
explicit UDPClient(std::string input_engine_);
~UDPClient() override;
void ReloadSockets();
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
bool IsStickInverted(const Common::ParamPackage& params) override;
private:
enum class PadButton {
Undefined = 0x0000,
Share = 0x0001,
L3 = 0x0002,
R3 = 0x0004,
Options = 0x0008,
Up = 0x0010,
Right = 0x0020,
Down = 0x0040,
Left = 0x0080,
L2 = 0x0100,
R2 = 0x0200,
L1 = 0x0400,
R1 = 0x0800,
Triangle = 0x1000,
Circle = 0x2000,
Cross = 0x4000,
Square = 0x8000,
Touch1 = 0x10000,
Touch2 = 0x20000,
Home = 0x40000,
TouchHardPress = 0x80000,
};
enum class PadAxes : u8 {
LeftStickX,
LeftStickY,
RightStickX,
RightStickY,
AnalogLeft,
AnalogDown,
AnalogRight,
AnalogUp,
AnalogSquare,
AnalogCross,
AnalogCircle,
AnalogTriangle,
AnalogR1,
AnalogL1,
AnalogR2,
AnalogL3,
AnalogR3,
Touch1X,
Touch1Y,
Touch2X,
Touch2Y,
Undefined,
};
struct PadData {
std::size_t pad_index{};
bool connected{};
DeviceStatus status;
u64 packet_sequence{};
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
struct ClientConnection {
ClientConnection();
~ClientConnection();
Common::UUID uuid{"00000000-0000-0000-0000-00007F000001"};
std::string host{"127.0.0.1"};
u16 port{26760};
s8 active{-1};
std::unique_ptr<Socket> socket;
std::thread thread;
};
// For shutting down, clear all data, join all threads, release usb
void Reset();
// Translates configuration to client number
std::size_t GetClientNumber(std::string_view host, u16 port) const;
void OnVersion(Response::Version);
void OnPortInfo(Response::PortInfo);
void OnPadData(Response::PadData, std::size_t client);
void StartCommunication(std::size_t client, const std::string& host, u16 port, u8 pad_index);
PadIdentifier GetPadIdentifier(std::size_t pad_index) const;
Common::UUID GetHostUUID(const std::string& host) const;
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
// Allocate clients for 8 udp servers
static constexpr std::size_t MAX_UDP_CLIENTS = 8;
static constexpr std::size_t PADS_PER_CLIENT = 4;
std::array<PadData, MAX_UDP_CLIENTS * PADS_PER_CLIENT> pads{};
std::array<ClientConnection, MAX_UDP_CLIENTS> clients{};
};
/// An async job allowing configuration of the touchpad calibration.
class CalibrationConfigurationJob {
public:
enum class Status {
Initialized,
Ready,
Stage1Completed,
Completed,
};
/**
* Constructs and starts the job with the specified parameter.
*
* @param status_callback Callback for job status updates
* @param data_callback Called when calibration data is ready
*/
explicit CalibrationConfigurationJob(const std::string& host, u16 port, u8 pad_index,
std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback);
~CalibrationConfigurationJob();
void Stop();
private:
Common::Event complete_event;
};
void TestCommunication(const std::string& host, u16 port, u8 pad_index,
const std::function<void()>& success_callback,
const std::function<void()>& failure_callback);
} // namespace InputCommon::CemuhookUDP

View File

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "input_common/drivers/virtual_gamepad.h"
namespace InputCommon {
constexpr std::size_t PlayerIndexCount = 10;
VirtualGamepad::VirtualGamepad(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
PreSetController(GetIdentifier(i));
}
}
void VirtualGamepad::SetButtonState(std::size_t player_index, int button_id, bool value) {
if (player_index > PlayerIndexCount) {
return;
}
const auto identifier = GetIdentifier(player_index);
SetButton(identifier, button_id, value);
}
void VirtualGamepad::SetButtonState(std::size_t player_index, VirtualButton button_id, bool value) {
SetButtonState(player_index, static_cast<int>(button_id), value);
}
void VirtualGamepad::SetStickPosition(std::size_t player_index, int axis_id, float x_value,
float y_value) {
if (player_index > PlayerIndexCount) {
return;
}
const auto identifier = GetIdentifier(player_index);
SetAxis(identifier, axis_id * 2, x_value);
SetAxis(identifier, (axis_id * 2) + 1, y_value);
}
void VirtualGamepad::SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
float y_value) {
SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value);
}
void VirtualGamepad::ResetControllers() {
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f);
SetStickPosition(i, VirtualStick::Right, 0.0f, 0.0f);
SetButtonState(i, VirtualButton::ButtonA, false);
SetButtonState(i, VirtualButton::ButtonB, false);
SetButtonState(i, VirtualButton::ButtonX, false);
SetButtonState(i, VirtualButton::ButtonY, false);
SetButtonState(i, VirtualButton::StickL, false);
SetButtonState(i, VirtualButton::StickR, false);
SetButtonState(i, VirtualButton::TriggerL, false);
SetButtonState(i, VirtualButton::TriggerR, false);
SetButtonState(i, VirtualButton::TriggerZL, false);
SetButtonState(i, VirtualButton::TriggerZR, false);
SetButtonState(i, VirtualButton::ButtonStart, false);
SetButtonState(i, VirtualButton::ButtonSelect, false);
SetButtonState(i, VirtualButton::ButtonLeft, false);
SetButtonState(i, VirtualButton::ButtonUp, false);
SetButtonState(i, VirtualButton::ButtonRight, false);
SetButtonState(i, VirtualButton::ButtonDown, false);
SetButtonState(i, VirtualButton::ButtonHome, false);
}
}
PadIdentifier VirtualGamepad::GetIdentifier(std::size_t player_index) const {
return {
.guid = Common::UUID{},
.port = player_index,
.pad = 0,
};
}
} // namespace InputCommon

View File

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A virtual controller that is always assigned to the game input
*/
class VirtualGamepad final : public InputEngine {
public:
enum class VirtualButton {
ButtonA,
ButtonB,
ButtonX,
ButtonY,
StickL,
StickR,
TriggerL,
TriggerR,
TriggerZL,
TriggerZR,
ButtonStart,
ButtonSelect,
ButtonLeft,
ButtonUp,
ButtonRight,
ButtonDown,
ButtonHome,
};
enum class VirtualStick {
Left = 0,
Right = 1,
};
explicit VirtualGamepad(std::string input_engine_);
/**
* Sets the status of all buttons bound with the key to pressed
* @param player_index the player number that will take this action
* @param button_id the id of the button
* @param value indicates if the button is pressed or not
*/
void SetButtonState(std::size_t player_index, int button_id, bool value);
void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value);
/**
* Sets the status of all buttons bound with the key to released
* @param player_index the player number that will take this action
* @param axis_id the id of the axis to move
* @param x_value the position of the stick in the x axis
* @param y_value the position of the stick in the y axis
*/
void SetStickPosition(std::size_t player_index, int axis_id, float x_value, float y_value);
void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
float y_value);
/// Restores all inputs into the neutral position
void ResetControllers();
private:
/// Returns the correct identifier corresponding to the player index
PadIdentifier GetIdentifier(std::size_t player_index) const;
};
} // namespace InputCommon

View File

@ -1,385 +0,0 @@
// Copyright 2014 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <chrono>
#include <thread>
#include <vector>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4200) // nonstandard extension used : zero-sized array in struct/union
#endif
#include <libusb.h>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include "common/logging/log.h"
#include "common/param_package.h"
#include "input_common/gcadapter/gc_adapter.h"
// Workaround for older libusb versions not having libusb_init_context.
// libusb_init is deprecated and causes a compile error in newer versions.
#if !defined(LIBUSB_API_VERSION) || (LIBUSB_API_VERSION < 0x0100010A)
#define libusb_init_context(a, b, c) libusb_init(a)
#endif
namespace GCAdapter {
Adapter::Adapter() {
if (usb_adapter_handle != nullptr) {
return;
}
const int init_res = libusb_init_context(&libusb_ctx, nullptr, 0);
if (init_res == LIBUSB_SUCCESS) {
adapter_scan_thread = std::thread(&Adapter::AdapterScanThread, this);
} else {
LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res);
}
}
Adapter::~Adapter() {
JoinThreads();
ClearLibusbHandle();
ResetDevices();
if (libusb_ctx) {
libusb_exit(libusb_ctx);
}
}
void Adapter::AdapterInputThread() {
LOG_DEBUG(Input, "GC Adapter input thread started");
s32 payload_size{};
AdapterPayload adapter_payload{};
if (adapter_scan_thread.joinable()) {
adapter_scan_thread.join();
}
while (adapter_input_thread_running) {
libusb_interrupt_transfer(usb_adapter_handle, input_endpoint, adapter_payload.data(),
static_cast<s32>(adapter_payload.size()), &payload_size, 16);
if (IsPayloadCorrect(adapter_payload, payload_size)) {
UpdateControllers(adapter_payload);
}
std::this_thread::yield();
}
if (restart_scan_thread) {
adapter_scan_thread = std::thread(&Adapter::AdapterScanThread, this);
restart_scan_thread = false;
}
}
bool Adapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size) {
if (payload_size != static_cast<s32>(adapter_payload.size()) ||
adapter_payload[0] != LIBUSB_DT_HID) {
LOG_DEBUG(Input, "Error reading payload (size: {}, type: {:02x})", payload_size,
adapter_payload[0]);
if (++input_error_counter > 20) {
LOG_ERROR(Input, "GC adapter timeout, Is the adapter connected?");
adapter_input_thread_running = false;
restart_scan_thread = true;
}
return false;
}
input_error_counter = 0;
return true;
}
void Adapter::UpdateControllers(const AdapterPayload& adapter_payload) {
for (std::size_t port = 0; port < pads.size(); ++port) {
const std::size_t offset = 1 + (9 * port);
const auto type = static_cast<ControllerTypes>(adapter_payload[offset] >> 4);
UpdatePadType(port, type);
if (DeviceConnected(port)) {
const u8 b1 = adapter_payload[offset + 1];
const u8 b2 = adapter_payload[offset + 2];
UpdateStateButtons(port, b1, b2);
UpdateStateAxes(port, adapter_payload);
if (configuring) {
UpdateSettings(port);
}
}
}
}
void Adapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) {
if (pads[port].type == pad_type) {
return;
}
// Device changed reset device and set new type
ResetDevice(port);
pads[port].type = pad_type;
}
void Adapter::UpdateStateButtons(std::size_t port, u8 b1, u8 b2) {
if (port >= pads.size()) {
return;
}
static constexpr std::array<PadButton, 8> b1_buttons{
PadButton::ButtonA, PadButton::ButtonB, PadButton::ButtonX, PadButton::ButtonY,
PadButton::ButtonLeft, PadButton::ButtonRight, PadButton::ButtonDown, PadButton::ButtonUp,
};
static constexpr std::array<PadButton, 4> b2_buttons{
PadButton::ButtonStart,
PadButton::TriggerZ,
PadButton::TriggerR,
PadButton::TriggerL,
};
pads[port].buttons = 0;
for (std::size_t i = 0; i < b1_buttons.size(); ++i) {
if ((b1 & (1U << i)) != 0) {
pads[port].buttons =
static_cast<u16>(pads[port].buttons | static_cast<u16>(b1_buttons[i]));
pads[port].last_button = b1_buttons[i];
}
}
for (std::size_t j = 0; j < b2_buttons.size(); ++j) {
if ((b2 & (1U << j)) != 0) {
pads[port].buttons =
static_cast<u16>(pads[port].buttons | static_cast<u16>(b2_buttons[j]));
pads[port].last_button = b2_buttons[j];
}
}
}
void Adapter::UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload) {
if (port >= pads.size()) {
return;
}
const std::size_t offset = 1 + (9 * port);
static constexpr std::array<PadAxes, 6> axes{
PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX,
PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight,
};
for (const PadAxes axis : axes) {
const auto index = static_cast<std::size_t>(axis);
const u8 axis_value = adapter_payload[offset + 3 + index];
if (pads[port].axis_origin[index] == 255) {
pads[port].axis_origin[index] = axis_value;
}
pads[port].axis_values[index] =
static_cast<s16>(axis_value - pads[port].axis_origin[index]);
}
}
void Adapter::UpdateSettings(std::size_t port) {
if (port >= pads.size()) {
return;
}
constexpr u8 axis_threshold = 50;
GCPadStatus pad_status = {port};
if (pads[port].buttons != 0) {
pad_status.button = pads[port].last_button;
pad_queue.Push(pad_status);
}
// Accounting for a threshold here to ensure an intentional press
for (std::size_t i = 0; i < pads[port].axis_values.size(); ++i) {
const s16 value = pads[port].axis_values[i];
if (value > axis_threshold || value < -axis_threshold) {
pad_status.axis = static_cast<PadAxes>(i);
pad_status.axis_value = value;
pad_status.axis_threshold = axis_threshold;
pad_queue.Push(pad_status);
}
}
}
void Adapter::AdapterScanThread() {
adapter_scan_thread_running = true;
adapter_input_thread_running = false;
if (adapter_input_thread.joinable()) {
adapter_input_thread.join();
}
ClearLibusbHandle();
ResetDevices();
while (adapter_scan_thread_running && !adapter_input_thread_running) {
Setup();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void Adapter::Setup() {
usb_adapter_handle = libusb_open_device_with_vid_pid(libusb_ctx, 0x057e, 0x0337);
if (usb_adapter_handle == NULL) {
return;
}
if (!CheckDeviceAccess()) {
ClearLibusbHandle();
return;
}
libusb_device* device = libusb_get_device(usb_adapter_handle);
LOG_INFO(Input, "GC adapter is now connected");
// GC Adapter found and accessible, registering it
if (GetGCEndpoint(device)) {
adapter_scan_thread_running = false;
adapter_input_thread_running = true;
input_error_counter = 0;
adapter_input_thread = std::thread(&Adapter::AdapterInputThread, this);
}
}
bool Adapter::CheckDeviceAccess() {
// This fixes payload problems from offbrand GCAdapters
const s32 control_transfer_error =
libusb_control_transfer(usb_adapter_handle, 0x21, 11, 0x0001, 0, nullptr, 0, 1000);
if (control_transfer_error < 0) {
LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error);
}
s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle, 0);
if (kernel_driver_error == 1) {
kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle, 0);
if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}",
kernel_driver_error);
}
}
if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
libusb_close(usb_adapter_handle);
usb_adapter_handle = nullptr;
return false;
}
const int interface_claim_error = libusb_claim_interface(usb_adapter_handle, 0);
if (interface_claim_error) {
LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error);
libusb_close(usb_adapter_handle);
usb_adapter_handle = nullptr;
return false;
}
return true;
}
bool Adapter::GetGCEndpoint(libusb_device* device) {
libusb_config_descriptor* config = nullptr;
const int config_descriptor_return = libusb_get_config_descriptor(device, 0, &config);
if (config_descriptor_return != LIBUSB_SUCCESS) {
LOG_ERROR(Input, "libusb_get_config_descriptor failed with error = {}",
config_descriptor_return);
return false;
}
for (u8 ic = 0; ic < config->bNumInterfaces; ic++) {
const libusb_interface* interfaceContainer = &config->interface[ic];
for (int i = 0; i < interfaceContainer->num_altsetting; i++) {
const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i];
for (u8 e = 0; e < interface->bNumEndpoints; e++) {
const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e];
if ((endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0) {
input_endpoint = endpoint->bEndpointAddress;
} else {
output_endpoint = endpoint->bEndpointAddress;
}
}
}
}
// This transfer seems to be responsible for clearing the state of the adapter
// Used to clear the "busy" state of when the device is unexpectedly unplugged
unsigned char clear_payload = 0x13;
libusb_interrupt_transfer(usb_adapter_handle, output_endpoint, &clear_payload,
sizeof(clear_payload), nullptr, 16);
return true;
}
void Adapter::JoinThreads() {
restart_scan_thread = false;
adapter_input_thread_running = false;
adapter_scan_thread_running = false;
if (adapter_scan_thread.joinable()) {
adapter_scan_thread.join();
}
if (adapter_input_thread.joinable()) {
adapter_input_thread.join();
}
}
void Adapter::ClearLibusbHandle() {
if (usb_adapter_handle) {
libusb_release_interface(usb_adapter_handle, 1);
libusb_close(usb_adapter_handle);
usb_adapter_handle = nullptr;
}
}
void Adapter::ResetDevices() {
for (std::size_t i = 0; i < pads.size(); ++i) {
ResetDevice(i);
}
}
void Adapter::ResetDevice(std::size_t port) {
pads[port].type = ControllerTypes::None;
pads[port].buttons = 0;
pads[port].last_button = PadButton::Undefined;
pads[port].axis_values.fill(0);
pads[port].axis_origin.fill(255);
}
std::vector<Common::ParamPackage> Adapter::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
for (std::size_t port = 0; port < pads.size(); ++port) {
if (!DeviceConnected(port)) {
continue;
}
std::string name = fmt::format("Gamecube Controller {}", port + 1);
devices.emplace_back(Common::ParamPackage{
{"class", "gcpad"},
{"display", std::move(name)},
{"port", std::to_string(port)},
});
}
return devices;
}
bool Adapter::DeviceConnected(std::size_t port) const {
return pads[port].type != ControllerTypes::None;
}
void Adapter::BeginConfiguration() {
pad_queue.Clear();
configuring = true;
}
void Adapter::EndConfiguration() {
pad_queue.Clear();
configuring = false;
}
Common::SPSCQueue<GCPadStatus>& Adapter::GetPadQueue() {
return pad_queue;
}
const Common::SPSCQueue<GCPadStatus>& Adapter::GetPadQueue() const {
return pad_queue;
}
GCController& Adapter::GetPadState(std::size_t port) {
return pads.at(port);
}
const GCController& Adapter::GetPadState(std::size_t port) const {
return pads.at(port);
}
} // namespace GCAdapter

View File

@ -1,153 +0,0 @@
// Copyright 2014 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <algorithm>
#include <array>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
#include "common/common_types.h"
#include "common/threadsafe_queue.h"
struct libusb_context;
struct libusb_device;
struct libusb_device_handle;
namespace Common {
class ParamPackage;
}
namespace GCAdapter {
enum class PadButton {
Undefined = 0x0000,
ButtonLeft = 0x0001,
ButtonRight = 0x0002,
ButtonDown = 0x0004,
ButtonUp = 0x0008,
TriggerZ = 0x0010,
TriggerR = 0x0020,
TriggerL = 0x0040,
ButtonA = 0x0100,
ButtonB = 0x0200,
ButtonX = 0x0400,
ButtonY = 0x0800,
ButtonStart = 0x1000,
// Below is for compatibility with "AxisButton" type
Stick = 0x2000,
};
enum class PadAxes : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
TriggerLeft,
TriggerRight,
Undefined,
};
enum class ControllerTypes {
None,
Wired,
Wireless,
};
struct GCPadStatus {
std::size_t port{};
PadButton button{PadButton::Undefined}; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits
PadAxes axis{PadAxes::Undefined};
s16 axis_value{};
u8 axis_threshold{50};
};
struct GCController {
ControllerTypes type{};
u16 buttons{};
PadButton last_button{};
std::array<s16, 6> axis_values{};
std::array<u8, 6> axis_origin{};
};
class Adapter {
public:
Adapter();
~Adapter();
/// Used for polling
void BeginConfiguration();
void EndConfiguration();
Common::SPSCQueue<GCPadStatus>& GetPadQueue();
const Common::SPSCQueue<GCPadStatus>& GetPadQueue() const;
GCController& GetPadState(std::size_t port);
const GCController& GetPadState(std::size_t port) const;
/// Returns true if there is a device connected to port
bool DeviceConnected(std::size_t port) const;
std::vector<Common::ParamPackage> GetInputDevices() const;
private:
using AdapterPayload = std::array<u8, 37>;
void UpdatePadType(std::size_t port, ControllerTypes pad_type);
void UpdateControllers(const AdapterPayload& adapter_payload);
void UpdateSettings(std::size_t port);
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2);
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload);
void AdapterInputThread();
void AdapterScanThread();
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size);
/// For use in initialization, querying devices to find the adapter
void Setup();
/// Resets status of all GC controller devices to a disconnected state
void ResetDevices();
/// Resets status of device connected to a disconnected state
void ResetDevice(std::size_t port);
/// Returns true if we successfully gain access to GC Adapter
bool CheckDeviceAccess();
/// Captures GC Adapter endpoint address
/// Returns true if the endpoint was set correctly
bool GetGCEndpoint(libusb_device* device);
// Join all threads
void JoinThreads();
// Release usb handles
void ClearLibusbHandle();
libusb_device_handle* usb_adapter_handle = nullptr;
std::array<GCController, 4> pads;
Common::SPSCQueue<GCPadStatus> pad_queue;
std::thread adapter_input_thread;
std::thread adapter_scan_thread;
bool adapter_input_thread_running;
bool adapter_scan_thread_running;
bool restart_scan_thread;
libusb_context* libusb_ctx;
u8 input_endpoint{0};
u8 output_endpoint{0};
u8 input_error_counter{0};
bool configuring{false};
};
} // namespace GCAdapter

View File

@ -1,323 +0,0 @@
// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <atomic>
#include <list>
#include <mutex>
#include <utility>
#include "common/assert.h"
#include "common/threadsafe_queue.h"
#include "input_common/gcadapter/gc_adapter.h"
#include "input_common/gcadapter/gc_poller.h"
namespace InputCommon {
namespace {
constexpr std::array<GCAdapter::PadButton, Settings::NativeButton::NumButtons> gc_to_3ds_mapping{{
GCAdapter::PadButton::ButtonA,
GCAdapter::PadButton::ButtonB,
GCAdapter::PadButton::ButtonX,
GCAdapter::PadButton::ButtonY,
GCAdapter::PadButton::ButtonUp,
GCAdapter::PadButton::ButtonDown,
GCAdapter::PadButton::ButtonLeft,
GCAdapter::PadButton::ButtonRight,
GCAdapter::PadButton::TriggerL,
GCAdapter::PadButton::TriggerR,
GCAdapter::PadButton::ButtonStart,
GCAdapter::PadButton::TriggerZ,
GCAdapter::PadButton::Undefined,
GCAdapter::PadButton::Undefined,
GCAdapter::PadButton::Undefined,
GCAdapter::PadButton::Undefined,
GCAdapter::PadButton::Undefined,
}};
}
class GCButton final : public Input::ButtonDevice {
public:
explicit GCButton(int port_, int button_, GCAdapter::Adapter* adapter)
: port(port_), button(button_), gcadapter(adapter) {}
~GCButton() override;
bool GetStatus() const override {
if (gcadapter->DeviceConnected(port)) {
return (gcadapter->GetPadState(port).buttons & button) != 0;
}
return false;
}
private:
const int port;
const int button;
GCAdapter::Adapter* gcadapter;
};
class GCAxisButton final : public Input::ButtonDevice {
public:
explicit GCAxisButton(int port_, int axis_, float threshold_, bool trigger_if_greater_,
GCAdapter::Adapter* adapter)
: port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_),
gcadapter(adapter) {}
bool GetStatus() const override {
if (gcadapter->DeviceConnected(port)) {
const float current_axis_value = gcadapter->GetPadState(port).axis_values.at(axis);
const float axis_value = current_axis_value / 128.0f;
if (trigger_if_greater) {
return axis_value > threshold;
}
return axis_value < -threshold;
}
return false;
}
private:
const u32 port;
const u32 axis;
float threshold;
bool trigger_if_greater;
const GCAdapter::Adapter* gcadapter;
};
GCButtonFactory::GCButtonFactory(std::shared_ptr<GCAdapter::Adapter> adapter_)
: adapter(std::move(adapter_)) {}
GCButton::~GCButton() = default;
std::unique_ptr<Input::ButtonDevice> GCButtonFactory::Create(const Common::ParamPackage& params) {
const int button_id = params.Get("button", 0);
const int port = params.Get("port", 0);
constexpr s32 PAD_STICK_ID = static_cast<s32>(GCAdapter::PadButton::Stick);
// button is not an axis/stick button
if (button_id != PAD_STICK_ID) {
return std::make_unique<GCButton>(port, button_id, adapter.get());
}
// For Axis buttons, used by the binary sticks.
if (button_id == PAD_STICK_ID) {
const int axis = params.Get("axis", 0);
const float threshold = params.Get("threshold", 0.25f);
const std::string direction_name = params.Get("direction", "");
bool trigger_if_greater;
if (direction_name == "+") {
trigger_if_greater = true;
} else if (direction_name == "-") {
trigger_if_greater = false;
} else {
trigger_if_greater = true;
LOG_ERROR(Input, "Unknown direction {}", direction_name);
}
return std::make_unique<GCAxisButton>(port, axis, threshold, trigger_if_greater,
adapter.get());
}
UNREACHABLE();
return nullptr;
}
Common::ParamPackage GCButtonFactory::GetNextInput() {
Common::ParamPackage params;
GCAdapter::GCPadStatus pad;
auto& queue = adapter->GetPadQueue();
while (queue.Pop(pad)) {
// This while loop will break on the earliest detected button
params.Set("engine", "gcpad");
params.Set("port", static_cast<s32>(pad.port));
if (pad.button != GCAdapter::PadButton::Undefined) {
params.Set("button", static_cast<u16>(pad.button));
}
// For Axis button implementation
if (pad.axis != GCAdapter::PadAxes::Undefined) {
params.Set("axis", static_cast<u8>(pad.axis));
params.Set("button", static_cast<u16>(GCAdapter::PadButton::Stick));
params.Set("threshold", "0.25");
if (pad.axis_value > 0) {
params.Set("direction", "+");
} else {
params.Set("direction", "-");
}
break;
}
}
return params;
}
Common::ParamPackage GCButtonFactory::GetGcTo3DSMappedButton(
int port, Settings::NativeButton::Values button) {
Common::ParamPackage params({{"engine", "gcpad"}});
params.Set("port", port);
auto mapped_button = gc_to_3ds_mapping[static_cast<int>(button)];
if (mapped_button != GCAdapter::PadButton::Undefined) {
params.Set("button", static_cast<u16>(mapped_button));
}
return params;
}
void GCButtonFactory::Start() {
polling = true;
adapter->BeginConfiguration();
}
void GCButtonFactory::Stop() {
polling = false;
adapter->EndConfiguration();
}
class GCAnalog final : public Input::AnalogDevice {
public:
explicit GCAnalog(u32 port_, u32 axis_x_, u32 axis_y_, float deadzone_,
const GCAdapter::Adapter* adapter)
: port(port_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), gcadapter(adapter) {}
float GetAxis(u32 axis) const {
if (gcadapter->DeviceConnected(port)) {
std::lock_guard lock{mutex};
const auto axis_value =
static_cast<float>(gcadapter->GetPadState(port).axis_values.at(axis));
return (axis_value) / 50.0f;
}
return 0.0f;
}
std::pair<float, float> GetAnalog(u32 analog_axis_x, u32 analog_axis_y) const {
float x = GetAxis(analog_axis_x);
float y = GetAxis(analog_axis_y);
// Make sure the coordinates are in the unit circle,
// otherwise normalize it.
float r = x * x + y * y;
if (r > 1.0f) {
r = std::sqrt(r);
x /= r;
y /= r;
}
return {x, y};
}
std::tuple<float, float> GetStatus() const override {
const auto [x, y] = GetAnalog(axis_x, axis_y);
const float r = std::sqrt((x * x) + (y * y));
if (r > deadzone) {
return {x / r * (r - deadzone) / (1 - deadzone),
y / r * (r - deadzone) / (1 - deadzone)};
}
return {0.0f, 0.0f};
}
private:
const u32 port;
const u32 axis_x;
const u32 axis_y;
const float deadzone;
const GCAdapter::Adapter* gcadapter;
mutable std::mutex mutex;
};
/// An analog device factory that creates analog devices from GC Adapter
GCAnalogFactory::GCAnalogFactory(std::shared_ptr<GCAdapter::Adapter> adapter_)
: adapter(std::move(adapter_)) {}
/**
* Creates analog device from joystick axes
* @param params contains parameters for creating the device:
* - "port": the nth gcpad on the adapter
* - "axis_x": the index of the axis to be bind as x-axis
* - "axis_y": the index of the axis to be bind as y-axis
*/
std::unique_ptr<Input::AnalogDevice> GCAnalogFactory::Create(const Common::ParamPackage& params) {
const auto port = static_cast<u32>(params.Get("port", 0));
const auto axis_x = static_cast<u32>(params.Get("axis_x", 0));
const auto axis_y = static_cast<u32>(params.Get("axis_y", 1));
const auto deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, 1.0f);
return std::make_unique<GCAnalog>(port, axis_x, axis_y, deadzone, adapter.get());
}
void GCAnalogFactory::Start() {
polling = true;
adapter->BeginConfiguration();
}
void GCAnalogFactory::Stop() {
polling = false;
adapter->EndConfiguration();
}
Common::ParamPackage GCAnalogFactory::GetNextInput() {
GCAdapter::GCPadStatus pad;
Common::ParamPackage params;
auto& queue = adapter->GetPadQueue();
while (queue.Pop(pad)) {
if (pad.button != GCAdapter::PadButton::Undefined) {
params.Set("engine", "gcpad");
params.Set("port", static_cast<s32>(pad.port));
params.Set("button", static_cast<u16>(pad.button));
return params;
}
if (pad.axis == GCAdapter::PadAxes::Undefined ||
std::abs(static_cast<float>(pad.axis_value) / 128.0f) < 0.1f) {
continue;
}
// An analog device needs two axes, so we need to store the axis for later and wait for
// a second input event. The axes also must be from the same joystick.
const u8 axis = static_cast<u8>(pad.axis);
if (axis == 0 || axis == 1) {
analog_x_axis = 0;
analog_y_axis = 1;
controller_number = static_cast<s32>(pad.port);
break;
}
if (axis == 2 || axis == 3) {
analog_x_axis = 2;
analog_y_axis = 3;
controller_number = static_cast<s32>(pad.port);
break;
}
if (analog_x_axis == -1) {
analog_x_axis = axis;
controller_number = static_cast<s32>(pad.port);
} else if (analog_y_axis == -1 && analog_x_axis != axis &&
controller_number == static_cast<s32>(pad.port)) {
analog_y_axis = axis;
break;
}
}
if (analog_x_axis != -1 && analog_y_axis != -1) {
params.Set("engine", "gcpad");
params.Set("port", controller_number);
params.Set("axis_x", analog_x_axis);
params.Set("axis_y", analog_y_axis);
analog_x_axis = -1;
analog_y_axis = -1;
controller_number = -1;
return params;
}
return params;
}
Common::ParamPackage GCAnalogFactory::GetGcTo3DSMappedAnalog(
int port, Settings::NativeAnalog::Values analog) {
int x_axis, y_axis;
Common::ParamPackage params({{"engine", "gcpad"}});
params.Set("port", port);
if (analog == Settings::NativeAnalog::Values::CirclePad) {
x_axis = static_cast<s32>(GCAdapter::PadAxes::StickX);
y_axis = static_cast<s32>(GCAdapter::PadAxes::StickY);
} else if (analog == Settings::NativeAnalog::Values::CStick) {
x_axis = static_cast<s32>(GCAdapter::PadAxes::SubstickX);
y_axis = static_cast<s32>(GCAdapter::PadAxes::SubstickY);
} else {
LOG_WARNING(Input, "analog value out of range {}", analog);
return {{}};
}
params.Set("axis_x", x_axis);
params.Set("axis_y", y_axis);
return params;
}
} // namespace InputCommon

View File

@ -1,70 +0,0 @@
// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include "common/settings.h"
#include "core/frontend/input.h"
#include "input_common/gcadapter/gc_adapter.h"
#include "input_common/main.h"
namespace InputCommon {
/**
* A button device factory representing a gcpad. It receives gcpad events and forward them
* to all button devices it created.
*/
class GCButtonFactory final : public Input::Factory<Input::ButtonDevice>,
public Polling::DevicePoller {
public:
public:
explicit GCButtonFactory(std::shared_ptr<GCAdapter::Adapter> adapter_);
std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override;
Common::ParamPackage GetNextInput() override;
Common::ParamPackage GetGcTo3DSMappedButton(int port, Settings::NativeButton::Values button);
/// For device input configuration/polling
void Start() override;
void Stop() override;
bool IsPolling() const {
return polling;
}
private:
std::shared_ptr<GCAdapter::Adapter> adapter;
bool polling{false};
};
/// An analog device factory that creates analog devices from GC Adapter
class GCAnalogFactory final : public Input::Factory<Input::AnalogDevice>,
public Polling::DevicePoller {
public:
explicit GCAnalogFactory(std::shared_ptr<GCAdapter::Adapter> adapter_);
std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override;
Common::ParamPackage GetNextInput() override;
Common::ParamPackage GetGcTo3DSMappedAnalog(int port, Settings::NativeAnalog::Values analog);
/// For device input configuration/polling
void Start() override;
void Stop() override;
bool IsPolling() const {
return polling;
}
private:
std::shared_ptr<GCAdapter::Adapter> adapter;
int analog_x_axis{-1};
int analog_y_axis{-1};
int controller_number{-1};
bool polling{false};
};
} // namespace InputCommon

View File

@ -0,0 +1,327 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <cmath>
#include "common/math_util.h"
#include "common/settings.h"
#include "input_common/helpers/stick_from_buttons.h"
namespace InputCommon {
class Stick final : public Common::Input::InputDevice {
public:
// Some games such as EARTH DEFENSE FORCE: WORLD BROTHERS
// do not play nicely with the theoretical maximum range.
// Using a value one lower from the maximum emulates real stick behavior.
static constexpr float MAX_RANGE = 32766.0f / 32767.0f;
static constexpr float TAU = Common::PI * 2.0f;
// Use wider angle to ease the transition.
static constexpr float APERTURE = TAU * 0.15f;
using Button = std::unique_ptr<Common::Input::InputDevice>;
Stick(Button up_, Button down_, Button left_, Button right_, Button modifier_,
float modifier_scale_, float modifier_angle_)
: up(std::move(up_)), down(std::move(down_)), left(std::move(left_)),
right(std::move(right_)), modifier(std::move(modifier_)), modifier_scale(modifier_scale_),
modifier_angle(modifier_angle_) {
up->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateUpButtonStatus(callback_);
},
});
down->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateDownButtonStatus(callback_);
},
});
left->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateLeftButtonStatus(callback_);
},
});
right->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateRightButtonStatus(callback_);
},
});
modifier->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateModButtonStatus(callback_);
},
});
last_x_axis_value = 0.0f;
last_y_axis_value = 0.0f;
}
bool IsAngleGreater(float old_angle, float new_angle) const {
const float top_limit = new_angle + APERTURE;
return (old_angle > new_angle && old_angle <= top_limit) ||
(old_angle + TAU > new_angle && old_angle + TAU <= top_limit);
}
bool IsAngleSmaller(float old_angle, float new_angle) const {
const float bottom_limit = new_angle - APERTURE;
return (old_angle >= bottom_limit && old_angle < new_angle) ||
(old_angle - TAU >= bottom_limit && old_angle - TAU < new_angle);
}
float GetAngle(std::chrono::time_point<std::chrono::steady_clock> now) const {
float new_angle = angle;
auto time_difference = static_cast<float>(
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
time_difference /= 1000.0f;
if (time_difference > 0.5f) {
time_difference = 0.5f;
}
if (IsAngleGreater(new_angle, goal_angle)) {
new_angle -= modifier_angle * time_difference;
if (new_angle < 0) {
new_angle += TAU;
}
if (!IsAngleGreater(new_angle, goal_angle)) {
return goal_angle;
}
} else if (IsAngleSmaller(new_angle, goal_angle)) {
new_angle += modifier_angle * time_difference;
if (new_angle >= TAU) {
new_angle -= TAU;
}
if (!IsAngleSmaller(new_angle, goal_angle)) {
return goal_angle;
}
} else {
return goal_angle;
}
return new_angle;
}
void SetGoalAngle(bool r, bool l, bool u, bool d) {
// Move to the right
if (r && !u && !d) {
goal_angle = 0.0f;
}
// Move to the upper right
if (r && u && !d) {
goal_angle = Common::PI * 0.25f;
}
// Move up
if (u && !l && !r) {
goal_angle = Common::PI * 0.5f;
}
// Move to the upper left
if (l && u && !d) {
goal_angle = Common::PI * 0.75f;
}
// Move to the left
if (l && !u && !d) {
goal_angle = Common::PI;
}
// Move to the bottom left
if (l && !u && d) {
goal_angle = Common::PI * 1.25f;
}
// Move down
if (d && !l && !r) {
goal_angle = Common::PI * 1.5f;
}
// Move to the bottom right
if (r && !u && d) {
goal_angle = Common::PI * 1.75f;
}
}
void UpdateUpButtonStatus(const Common::Input::CallbackStatus& button_callback) {
up_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateDownButtonStatus(const Common::Input::CallbackStatus& button_callback) {
down_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateLeftButtonStatus(const Common::Input::CallbackStatus& button_callback) {
left_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateRightButtonStatus(const Common::Input::CallbackStatus& button_callback) {
right_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateModButtonStatus(const Common::Input::CallbackStatus& button_callback) {
const auto& new_status = button_callback.button_status;
const bool new_button_value = new_status.inverted ? !new_status.value : new_status.value;
modifier_status.toggle = new_status.toggle;
// Update button status with current
if (!modifier_status.toggle) {
modifier_status.locked = false;
if (modifier_status.value != new_button_value) {
modifier_status.value = new_button_value;
}
} else {
// Toggle button and lock status
if (new_button_value && !modifier_status.locked) {
modifier_status.locked = true;
modifier_status.value = !modifier_status.value;
}
// Unlock button ready for next press
if (!new_button_value && modifier_status.locked) {
modifier_status.locked = false;
}
}
UpdateStatus();
}
void UpdateStatus() {
bool r = right_status;
bool l = left_status;
bool u = up_status;
bool d = down_status;
// Eliminate contradictory movements
if (r && l) {
r = false;
l = false;
}
if (u && d) {
u = false;
d = false;
}
// Move if a key is pressed
if (r || l || u || d) {
amplitude = modifier_status.value ? modifier_scale : MAX_RANGE;
} else {
amplitude = 0;
}
const auto now = std::chrono::steady_clock::now();
const auto time_difference = static_cast<u64>(
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
if (time_difference < 10) {
// Disable analog mode if inputs are too fast
SetGoalAngle(r, l, u, d);
angle = goal_angle;
} else {
angle = GetAngle(now);
SetGoalAngle(r, l, u, d);
}
last_update = now;
Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Stick,
.stick_status = GetStatus(),
};
last_x_axis_value = status.stick_status.x.raw_value;
last_y_axis_value = status.stick_status.y.raw_value;
TriggerOnChange(status);
}
void ForceUpdate() override {
up->ForceUpdate();
down->ForceUpdate();
left->ForceUpdate();
right->ForceUpdate();
modifier->ForceUpdate();
}
void SoftUpdate() override {
Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Stick,
.stick_status = GetStatus(),
};
if (last_x_axis_value == status.stick_status.x.raw_value &&
last_y_axis_value == status.stick_status.y.raw_value) {
return;
}
last_x_axis_value = status.stick_status.x.raw_value;
last_y_axis_value = status.stick_status.y.raw_value;
TriggerOnChange(status);
}
Common::Input::StickStatus GetStatus() const {
Common::Input::StickStatus status{};
status.x.properties = properties;
status.y.properties = properties;
if (Settings::values.emulate_analog_keyboard) {
const auto now = std::chrono::steady_clock::now();
const float angle_ = GetAngle(now);
status.x.raw_value = std::cos(angle_) * amplitude;
status.y.raw_value = std::sin(angle_) * amplitude;
return status;
}
status.x.raw_value = std::cos(goal_angle) * amplitude;
status.y.raw_value = std::sin(goal_angle) * amplitude;
return status;
}
private:
static constexpr Common::Input::AnalogProperties properties{
.deadzone = 0.0f,
.range = 1.0f,
.threshold = 0.5f,
.offset = 0.0f,
.inverted = false,
.toggle = false,
};
Button up;
Button down;
Button left;
Button right;
Button modifier;
float modifier_scale{};
float modifier_angle{};
float angle{};
float goal_angle{};
float amplitude{};
bool up_status{};
bool down_status{};
bool left_status{};
bool right_status{};
float last_x_axis_value{};
float last_y_axis_value{};
Common::Input::ButtonStatus modifier_status{};
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
std::unique_ptr<Common::Input::InputDevice> StickFromButton::Create(
const Common::ParamPackage& params) {
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
auto up = Common::Input::CreateInputDeviceFromString(params.Get("up", null_engine));
auto down = Common::Input::CreateInputDeviceFromString(params.Get("down", null_engine));
auto left = Common::Input::CreateInputDeviceFromString(params.Get("left", null_engine));
auto right = Common::Input::CreateInputDeviceFromString(params.Get("right", null_engine));
auto modifier = Common::Input::CreateInputDeviceFromString(params.Get("modifier", null_engine));
auto modifier_scale = params.Get("modifier_scale", 0.5f);
auto modifier_angle = params.Get("modifier_angle", 5.5f);
return std::make_unique<Stick>(std::move(up), std::move(down), std::move(left),
std::move(right), std::move(modifier), modifier_scale,
modifier_angle);
}
} // namespace InputCommon

View File

@ -1,11 +1,9 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include "core/frontend/input.h"
#include "common/input.h"
namespace InputCommon {
@ -13,7 +11,7 @@ namespace InputCommon {
* An analog device factory that takes direction button devices and combines them into a analog
* device.
*/
class AnalogFromButton final : public Input::Factory<Input::AnalogDevice> {
class StickFromButton final : public Common::Input::Factory<Common::Input::InputDevice> {
public:
/**
* Creates an analog device from direction button devices
@ -25,7 +23,7 @@ public:
* - "modifier": a serialized ParamPackage for creating a button device as the modifier
* - "modifier_scale": a float for the multiplier the modifier gives to the position
*/
std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override;
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
};
} // namespace InputCommon

View File

@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include "common/settings.h"
#include "input_common/helpers/touch_from_buttons.h"
namespace InputCommon {
class TouchFromButtonDevice final : public Common::Input::InputDevice {
public:
using Button = std::unique_ptr<Common::Input::InputDevice>;
TouchFromButtonDevice(Button button_, float x_, float y_)
: button(std::move(button_)), x(x_), y(y_) {
last_button_value = false;
button->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateButtonStatus(callback_);
},
});
button->ForceUpdate();
}
void ForceUpdate() override {
button->ForceUpdate();
}
Common::Input::TouchStatus GetStatus(bool pressed) const {
const Common::Input::ButtonStatus button_status{
.value = pressed,
};
Common::Input::TouchStatus status{
.pressed = button_status,
.x = {},
.y = {},
};
status.x.properties = properties;
status.y.properties = properties;
if (!pressed) {
return status;
}
status.x.raw_value = x;
status.y.raw_value = y;
return status;
}
void UpdateButtonStatus(const Common::Input::CallbackStatus& button_callback) {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Touch,
.touch_status = GetStatus(button_callback.button_status.value),
};
if (last_button_value != button_callback.button_status.value) {
last_button_value = button_callback.button_status.value;
TriggerOnChange(status);
}
}
private:
static constexpr Common::Input::AnalogProperties properties{
.deadzone = 0.0f,
.range = 1.0f,
.threshold = 0.5f,
.offset = 0.0f,
.inverted = false,
.toggle = false,
};
Button button;
bool last_button_value;
const float x;
const float y;
};
std::unique_ptr<Common::Input::InputDevice> TouchFromButton::Create(
const Common::ParamPackage& params) {
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
auto button = Common::Input::CreateInputDeviceFromString(params.Get("button", null_engine));
const float x = params.Get("x", 0.0f) / 1280.0f;
const float y = params.Get("y", 0.0f) / 720.0f;
return std::make_unique<TouchFromButtonDevice>(std::move(button), x, y);
}
} // namespace InputCommon

View File

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/input.h"
namespace InputCommon {
/**
* A touch device factory that takes a list of button devices and combines them into a touch device.
*/
class TouchFromButton final : public Common::Input::Factory<Common::Input::InputDevice> {
public:
/**
* Creates a touch device from a list of button devices
*/
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
};
} // namespace InputCommon

View File

@ -1,11 +1,10 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstddef>
#include <cstring>
#include "common/logging/log.h"
#include "input_common/udp/protocol.h"
#include "input_common/helpers/udp_protocol.h"
namespace InputCommon::CemuhookUDP {

View File

@ -1,14 +1,23 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <optional>
#include <type_traits>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4701) // Potentially uninitialized local variable 'result' used
#endif
#include <boost/crc.hpp>
#include "common/bit_field.h"
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include "common/swap.h"
namespace InputCommon::CemuhookUDP {
@ -52,8 +61,26 @@ struct Message {
template <typename T>
constexpr Type GetMessageType();
template <typename T>
Message<T> CreateMessage(const u32 magic, const T data, const u32 sender_id) {
boost::crc_32_type crc;
Header header{
magic, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, sender_id, GetMessageType<T>(),
};
Message<T> message{header, data};
crc.process_bytes(&message, sizeof(Message<T>));
message.header.crc = crc.checksum();
return message;
}
namespace Request {
enum RegisterFlags : u8 {
AllPads,
PadID,
PadMACAdddress,
};
struct Version {};
/**
* Requests the server to send information about what controllers are plugged into the ports
@ -75,13 +102,8 @@ static_assert(std::is_trivially_copyable_v<PortInfo>,
* timeout seems to be 5 seconds.
*/
struct PadData {
enum class Flags : u8 {
AllPorts,
Id,
Mac,
};
/// Determines which method will be used as a look up for the controller
Flags flags{};
RegisterFlags flags{};
/// Index of the port of the controller to retrieve data about
u8 port_id{};
/// Mac address of the controller to retrieve data about
@ -93,24 +115,47 @@ static_assert(std::is_trivially_copyable_v<PadData>,
/**
* Creates a message with the proper header data that can be sent to the server.
* @param T data Request body to send
* @param data Request body to send
* @param client_id ID of the udp client (usually not checked on the server)
*/
template <typename T>
Message<T> Create(const T data, const u32 client_id = 0) {
boost::crc_32_type crc;
Header header{
CLIENT_MAGIC, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, client_id, GetMessageType<T>(),
};
Message<T> message{header, data};
crc.process_bytes(&message, sizeof(Message<T>));
message.header.crc = crc.checksum();
return message;
return CreateMessage(CLIENT_MAGIC, data, client_id);
}
} // namespace Request
namespace Response {
enum class ConnectionType : u8 {
None,
Usb,
Bluetooth,
};
enum class State : u8 {
Disconnected,
Reserved,
Connected,
};
enum class Model : u8 {
None,
PartialGyro,
FullGyro,
Generic,
};
enum class Battery : u8 {
None = 0x00,
Dying = 0x01,
Low = 0x02,
Medium = 0x03,
High = 0x04,
Full = 0x05,
Charging = 0xEE,
Charged = 0xEF,
};
struct Version {
u16_le version{};
};
@ -120,17 +165,25 @@ static_assert(std::is_trivially_copyable_v<Version>,
struct PortInfo {
u8 id{};
u8 state{};
u8 model{};
u8 connection_type{};
State state{};
Model model{};
ConnectionType connection_type{};
MacAddress mac;
u8 battery{};
Battery battery{};
u8 is_pad_active{};
};
static_assert(sizeof(PortInfo) == 12, "UDP Response PortInfo struct has wrong size");
static_assert(std::is_trivially_copyable_v<PortInfo>,
"UDP Response PortInfo is not trivially copyable");
struct TouchPad {
u8 is_active{};
u8 id{};
u16_le x{};
u16_le y{};
};
static_assert(sizeof(TouchPad) == 6, "UDP Response TouchPad struct has wrong size ");
#pragma pack(push, 1)
struct PadData {
PortInfo info{};
@ -167,26 +220,21 @@ struct PadData {
u8 right_stick_y{};
struct AnalogButton {
u8 button_8{};
u8 button_7{};
u8 button_6{};
u8 button_5{};
u8 button_12{};
u8 button_11{};
u8 button_10{};
u8 button_9{};
u8 button_16{};
u8 button_15{};
u8 button_14{};
u8 button_13{};
u8 button_dpad_left_analog{};
u8 button_dpad_down_analog{};
u8 button_dpad_right_analog{};
u8 button_dpad_up_analog{};
u8 button_square_analog{};
u8 button_cross_analog{};
u8 button_circle_analog{};
u8 button_triangle_analog{};
u8 button_r1_analog{};
u8 button_l1_analog{};
u8 trigger_r2{};
u8 trigger_l2{};
} analog_button;
struct TouchPad {
u8 is_active{};
u8 id{};
u16_le x{};
u16_le y{};
} touch_1, touch_2;
std::array<TouchPad, 2> touch;
u64_le motion_timestamp;
@ -213,7 +261,6 @@ static_assert(sizeof(Message<PadData>) == MAX_PACKET_SIZE,
static_assert(sizeof(PadData::AnalogButton) == 12,
"UDP Response AnalogButton struct has wrong size ");
static_assert(sizeof(PadData::TouchPad) == 6, "UDP Response TouchPad struct has wrong size ");
static_assert(sizeof(PadData::Accelerometer) == 12,
"UDP Response Accelerometer struct has wrong size ");
static_assert(sizeof(PadData::Gyroscope) == 12, "UDP Response Gyroscope struct has wrong size ");

View File

@ -0,0 +1,331 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/input_engine.h"
namespace InputCommon {
void InputEngine::PreSetController(const PadIdentifier& identifier) {
std::scoped_lock lock{mutex};
controller_list.try_emplace(identifier);
}
void InputEngine::PreSetButton(const PadIdentifier& identifier, int button) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.buttons.try_emplace(button, false);
}
void InputEngine::PreSetHatButton(const PadIdentifier& identifier, int button) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.hat_buttons.try_emplace(button, u8{0});
}
void InputEngine::PreSetAxis(const PadIdentifier& identifier, int axis) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.axes.try_emplace(axis, 0.0f);
}
void InputEngine::PreSetMotion(const PadIdentifier& identifier, int motion) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.motions.try_emplace(motion);
}
void InputEngine::SetButton(const PadIdentifier& identifier, int button, bool value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.buttons.insert_or_assign(button, value);
}
}
TriggerOnButtonChange(identifier, button, value);
}
void InputEngine::SetHatButton(const PadIdentifier& identifier, int button, u8 value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.hat_buttons.insert_or_assign(button, value);
}
}
TriggerOnHatButtonChange(identifier, button, value);
}
void InputEngine::SetAxis(const PadIdentifier& identifier, int axis, f32 value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.axes.insert_or_assign(axis, value);
}
}
TriggerOnAxisChange(identifier, axis, value);
}
void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.motions.insert_or_assign(motion, value);
}
}
TriggerOnMotionChange(identifier, motion, value);
}
bool InputEngine::GetButton(const PadIdentifier& identifier, int button) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return false;
}
const ControllerData& controller = controller_iter->second;
const auto button_iter = controller.buttons.find(button);
if (button_iter == controller.buttons.cend()) {
LOG_ERROR(Input, "Invalid button {}", button);
return false;
}
return button_iter->second;
}
bool InputEngine::GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return false;
}
const ControllerData& controller = controller_iter->second;
const auto hat_iter = controller.hat_buttons.find(button);
if (hat_iter == controller.hat_buttons.cend()) {
LOG_ERROR(Input, "Invalid hat button {}", button);
return false;
}
return (hat_iter->second & direction) != 0;
}
f32 InputEngine::GetAxis(const PadIdentifier& identifier, int axis) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return 0.0f;
}
const ControllerData& controller = controller_iter->second;
const auto axis_iter = controller.axes.find(axis);
if (axis_iter == controller.axes.cend()) {
LOG_ERROR(Input, "Invalid axis {}", axis);
return 0.0f;
}
return axis_iter->second;
}
BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return {};
}
const ControllerData& controller = controller_iter->second;
return controller.motions.at(motion);
}
void InputEngine::ResetButtonState() {
for (const auto& controller : controller_list) {
for (const auto& button : controller.second.buttons) {
SetButton(controller.first, button.first, false);
}
for (const auto& button : controller.second.hat_buttons) {
SetHatButton(controller.first, button.first, 0);
}
}
}
void InputEngine::ResetAnalogState() {
for (const auto& controller : controller_list) {
for (const auto& axis : controller.second.axes) {
SetAxis(controller.first, axis.first, 0.0);
}
}
}
void InputEngine::TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Button, button)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
PreSetButton(identifier, button);
if (value == GetButton(identifier, button)) {
return;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::Button,
.index = button,
.button_value = value,
});
}
void InputEngine::TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::HatButton, button)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
for (std::size_t index = 1; index < 0xff; index <<= 1) {
bool button_value = (value & index) != 0;
if (button_value == GetHatButton(identifier, button, static_cast<u8>(index))) {
continue;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::HatButton,
.index = button,
.hat_name = GetHatButtonName(static_cast<u8>(index)),
});
}
}
void InputEngine::TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Analog, axis)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
if (std::abs(value - GetAxis(identifier, axis)) < 0.5f) {
return;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::Analog,
.index = axis,
.axis_value = value,
});
}
void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Motion, motion)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
bool is_active = false;
if (std::abs(value.accel_x) > 1.5f || std::abs(value.accel_y) > 1.5f ||
std::abs(value.accel_z) > 1.5f) {
is_active = true;
}
if (std::abs(value.gyro_x) > 0.6f || std::abs(value.gyro_y) > 0.6f ||
std::abs(value.gyro_z) > 0.6f) {
is_active = true;
}
if (!is_active) {
return;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::Motion,
.index = motion,
.motion_value = value,
});
}
bool InputEngine::IsInputIdentifierEqual(const InputIdentifier& input_identifier,
const PadIdentifier& identifier, EngineInputType type,
int index) const {
if (input_identifier.type != type) {
return false;
}
if (input_identifier.index != index) {
return false;
}
if (input_identifier.identifier != identifier) {
return false;
}
return true;
}
void InputEngine::BeginConfiguration() {
configuring = true;
}
void InputEngine::EndConfiguration() {
configuring = false;
}
const std::string& InputEngine::GetEngineName() const {
return input_engine;
}
int InputEngine::SetCallback(InputIdentifier input_identifier) {
std::scoped_lock lock{mutex_callback};
callback_list.insert_or_assign(last_callback_key, std::move(input_identifier));
return last_callback_key++;
}
void InputEngine::SetMappingCallback(MappingCallback callback) {
std::scoped_lock lock{mutex_callback};
mapping_callback = std::move(callback);
}
void InputEngine::DeleteCallback(int key) {
std::scoped_lock lock{mutex_callback};
const auto& iterator = callback_list.find(key);
if (iterator == callback_list.end()) {
LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
return;
}
callback_list.erase(iterator);
}
} // namespace InputCommon

View File

@ -0,0 +1,219 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <mutex>
#include <unordered_map>
#include "common/common_types.h"
#include "common/input.h"
#include "common/param_package.h"
#include "common/uuid.h"
#include "input_common/main.h"
// Pad Identifier of data source
struct PadIdentifier {
Common::UUID guid{};
std::size_t port{};
std::size_t pad{};
friend constexpr bool operator==(const PadIdentifier&, const PadIdentifier&) = default;
};
// Basic motion data containing data from the sensors and a timestamp in microseconds
struct BasicMotion {
float gyro_x{};
float gyro_y{};
float gyro_z{};
float accel_x{};
float accel_y{};
float accel_z{};
u64 delta_timestamp{};
};
// Types of input that are stored in the engine
enum class EngineInputType {
None,
Analog,
Button,
HatButton,
Motion,
};
namespace std {
// Hash used to create lists from PadIdentifier data
template <>
struct hash<PadIdentifier> {
size_t operator()(const PadIdentifier& pad_id) const noexcept {
u64 hash_value = pad_id.guid.Hash();
hash_value ^= (static_cast<u64>(pad_id.port) << 32);
hash_value ^= static_cast<u64>(pad_id.pad);
return static_cast<size_t>(hash_value);
}
};
} // namespace std
namespace InputCommon {
// Data from the engine and device needed for creating a ParamPackage
struct MappingData {
std::string engine{};
PadIdentifier pad{};
EngineInputType type{};
int index{};
bool button_value{};
std::string hat_name{};
f32 axis_value{};
BasicMotion motion_value{};
};
// Triggered if data changed on the controller
struct UpdateCallback {
std::function<void()> on_change;
};
// Triggered if data changed on the controller and the engine is on configuring mode
struct MappingCallback {
std::function<void(const MappingData&)> on_data;
};
// Input Identifier of data source
struct InputIdentifier {
PadIdentifier identifier;
EngineInputType type;
int index;
UpdateCallback callback;
};
class InputEngine {
public:
explicit InputEngine(std::string input_engine_) : input_engine{std::move(input_engine_)} {}
virtual ~InputEngine() = default;
// Enable configuring mode for mapping
void BeginConfiguration();
// Disable configuring mode for mapping
void EndConfiguration();
// Sets rumble to a controller
virtual Common::Input::DriverResult SetVibration(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
return Common::Input::DriverResult::NotSupported;
}
// Returns true if device supports vibrations
virtual bool IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
return false;
}
// Sets polling mode to a controller
virtual Common::Input::PollingError SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::PollingMode polling_mode) {
return Common::Input::PollingError::NotSupported;
}
// Returns the engine name
[[nodiscard]] const std::string& GetEngineName() const;
/// Used for automapping features
virtual std::vector<Common::ParamPackage> GetInputDevices() const {
return {};
}
/// Retrieves the button mappings for the given device
virtual ButtonMapping GetButtonMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
return {};
}
/// Retrieves the analog mappings for the given device
virtual AnalogMapping GetAnalogMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
return {};
}
/// Retrieves the motion mappings for the given device
virtual MotionMapping GetMotionMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
return {};
}
/// Retrieves the name of the given input.
virtual Common::Input::ButtonNames GetUIName(
[[maybe_unused]] const Common::ParamPackage& params) const {
return Common::Input::ButtonNames::Engine;
}
/// Retrieves the index number of the given hat button direction
virtual u8 GetHatButtonId([[maybe_unused]] const std::string& direction_name) const {
return 0;
}
/// Returns true if axis of a stick aren't mapped in the correct direction
virtual bool IsStickInverted([[maybe_unused]] const Common::ParamPackage& params) {
return false;
}
void PreSetController(const PadIdentifier& identifier);
void PreSetButton(const PadIdentifier& identifier, int button);
void PreSetHatButton(const PadIdentifier& identifier, int button);
void PreSetAxis(const PadIdentifier& identifier, int axis);
void PreSetMotion(const PadIdentifier& identifier, int motion);
void ResetButtonState();
void ResetAnalogState();
bool GetButton(const PadIdentifier& identifier, int button) const;
bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const;
f32 GetAxis(const PadIdentifier& identifier, int axis) const;
BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const;
int SetCallback(InputIdentifier input_identifier);
void SetMappingCallback(MappingCallback callback);
void DeleteCallback(int key);
protected:
void SetButton(const PadIdentifier& identifier, int button, bool value);
void SetHatButton(const PadIdentifier& identifier, int button, u8 value);
void SetAxis(const PadIdentifier& identifier, int axis, f32 value);
void SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value);
virtual std::string GetHatButtonName([[maybe_unused]] u8 direction_value) const {
return "Unknown";
}
private:
struct ControllerData {
std::unordered_map<int, bool> buttons;
std::unordered_map<int, u8> hat_buttons;
std::unordered_map<int, float> axes;
std::unordered_map<int, BasicMotion> motions;
};
void TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool value);
void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value);
void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value);
void TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value);
bool IsInputIdentifierEqual(const InputIdentifier& input_identifier,
const PadIdentifier& identifier, EngineInputType type,
int index) const;
mutable std::mutex mutex;
mutable std::mutex mutex_callback;
bool configuring{false};
const std::string input_engine;
int last_callback_key = 0;
std::unordered_map<PadIdentifier, ControllerData> controller_list;
std::unordered_map<int, InputIdentifier> callback_list;
MappingCallback mapping_callback;
};
} // namespace InputCommon

View File

@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/settings.h"
#include "input_common/input_engine.h"
#include "input_common/input_mapping.h"
namespace InputCommon {
MappingFactory::MappingFactory() = default;
void MappingFactory::BeginMapping(Polling::InputType type) {
is_enabled = true;
input_type = type;
input_queue.Clear();
first_axis = -1;
second_axis = -1;
}
Common::ParamPackage MappingFactory::GetNextInput() {
Common::ParamPackage input;
input_queue.Pop(input);
return input;
}
void MappingFactory::RegisterInput(const MappingData& data) {
if (!is_enabled) {
return;
}
if (!IsDriverValid(data)) {
return;
}
switch (input_type) {
case Polling::InputType::Button:
RegisterButton(data);
return;
case Polling::InputType::Stick:
RegisterStick(data);
return;
case Polling::InputType::Motion:
RegisterMotion(data);
return;
default:
return;
}
}
void MappingFactory::StopMapping() {
is_enabled = false;
input_type = Polling::InputType::None;
input_queue.Clear();
}
void MappingFactory::RegisterButton(const MappingData& data) {
Common::ParamPackage new_input;
new_input.Set("engine", data.engine);
if (data.pad.guid.IsValid()) {
new_input.Set("guid", data.pad.guid.RawString());
}
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
switch (data.type) {
case EngineInputType::Button:
// Workaround for old compatibility
if (data.engine == "keyboard") {
new_input.Set("code", data.index);
break;
}
new_input.Set("button", data.index);
break;
case EngineInputType::HatButton:
new_input.Set("hat", data.index);
new_input.Set("direction", data.hat_name);
break;
case EngineInputType::Analog:
// Ignore mouse axis when mapping buttons
if (data.engine == "mouse") {
return;
}
new_input.Set("axis", data.index);
new_input.Set("threshold", 0.5f);
break;
default:
return;
}
input_queue.Push(new_input);
}
void MappingFactory::RegisterStick(const MappingData& data) {
Common::ParamPackage new_input;
new_input.Set("engine", data.engine);
if (data.pad.guid.IsValid()) {
new_input.Set("guid", data.pad.guid.RawString());
}
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
// If engine is mouse map the mouse position as a joystick
if (data.engine == "mouse") {
new_input.Set("axis_x", 0);
new_input.Set("axis_y", 1);
new_input.Set("threshold", 0.5f);
new_input.Set("range", 1.0f);
new_input.Set("deadzone", 0.0f);
input_queue.Push(new_input);
return;
}
switch (data.type) {
case EngineInputType::Button:
case EngineInputType::HatButton:
RegisterButton(data);
return;
case EngineInputType::Analog:
if (first_axis == data.index) {
return;
}
if (first_axis == -1) {
first_axis = data.index;
return;
}
new_input.Set("axis_x", first_axis);
new_input.Set("axis_y", data.index);
new_input.Set("threshold", 0.5f);
new_input.Set("range", 0.95f);
new_input.Set("deadzone", 0.15f);
break;
default:
return;
}
input_queue.Push(new_input);
}
void MappingFactory::RegisterMotion(const MappingData& data) {
Common::ParamPackage new_input;
new_input.Set("engine", data.engine);
if (data.pad.guid.IsValid()) {
new_input.Set("guid", data.pad.guid.RawString());
}
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
// If engine is mouse map the mouse position as 3 axis motion
if (data.engine == "mouse") {
new_input.Set("axis_x", 1);
new_input.Set("invert_x", "-");
new_input.Set("axis_y", 0);
new_input.Set("axis_z", 4);
new_input.Set("range", 1.0f);
new_input.Set("deadzone", 0.0f);
input_queue.Push(new_input);
return;
}
switch (data.type) {
case EngineInputType::Button:
case EngineInputType::HatButton:
RegisterButton(data);
return;
case EngineInputType::Analog:
if (first_axis == data.index) {
return;
}
if (second_axis == data.index) {
return;
}
if (first_axis == -1) {
first_axis = data.index;
return;
}
if (second_axis == -1) {
second_axis = data.index;
return;
}
new_input.Set("axis_x", first_axis);
new_input.Set("axis_y", second_axis);
new_input.Set("axis_z", data.index);
new_input.Set("range", 1.0f);
new_input.Set("deadzone", 0.20f);
break;
case EngineInputType::Motion:
new_input.Set("motion", data.index);
break;
default:
return;
}
input_queue.Push(new_input);
}
bool MappingFactory::IsDriverValid(const MappingData& data) const {
// Only port 0 can be mapped on the keyboard
if (data.engine == "keyboard" && data.pad.port != 0) {
return false;
}
// Only port 0 can be mapped on the mouse
if (data.engine == "mouse" && data.pad.port != 0) {
return false;
}
// To prevent mapping with two devices we disable any UDP except motion
if (data.engine == "cemuhookudp" && data.type != EngineInputType::Motion) {
return false;
}
// The following drivers don't need to be mapped
if (data.engine == "touch_from_button") {
return false;
}
if (data.engine == "analog_from_button") {
return false;
}
return true;
}
} // namespace InputCommon

View File

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/param_package.h"
#include "common/threadsafe_queue.h"
namespace InputCommon::Polling {
enum class InputType;
}
namespace InputCommon {
class InputEngine;
struct MappingData;
class MappingFactory {
public:
MappingFactory();
/**
* Resets all variables to begin the mapping process
* @param type type of input desired to be returned
*/
void BeginMapping(Polling::InputType type);
/// Returns an input event with mapping information from the input_queue
[[nodiscard]] Common::ParamPackage GetNextInput();
/**
* Registers mapping input data from the driver
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterInput(const MappingData& data);
/// Stop polling from all backends
void StopMapping();
private:
/**
* If provided data satisfies the requirements it will push an element to the input_queue
* Supported input:
* - Button: Creates a basic button ParamPackage
* - HatButton: Creates a basic hat button ParamPackage
* - Analog: Creates a basic analog ParamPackage
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterButton(const MappingData& data);
/**
* If provided data satisfies the requirements it will push an element to the input_queue
* Supported input:
* - Button, HatButton: Pass the data to RegisterButton
* - Analog: Stores the first axis and on the second axis creates a basic stick ParamPackage
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterStick(const MappingData& data);
/**
* If provided data satisfies the requirements it will push an element to the input_queue
* Supported input:
* - Button, HatButton: Pass the data to RegisterButton
* - Analog: Stores the first two axis and on the third axis creates a basic Motion
* ParamPackage
* - Motion: Creates a basic Motion ParamPackage
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterMotion(const MappingData& data);
/**
* Returns true if driver can be mapped
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
bool IsDriverValid(const MappingData& data) const;
Common::SPSCQueue<Common::ParamPackage> input_queue;
Polling::InputType input_type{Polling::InputType::None};
bool is_enabled{};
int first_axis = -1;
int second_axis = -1;
};
} // namespace InputCommon

View File

@ -0,0 +1,790 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/common_types.h"
#include "common/input.h"
#include "input_common/input_engine.h"
#include "input_common/input_poller.h"
namespace InputCommon {
class DummyInput final : public Common::Input::InputDevice {
public:
explicit DummyInput() = default;
};
class InputFromButton final : public Common::Input::InputDevice {
public:
explicit InputFromButton(PadIdentifier identifier_, int button_, bool toggle_, bool inverted_,
InputEngine* input_engine_)
: identifier(identifier_), button(button_), toggle(toggle_), inverted(inverted_),
input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
.type = EngineInputType::Button,
.index = button,
.callback = engine_callback,
};
last_button_value = false;
callback_key = input_engine->SetCallback(input_identifier);
}
~InputFromButton() override {
input_engine->DeleteCallback(callback_key);
}
Common::Input::ButtonStatus GetStatus() const {
return {
.value = input_engine->GetButton(identifier, button),
.inverted = inverted,
.toggle = toggle,
};
}
void ForceUpdate() override {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Button,
.button_status = GetStatus(),
};
last_button_value = status.button_status.value;
TriggerOnChange(status);
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Button,
.button_status = GetStatus(),
};
if (status.button_status.value != last_button_value) {
last_button_value = status.button_status.value;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
const int button;
const bool toggle;
const bool inverted;
int callback_key;
bool last_button_value;
InputEngine* input_engine;
};
class InputFromHatButton final : public Common::Input::InputDevice {
public:
explicit InputFromHatButton(PadIdentifier identifier_, int button_, u8 direction_, bool toggle_,
bool inverted_, InputEngine* input_engine_)
: identifier(identifier_), button(button_), direction(direction_), toggle(toggle_),
inverted(inverted_), input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
.type = EngineInputType::HatButton,
.index = button,
.callback = engine_callback,
};
last_button_value = false;
callback_key = input_engine->SetCallback(input_identifier);
}
~InputFromHatButton() override {
input_engine->DeleteCallback(callback_key);
}
Common::Input::ButtonStatus GetStatus() const {
return {
.value = input_engine->GetHatButton(identifier, button, direction),
.inverted = inverted,
.toggle = toggle,
};
}
void ForceUpdate() override {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Button,
.button_status = GetStatus(),
};
last_button_value = status.button_status.value;
TriggerOnChange(status);
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Button,
.button_status = GetStatus(),
};
if (status.button_status.value != last_button_value) {
last_button_value = status.button_status.value;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
const int button;
const u8 direction;
const bool toggle;
const bool inverted;
int callback_key;
bool last_button_value;
InputEngine* input_engine;
};
class InputFromStick final : public Common::Input::InputDevice {
public:
explicit InputFromStick(PadIdentifier identifier_, int axis_x_, int axis_y_,
Common::Input::AnalogProperties properties_x_,
Common::Input::AnalogProperties properties_y_,
InputEngine* input_engine_)
: identifier(identifier_), axis_x(axis_x_), axis_y(axis_y_), properties_x(properties_x_),
properties_y(properties_y_),
input_engine(input_engine_), invert_axis_y{input_engine_->GetEngineName() == "sdl"} {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier x_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_x,
.callback = engine_callback,
};
const InputIdentifier y_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_y,
.callback = engine_callback,
};
last_axis_x_value = 0.0f;
last_axis_y_value = 0.0f;
callback_key_x = input_engine->SetCallback(x_input_identifier);
callback_key_y = input_engine->SetCallback(y_input_identifier);
}
~InputFromStick() override {
input_engine->DeleteCallback(callback_key_x);
input_engine->DeleteCallback(callback_key_y);
}
Common::Input::StickStatus GetStatus() const {
Common::Input::StickStatus status;
status.x = {
.raw_value = input_engine->GetAxis(identifier, axis_x),
.properties = properties_x,
};
status.y = {
.raw_value = input_engine->GetAxis(identifier, axis_y),
.properties = properties_y,
};
// This is a workaround to keep compatibility with old yuzu versions. Vertical axis is
// inverted on SDL compared to Nintendo
if (invert_axis_y) {
status.y.raw_value = -status.y.raw_value;
}
return status;
}
void ForceUpdate() override {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Stick,
.stick_status = GetStatus(),
};
last_axis_x_value = status.stick_status.x.raw_value;
last_axis_y_value = status.stick_status.y.raw_value;
TriggerOnChange(status);
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Stick,
.stick_status = GetStatus(),
};
if (status.stick_status.x.raw_value != last_axis_x_value ||
status.stick_status.y.raw_value != last_axis_y_value) {
last_axis_x_value = status.stick_status.x.raw_value;
last_axis_y_value = status.stick_status.y.raw_value;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
const int axis_x;
const int axis_y;
const Common::Input::AnalogProperties properties_x;
const Common::Input::AnalogProperties properties_y;
int callback_key_x;
int callback_key_y;
float last_axis_x_value;
float last_axis_y_value;
InputEngine* input_engine;
const bool invert_axis_y;
};
class InputFromTouch final : public Common::Input::InputDevice {
public:
explicit InputFromTouch(PadIdentifier identifier_, int button_, bool toggle_, bool inverted_,
int axis_x_, int axis_y_, Common::Input::AnalogProperties properties_x_,
Common::Input::AnalogProperties properties_y_,
InputEngine* input_engine_)
: identifier(identifier_), button(button_), toggle(toggle_), inverted(inverted_),
axis_x(axis_x_), axis_y(axis_y_), properties_x(properties_x_),
properties_y(properties_y_), input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier button_input_identifier{
.identifier = identifier,
.type = EngineInputType::Button,
.index = button,
.callback = engine_callback,
};
const InputIdentifier x_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_x,
.callback = engine_callback,
};
const InputIdentifier y_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_y,
.callback = engine_callback,
};
last_axis_x_value = 0.0f;
last_axis_y_value = 0.0f;
last_button_value = false;
callback_key_button = input_engine->SetCallback(button_input_identifier);
callback_key_x = input_engine->SetCallback(x_input_identifier);
callback_key_y = input_engine->SetCallback(y_input_identifier);
}
~InputFromTouch() override {
input_engine->DeleteCallback(callback_key_button);
input_engine->DeleteCallback(callback_key_x);
input_engine->DeleteCallback(callback_key_y);
}
Common::Input::TouchStatus GetStatus() const {
Common::Input::TouchStatus status{};
status.pressed = {
.value = input_engine->GetButton(identifier, button),
.inverted = inverted,
.toggle = toggle,
};
status.x = {
.raw_value = input_engine->GetAxis(identifier, axis_x),
.properties = properties_x,
};
status.y = {
.raw_value = input_engine->GetAxis(identifier, axis_y),
.properties = properties_y,
};
return status;
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Touch,
.touch_status = GetStatus(),
};
if (status.touch_status.x.raw_value != last_axis_x_value ||
status.touch_status.y.raw_value != last_axis_y_value ||
status.touch_status.pressed.value != last_button_value) {
last_axis_x_value = status.touch_status.x.raw_value;
last_axis_y_value = status.touch_status.y.raw_value;
last_button_value = status.touch_status.pressed.value;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
const int button;
const bool toggle;
const bool inverted;
const int axis_x;
const int axis_y;
const Common::Input::AnalogProperties properties_x;
const Common::Input::AnalogProperties properties_y;
int callback_key_button;
int callback_key_x;
int callback_key_y;
bool last_button_value;
float last_axis_x_value;
float last_axis_y_value;
InputEngine* input_engine;
};
class InputFromAnalog final : public Common::Input::InputDevice {
public:
explicit InputFromAnalog(PadIdentifier identifier_, int axis_,
Common::Input::AnalogProperties properties_,
InputEngine* input_engine_)
: identifier(identifier_), axis(axis_), properties(properties_),
input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis,
.callback = engine_callback,
};
last_axis_value = 0.0f;
callback_key = input_engine->SetCallback(input_identifier);
}
~InputFromAnalog() override {
input_engine->DeleteCallback(callback_key);
}
Common::Input::AnalogStatus GetStatus() const {
return {
.raw_value = input_engine->GetAxis(identifier, axis),
.properties = properties,
};
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Analog,
.analog_status = GetStatus(),
};
if (status.analog_status.raw_value != last_axis_value) {
last_axis_value = status.analog_status.raw_value;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
const int axis;
const Common::Input::AnalogProperties properties;
int callback_key;
float last_axis_value;
InputEngine* input_engine;
};
class InputFromMotion final : public Common::Input::InputDevice {
public:
explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_,
InputEngine* input_engine_)
: identifier(identifier_), motion_sensor(motion_sensor_), gyro_threshold(gyro_threshold_),
input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
.type = EngineInputType::Motion,
.index = motion_sensor,
.callback = engine_callback,
};
callback_key = input_engine->SetCallback(input_identifier);
}
~InputFromMotion() override {
input_engine->DeleteCallback(callback_key);
}
Common::Input::MotionStatus GetStatus() const {
const auto basic_motion = input_engine->GetMotion(identifier, motion_sensor);
Common::Input::MotionStatus status{};
const Common::Input::AnalogProperties properties = {
.deadzone = 0.0f,
.range = 1.0f,
.threshold = gyro_threshold,
.offset = 0.0f,
};
status.accel.x = {.raw_value = basic_motion.accel_x, .properties = properties};
status.accel.y = {.raw_value = basic_motion.accel_y, .properties = properties};
status.accel.z = {.raw_value = basic_motion.accel_z, .properties = properties};
status.gyro.x = {.raw_value = basic_motion.gyro_x, .properties = properties};
status.gyro.y = {.raw_value = basic_motion.gyro_y, .properties = properties};
status.gyro.z = {.raw_value = basic_motion.gyro_z, .properties = properties};
status.delta_timestamp = basic_motion.delta_timestamp;
return status;
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Motion,
.motion_status = GetStatus(),
};
TriggerOnChange(status);
}
private:
const PadIdentifier identifier;
const int motion_sensor;
const float gyro_threshold;
int callback_key;
InputEngine* input_engine;
};
class InputFromAxisMotion final : public Common::Input::InputDevice {
public:
explicit InputFromAxisMotion(PadIdentifier identifier_, int axis_x_, int axis_y_, int axis_z_,
Common::Input::AnalogProperties properties_x_,
Common::Input::AnalogProperties properties_y_,
Common::Input::AnalogProperties properties_z_,
InputEngine* input_engine_)
: identifier(identifier_), axis_x(axis_x_), axis_y(axis_y_), axis_z(axis_z_),
properties_x(properties_x_), properties_y(properties_y_), properties_z(properties_z_),
input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier x_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_x,
.callback = engine_callback,
};
const InputIdentifier y_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_y,
.callback = engine_callback,
};
const InputIdentifier z_input_identifier{
.identifier = identifier,
.type = EngineInputType::Analog,
.index = axis_z,
.callback = engine_callback,
};
last_axis_x_value = 0.0f;
last_axis_y_value = 0.0f;
last_axis_z_value = 0.0f;
callback_key_x = input_engine->SetCallback(x_input_identifier);
callback_key_y = input_engine->SetCallback(y_input_identifier);
callback_key_z = input_engine->SetCallback(z_input_identifier);
}
~InputFromAxisMotion() override {
input_engine->DeleteCallback(callback_key_x);
input_engine->DeleteCallback(callback_key_y);
input_engine->DeleteCallback(callback_key_z);
}
Common::Input::MotionStatus GetStatus() const {
Common::Input::MotionStatus status{};
status.gyro.x = {
.raw_value = input_engine->GetAxis(identifier, axis_x),
.properties = properties_x,
};
status.gyro.y = {
.raw_value = input_engine->GetAxis(identifier, axis_y),
.properties = properties_y,
};
status.gyro.z = {
.raw_value = input_engine->GetAxis(identifier, axis_z),
.properties = properties_z,
};
status.delta_timestamp = 5000;
status.force_update = true;
return status;
}
void ForceUpdate() override {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Motion,
.motion_status = GetStatus(),
};
last_axis_x_value = status.motion_status.gyro.x.raw_value;
last_axis_y_value = status.motion_status.gyro.y.raw_value;
last_axis_z_value = status.motion_status.gyro.z.raw_value;
TriggerOnChange(status);
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Motion,
.motion_status = GetStatus(),
};
if (status.motion_status.gyro.x.raw_value != last_axis_x_value ||
status.motion_status.gyro.y.raw_value != last_axis_y_value ||
status.motion_status.gyro.z.raw_value != last_axis_z_value) {
last_axis_x_value = status.motion_status.gyro.x.raw_value;
last_axis_y_value = status.motion_status.gyro.y.raw_value;
last_axis_z_value = status.motion_status.gyro.z.raw_value;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
const int axis_x;
const int axis_y;
const int axis_z;
const Common::Input::AnalogProperties properties_x;
const Common::Input::AnalogProperties properties_y;
const Common::Input::AnalogProperties properties_z;
int callback_key_x;
int callback_key_y;
int callback_key_z;
float last_axis_x_value;
float last_axis_y_value;
float last_axis_z_value;
InputEngine* input_engine;
};
class OutputFromIdentifier final : public Common::Input::OutputDevice {
public:
explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_)
: identifier(identifier_), input_engine(input_engine_) {}
Common::Input::PollingError SetPollingMode(Common::Input::PollingMode polling_mode) override {
return input_engine->SetPollingMode(identifier, polling_mode);
}
private:
const PadIdentifier identifier;
InputEngine* input_engine;
};
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateButtonDevice(
const Common::ParamPackage& params) {
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
const auto button_id = params.Get("button", 0);
const auto keyboard_key = params.Get("code", 0);
const auto toggle = params.Get("toggle", false) != 0;
const auto inverted = params.Get("inverted", false) != 0;
input_engine->PreSetController(identifier);
input_engine->PreSetButton(identifier, button_id);
input_engine->PreSetButton(identifier, keyboard_key);
if (keyboard_key != 0) {
return std::make_unique<InputFromButton>(identifier, keyboard_key, toggle, inverted,
input_engine.get());
}
return std::make_unique<InputFromButton>(identifier, button_id, toggle, inverted,
input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateHatButtonDevice(
const Common::ParamPackage& params) {
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
const auto button_id = params.Get("hat", 0);
const auto direction = input_engine->GetHatButtonId(params.Get("direction", ""));
const auto toggle = params.Get("toggle", false) != 0;
const auto inverted = params.Get("inverted", false) != 0;
input_engine->PreSetController(identifier);
input_engine->PreSetHatButton(identifier, button_id);
return std::make_unique<InputFromHatButton>(identifier, button_id, direction, toggle, inverted,
input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateStickDevice(
const Common::ParamPackage& params) {
const auto deadzone = std::clamp(params.Get("deadzone", 0.15f), 0.0f, 1.0f);
const auto range = std::clamp(params.Get("range", 0.95f), 0.25f, 1.50f);
const auto threshold = std::clamp(params.Get("threshold", 0.5f), 0.0f, 1.0f);
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
const auto axis_x = params.Get("axis_x", 0);
const Common::Input::AnalogProperties properties_x = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_x", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_x", "+") == "-",
};
const auto axis_y = params.Get("axis_y", 1);
const Common::Input::AnalogProperties properties_y = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_y", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_y", "+") != "+",
};
input_engine->PreSetController(identifier);
input_engine->PreSetAxis(identifier, axis_x);
input_engine->PreSetAxis(identifier, axis_y);
return std::make_unique<InputFromStick>(identifier, axis_x, axis_y, properties_x, properties_y,
input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateAnalogDevice(
const Common::ParamPackage& params) {
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
const auto axis = params.Get("axis", 0);
const Common::Input::AnalogProperties properties = {
.deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, 1.0f),
.range = std::clamp(params.Get("range", 1.0f), 0.25f, 1.50f),
.threshold = std::clamp(params.Get("threshold", 0.5f), 0.0f, 1.0f),
.offset = std::clamp(params.Get("offset", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert", "+") == "-",
.toggle = params.Get("toggle", false) != 0,
};
input_engine->PreSetController(identifier);
input_engine->PreSetAxis(identifier, axis);
return std::make_unique<InputFromAnalog>(identifier, axis, properties, input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateTouchDevice(
const Common::ParamPackage& params) {
const auto deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, 1.0f);
const auto range = std::clamp(params.Get("range", 1.0f), 0.25f, 1.50f);
const auto threshold = std::clamp(params.Get("threshold", 0.5f), 0.0f, 1.0f);
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
const auto button = params.Get("button", 0);
const auto toggle = params.Get("toggle", false) != 0;
const auto inverted = params.Get("inverted", false) != 0;
const auto axis_x = params.Get("axis_x", 0);
const Common::Input::AnalogProperties properties_x = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_x", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_x", "+") == "-",
};
const auto axis_y = params.Get("axis_y", 1);
const Common::Input::AnalogProperties properties_y = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_y", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_y", false) != 0,
};
input_engine->PreSetController(identifier);
input_engine->PreSetAxis(identifier, axis_x);
input_engine->PreSetAxis(identifier, axis_y);
input_engine->PreSetButton(identifier, button);
return std::make_unique<InputFromTouch>(identifier, button, toggle, inverted, axis_x, axis_y,
properties_x, properties_y, input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateMotionDevice(
Common::ParamPackage params) {
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
if (params.Has("motion")) {
const auto motion_sensor = params.Get("motion", 0);
const auto gyro_threshold = params.Get("threshold", 0.007f);
input_engine->PreSetController(identifier);
input_engine->PreSetMotion(identifier, motion_sensor);
return std::make_unique<InputFromMotion>(identifier, motion_sensor, gyro_threshold,
input_engine.get());
}
const auto deadzone = std::clamp(params.Get("deadzone", 0.15f), 0.0f, 1.0f);
const auto range = std::clamp(params.Get("range", 1.0f), 0.25f, 1.50f);
const auto threshold = std::clamp(params.Get("threshold", 0.5f), 0.0f, 1.0f);
const auto axis_x = params.Get("axis_x", 0);
const Common::Input::AnalogProperties properties_x = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_x", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_x", "+") == "-",
};
const auto axis_y = params.Get("axis_y", 1);
const Common::Input::AnalogProperties properties_y = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_y", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_y", "+") != "+",
};
const auto axis_z = params.Get("axis_z", 1);
const Common::Input::AnalogProperties properties_z = {
.deadzone = deadzone,
.range = range,
.threshold = threshold,
.offset = std::clamp(params.Get("offset_z", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert_z", "+") != "+",
};
input_engine->PreSetController(identifier);
input_engine->PreSetAxis(identifier, axis_x);
input_engine->PreSetAxis(identifier, axis_y);
input_engine->PreSetAxis(identifier, axis_z);
return std::make_unique<InputFromAxisMotion>(identifier, axis_x, axis_y, axis_z, properties_x,
properties_y, properties_z, input_engine.get());
}
InputFactory::InputFactory(std::shared_ptr<InputEngine> input_engine_)
: input_engine(std::move(input_engine_)) {}
std::unique_ptr<Common::Input::InputDevice> InputFactory::Create(
const Common::ParamPackage& params) {
if (params.Has("button") && params.Has("axis_x") && params.Has("axis_y")) {
return CreateTouchDevice(params);
}
if (params.Has("button") || params.Has("code")) {
return CreateButtonDevice(params);
}
if (params.Has("hat")) {
return CreateHatButtonDevice(params);
}
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return CreateMotionDevice(params);
}
if (params.Has("motion")) {
return CreateMotionDevice(params);
}
if (params.Has("axis_x") && params.Has("axis_y")) {
return CreateStickDevice(params);
}
if (params.Has("axis")) {
return CreateAnalogDevice(params);
}
LOG_ERROR(Input, "Invalid parameters given");
return std::make_unique<DummyInput>();
}
OutputFactory::OutputFactory(std::shared_ptr<InputEngine> input_engine_)
: input_engine(std::move(input_engine_)) {}
std::unique_ptr<Common::Input::OutputDevice> OutputFactory::Create(
const Common::ParamPackage& params) {
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
input_engine->PreSetController(identifier);
return std::make_unique<OutputFromIdentifier>(identifier, input_engine.get());
}
} // namespace InputCommon

View File

@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Input {
class InputDevice;
template <typename InputDevice>
class Factory;
}; // namespace Input
namespace InputCommon {
class InputEngine;
class OutputFactory final : public Common::Input::Factory<Common::Input::OutputDevice> {
public:
explicit OutputFactory(std::shared_ptr<InputEngine> input_engine_);
/**
* Creates an output device from the parameters given.
* @param params contains parameters for creating the device:
* - "guid" text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique output device with the parameters specified
*/
std::unique_ptr<Common::Input::OutputDevice> Create(
const Common::ParamPackage& params) override;
private:
std::shared_ptr<InputEngine> input_engine;
};
/**
* An Input factory. It receives input events and forward them to all input devices it created.
*/
class InputFactory final : public Common::Input::Factory<Common::Input::InputDevice> {
public:
explicit InputFactory(std::shared_ptr<InputEngine> input_engine_);
/**
* Creates an input device from the parameters given. Identifies the type of input to be
* returned if it contains the following parameters:
* - button: Contains "button" or "code"
* - hat_button: Contains "hat"
* - analog: Contains "axis"
* - stick: Contains "axis_x" and "axis_y"
* - motion: Contains "axis_x", "axis_y" and "axis_z"
* - motion: Contains "motion"
* - touch: Contains "button", "axis_x" and "axis_y"
* - output: Contains "output"
* @param params contains parameters for creating the device:
* - "code": the code of the keyboard key to bind with the input
* - "button": same as "code" but for controller buttons
* - "hat": similar as "button" but it's a group of hat buttons from SDL
* - "axis": the axis number of the axis to bind with the input
* - "motion": the motion number of the motion to bind with the input
* - "axis_x": same as axis but specifying horizontal direction
* - "axis_y": same as axis but specifying vertical direction
* - "axis_z": same as axis but specifying forward direction
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
private:
/**
* Creates a button device from the parameters given.
* @param params contains parameters for creating the device:
* - "code": the code of the keyboard key to bind with the input
* - "button": same as "code" but for controller buttons
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateButtonDevice(
const Common::ParamPackage& params);
/**
* Creates a hat button device from the parameters given.
* @param params contains parameters for creating the device:
* - "button": the controller hat id to bind with the input
* - "direction": the direction id to be detected
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateHatButtonDevice(
const Common::ParamPackage& params);
/**
* Creates a stick device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis_x": the controller horizontal axis id to bind with the input
* - "axis_y": the controller vertical axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset_x": the amount of offset in the x axis
* - "offset_y": the amount of offset in the y axis
* - "invert_x": inverts the sign of the horizontal axis
* - "invert_y": inverts the sign of the vertical axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateStickDevice(
const Common::ParamPackage& params);
/**
* Creates an analog device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis": the controller axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset": the amount of offset in the axis
* - "invert": inverts the sign of the axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateAnalogDevice(
const Common::ParamPackage& params);
/**
* Creates a touch device from the parameters given.
* @param params contains parameters for creating the device:
* - "button": the controller hat id to bind with the input
* - "direction": the direction id to be detected
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "axis_x": the controller horizontal axis id to bind with the input
* - "axis_y": the controller vertical axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset_x": the amount of offset in the x axis
* - "offset_y": the amount of offset in the y axis
* - "invert_x": inverts the sign of the horizontal axis
* - "invert_y": inverts the sign of the vertical axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateTouchDevice(
const Common::ParamPackage& params);
/**
* Creates a motion device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis_x": the controller horizontal axis id to bind with the input
* - "axis_y": the controller vertical axis id to bind with the input
* - "axis_z": the controller forward axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "offset_x": the amount of offset in the x axis
* - "offset_y": the amount of offset in the y axis
* - "offset_z": the amount of offset in the z axis
* - "invert_x": inverts the sign of the horizontal axis
* - "invert_y": inverts the sign of the vertical axis
* - "invert_z": inverts the sign of the forward axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateMotionDevice(Common::ParamPackage params);
std::shared_ptr<InputEngine> input_engine;
};
} // namespace InputCommon

View File

@ -1,89 +0,0 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <atomic>
#include <list>
#include <mutex>
#include <utility>
#include "input_common/keyboard.h"
namespace InputCommon {
class KeyButton final : public Input::ButtonDevice {
public:
explicit KeyButton(std::atomic<bool>& status_) : status(status_) {}
~KeyButton() override = default;
bool GetStatus() const override {
return status.load();
}
friend class KeyButtonList;
private:
std::atomic<bool>& status;
};
struct KeyButtonPair {
explicit KeyButtonPair(int key_code_) : key_code(key_code_) {}
int key_code;
std::atomic<bool> status{false};
};
class KeyButtonList {
public:
KeyButtonPair& AddKeyButton(int key_code) {
std::lock_guard guard{mutex};
auto it = std::find_if(list.begin(), list.end(), [key_code](const KeyButtonPair& pair) {
return pair.key_code == key_code;
});
if (it == list.end()) {
return list.emplace_back(key_code);
}
return *it;
}
void ChangeKeyStatus(int key_code, bool pressed) {
std::lock_guard guard{mutex};
for (KeyButtonPair& pair : list) {
if (pair.key_code == key_code)
pair.status.store(pressed);
}
}
void ChangeAllKeyStatus(bool pressed) {
std::lock_guard guard{mutex};
for (KeyButtonPair& pair : list) {
pair.status.store(pressed);
}
}
private:
std::mutex mutex;
std::list<KeyButtonPair> list;
};
Keyboard::Keyboard() : key_button_list{std::make_shared<KeyButtonList>()} {}
std::unique_ptr<Input::ButtonDevice> Keyboard::Create(const Common::ParamPackage& params) {
int key_code = params.Get("code", 0);
auto& pair = key_button_list->AddKeyButton(key_code);
return std::make_unique<KeyButton>(pair.status);
}
void Keyboard::PressKey(int key_code) {
key_button_list->ChangeKeyStatus(key_code, true);
}
void Keyboard::ReleaseKey(int key_code) {
key_button_list->ChangeKeyStatus(key_code, false);
}
void Keyboard::ReleaseAllKeys() {
key_button_list->ChangeAllKeyStatus(false);
}
} // namespace InputCommon

View File

@ -1,47 +0,0 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include "core/frontend/input.h"
namespace InputCommon {
class KeyButtonList;
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Keyboard final : public Input::Factory<Input::ButtonDevice> {
public:
Keyboard();
/**
* Creates a button device from a keyboard key
* @param params contains parameters for creating the device:
* - "code": the code of the key to bind with the button
*/
std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override;
/**
* Sets the status of all buttons bound with the key to pressed
* @param key_code the code of the key to press
*/
void PressKey(int key_code);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseKey(int key_code);
void ReleaseAllKeys();
private:
std::shared_ptr<KeyButtonList> key_button_list;
};
} // namespace InputCommon

View File

@ -1,165 +1,387 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include <thread>
#include "common/input.h"
#include "common/param_package.h"
#include "input_common/analog_from_button.h"
#ifdef ENABLE_GCADAPTER
#include "input_common/gcadapter/gc_adapter.h"
#include "input_common/gcadapter/gc_poller.h"
#endif
#include "input_common/keyboard.h"
#include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/drivers/virtual_gamepad.h"
#include "input_common/helpers/stick_from_buttons.h"
#include "input_common/helpers/touch_from_buttons.h"
#include "input_common/input_engine.h"
#include "input_common/input_mapping.h"
#include "input_common/input_poller.h"
#include "input_common/main.h"
#include "input_common/motion_emu.h"
#include "input_common/sdl/sdl.h"
#include "input_common/sdl/sdl_impl.h"
#include "input_common/touch_from_button.h"
#include "input_common/udp/udp.h"
#ifdef HAVE_LIBUSB
#include "input_common/drivers/gc_adapter.h"
#endif
#ifdef HAVE_SDL2
#include "input_common/drivers/sdl_driver.h"
#endif
namespace InputCommon {
#ifdef ENABLE_GCADAPTER
std::shared_ptr<GCButtonFactory> gcbuttons;
std::shared_ptr<GCAnalogFactory> gcanalog;
std::shared_ptr<GCAdapter::Adapter> gcadapter;
struct InputSubsystem::Impl {
template <typename Engine>
void RegisterEngine(std::string name, std::shared_ptr<Engine>& engine) {
MappingCallback mapping_callback{[this](const MappingData& data) { RegisterInput(data); }};
engine = std::make_shared<Engine>(name);
engine->SetMappingCallback(mapping_callback);
std::shared_ptr<InputFactory> input_factory = std::make_shared<InputFactory>(engine);
std::shared_ptr<OutputFactory> output_factory = std::make_shared<OutputFactory>(engine);
Common::Input::RegisterInputFactory(engine->GetEngineName(), std::move(input_factory));
Common::Input::RegisterOutputFactory(engine->GetEngineName(), std::move(output_factory));
}
void Initialize() {
mapping_factory = std::make_shared<MappingFactory>();
RegisterEngine("keyboard", keyboard);
RegisterEngine("mouse", mouse);
RegisterEngine("touch", touch_screen);
#ifdef HAVE_LIBUSB
RegisterEngine("gcpad", gcadapter);
#endif
static std::shared_ptr<Keyboard> keyboard;
static std::shared_ptr<MotionEmu> motion_emu;
static std::unique_ptr<CemuhookUDP::State> udp;
static std::unique_ptr<SDL::State> sdl;
void Init() {
#ifdef ENABLE_GCADAPTER
gcadapter = std::make_shared<GCAdapter::Adapter>();
gcbuttons = std::make_shared<GCButtonFactory>(gcadapter);
Input::RegisterFactory<Input::ButtonDevice>("gcpad", gcbuttons);
gcanalog = std::make_shared<GCAnalogFactory>(gcadapter);
Input::RegisterFactory<Input::AnalogDevice>("gcpad", gcanalog);
RegisterEngine("cemuhookudp", udp_client);
RegisterEngine("virtual_gamepad", virtual_gamepad);
#ifdef HAVE_SDL2
RegisterEngine("sdl", sdl);
#endif
keyboard = std::make_shared<Keyboard>();
Input::RegisterFactory<Input::ButtonDevice>("keyboard", keyboard);
Input::RegisterFactory<Input::AnalogDevice>("analog_from_button",
std::make_shared<AnalogFromButton>());
motion_emu = std::make_shared<MotionEmu>();
Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu);
Input::RegisterFactory<Input::TouchDevice>("touch_from_button",
std::make_shared<TouchFromButtonFactory>());
sdl = SDL::Init();
Common::Input::RegisterInputFactory("touch_from_button",
std::make_shared<TouchFromButton>());
Common::Input::RegisterInputFactory("analog_from_button",
std::make_shared<StickFromButton>());
}
udp = CemuhookUDP::Init();
template <typename Engine>
void UnregisterEngine(std::shared_ptr<Engine>& engine) {
Common::Input::UnregisterInputFactory(engine->GetEngineName());
Common::Input::UnregisterOutputFactory(engine->GetEngineName());
engine.reset();
}
void Shutdown() {
UnregisterEngine(keyboard);
UnregisterEngine(mouse);
UnregisterEngine(touch_screen);
#ifdef HAVE_LIBUSB
UnregisterEngine(gcadapter);
#endif
UnregisterEngine(udp_client);
UnregisterEngine(virtual_gamepad);
#ifdef HAVE_SDL2
UnregisterEngine(sdl);
#endif
Common::Input::UnregisterInputFactory("touch_from_button");
Common::Input::UnregisterInputFactory("analog_from_button");
}
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
std::vector<Common::ParamPackage> devices = {
Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},
};
auto keyboard_devices = keyboard->GetInputDevices();
devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());
auto mouse_devices = mouse->GetInputDevices();
devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end());
#ifdef HAVE_LIBUSB
auto gcadapter_devices = gcadapter->GetInputDevices();
devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end());
#endif
auto udp_devices = udp_client->GetInputDevices();
devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
#ifdef HAVE_SDL2
auto sdl_devices = sdl->GetInputDevices();
devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
#endif
return devices;
}
[[nodiscard]] std::shared_ptr<InputEngine> GetInputEngine(
const Common::ParamPackage& params) const {
if (!params.Has("engine") || params.Get("engine", "") == "any") {
return nullptr;
}
const std::string engine = params.Get("engine", "");
if (engine == keyboard->GetEngineName()) {
return keyboard;
}
if (engine == mouse->GetEngineName()) {
return mouse;
}
#ifdef HAVE_LIBUSB
if (engine == gcadapter->GetEngineName()) {
return gcadapter;
}
#endif
if (engine == udp_client->GetEngineName()) {
return udp_client;
}
#ifdef HAVE_SDL2
if (engine == sdl->GetEngineName()) {
return sdl;
}
#endif
return nullptr;
}
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(
const Common::ParamPackage& params) const {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return {};
}
return input_engine->GetAnalogMappingForDevice(params);
}
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(
const Common::ParamPackage& params) const {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return {};
}
return input_engine->GetButtonMappingForDevice(params);
}
[[nodiscard]] MotionMapping GetMotionMappingForDevice(
const Common::ParamPackage& params) const {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return {};
}
return input_engine->GetMotionMappingForDevice(params);
}
Common::Input::ButtonNames GetButtonName(const Common::ParamPackage& params) const {
if (!params.Has("engine") || params.Get("engine", "") == "any") {
return Common::Input::ButtonNames::Undefined;
}
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return Common::Input::ButtonNames::Invalid;
}
return input_engine->GetUIName(params);
}
bool IsStickInverted(const Common::ParamPackage& params) {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return false;
}
return input_engine->IsStickInverted(params);
}
bool IsController(const Common::ParamPackage& params) {
const std::string engine = params.Get("engine", "");
if (engine == mouse->GetEngineName()) {
return true;
}
#ifdef HAVE_LIBUSB
if (engine == gcadapter->GetEngineName()) {
return true;
}
#endif
if (engine == udp_client->GetEngineName()) {
return true;
}
if (engine == virtual_gamepad->GetEngineName()) {
return true;
}
#ifdef HAVE_SDL2
if (engine == sdl->GetEngineName()) {
return true;
}
#endif
return false;
}
void BeginConfiguration() {
keyboard->BeginConfiguration();
mouse->BeginConfiguration();
#ifdef HAVE_LIBUSB
gcadapter->BeginConfiguration();
#endif
udp_client->BeginConfiguration();
#ifdef HAVE_SDL2
sdl->BeginConfiguration();
#endif
}
void EndConfiguration() {
keyboard->EndConfiguration();
mouse->EndConfiguration();
#ifdef HAVE_LIBUSB
gcadapter->EndConfiguration();
#endif
udp_client->EndConfiguration();
#ifdef HAVE_SDL2
sdl->EndConfiguration();
#endif
}
void PumpEvents() const {
#ifdef HAVE_SDL2
sdl->PumpEvents();
#endif
}
void RegisterInput(const MappingData& data) {
mapping_factory->RegisterInput(data);
}
std::shared_ptr<MappingFactory> mapping_factory;
std::shared_ptr<Keyboard> keyboard;
std::shared_ptr<Mouse> mouse;
std::shared_ptr<TouchScreen> touch_screen;
std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
std::shared_ptr<VirtualGamepad> virtual_gamepad;
#ifdef HAVE_LIBUSB
std::shared_ptr<GCAdapter> gcadapter;
#endif
#ifdef HAVE_SDL2
std::shared_ptr<SDLDriver> sdl;
#endif
};
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
InputSubsystem::~InputSubsystem() = default;
void InputSubsystem::Initialize() {
impl->Initialize();
}
void Shutdown() {
#ifdef ENABLE_GCADAPTER
Input::UnregisterFactory<Input::ButtonDevice>("gcpad");
Input::UnregisterFactory<Input::AnalogDevice>("gcpad");
gcbuttons.reset();
gcanalog.reset();
#endif
Input::UnregisterFactory<Input::ButtonDevice>("keyboard");
keyboard.reset();
Input::UnregisterFactory<Input::AnalogDevice>("analog_from_button");
Input::UnregisterFactory<Input::MotionDevice>("motion_emu");
motion_emu.reset();
Input::UnregisterFactory<Input::TouchDevice>("emu_window");
Input::UnregisterFactory<Input::TouchDevice>("touch_from_button");
sdl.reset();
udp.reset();
void InputSubsystem::Shutdown() {
impl->Shutdown();
}
Keyboard* GetKeyboard() {
return keyboard.get();
Keyboard* InputSubsystem::GetKeyboard() {
return impl->keyboard.get();
}
MotionEmu* GetMotionEmu() {
return motion_emu.get();
const Keyboard* InputSubsystem::GetKeyboard() const {
return impl->keyboard.get();
}
Mouse* InputSubsystem::GetMouse() {
return impl->mouse.get();
}
const Mouse* InputSubsystem::GetMouse() const {
return impl->mouse.get();
}
TouchScreen* InputSubsystem::GetTouchScreen() {
return impl->touch_screen.get();
}
const TouchScreen* InputSubsystem::GetTouchScreen() const {
return impl->touch_screen.get();
}
VirtualGamepad* InputSubsystem::GetVirtualGamepad() {
return impl->virtual_gamepad.get();
}
const VirtualGamepad* InputSubsystem::GetVirtualGamepad() const {
return impl->virtual_gamepad.get();
}
std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
return impl->GetInputDevices();
}
AnalogMapping InputSubsystem::GetAnalogMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetAnalogMappingForDevice(device);
}
ButtonMapping InputSubsystem::GetButtonMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetButtonMappingForDevice(device);
}
MotionMapping InputSubsystem::GetMotionMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetMotionMappingForDevice(device);
}
Common::Input::ButtonNames InputSubsystem::GetButtonName(const Common::ParamPackage& params) const {
return impl->GetButtonName(params);
}
bool InputSubsystem::IsController(const Common::ParamPackage& params) const {
return impl->IsController(params);
}
bool InputSubsystem::IsStickInverted(const Common::ParamPackage& params) const {
if (params.Has("axis_x") && params.Has("axis_y")) {
return impl->IsStickInverted(params);
}
return false;
}
void InputSubsystem::ReloadInputDevices() {
impl->udp_client.get()->ReloadSockets();
}
void InputSubsystem::BeginMapping(Polling::InputType type) {
impl->BeginConfiguration();
impl->mapping_factory->BeginMapping(type);
}
Common::ParamPackage InputSubsystem::GetNextInput() const {
return impl->mapping_factory->GetNextInput();
}
void InputSubsystem::StopMapping() const {
impl->EndConfiguration();
impl->mapping_factory->StopMapping();
}
void InputSubsystem::PumpEvents() const {
impl->PumpEvents();
}
std::string GenerateKeyboardParam(int key_code) {
Common::ParamPackage param{
{"engine", "keyboard"},
{"code", std::to_string(key_code)},
};
Common::ParamPackage param;
param.Set("engine", "keyboard");
param.Set("code", key_code);
param.Set("toggle", false);
return param.Serialize();
}
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
int key_modifier, float modifier_scale) {
Common::ParamPackage circle_pad_param{
{"engine", "analog_from_button"},
{"up", GenerateKeyboardParam(key_up)},
{"down", GenerateKeyboardParam(key_down)},
{"left", GenerateKeyboardParam(key_left)},
{"right", GenerateKeyboardParam(key_right)},
{"modifier", GenerateKeyboardParam(key_modifier)},
{"modifier_scale", std::to_string(modifier_scale)},
};
{"engine", "analog_from_button"},
{"up", GenerateKeyboardParam(key_up)},
{"down", GenerateKeyboardParam(key_down)},
{"left", GenerateKeyboardParam(key_left)},
{"right", GenerateKeyboardParam(key_right)},
{"modifier", GenerateKeyboardParam(key_modifier)},
{"modifier_scale", std::to_string(modifier_scale)},
};
return circle_pad_param.Serialize();
}
Common::ParamPackage GetControllerButtonBinds(const Common::ParamPackage& params, int button) {
const auto native_button{static_cast<Settings::NativeButton::Values>(button)};
const auto engine{params.Get("engine", "")};
if (engine == "sdl") {
return dynamic_cast<SDL::SDLState*>(sdl.get())->GetSDLControllerButtonBindByGUID(
params.Get("guid", "0"), params.Get("port", 0), native_button);
}
#ifdef ENABLE_GCADAPTER
if (engine == "gcpad") {
return gcbuttons->GetGcTo3DSMappedButton(params.Get("port", 0), native_button);
}
#endif
return {};
}
Common::ParamPackage GetControllerAnalogBinds(const Common::ParamPackage& params, int analog) {
const auto native_analog{static_cast<Settings::NativeAnalog::Values>(analog)};
const auto engine{params.Get("engine", "")};
if (engine == "sdl") {
return dynamic_cast<SDL::SDLState*>(sdl.get())->GetSDLControllerAnalogBindByGUID(
params.Get("guid", "0"), params.Get("port", 0), native_analog);
}
#ifdef ENABLE_GCADAPTER
if (engine == "gcpad") {
return gcanalog->GetGcTo3DSMappedAnalog(params.Get("port", 0), native_analog);
}
#endif
return {};
}
void ReloadInputDevices() {
if (!udp) {
return;
}
udp->ReloadUDPClient();
}
namespace Polling {
std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) {
std::vector<std::unique_ptr<DevicePoller>> pollers;
#ifdef HAVE_SDL2
pollers = sdl->GetPollers(type);
#endif
#ifdef ENABLE_GCADAPTER
switch (type) {
case DeviceType::Analog:
pollers.push_back(std::make_unique<GCAnalogFactory>(*gcanalog));
break;
case DeviceType::Button:
pollers.push_back(std::make_unique<GCButtonFactory>(*gcbuttons));
break;
default:
break;
}
#endif
return pollers;
}
} // namespace Polling
} // namespace InputCommon

View File

@ -1,72 +1,147 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace Common {
class ParamPackage;
}
namespace Common::Input {
enum class ButtonNames;
}
namespace Settings::NativeAnalog {
enum Values : int;
}
namespace Settings::NativeButton {
enum Values : int;
}
namespace Settings::NativeMotion {
enum Values : int;
}
namespace InputCommon {
/// Initializes and registers all built-in input device factories.
void Init();
/// Deregisters all built-in input device factories and shuts them down.
void Shutdown();
class Keyboard;
class Mouse;
class TouchScreen;
class VirtualGamepad;
struct MappingData;
} // namespace InputCommon
/// Gets the keyboard button device factory.
Keyboard* GetKeyboard();
class MotionEmu;
/// Gets the motion emulation factory.
MotionEmu* GetMotionEmu();
/// Generates a serialized param package for creating a keyboard button device
std::string GenerateKeyboardParam(int key_code);
/// Generates a serialized param package for creating an analog device taking input from keyboard
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
int key_modifier, float modifier_scale);
Common::ParamPackage GetControllerButtonBinds(const Common::ParamPackage& params, int button);
Common::ParamPackage GetControllerAnalogBinds(const Common::ParamPackage& params, int analog);
/// Reloads the input devices
void ReloadInputDevices();
namespace InputCommon {
namespace Polling {
enum class DeviceType { Button, Analog };
/// Type of input desired for mapping purposes
enum class InputType { None, Button, Stick, Motion, Touch };
} // namespace Polling
/**
* A class that can be used to get inputs from an input device like controllers without having to
* poll the device's status yourself
* Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default
* mapping for the device.
*/
class DevicePoller {
using AnalogMapping = std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>;
using ButtonMapping = std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>;
using MotionMapping = std::unordered_map<Settings::NativeMotion::Values, Common::ParamPackage>;
class InputSubsystem {
public:
virtual ~DevicePoller() = default;
/// Setup and start polling for inputs, should be called before GetNextInput
virtual void Start() = 0;
/// Stop polling
virtual void Stop() = 0;
explicit InputSubsystem();
~InputSubsystem();
InputSubsystem(const InputSubsystem&) = delete;
InputSubsystem& operator=(const InputSubsystem&) = delete;
InputSubsystem(InputSubsystem&&) = delete;
InputSubsystem& operator=(InputSubsystem&&) = delete;
/// Initializes and registers all built-in input device factories.
void Initialize();
/// Unregisters all built-in input device factories and shuts them down.
void Shutdown();
/// Retrieves the underlying keyboard device.
[[nodiscard]] Keyboard* GetKeyboard();
/// Retrieves the underlying keyboard device.
[[nodiscard]] const Keyboard* GetKeyboard() const;
/// Retrieves the underlying mouse device.
[[nodiscard]] Mouse* GetMouse();
/// Retrieves the underlying mouse device.
[[nodiscard]] const Mouse* GetMouse() const;
/// Retrieves the underlying touch screen device.
[[nodiscard]] TouchScreen* GetTouchScreen();
/// Retrieves the underlying touch screen device.
[[nodiscard]] const TouchScreen* GetTouchScreen() const;
/// Retrieves the underlying virtual gamepad input device.
[[nodiscard]] VirtualGamepad* GetVirtualGamepad();
/// Retrieves the underlying virtual gamepad input device.
[[nodiscard]] const VirtualGamepad* GetVirtualGamepad() const;
/**
* Every call to this function returns the next input recorded since calling Start
* @return A ParamPackage of the recorded input, which can be used to create an InputDevice.
* If there has been no input, the package is empty
* Returns all available input devices that this Factory can create a new device with.
* Each returned ParamPackage should have a `display` field used for display, a `engine` field
* for backends to determine if this backend is meant to service the request and any other
* information needed to identify this in the backend later.
*/
virtual Common::ParamPackage GetNextInput() = 0;
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const;
/// Retrieves the analog mappings for the given device.
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& device) const;
/// Retrieves the button mappings for the given device.
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& device) const;
/// Retrieves the motion mappings for the given device.
[[nodiscard]] MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& device) const;
/// Returns an enum contaning the name to be displayed from the input engine.
[[nodiscard]] Common::Input::ButtonNames GetButtonName(
const Common::ParamPackage& params) const;
/// Returns true if device is a controller.
[[nodiscard]] bool IsController(const Common::ParamPackage& params) const;
/// Returns true if axis of a stick aren't mapped in the correct direction
[[nodiscard]] bool IsStickInverted(const Common::ParamPackage& device) const;
/// Reloads the input devices.
void ReloadInputDevices();
/// Start polling from all backends for a desired input type.
void BeginMapping(Polling::InputType type);
/// Returns an input event with mapping information.
[[nodiscard]] Common::ParamPackage GetNextInput() const;
/// Stop polling from all backends.
void StopMapping() const;
/// Signals SDL driver for new input events
void PumpEvents() const;
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
// Get all DevicePoller from all backends for a specific device type
std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type);
} // namespace Polling
/// Generates a serialized param package for creating a keyboard button device.
std::string GenerateKeyboardParam(int key_code);
/// Generates a serialized param package for creating an analog device taking input from keyboard.
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
int key_modifier, float modifier_scale);
} // namespace InputCommon

View File

@ -1,173 +0,0 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <chrono>
#include <mutex>
#include <thread>
#include <tuple>
#include "common/math_util.h"
#include "common/quaternion.h"
#include "common/thread.h"
#include "common/vector_math.h"
#include "input_common/motion_emu.h"
namespace InputCommon {
// Implementation class of the motion emulation device
class MotionEmuDevice {
public:
MotionEmuDevice(int update_millisecond, float sensitivity, float tilt_clamp)
: update_millisecond(update_millisecond),
update_duration(std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::milliseconds(update_millisecond))),
sensitivity(sensitivity), tilt_clamp(tilt_clamp),
motion_emu_thread(&MotionEmuDevice::MotionEmuThread, this) {}
~MotionEmuDevice() {
if (motion_emu_thread.joinable()) {
shutdown_event.Set();
motion_emu_thread.join();
}
}
void BeginTilt(int x, int y) {
mouse_origin = Common::MakeVec(x, y);
is_tilting = true;
}
void Tilt(int x, int y) {
auto mouse_move = Common::MakeVec(x, y) - mouse_origin;
if (is_tilting) {
std::lock_guard guard{tilt_mutex};
if (mouse_move.x == 0 && mouse_move.y == 0) {
tilt_angle = 0;
} else {
tilt_direction = mouse_move.Cast<float>();
tilt_angle = std::clamp(tilt_direction.Normalize() * sensitivity, 0.0f,
Common::PI * this->tilt_clamp / 180.0f);
}
}
}
void EndTilt() {
std::lock_guard guard{tilt_mutex};
tilt_angle = 0;
is_tilting = false;
}
std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() {
std::lock_guard guard{status_mutex};
return status;
}
private:
const int update_millisecond;
const std::chrono::steady_clock::duration update_duration;
const float sensitivity;
Common::Vec2<int> mouse_origin;
std::mutex tilt_mutex;
Common::Vec2<float> tilt_direction;
float tilt_angle = 0;
float tilt_clamp = 90;
bool is_tilting = false;
Common::Event shutdown_event;
std::tuple<Common::Vec3<float>, Common::Vec3<float>> status;
std::mutex status_mutex;
// Note: always keep the thread declaration at the end so that other objects are initialized
// before this!
std::thread motion_emu_thread;
void MotionEmuThread() {
auto update_time = std::chrono::steady_clock::now();
Common::Quaternion<float> q = Common::MakeQuaternion(Common::Vec3<float>(), 0);
Common::Quaternion<float> old_q;
while (!shutdown_event.WaitUntil(update_time)) {
update_time += update_duration;
old_q = q;
{
std::lock_guard guard{tilt_mutex};
// Find the quaternion describing current 3DS tilting
q = Common::MakeQuaternion(
Common::MakeVec(-tilt_direction.y, 0.0f, tilt_direction.x), tilt_angle);
}
auto inv_q = q.Inverse();
// Set the gravity vector in world space
auto gravity = Common::MakeVec(0.0f, -1.0f, 0.0f);
// Find the angular rate vector in world space
auto angular_rate = ((q - old_q) * inv_q).xyz * 2;
angular_rate *= 1000 / update_millisecond / Common::PI * 180;
// Transform the two vectors from world space to 3DS space
gravity = QuaternionRotate(inv_q, gravity);
angular_rate = QuaternionRotate(inv_q, angular_rate);
// Update the sensor state
{
std::lock_guard guard{status_mutex};
status = std::make_tuple(gravity, angular_rate);
}
}
}
};
// Interface wrapper held by input receiver as a unique_ptr. It holds the implementation class as
// a shared_ptr, which is also observed by the factory class as a weak_ptr. In this way the factory
// can forward all the inputs to the implementation only when it is valid.
class MotionEmuDeviceWrapper : public Input::MotionDevice {
public:
MotionEmuDeviceWrapper(int update_millisecond, float sensitivity, float tilt_clamp) {
device = std::make_shared<MotionEmuDevice>(update_millisecond, sensitivity, tilt_clamp);
}
std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const override {
return device->GetStatus();
}
std::shared_ptr<MotionEmuDevice> device;
};
std::unique_ptr<Input::MotionDevice> MotionEmu::Create(const Common::ParamPackage& params) {
int update_period = params.Get("update_period", 100);
float sensitivity = params.Get("sensitivity", 0.01f);
float tilt_clamp = params.Get("tilt_clamp", 90.0f);
auto device_wrapper =
std::make_unique<MotionEmuDeviceWrapper>(update_period, sensitivity, tilt_clamp);
// Previously created device is disconnected here. Having two motion devices for 3DS is not
// expected.
current_device = device_wrapper->device;
return std::move(device_wrapper);
}
void MotionEmu::BeginTilt(int x, int y) {
if (auto ptr = current_device.lock()) {
ptr->BeginTilt(x, y);
}
}
void MotionEmu::Tilt(int x, int y) {
if (auto ptr = current_device.lock()) {
ptr->Tilt(x, y);
}
}
void MotionEmu::EndTilt() {
if (auto ptr = current_device.lock()) {
ptr->EndTilt();
}
}
} // namespace InputCommon

Some files were not shown because too many files have changed in this diff Show More