Merge pull request #10561 from shuffle2/sdl-motion

ControllerInterface: Add support for motion and rumble to SDL backend
This commit is contained in:
JMC47
2022-07-11 16:28:30 -04:00
committed by GitHub
13 changed files with 742 additions and 69 deletions

View File

@ -493,6 +493,7 @@
<ClInclude Include="InputCommon\ControllerInterface\Wiimote\WiimoteController.h" />
<ClInclude Include="InputCommon\ControllerInterface\Win32\Win32.h" />
<ClInclude Include="InputCommon\ControllerInterface\XInput\XInput.h" />
<ClInclude Include="InputCommon\ControllerInterface\SDL\SDL.h" />
<ClInclude Include="InputCommon\ControlReference\ControlReference.h" />
<ClInclude Include="InputCommon\ControlReference\ExpressionParser.h" />
<ClInclude Include="InputCommon\ControlReference\FunctionExpression.h" />
@ -1104,6 +1105,7 @@
<ClCompile Include="InputCommon\ControllerInterface\Wiimote\WiimoteController.cpp" />
<ClCompile Include="InputCommon\ControllerInterface\Win32\Win32.cpp" />
<ClCompile Include="InputCommon\ControllerInterface\XInput\XInput.cpp" />
<ClCompile Include="InputCommon\ControllerInterface\SDL\SDL.cpp" />
<ClCompile Include="InputCommon\ControlReference\ControlReference.cpp" />
<ClCompile Include="InputCommon\ControlReference\ExpressionParser.cpp" />
<ClCompile Include="InputCommon\ControlReference\FunctionExpression.cpp" />

View File

@ -174,32 +174,12 @@ if(UNIX)
)
endif()
if(ENABLE_SDL)
find_package(SDL2)
if(SDL2_FOUND)
message(STATUS "Using shared SDL2")
set(SDL_TARGET SDL2::SDL2)
else()
# SDL2 not found, try SDL
find_package(SDL)
if(SDL_FOUND)
message(STATUS "Using shared SDL")
add_library(System_SDL INTERFACE)
target_include_directories(System_SDL INTERFACE ${SDL_INCLUDE_DIR})
target_link_libraries(System_SDL INTERFACE ${SDL_LIBRARY})
set(SDL_TARGET System_SDL)
endif()
endif()
if(SDL_TARGET AND TARGET ${SDL_TARGET})
target_sources(inputcommon PRIVATE
ControllerInterface/SDL/SDL.cpp
ControllerInterface/SDL/SDL.h
)
target_link_libraries(inputcommon PRIVATE ${SDL_TARGET})
target_compile_definitions(inputcommon PRIVATE "CIFACE_USE_SDL=1")
else()
message(STATUS "SDL NOT found, disabling SDL input")
endif()
if(SDL2_FOUND)
target_sources(inputcommon PRIVATE
ControllerInterface/SDL/SDL.cpp
ControllerInterface/SDL/SDL.h
)
target_link_libraries(inputcommon PRIVATE SDL2::SDL2)
endif()
if(MSVC)

View File

@ -30,6 +30,9 @@
#define CIFACE_USE_PIPES
#endif
#define CIFACE_USE_DUALSHOCKUDPCLIENT
#if defined(HAVE_SDL2)
#define CIFACE_USE_SDL
#endif
namespace ciface
{

View File

@ -15,6 +15,8 @@
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#ifdef _WIN32
#include <Windows.h>
#pragma comment(lib, "SDL2.lib")
#endif
@ -56,8 +58,9 @@ static bool HandleEventAndContinue(const SDL_Event& e)
else if (e.type == SDL_JOYDEVICEREMOVED)
{
g_controller_interface.RemoveDevice([&e](const auto* device) {
const Joystick* joystick = dynamic_cast<const Joystick*>(device);
return joystick && SDL_JoystickInstanceID(joystick->GetSDLJoystick()) == e.jdevice.which;
return device->GetSource() == "SDL" &&
SDL_JoystickInstanceID(static_cast<const Joystick*>(device)->GetSDLJoystick()) ==
e.jdevice.which;
});
}
else if (e.type == s_populate_event_type)
@ -76,10 +79,77 @@ static bool HandleEventAndContinue(const SDL_Event& e)
}
#endif
static void EnableSDLLogging()
{
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
SDL_LogSetOutputFunction(
[](void*, int category, SDL_LogPriority priority, const char* message) {
std::string category_name;
switch (category)
{
case SDL_LOG_CATEGORY_APPLICATION:
category_name = "app";
break;
case SDL_LOG_CATEGORY_ERROR:
category_name = "error";
break;
case SDL_LOG_CATEGORY_ASSERT:
category_name = "assert";
break;
case SDL_LOG_CATEGORY_SYSTEM:
category_name = "system";
break;
case SDL_LOG_CATEGORY_AUDIO:
category_name = "audio";
break;
case SDL_LOG_CATEGORY_VIDEO:
category_name = "video";
break;
case SDL_LOG_CATEGORY_RENDER:
category_name = "render";
break;
case SDL_LOG_CATEGORY_INPUT:
category_name = "input";
break;
case SDL_LOG_CATEGORY_TEST:
category_name = "test";
break;
default:
category_name = fmt::format("unknown({})", category);
break;
}
auto log_level = Common::Log::LogLevel::LNOTICE;
switch (priority)
{
case SDL_LOG_PRIORITY_VERBOSE:
case SDL_LOG_PRIORITY_DEBUG:
log_level = Common::Log::LogLevel::LDEBUG;
break;
case SDL_LOG_PRIORITY_INFO:
log_level = Common::Log::LogLevel::LINFO;
break;
case SDL_LOG_PRIORITY_WARN:
log_level = Common::Log::LogLevel::LWARNING;
break;
case SDL_LOG_PRIORITY_ERROR:
log_level = Common::Log::LogLevel::LERROR;
break;
case SDL_LOG_PRIORITY_CRITICAL:
log_level = Common::Log::LogLevel::LNOTICE;
break;
}
GENERIC_LOG_FMT(Common::Log::LogType::CONTROLLERINTERFACE, log_level, "{}: {}",
category_name, message);
},
nullptr);
}
void Init()
{
#if !SDL_VERSION_ATLEAST(2, 0, 0)
if (SDL_Init(SDL_INIT_JOYSTICK) != 0)
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC) != 0)
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL failed to initialize");
return;
#else
@ -88,16 +158,27 @@ void Init()
SDL_InitSubSystem(SDL_INIT_JOYSTICK);
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
#endif
EnableSDLLogging();
#if SDL_VERSION_ATLEAST(2, 0, 14)
// This is required on windows so that SDL's joystick code properly pumps window messages
SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1");
#endif
#if SDL_VERSION_ATLEAST(2, 0, 9)
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
#endif
s_hotplug_thread = std::thread([] {
Common::ScopeGuard quit_guard([] {
// TODO: there seems to be some sort of memory leak with SDL, quit isn't freeing everything up
SDL_Quit();
});
{
Common::ScopeGuard init_guard([] { s_init_event.Set(); });
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC) != 0)
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER) != 0)
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL failed to initialize");
return;
@ -125,11 +206,34 @@ void Init()
}
}
#ifdef _WIN32
// This is a hack to workaround SDL_hidapi using window messages to detect device
// removal/arrival, yet no part of SDL pumps messages for it. It can hopefully be removed in the
// future when SDL fixes the issue. Note this is a separate issue from SDL_HINT_JOYSTICK_THREAD.
// Also note that SDL_WaitEvent may block while device detection window messages get queued up,
// causing some noticible stutter. This is just another reason it should be fixed properly by
// SDL...
const auto window_handle =
FindWindowEx(HWND_MESSAGE, nullptr, TEXT("SDL_HIDAPI_DEVICE_DETECTION"), nullptr);
#endif
SDL_Event e;
while (SDL_WaitEvent(&e) != 0)
{
if (!HandleEventAndContinue(e))
return;
#ifdef _WIN32
MSG msg;
while (window_handle && PeekMessage(&msg, window_handle, 0, 0, PM_NOREMOVE))
{
if (GetMessageA(&msg, window_handle, 0, 0) != 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
#endif
}
});
@ -172,10 +276,13 @@ void PopulateDevices()
Joystick::Joystick(SDL_Joystick* const joystick, const int sdl_index)
: m_joystick(joystick), m_name(StripSpaces(GetJoystickName(sdl_index)))
{
// really bad HACKS:
// to not use SDL for an XInput device
// too many people on the forums pick the SDL device and ask:
// "why don't my 360 gamepad triggers/rumble work correctly"
// really bad HACKS:
// to not use SDL for an XInput device
// too many people on the forums pick the SDL device and ask:
// "why don't my 360 gamepad triggers/rumble work correctly"
// XXX x360 controllers _should_ work on modern SDL2, so it's unclear why they're
// still broken. Perhaps it's because we're not pumping window messages, which SDL seems to
// expect.
#ifdef _WIN32
// checking the name is probably good (and hacky) enough
// but I'll double check with the num of buttons/axes
@ -183,7 +290,7 @@ Joystick::Joystick(SDL_Joystick* const joystick, const int sdl_index)
Common::ToLower(&lcasename);
if ((std::string::npos != lcasename.find("xbox 360")) &&
(10 == SDL_JoystickNumButtons(joystick)) && (5 == SDL_JoystickNumAxes(joystick)) &&
(11 == SDL_JoystickNumButtons(joystick)) && (6 == SDL_JoystickNumAxes(joystick)) &&
(1 == SDL_JoystickNumHats(joystick)) && (0 == SDL_JoystickNumBalls(joystick)))
{
// this device won't be used
@ -220,53 +327,110 @@ Joystick::Joystick(SDL_Joystick* const joystick, const int sdl_index)
}
#ifdef USE_SDL_HAPTIC
m_haptic = SDL_HapticOpenFromJoystick(m_joystick);
if (!m_haptic)
return;
const unsigned int supported_effects = SDL_HapticQuery(m_haptic);
// Disable autocenter:
if (supported_effects & SDL_HAPTIC_AUTOCENTER)
SDL_HapticSetAutocenter(m_haptic, 0);
// Constant
if (supported_effects & SDL_HAPTIC_CONSTANT)
AddOutput(new ConstantEffect(m_haptic));
// Ramp
if (supported_effects & SDL_HAPTIC_RAMP)
AddOutput(new RampEffect(m_haptic));
// Periodic
for (auto waveform :
{SDL_HAPTIC_SINE, SDL_HAPTIC_TRIANGLE, SDL_HAPTIC_SAWTOOTHUP, SDL_HAPTIC_SAWTOOTHDOWN})
if (SDL_JoystickIsHaptic(m_joystick))
{
if (supported_effects & waveform)
AddOutput(new PeriodicEffect(m_haptic, waveform));
m_haptic = SDL_HapticOpenFromJoystick(m_joystick);
if (m_haptic)
{
const unsigned int supported_effects = SDL_HapticQuery(m_haptic);
// Disable autocenter:
if (supported_effects & SDL_HAPTIC_AUTOCENTER)
SDL_HapticSetAutocenter(m_haptic, 0);
// Constant
if (supported_effects & SDL_HAPTIC_CONSTANT)
AddOutput(new ConstantEffect(m_haptic));
// Ramp
if (supported_effects & SDL_HAPTIC_RAMP)
AddOutput(new RampEffect(m_haptic));
// Periodic
for (auto waveform :
{SDL_HAPTIC_SINE, SDL_HAPTIC_TRIANGLE, SDL_HAPTIC_SAWTOOTHUP, SDL_HAPTIC_SAWTOOTHDOWN})
{
if (supported_effects & waveform)
AddOutput(new PeriodicEffect(m_haptic, waveform));
}
// LeftRight
if (supported_effects & SDL_HAPTIC_LEFTRIGHT)
{
AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Strong));
AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Weak));
}
}
}
#endif
// LeftRight
if (supported_effects & SDL_HAPTIC_LEFTRIGHT)
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (!m_haptic)
{
AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Strong));
AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Weak));
AddOutput(new Motor(m_joystick));
}
#endif
#ifdef USE_SDL_GAMECONTROLLER
if (SDL_IsGameController(sdl_index))
{
m_controller = SDL_GameControllerOpen(sdl_index);
if (m_controller)
{
if (SDL_GameControllerSetSensorEnabled(m_controller, SDL_SENSOR_ACCEL, SDL_TRUE) == 0)
{
AddInput(new MotionInput("Accel Up", m_controller, SDL_SENSOR_ACCEL, 1, 1));
AddInput(new MotionInput("Accel Down", m_controller, SDL_SENSOR_ACCEL, 1, -1));
AddInput(new MotionInput("Accel Left", m_controller, SDL_SENSOR_ACCEL, 0, -1));
AddInput(new MotionInput("Accel Right", m_controller, SDL_SENSOR_ACCEL, 0, 1));
AddInput(new MotionInput("Accel Forward", m_controller, SDL_SENSOR_ACCEL, 2, -1));
AddInput(new MotionInput("Accel Backward", m_controller, SDL_SENSOR_ACCEL, 2, 1));
}
if (SDL_GameControllerSetSensorEnabled(m_controller, SDL_SENSOR_GYRO, SDL_TRUE) == 0)
{
AddInput(new MotionInput("Gyro Pitch Up", m_controller, SDL_SENSOR_GYRO, 0, 1));
AddInput(new MotionInput("Gyro Pitch Down", m_controller, SDL_SENSOR_GYRO, 0, -1));
AddInput(new MotionInput("Gyro Roll Left", m_controller, SDL_SENSOR_GYRO, 2, 1));
AddInput(new MotionInput("Gyro Roll Right", m_controller, SDL_SENSOR_GYRO, 2, -1));
AddInput(new MotionInput("Gyro Yaw Left", m_controller, SDL_SENSOR_GYRO, 1, 1));
AddInput(new MotionInput("Gyro Yaw Right", m_controller, SDL_SENSOR_GYRO, 1, -1));
}
}
}
#endif
}
Joystick::~Joystick()
{
#ifdef USE_SDL_GAMECONTROLLER
if (m_controller)
{
SDL_GameControllerClose(m_controller);
m_controller = nullptr;
}
#endif
#ifdef USE_SDL_HAPTIC
if (m_haptic)
{
// stop/destroy all effects
SDL_HapticStopAll(m_haptic);
// close haptic first
// close haptic before joystick
SDL_HapticClose(m_haptic);
m_haptic = nullptr;
}
#endif
#if SDL_VERSION_ATLEAST(2, 0, 9)
// stop all rumble
SDL_JoystickRumble(m_joystick, 0, 0, 0);
#endif
// close joystick
SDL_JoystickClose(m_joystick);
}
@ -442,6 +606,19 @@ bool Joystick::LeftRightEffect::UpdateParameters(s16 value)
}
#endif
#if SDL_VERSION_ATLEAST(2, 0, 9)
std::string Joystick::Motor::GetName() const
{
return "Motor";
}
void Joystick::Motor::SetState(ControlState state)
{
Uint16 rumble = state * std::numeric_limits<Uint16>::max();
SDL_JoystickRumble(m_js, rumble, rumble, std::numeric_limits<Uint32>::max());
}
#endif
void Joystick::UpdateInput()
{
// TODO: Don't call this for every Joystick, only once per ControllerInterface::UpdateInput()
@ -492,4 +669,14 @@ ControlState Joystick::Hat::GetState() const
{
return (SDL_JoystickGetHat(m_js, m_index) & (1 << m_direction)) > 0;
}
#ifdef USE_SDL_GAMECONTROLLER
ControlState Joystick::MotionInput::GetState() const
{
std::array<float, 3> data{};
SDL_GameControllerGetSensorData(m_gc, m_type, data.data(), (int)data.size());
return m_scale * data[m_index];
}
#endif
} // namespace ciface::SDL

View File

@ -9,10 +9,18 @@
#define USE_SDL_HAPTIC
#endif
#if SDL_VERSION_ATLEAST(2, 0, 14)
#define USE_SDL_GAMECONTROLLER
#endif
#ifdef USE_SDL_HAPTIC
#include <SDL_haptic.h>
#endif
#ifdef USE_SDL_GAMECONTROLLER
#include <SDL_gamecontroller.h>
#endif
#include "InputCommon/ControllerInterface/CoreDevice.h"
namespace ciface::SDL
@ -137,6 +145,42 @@ private:
};
#endif
#if SDL_VERSION_ATLEAST(2, 0, 9)
class Motor : public Output
{
public:
explicit Motor(SDL_Joystick* js) : m_js(js){};
std::string GetName() const override;
void SetState(ControlState state) override;
private:
SDL_Joystick* const m_js;
};
#endif
#ifdef USE_SDL_GAMECONTROLLER
class MotionInput : public Input
{
public:
MotionInput(const char* name, SDL_GameController* gc, SDL_SensorType type, int index,
ControlState scale)
: m_name(name), m_gc(gc), m_type(type), m_index(index), m_scale(scale){};
std::string GetName() const override { return m_name; };
bool IsDetectable() const override { return false; };
ControlState GetState() const override;
private:
const char* m_name;
SDL_GameController* const m_gc;
SDL_SensorType const m_type;
int const m_index;
ControlState const m_scale;
};
#endif
public:
void UpdateInput() override;
@ -152,7 +196,11 @@ private:
std::string m_name;
#ifdef USE_SDL_HAPTIC
SDL_Haptic* m_haptic;
SDL_Haptic* m_haptic = nullptr;
#endif
#ifdef USE_SDL_GAMECONTROLLER
SDL_GameController* m_controller = nullptr;
#endif
};
} // namespace ciface::SDL

View File

@ -73,11 +73,13 @@ void ciface::Win32::Init(void* hwnd)
}
Common::ScopeGuard uninit([] { CoUninitialize(); });
const auto window_name = TEXT("DolphinWin32ControllerInterface");
WNDCLASSEX window_class_info{};
window_class_info.cbSize = sizeof(window_class_info);
window_class_info.lpfnWndProc = WindowProc;
window_class_info.hInstance = GetModuleHandle(nullptr);
window_class_info.lpszClassName = L"Message";
window_class_info.lpszClassName = window_name;
ATOM window_class = RegisterClassEx(&window_class_info);
if (!window_class)
@ -92,7 +94,7 @@ void ciface::Win32::Init(void* hwnd)
Common::HRWrap(GetLastError()));
});
message_window = CreateWindowEx(0, L"Message", nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
message_window = CreateWindowEx(0, window_name, nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
nullptr, nullptr);
promise_guard.Exit();
if (!message_window)