diff --git a/CMakeLists.txt b/CMakeLists.txt index 7dc50407..975bdad5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ else () endif () option(CUBEB_ENABLED "Enable the cubeb audio backend" ON) +option(SDL_ENABLED "Enable building SDL files" ON) if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") if (TARGET_ARCHS MATCHES "arm64") option(HYPERVISOR_ENABLED "Enable the Hypervisor CPU backend" ON) @@ -29,6 +30,10 @@ else () set(FRONTEND "SDL3") endif () +if (FRONTEND STREQUAL "SDL3") + set(SDL_ENABLED ON) +endif () + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") option(MACOS_BUNDLE "Build a macOS app bundle" OFF) elseif (CMAKE_SYSTEM_NAME STREQUAL "iOS") @@ -161,6 +166,12 @@ else () add_compile_definitions(HYDRA_CUBEB_ENABLED=0) endif () +if (SDL_ENABLED) + add_compile_definitions(HYDRA_SDL_ENABLED=1) +else () + add_compile_definitions(HYDRA_SDL_ENABLED=0) +endif () + if (APPLE) set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC NO) endif () diff --git a/src/common/config.cpp b/src/common/config.cpp index 6b5adea1..df4733e3 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -1,9 +1,9 @@ #include "common/config.hpp" -#include "common/log.hpp" -#include "common/platform.hpp" #include "common/toml_helper.hpp" +TOML11_DEFINE_CONVERSION_ENUM(hydra::InputBackend, Sdl, "SDL", + AppleGameController, "Apple GameController") TOML11_DEFINE_CONVERSION_ENUM(hydra::CpuBackend, AppleHypervisor, "Apple Hypervisor", Dynarmic, "dynarmic") TOML11_DEFINE_CONVERSION_ENUM(hydra::GpuRenderer, Metal, "Metal") @@ -12,7 +12,7 @@ TOML11_DEFINE_CONVERSION_ENUM(hydra::Resolution, Auto, "auto", _720p, "720p", _1080p, "1080p", _1440p, "1440p", _2160p, "2160p", _4320p, "4320p", AutoExact, "Auto exact", Custom, "custom") -TOML11_DEFINE_CONVERSION_ENUM(hydra::AudioBackend, Null, "Null", Cubeb, "Cubeb") +TOML11_DEFINE_CONVERSION_ENUM(hydra::AudioBackend, Null, "null", Cubeb, "Cubeb") TOML11_DEFINE_CONVERSION_ENUM(hydra::LogOutput, None, "none", StdOut, "stdout", File, "file") @@ -120,6 +120,7 @@ void Config::LoadDefaults() { game_paths = GetDefaultGamePaths(); loader_plugins = GetDefaultLoaderPlugins(); patch_paths = GetDefaultPatchPaths(); + input_backend = GetDefaultInputBackend(); input_profiles = GetDefaultInputProfiles(); cpu_backend = GetDefaultCpuBackend(); gpu_renderer = GetDefaultGpuRenderer(); @@ -174,6 +175,7 @@ void Config::Serialize() { { auto& input = data.at("Input"); + input["backend"] = input_backend; input["profiles"] = input_profiles; } @@ -253,6 +255,8 @@ void Config::Deserialize() { } if (data.contains("Input")) { const auto& input = data.at("Input"); + input_backend = toml::find_or(input, "backend", + GetDefaultInputBackend()); input_profiles = toml::find_or>( input, "profiles", GetDefaultInputProfiles()); } @@ -315,6 +319,12 @@ void Config::Deserialize() { } // Validate + if (input_backend == InputBackend::Invalid) { + input_backend = GetDefaultInputBackend(); + LOG_WARN(Other, "Invalid input backend, falling back to {}", + input_backend); + } + if (cpu_backend == CpuBackend::Invalid) { cpu_backend = GetDefaultCpuBackend(); LOG_WARN(Other, "Invalid CPU backend, falling back to {}", cpu_backend); @@ -351,6 +361,7 @@ void Config::Log() { LOG_INFO(Other, "Game paths: [{}]", fmt::join(game_paths, ", ")); LOG_INFO(Other, "Loader plugins: [{}]", fmt::join(loader_plugins, ", ")); LOG_INFO(Other, "Patch paths: [{}]", fmt::join(patch_paths, ", ")); + LOG_INFO(Other, "Input backend: {}", input_backend); LOG_INFO(Other, "Input profiles: [{}]", fmt::join(input_profiles, ", ")); LOG_INFO(Other, "CPU backend: {}", cpu_backend); LOG_INFO(Other, "Gpu renderer: {}", gpu_renderer); diff --git a/src/common/config.hpp b/src/common/config.hpp index 113999e7..be72895d 100644 --- a/src/common/config.hpp +++ b/src/common/config.hpp @@ -3,12 +3,20 @@ #include #include "common/log.hpp" +#include "common/platform.hpp" #include "common/types.hpp" #define CONFIG_INSTANCE Config::GetInstance() namespace hydra { +enum class InputBackend : u32 { + Invalid = 0, + + Sdl, + AppleGameController, +}; + enum class CpuBackend : u32 { Invalid = 0, @@ -90,6 +98,7 @@ class Config { std::vector game_paths; std::vector loader_plugins; std::vector patch_paths; + InputBackend input_backend; std::vector input_profiles; CpuBackend cpu_backend; GpuRenderer gpu_renderer; @@ -116,6 +125,13 @@ class Config { std::vector GetDefaultGamePaths() const { return {}; } std::vector GetDefaultLoaderPlugins() const { return {}; } std::vector GetDefaultPatchPaths() const { return {}; } + InputBackend GetDefaultInputBackend() const { +#ifdef PLATFORM_APPLE + return InputBackend::AppleGameController; +#else + return InputBackend::Sdl; +#endif + } std::vector GetDefaultInputProfiles() const { return {"Default", "", "", "", "", "", "", "", "", ""}; } @@ -164,6 +180,7 @@ class Config { REF_GETTER(game_paths, GetGamePaths); REF_GETTER(loader_plugins, GetLoaderPlugins); REF_GETTER(patch_paths, GetPatchPaths); + REF_GETTER(input_backend, GetInputBackend); REF_GETTER(input_profiles, GetInputProfiles); REF_GETTER(cpu_backend, GetCpuBackend); REF_GETTER(gpu_renderer, GetGpuRenderer); @@ -189,6 +206,9 @@ class Config { } // namespace hydra +ENABLE_ENUM_FORMATTING_AND_CASTING(hydra, InputBackend, input_backend, Sdl, + "SDL", AppleGameController, + "Apple GameController") ENABLE_ENUM_FORMATTING_AND_CASTING(hydra, CpuBackend, cpu_backend, AppleHypervisor, "Apple Hypervisor", Dynarmic, "dynarmic") diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9bbf4059..95d8460c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -670,6 +670,17 @@ if (CUBEB_ENABLED) ) endif () +if (SDL_ENABLED) + target_sources(hydra-core PRIVATE + input/sdl/keyboard.cpp + input/sdl/keyboard.hpp + input/sdl/controller.cpp + input/sdl/controller.hpp + input/sdl/device_list.cpp + input/sdl/device_list.hpp + ) +endif () + target_include_directories(hydra-core PRIVATE ${CMAKE_SOURCE_DIR}/externals/libyaz0/include) # TODO: only link Apple frameworks if Apple target_link_libraries(hydra-core PRIVATE hydra_warnings "-framework Foundation" "-framework Metal" "-framework QuartzCore" "-framework GameController" dynarmic hatch libyaz0 nx2elf hydra-common) @@ -682,6 +693,12 @@ if (CUBEB_ENABLED) target_link_libraries(hydra-core PRIVATE cubeb) endif () +if (SDL_ENABLED) + find_package(SDL3 CONFIG REQUIRED) + + target_link_libraries(hydra-core PRIVATE SDL3::SDL3) +endif () + if (CMAKE_SYSTEM_NAME STREQUAL "iOS") set_target_properties(hydra-core PROPERTIES XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET ${IOS_DEPLOYMENT_TARGET} diff --git a/src/core/input/apple_gc/controller.mm b/src/core/input/apple_gc/controller.mm index f72b66ba..eb36534a 100644 --- a/src/core/input/apple_gc/controller.mm +++ b/src/core/input/apple_gc/controller.mm @@ -5,6 +5,8 @@ namespace hydra::input::apple_gc { bool Controller::IsPressedImpl(ControllerInput input) { + const auto controller = reinterpret_cast(handle); + #define BUTTON_CASE(input, gc_button) \ case ControllerInput::input: \ gc_button_name = GCInput##gc_button; \ @@ -18,22 +20,26 @@ BUTTON_CASE(Y, ButtonX) BUTTON_CASE(StickL, LeftThumbstickButton) BUTTON_CASE(StickR, RightThumbstickButton) - BUTTON_CASE(L, LeftBumper) - BUTTON_CASE(R, RightBumper) + BUTTON_CASE(L, LeftShoulder) + BUTTON_CASE(R, RightShoulder) BUTTON_CASE(ZL, LeftTrigger) BUTTON_CASE(ZR, RightTrigger) - // TODO: implement - case ControllerInput::Plus: - case ControllerInput::Minus: + BUTTON_CASE(Plus, ButtonMenu) + BUTTON_CASE(Minus, ButtonOptions) case ControllerInput::Left: + return controller.extendedGamepad.dpad.left.isPressed; case ControllerInput::Up: + return controller.extendedGamepad.dpad.up.isPressed; case ControllerInput::Right: + return controller.extendedGamepad.dpad.right.isPressed; case ControllerInput::Down: + return controller.extendedGamepad.dpad.down.isPressed; + // TODO: implement case ControllerInput::LeftSL: case ControllerInput::LeftSR: case ControllerInput::RightSL: case ControllerInput::RightSR: - break; + return false; default: LOG_NOT_IMPLEMENTED(Input, "Controller button {}", input); return false; @@ -41,9 +47,7 @@ #undef BUTTON_CASE - return reinterpret_cast(handle) - .physicalInputProfile.buttons[gc_button_name] - .isPressed; + return controller.physicalInputProfile.buttons[gc_button_name].isPressed; } f32 Controller::GetAxisValueImpl(ControllerInput input) { diff --git a/src/core/input/apple_gc/device_list.hpp b/src/core/input/apple_gc/device_list.hpp index 309a0ff2..a41d70e2 100644 --- a/src/core/input/apple_gc/device_list.hpp +++ b/src/core/input/apple_gc/device_list.hpp @@ -7,13 +7,12 @@ namespace hydra::input::apple_gc { class DeviceList : public IDeviceList { public: DeviceList(); - ~DeviceList(); + ~DeviceList() override; - // For the implementation - void _AddController(id controller); - void _RemoveController(id controller); - void _AddKeyboard(id keyboard); - void _RemoveKeyboard(id keyboard); + void AddController(id controller); + void RemoveController(id controller); + void AddKeyboard(id keyboard); + void RemoveKeyboard(id keyboard); private: id impl; diff --git a/src/core/input/apple_gc/device_list.mm b/src/core/input/apple_gc/device_list.mm index 73b9a665..45b84f2e 100644 --- a/src/core/input/apple_gc/device_list.mm +++ b/src/core/input/apple_gc/device_list.mm @@ -45,13 +45,13 @@ - (id)initWithParent:(DeviceList*)parent { if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { GCKeyboard* keyboard = [GCKeyboard coalescedKeyboard]; if (keyboard) { - self.parent->_AddKeyboard(keyboard); + self.parent->AddKeyboard(keyboard); } } // Connected controllers for (GCController* controller in [GCController controllers]) { - self.parent->_AddController(controller); + self.parent->AddController(controller); } } @@ -66,23 +66,23 @@ - (void)dealloc { - (void)controllerConnected:(NSNotification*)notification { GCController* controller = reinterpret_cast(notification.object); - _parent->_AddController(controller); + _parent->AddController(controller); } - (void)controllerDisconnected:(NSNotification*)notification { GCController* controller = reinterpret_cast(notification.object); - _parent->_RemoveController(controller); + _parent->RemoveController(controller); } - (void)keyboardConnected:(NSNotification*)notification { GCKeyboard* keyboard = reinterpret_cast(notification.object); - _parent->_AddKeyboard(keyboard); + _parent->AddKeyboard(keyboard); } - (void)keyboardDisconnected:(NSNotification*)notification { GCKeyboard* keyboard = reinterpret_cast(notification.object); - _parent->_RemoveKeyboard(keyboard); + _parent->RemoveKeyboard(keyboard); } @end @@ -91,7 +91,7 @@ - (void)keyboardDisconnected:(NSNotification*)notification { namespace { -std::string get_device_name(id device) { +std::string GetDeviceName(id device) { return [[device vendorName] UTF8String]; } @@ -103,34 +103,20 @@ - (void)keyboardDisconnected:(NSNotification*)notification { DeviceList::~DeviceList() { [impl release]; } -void DeviceList::_AddController(id controller) { - auto name = get_device_name(controller); - LOG_INFO(Input, "Controller connected: {}", name); - devices[name] = new Controller(controller); +void DeviceList::AddController(id controller) { + AddDevice(GetDeviceName(controller), new Controller(controller)); } -void DeviceList::_RemoveController(id controller) { - auto name = get_device_name(controller); - LOG_INFO(Input, "Controller disconnected: {}", name); - auto it = devices.find(name); - ASSERT(it != devices.end(), Input, "Controller not connected"); - delete it->second; - devices.erase(it); +void DeviceList::RemoveController(id controller) { + RemoveDevice(GetDeviceName(controller)); } -void DeviceList::_AddKeyboard(id keyboard) { - auto name = get_device_name(keyboard); - LOG_INFO(Input, "Keyboard connected: {}", name); - devices[name] = new Keyboard(keyboard); +void DeviceList::AddKeyboard(id keyboard) { + AddDevice(GetDeviceName(keyboard), new Keyboard(keyboard)); } -void DeviceList::_RemoveKeyboard(id keyboard) { - auto name = get_device_name(keyboard); - LOG_INFO(Input, "Keyboard disconnected: {}", name); - auto it = devices.find(name); - ASSERT(it != devices.end(), Input, "Keyboard not connected"); - delete it->second; - devices.erase(it); +void DeviceList::RemoveKeyboard(id keyboard) { + RemoveDevice(GetDeviceName(keyboard)); } } // namespace hydra::input::apple_gc diff --git a/src/core/input/device_list.hpp b/src/core/input/device_list.hpp index 680dce1a..e62db7f1 100644 --- a/src/core/input/device_list.hpp +++ b/src/core/input/device_list.hpp @@ -5,16 +5,44 @@ namespace hydra::input { class IDeviceList { - friend class DeviceManager; - public: virtual ~IDeviceList() { for (auto [name, device] : devices) delete device; } - protected: - std::map devices; + virtual void PumpEvents() {} + + void AddDevice(std::string_view name, IDevice* device) { + std::scoped_lock lock(mutex); + const auto res = devices.emplace(name, device); + ASSERT(res.second, Input, "{} already connected", name); + LOG_INFO(Input, "Device connected: {}", name); + } + + void RemoveDevice(std::string_view name) { + std::scoped_lock lock(mutex); + const auto it = devices.find(name); + ASSERT(it != devices.end(), Input, "{} not connected", name); + delete it->second; + devices.erase(it); + LOG_INFO(Input, "Device disconnected: {}", name); + } + + IDevice* GetDevice(std::string_view name) { + auto it = devices.find(name); + if (it == devices.end()) + return nullptr; + + return it->second; + } + + private: + std::mutex mutex; + std::map> devices; + + public: + REF_GETTER(mutex, GetMutex); }; } // namespace hydra::input diff --git a/src/core/input/device_manager.cpp b/src/core/input/device_manager.cpp index 21fdfbfe..744eeadf 100644 --- a/src/core/input/device_manager.cpp +++ b/src/core/input/device_manager.cpp @@ -1,10 +1,38 @@ #include "core/input/device_manager.hpp" +#ifdef PLATFORM_APPLE #include "core/input/apple_gc/device_list.hpp" +#endif + +#include "core/input/sdl/device_list.hpp" namespace hydra::input { -DeviceManager::DeviceManager() { +namespace { + +IDeviceList* CreateDeviceList() { + const auto input_backend = CONFIG_INSTANCE.GetInputBackend(); + switch (input_backend) { + case InputBackend::Sdl: +#if HYDRA_SDL_ENABLED + return new sdl::DeviceList(); +#else + LOG_FATAL(Input, "SDL not supported"); +#endif + case InputBackend::AppleGameController: +#ifdef PLATFORM_APPLE + return new apple_gc::DeviceList(); +#else + LOG_FATAL(Input, "Apple GameController not supported"); +#endif + default: + LOG_FATAL(Input, "Unknown input backend {}", input_backend); + } +} + +} // namespace + +DeviceManager::DeviceManager() : device_list{CreateDeviceList()} { // Profiles for (u32 i = 0; i < horizon::services::hid::NPAD_COUNT; i++) { const auto& name = CONFIG_INSTANCE.GetInputProfiles()[i]; @@ -14,13 +42,8 @@ DeviceManager::DeviceManager() { profiles[i] = Profile( static_cast(i), name); } - - // Device list - device_list = new apple_gc::DeviceList(); } -DeviceManager::~DeviceManager() { delete device_list; } - NpadState DeviceManager::PollNpad(horizon::services::hid::internal::NpadIndex index) { NpadState state{}; @@ -31,7 +54,9 @@ DeviceManager::PollNpad(horizon::services::hid::internal::NpadIndex index) { const auto& profile = *profile_opt; for (const auto& device_name : profile.GetDeviceNames()) { - auto device = GetDevice(device_name); + std::scoped_lock lock(device_list->GetMutex()); + + auto device = device_list->GetDevice(device_name); if (!device) continue; @@ -83,12 +108,14 @@ DeviceManager::PollNpad(horizon::services::hid::internal::NpadIndex index) { } std::map DeviceManager::PollTouch() { + std::scoped_lock lock(device_list->GetMutex()); + std::map state; // TODO: get name from the config const std::string device_name = "cursor"; - auto device = GetDevice(device_name); + auto device = device_list->GetDevice(device_name); if (!device) return state; diff --git a/src/core/input/device_manager.hpp b/src/core/input/device_manager.hpp index e8790c1c..78abe9c3 100644 --- a/src/core/input/device_manager.hpp +++ b/src/core/input/device_manager.hpp @@ -11,28 +11,23 @@ constexpr usize MAX_FINGER_COUNT = 16; class DeviceManager { public: DeviceManager(); - ~DeviceManager(); + + void PumpEvents() { device_list->PumpEvents(); } NpadState PollNpad(horizon::services::hid::internal::NpadIndex index); std::map PollTouch(); // Touch screen devices - void ConnectTouchScreenDevice(const std::string& name, IDevice* device) { - ASSERT(device->ActsAsTouchScreen(), Input, - "Device \"{}\" does not act as a touch screen", name); - device_list->devices.insert({name, device}); + void ConnectTouchScreenDevice(std::string_view name, IDevice* device) { + device_list->AddDevice(name, device); } - IDevice* DisconnectTouchScreenDevice(const std::string& name) { - auto it = device_list->devices.find(name); - ASSERT(it != device_list->devices.end(), Input, - "Touch screen \"{}\" not connected", name); - device_list->devices.erase(it); - return it->second; + void DisconnectTouchScreenDevice(std::string_view name) { + device_list->RemoveDevice(name); } private: - IDeviceList* device_list; + std::unique_ptr device_list; std::optional profiles[horizon::services::hid::NPAD_COUNT]; std::map active_touches; @@ -40,14 +35,6 @@ class DeviceManager { u16 available_finger_mask{0xffff}; // Helpers - IDevice* GetDevice(const std::string& name) { - auto it = device_list->devices.find(name); - if (it == device_list->devices.end()) - return nullptr; - - return it->second; - } - u32 BeginTouch(); void EndTouch(u32 finger_id); }; diff --git a/src/core/input/sdl/controller.cpp b/src/core/input/sdl/controller.cpp new file mode 100644 index 00000000..9460249c --- /dev/null +++ b/src/core/input/sdl/controller.cpp @@ -0,0 +1,92 @@ +#include "core/input/sdl/controller.hpp" + +#include + +#include "frontend/sdl3/const.hpp" + +namespace hydra::input::sdl { + +bool Controller::IsPressedImpl(ControllerInput input) { +#define BUTTON_CASE(input, sdl_button) \ + case ControllerInput::input: \ + return SDL_GetGamepadButton(handle, sdl_button); \ + break; + + switch (input) { + BUTTON_CASE(A, SDL_GAMEPAD_BUTTON_EAST) + BUTTON_CASE(B, SDL_GAMEPAD_BUTTON_SOUTH) + BUTTON_CASE(X, SDL_GAMEPAD_BUTTON_NORTH) + BUTTON_CASE(Y, SDL_GAMEPAD_BUTTON_WEST) + BUTTON_CASE(StickL, SDL_GAMEPAD_BUTTON_LEFT_STICK) + BUTTON_CASE(StickR, SDL_GAMEPAD_BUTTON_RIGHT_STICK) + BUTTON_CASE(L, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) + BUTTON_CASE(R, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) + case ControllerInput::ZL: + return SDL_GetGamepadAxis(handle, SDL_GAMEPAD_AXIS_LEFT_TRIGGER); + case ControllerInput::ZR: + return SDL_GetGamepadAxis(handle, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER); + BUTTON_CASE(Plus, SDL_GAMEPAD_BUTTON_START); + BUTTON_CASE(Minus, SDL_GAMEPAD_BUTTON_BACK); + BUTTON_CASE(Left, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + BUTTON_CASE(Up, SDL_GAMEPAD_BUTTON_DPAD_UP); + BUTTON_CASE(Right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); + BUTTON_CASE(Down, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + // TODO: implement + case ControllerInput::LeftSL: + case ControllerInput::LeftSR: + case ControllerInput::RightSL: + case ControllerInput::RightSR: + return false; + default: + LOG_NOT_IMPLEMENTED(Input, "Controller button {}", input); + return false; + } +} + +f32 positive_axis_to_float(i16 value) { + return std::max(static_cast(0), value) / + static_cast(std::numeric_limits::max()); +} + +f32 negative_axis_to_float(i16 value) { + return std::min(static_cast(0), value) / + static_cast(std::numeric_limits::min()); +} + +f32 Controller::GetAxisValueImpl(ControllerInput input) { +#define AXIS_CASE(input, sdl_dpad, direction) \ + case ControllerInput::input: \ + value = SDL_GetGamepadAxis(handle, sdl_dpad); \ + dir = AnalogStickDirection::direction; \ + break; + + AnalogStickDirection dir; + i16 value; + + switch (input) { + AXIS_CASE(StickLLeft, SDL_GAMEPAD_AXIS_LEFTX, Left) + AXIS_CASE(StickLUp, SDL_GAMEPAD_AXIS_LEFTY, Up) + AXIS_CASE(StickLRight, SDL_GAMEPAD_AXIS_LEFTX, Right) + AXIS_CASE(StickLDown, SDL_GAMEPAD_AXIS_LEFTY, Down) + AXIS_CASE(StickRLeft, SDL_GAMEPAD_AXIS_RIGHTX, Left) + AXIS_CASE(StickRUp, SDL_GAMEPAD_AXIS_RIGHTY, Up) + AXIS_CASE(StickRRight, SDL_GAMEPAD_AXIS_RIGHTX, Right) + AXIS_CASE(StickRDown, SDL_GAMEPAD_AXIS_RIGHTY, Down) + default: + LOG_NOT_IMPLEMENTED(Input, "Controller axis {}", input); + return 0.0f; + } + + switch (dir) { + case AnalogStickDirection::Right: + return positive_axis_to_float(value); + case AnalogStickDirection::Left: + return negative_axis_to_float(value); + case AnalogStickDirection::Up: + return negative_axis_to_float(value); + case AnalogStickDirection::Down: + return positive_axis_to_float(value); + } +} + +} // namespace hydra::input::sdl diff --git a/src/core/input/sdl/controller.hpp b/src/core/input/sdl/controller.hpp new file mode 100644 index 00000000..801385c5 --- /dev/null +++ b/src/core/input/sdl/controller.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "core/input/controller.hpp" + +#include "frontend/sdl3/const.hpp" + +namespace hydra::input::sdl { + +class Controller : public IController { + public: + Controller(SDL_Gamepad* handle_) : handle{handle_} {} + ~Controller() { SDL_CloseGamepad(handle); } + + protected: + bool IsPressedImpl(ControllerInput input) override; + f32 GetAxisValueImpl(ControllerInput input) override; + + private: + SDL_Gamepad* handle; +}; + +} // namespace hydra::input::sdl diff --git a/src/core/input/sdl/device_list.cpp b/src/core/input/sdl/device_list.cpp new file mode 100644 index 00000000..51e728b9 --- /dev/null +++ b/src/core/input/sdl/device_list.cpp @@ -0,0 +1,107 @@ +#include "core/input/sdl/device_list.hpp" + +#include "core/input/sdl/controller.hpp" +#include "core/input/sdl/keyboard.hpp" +#include "frontend/sdl3/const.hpp" + +namespace hydra::input::sdl { + +namespace { + +bool EWWrapper(void* userdata, SDL_Event* e) { + auto o = static_cast(userdata); + o->EventWatcher(e); + return false; +} + +} // namespace + +DeviceList::DeviceList() { + has_frontend = SDL_WasInit(SDL_INIT_VIDEO); + + // Initialize + if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { + LOG_FATAL(SDL3Window, "Failed to initialize SDL: {}", SDL_GetError()); + } + + // Get initial keyboards + int kb_count = 0; + SDL_KeyboardID* keyboards = SDL_GetKeyboards(&kb_count); + if (keyboards) { + keyboard_count = static_cast(kb_count); + if (keyboard_count > 0) + ConnectGenericKeyboard(); + + SDL_free(keyboards); + } else { + keyboard_count = 0; + } + + // Get initial gamepads + int gp_count = 0; + SDL_JoystickID* gamepads = SDL_GetGamepads(&gp_count); + if (gamepads) { + for (int i = 0; i < gp_count; i++) + ConnectController(gamepads[i]); + + SDL_free(gamepads); + } + + // Register event watcher + SDL_AddEventWatch(EWWrapper, this); +} + +DeviceList::~DeviceList() { + SDL_RemoveEventWatch(EWWrapper, this); + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); +} + +void DeviceList::PumpEvents() { + // If there is no frontend event loop, events must be pumped manually + if (!has_frontend) + SDL_PumpEvents(); +} + +void DeviceList::EventWatcher(SDL_Event* e) { + switch (e->type) { + case SDL_EVENT_KEYBOARD_ADDED: { + if (keyboard_count++ == 0) + ConnectGenericKeyboard(); + keyboard_count++; + break; + } + case SDL_EVENT_KEYBOARD_REMOVED: { + if (--keyboard_count == 0) + RemoveDevice("Generic Keyboard"); + break; + } + case SDL_EVENT_GAMEPAD_ADDED: { + ConnectController(e->gdevice.which); + break; + } + case SDL_EVENT_GAMEPAD_REMOVED: { + SDL_Gamepad* gp = SDL_GetGamepadFromID(e->gdevice.which); + std::string name = SDL_GetGamepadName(gp); + RemoveDevice(name); + break; + } + default: + break; + } +} + +void DeviceList::ConnectGenericKeyboard() { + AddDevice("Generic Keyboard", new Keyboard()); +} + +void DeviceList::ConnectController(SDL_JoystickID id) { + SDL_Gamepad* gp = SDL_OpenGamepad(id); + if (gp) { + std::string name = SDL_GetGamepadName(gp); + AddDevice(name, new Controller(gp)); + } else { + LOG_ERROR(Input, "Failed to get controller: {}", SDL_GetError()); + } +} + +} // namespace hydra::input::sdl diff --git a/src/core/input/sdl/device_list.hpp b/src/core/input/sdl/device_list.hpp new file mode 100644 index 00000000..f4d8d081 --- /dev/null +++ b/src/core/input/sdl/device_list.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "core/input/device_list.hpp" +#include "frontend/sdl3/const.hpp" + +namespace hydra::input::sdl { + +class DeviceList : public IDeviceList { + public: + DeviceList(); + ~DeviceList() override; + + void PumpEvents() override; + + void EventWatcher(SDL_Event* e); + + private: + bool has_frontend; // Indicates whether the an SDL frontend is used + u32 keyboard_count; + + // Helpers + void ConnectGenericKeyboard(); + void ConnectController(SDL_JoystickID id); +}; + +} // namespace hydra::input::sdl diff --git a/src/core/input/sdl/keyboard.cpp b/src/core/input/sdl/keyboard.cpp new file mode 100644 index 00000000..4a59faea --- /dev/null +++ b/src/core/input/sdl/keyboard.cpp @@ -0,0 +1,73 @@ +#include "core/input/sdl/keyboard.hpp" + +#include "frontend/sdl3/const.hpp" + +namespace hydra::input::sdl { + +bool Keyboard::IsPressedImpl(Key key) { +#define KEY_CASE(key, sdl_key) \ + case Key::key: \ + return st[sdl_key]; + + const bool* st = SDL_GetKeyboardState(nullptr); + + switch (key) { + KEY_CASE(Q, SDL_SCANCODE_Q) + KEY_CASE(W, SDL_SCANCODE_W) + KEY_CASE(E, SDL_SCANCODE_E) + KEY_CASE(R, SDL_SCANCODE_R) + KEY_CASE(T, SDL_SCANCODE_T) + KEY_CASE(Y, SDL_SCANCODE_Y) + KEY_CASE(U, SDL_SCANCODE_U) + KEY_CASE(I, SDL_SCANCODE_I) + KEY_CASE(O, SDL_SCANCODE_O) + KEY_CASE(P, SDL_SCANCODE_P) + KEY_CASE(A, SDL_SCANCODE_A) + KEY_CASE(S, SDL_SCANCODE_S) + KEY_CASE(D, SDL_SCANCODE_D) + KEY_CASE(F, SDL_SCANCODE_F) + KEY_CASE(G, SDL_SCANCODE_G) + KEY_CASE(H, SDL_SCANCODE_H) + KEY_CASE(J, SDL_SCANCODE_J) + KEY_CASE(K, SDL_SCANCODE_K) + KEY_CASE(L, SDL_SCANCODE_L) + KEY_CASE(Z, SDL_SCANCODE_Z) + KEY_CASE(X, SDL_SCANCODE_X) + KEY_CASE(C, SDL_SCANCODE_C) + KEY_CASE(V, SDL_SCANCODE_V) + KEY_CASE(B, SDL_SCANCODE_B) + KEY_CASE(N, SDL_SCANCODE_N) + KEY_CASE(M, SDL_SCANCODE_M) + + // TODO: numbers + + // TODO: other stuff + + KEY_CASE(ArrowLeft, SDL_SCANCODE_LEFT) + KEY_CASE(ArrowRight, SDL_SCANCODE_RIGHT) + KEY_CASE(ArrowUp, SDL_SCANCODE_UP) + KEY_CASE(ArrowDown, SDL_SCANCODE_DOWN) + + KEY_CASE(Enter, SDL_SCANCODE_RETURN) + KEY_CASE(Tab, SDL_SCANCODE_TAB) + KEY_CASE(Backspace, SDL_SCANCODE_BACKSPACE) + KEY_CASE(Space, SDL_SCANCODE_SPACE) + + KEY_CASE(LeftShift, SDL_SCANCODE_LSHIFT) + KEY_CASE(RightShift, SDL_SCANCODE_RSHIFT) + KEY_CASE(LeftControl, SDL_SCANCODE_LCTRL) + KEY_CASE(RightControl, SDL_SCANCODE_RCTRL) + KEY_CASE(LeftAlt, SDL_SCANCODE_LALT) + KEY_CASE(RightAlt, SDL_SCANCODE_RALT) + // TODO: correct? + KEY_CASE(LeftSuper, SDL_SCANCODE_LGUI) + KEY_CASE(RightSuper, SDL_SCANCODE_RGUI) + default: + LOG_NOT_IMPLEMENTED(Input, "Key {}", key); + return false; + } + +#undef KEY_CASE +} + +} // namespace hydra::input::sdl diff --git a/src/core/input/sdl/keyboard.hpp b/src/core/input/sdl/keyboard.hpp new file mode 100644 index 00000000..4d5bcd48 --- /dev/null +++ b/src/core/input/sdl/keyboard.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "core/input/keyboard.hpp" + +namespace hydra::input::sdl { + +class Keyboard : public IKeyboard { + public: + Keyboard() {} + + protected: + bool IsPressedImpl(Key key) override; +}; + +} // namespace hydra::input::sdl diff --git a/src/core/system.cpp b/src/core/system.cpp index fa8f1e87..c981cb24 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -459,6 +459,9 @@ void System::Resume() { void System::ProgressFrame(u32 width, u32 height, bool& out_dt_average_updated) { + // Pump input events + input_device_manager.PumpEvents(); + // Set the resolution for OS os.SetSurfaceResolution({width, height}); diff --git a/src/frontend/sdl3/window.cpp b/src/frontend/sdl3/window.cpp index f491fa62..4cb002c3 100644 --- a/src/frontend/sdl3/window.cpp +++ b/src/frontend/sdl3/window.cpp @@ -5,13 +5,16 @@ namespace hydra::frontend::sdl3 { -Window::Window(int argc, const char* argv[]) : system(*this) { - // SLD3 initialization - if (!SDL_Init(SDL_INIT_VIDEO)) { - LOG_FATAL(SDL3Window, "Failed to initialize SDL3: {}", SDL_GetError()); - return; +Context::Context() { + if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) { + LOG_FATAL(SDL3Window, "Failed to initialize SDL: {}", SDL_GetError()); } +} +Context::~Context() { SDL_Quit(); } + +Window::Window(int argc, const char* argv[]) : system(*this) { + // Window and renderer SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); @@ -19,7 +22,6 @@ Window::Window(int argc, const char* argv[]) : system(*this) { &window, &renderer)) { LOG_FATAL(SDL3Window, "Failed to create window/renderer: {}", SDL_GetError()); - return; } // Parse arguments @@ -34,8 +36,8 @@ Window::Window(int argc, const char* argv[]) : system(*this) { Window::~Window() { system.GetInputDeviceManager().DisconnectTouchScreenDevice("cursor"); + SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); - SDL_Quit(); } void Window::Run() { diff --git a/src/frontend/sdl3/window.hpp b/src/frontend/sdl3/window.hpp index a7226862..211e2c20 100644 --- a/src/frontend/sdl3/window.hpp +++ b/src/frontend/sdl3/window.hpp @@ -10,6 +10,12 @@ using Native = hydra::frontend::native::cocoa::Native; namespace hydra::frontend::sdl3 { +class Context { + public: + Context(); + ~Context(); +}; + class Window : public horizon::ui::IHandler { public: Window(int argc, const char* argv[]); @@ -28,6 +34,8 @@ class Window : public horizon::ui::IHandler { std::string& out_text) override; private: + Context context; + SDL_Window* window; SDL_Renderer* renderer;