From 1febae3a4f83716a79662b9e6466002828222dd8 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 6 Aug 2026 09:42:27 -0400 Subject: [PATCH] sdl: port the joystick, gamepad and audio backends to SDL3 Moves every SDL2 call site to SDL3 and switches find_package(SDL2) to SDL3. ossia::sdl2 becomes ossia::sdl3, and the SDK prefix path moves from $OSSIA_SDK/SDL2 to $OSSIA_SDK/SDL3 to match ossia/sdk#27. Most of it is upstream's rename table (SDL_oldnames.h, which is also what build-scripts/rename_symbols.py applies): SDL_JoystickOpen -> SDL_OpenJoystick, SDL_GameControllerHasAxis -> SDL_GamepadHasAxis, SDL_CONTROLLER* -> SDL_GAMEPAD*, the SDL_Controller*Event structs -> SDL_Gamepad*Event and their union members caxis/cbutton/ctouchpad/csensor -> gaxis/gbutton/gtouchpad/gsensor, and the event enums to SDL_EVENT_*. Four things needed more than a rename: Device indices are gone. SDL2 enumerated joysticks as 0..SDL_NumJoysticks()-1 and mapped an index to an identity with SDL_JoystickGetDeviceInstanceID; SDL3 has only SDL_GetJoysticks(), returning an owned array of stable SDL_JoystickID. joystick_info keeps its index-based signatures on purpose: an index into that array plays exactly the role SDL2's device index did, and is just as transient (valid until the device list changes), so score's enumeration loop and the (id, index) pair it serialises into save files keep working untouched. The new sdl_joystick_ids RAII helper owns the array; index 0 maps to SDL3's invalid id of 0, which joystick_info translates back to the -1 sentinel its callers expect. game_controller_protocol no longer needs to remember the index at all, since SDL_GetGamepadTypeForID takes the id, so m_joystick_index is gone. Booleans changed shape. SDL_Init and the Has*/SetSensorEnabled family return bool rather than an int status, so `< 0`, `>= 0` and `== SDL_TRUE` tests are dropped; button events carry `down` instead of `state == SDL_PRESSED`. The rumble capability queries were replaced by properties: SDL_GameControllerHasRumble/HasRumbleTriggers become SDL_GetBooleanProperty on SDL_GetGamepadProperties with SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN and SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN. The audio engine is a real rewrite. SDL3 has no pull callback filling a fixed-size buffer: SDL_OpenAudioDeviceStream hands us a stream and asks for `additional_amount` bytes, which varies. The callback now services that request in whole blocks of effective_buffer_size frames and pushes each through SDL_PutAudioStreamData, with SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES set to the requested block size so the device buffer matches. Overshooting the request by at most one block is fine - the stream buffers it. SDL_AudioSpec lost `samples`, `callback` and `userdata`, so effective_buffer_size is now what we asked for rather than what the device reported. The interleave scratch buffers are preallocated members instead of per-callback alloca, which the fixed block size now makes possible. Status comes from SDL_AudioStreamDevicePaused instead of SDL_GetAudioDeviceStatus. Two guards were dropped. The SDL_VERSION_ATLEAST(2, 26, 0) fallback for SDL_SENSOR_GYRO_R is dead now that SDL3 is the floor. And sdl_protocol.hpp no longer includes SDL_config.h to test SDL_AUDIO_DISABLED: SDL3 does not install SDL_build_config.h, and it does not need to - verified that a -DSDL_AUDIO=0 SDL3 build still exports SDL_OpenAudioDeviceStream, SDL_PutAudioStreamData, SDL_ResumeAudioStreamDevice, SDL_DestroyAudioStream and SDL_AudioStreamDevicePaused, so the engine compiles and links either way and an audio-less build simply fails SDL_Init(SDL_INIT_AUDIO) at runtime. WASM moves to `-s USE_SDL=3`. Emscripten's sdl3 port exists at the EMSDK_VERSION the SDK pins and has SDL_JOYSTICK_EMSCRIPTEN and SDL_AUDIO_DRIVER_EMSCRIPTEN enabled, so both backends stay live in the browser; it does still warn that the port is experimental. Verified on linux-x86_64: game_controller_protocol.cpp, joystick_protocol.cpp and audio_engine.cpp compile clean against SDL3 3.4.14 under score's own flags and warnings, and audio_engine.cpp really does compile the SDL engine (the preprocessed TU contains class sdl_protocol and the stream API, with no SDL2 call left). A standalone harness mirroring the two rewritten paths confirms the behaviour: SDL_Init(JOYSTICK|GAMEPAD) succeeds, enumeration and GUID lookup work, an out-of-range index yields id 0, and the audio callback produced exactly 77 blocks of 256 frames with the expected paused -> resumed transition. No gamepad was attached to the test machine, so rumble, sensors and touchpad input are compile- and API-verified but not exercised against real hardware. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ --- cmake/OssiaDeps.cmake | 2 +- cmake/OssiaOptions.cmake | 5 +- cmake/deps/sdl.cmake | 36 +++--- src/ossia/audio/sdl_protocol.hpp | 121 +++++++++--------- .../joystick/game_controller_protocol.cpp | 69 ++++------ .../joystick/game_controller_protocol.hpp | 5 +- .../protocols/joystick/joystick_manager.hpp | 79 +++++++----- .../protocols/joystick/joystick_protocol.cpp | 43 ++++--- .../protocols/joystick/joystick_protocol.hpp | 2 +- src/ossia_features.cmake | 4 +- 10 files changed, 183 insertions(+), 183 deletions(-) diff --git a/cmake/OssiaDeps.cmake b/cmake/OssiaDeps.cmake index 3b5f3de885f..72d67a4e793 100644 --- a/cmake/OssiaDeps.cmake +++ b/cmake/OssiaDeps.cmake @@ -213,7 +213,7 @@ endif() if(OSSIA_ENABLE_SDL) include(deps/sdl) - if(NOT TARGET ossia::sdl2) + if(NOT TARGET ossia::sdl3) set(OSSIA_ENABLE_SDL FALSE CACHE "" INTERNAL FORCE) set(OSSIA_PROTOCOL_JOYSTICK FALSE CACHE "" INTERNAL FORCE) endif() diff --git a/cmake/OssiaOptions.cmake b/cmake/OssiaOptions.cmake index 138dd928035..098cbfbcf29 100644 --- a/cmake/OssiaOptions.cmake +++ b/cmake/OssiaOptions.cmake @@ -76,7 +76,7 @@ option(OSSIA_PROTOCOL_HTTP "Enable HTTP protocol" ON) # Requires Qt option(OSSIA_PROTOCOL_WEBSOCKETS "Enable WebSockets protocol" OFF) # Requires Qt option(OSSIA_PROTOCOL_SERIAL "Enable Serial port protocol" OFF) # Requires Qt option(OSSIA_PROTOCOL_PHIDGETS "Enable Phidgets protocol" OFF) # Requires Phidgets library -option(OSSIA_PROTOCOL_JOYSTICK "Enable Joystick protocol" ON) # Requires SDL2 library +option(OSSIA_PROTOCOL_JOYSTICK "Enable Joystick protocol" ON) # Requires SDL3 library option(OSSIA_PROTOCOL_WIIMOTE "Enable Wiimote Protocol" ON) #use wiiuse option(OSSIA_PROTOCOL_ARTNET "Enable artnet protocol" ON) option(OSSIA_PROTOCOL_LIBMAPPER "Enable libmapper protocol" OFF) #use external libmapper @@ -111,8 +111,7 @@ set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${PROJECT_SOURCE_DIR}/CMake;${PROJEC set(OSSIA_SUBMODULE_AUTOUPDATE ON CACHE BOOL "Auto update submodule") set(CMAKE_PREFIX_PATH - "${OSSIA_SDK}/SDL2" - "${OSSIA_SDK}/SDL2/cmake" + "${OSSIA_SDK}/SDL3" "${OSSIA_SDK}/portaudio/lib/cmake" "${CMAKE_PREFIX_PATH}" ) diff --git a/cmake/deps/sdl.cmake b/cmake/deps/sdl.cmake index fd90c868464..ace70ba0a32 100644 --- a/cmake/deps/sdl.cmake +++ b/cmake/deps/sdl.cmake @@ -1,29 +1,29 @@ set(SDL_BUILDING_LIBRARY TRUE) if(CMAKE_SYSTEM_NAME MATCHES Emscripten) - add_library(ossia_sdl2 INTERFACE IMPORTED GLOBAL) - add_library(ossia::sdl2 ALIAS ossia_sdl2) + add_library(ossia_sdl3 INTERFACE IMPORTED GLOBAL) + add_library(ossia::sdl3 ALIAS ossia_sdl3) - target_compile_options(ossia_sdl2 INTERFACE "SHELL:-s USE_SDL=2") - target_link_options(ossia_sdl2 INTERFACE "SHELL:-s USE_SDL=2") + target_compile_options(ossia_sdl3 INTERFACE "SHELL:-s USE_SDL=3") + target_link_options(ossia_sdl3 INTERFACE "SHELL:-s USE_SDL=3") return() else() - find_package(SDL2 CONFIG GLOBAL) + find_package(SDL3 CONFIG GLOBAL) endif() -if(TARGET SDL2::SDL2) - add_library(ossia_sdl2 INTERFACE IMPORTED GLOBAL) - add_library(ossia::sdl2 ALIAS ossia_sdl2) +if(TARGET SDL3::SDL3) + add_library(ossia_sdl3 INTERFACE IMPORTED GLOBAL) + add_library(ossia::sdl3 ALIAS ossia_sdl3) - target_link_libraries(ossia_sdl2 INTERFACE SDL2::SDL2) -elseif(TARGET SDL2::SDL2-static) - add_library(ossia_sdl2 INTERFACE IMPORTED GLOBAL) - add_library(ossia::sdl2 ALIAS ossia_sdl2) + target_link_libraries(ossia_sdl3 INTERFACE SDL3::SDL3) +elseif(TARGET SDL3::SDL3-static) + add_library(ossia_sdl3 INTERFACE IMPORTED GLOBAL) + add_library(ossia::sdl3 ALIAS ossia_sdl3) - target_link_libraries(ossia_sdl2 INTERFACE SDL2::SDL2-static) -elseif(SDL2_LIBRARIES AND SDL2_INCLUDE_DIRS) - add_library(ossia_sdl2 INTERFACE IMPORTED GLOBAL) - add_library(ossia::sdl2 ALIAS ossia_sdl2) + target_link_libraries(ossia_sdl3 INTERFACE SDL3::SDL3-static) +elseif(SDL3_LIBRARIES AND SDL3_INCLUDE_DIRS) + add_library(ossia_sdl3 INTERFACE IMPORTED GLOBAL) + add_library(ossia::sdl3 ALIAS ossia_sdl3) - target_include_directories(ossia_sdl2 PUBLIC "${SDL2_INCLUDE_DIRS}") - target_link_libraries(ossia_sdl2 INTERFACE "${SDL2_LIBRARIES}") + target_include_directories(ossia_sdl3 PUBLIC "${SDL3_INCLUDE_DIRS}") + target_link_libraries(ossia_sdl3 INTERFACE "${SDL3_LIBRARIES}") endif() diff --git a/src/ossia/audio/sdl_protocol.hpp b/src/ossia/audio/sdl_protocol.hpp index dd409526cb0..7c4d8d87ca4 100644 --- a/src/ossia/audio/sdl_protocol.hpp +++ b/src/ossia/audio/sdl_protocol.hpp @@ -2,15 +2,17 @@ #include #if defined(OSSIA_ENABLE_SDL) -#if __has_include() -#include -#if !defined(SDL_AUDIO_DISABLED) +#if __has_include() #include #include #include -#include -#include +#include +#include + +#include +#include +#include #define OSSIA_AUDIO_SDL 1 @@ -25,32 +27,32 @@ class sdl_protocol final : public audio_engine sdl_protocol(int rate, int bs) { SDL_Init(SDL_INIT_AUDIO); - m_desired.freq = rate; - m_desired.format = AUDIO_F32SYS; - m_desired.channels = outputs; - m_desired.samples = bs; - m_desired.callback = SDLCallback; - m_desired.userdata = this; - m_deviceId = SDL_OpenAudioDevice(nullptr, 0, &m_desired, &m_obtained, 0); + SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, std::to_string(bs).c_str()); + + m_spec.freq = rate; + m_spec.format = SDL_AUDIO_F32; + m_spec.channels = outputs; - if(m_deviceId < 2) + m_stream = SDL_OpenAudioDeviceStream( + SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &m_spec, SDLCallback, this); + + if(!m_stream) { using namespace std::literals; throw std::runtime_error("SDL: Couldn't open audio: "s + SDL_GetError()); } - this->effective_sample_rate = m_obtained.freq; - this->effective_buffer_size = m_obtained.samples; + this->effective_sample_rate = m_spec.freq; + this->effective_buffer_size = bs; this->effective_inputs = 0; - this->effective_outputs = m_obtained.channels; + this->effective_outputs = m_spec.channels; - // Preallocate so that the audio thread does not have to: this only ever - // gets resized if SDL changes the callback buffer size under our feet. - m_channels.resize(m_obtained.channels); - m_scratch.resize(std::size_t(m_obtained.samples) * m_obtained.channels); + m_interleaved.resize(std::size_t(bs) * m_spec.channels); + m_planar.resize(std::size_t(bs) * m_spec.channels); + m_channels.resize(m_spec.channels); - SDL_PauseAudioDevice(m_deviceId, 0); + SDL_ResumeAudioStreamDevice(m_stream); m_activated = true; } @@ -58,7 +60,7 @@ class sdl_protocol final : public audio_engine bool running() const override { - return m_activated && SDL_GetAudioDeviceStatus(m_deviceId) == SDL_AUDIO_PLAYING; + return m_activated && !SDL_AudioStreamDevicePaused(m_stream); } void stop() override @@ -66,7 +68,8 @@ class sdl_protocol final : public audio_engine audio_engine::stop(); if(m_activated) { - SDL_CloseAudioDevice(m_deviceId); + SDL_DestroyAudioStream(m_stream); + m_stream = nullptr; m_activated = false; } // Not SDL_Quit(): other parts of ossia (joystick, gamecontroller, sensors, @@ -75,7 +78,8 @@ class sdl_protocol final : public audio_engine } private: - static void SDLCallback(void* userData, Uint8* data, int bytes) + static void SDLCallback( + void* userData, SDL_AudioStream* stream, int additional_amount, int total_amount) { [[maybe_unused]] static const thread_local auto _ @@ -85,45 +89,37 @@ class sdl_protocol final : public audio_engine return 0; }(); - auto& self = *static_cast(userData); - self.tick_start(); - if(!self.m_start) - self.m_start = std::chrono::steady_clock::now(); + if(additional_amount <= 0) + return; - auto audio_out = reinterpret_cast(data); - const int out_chan = self.m_obtained.channels; + auto& self = *static_cast(userData); + const int out_chan = self.m_spec.channels; + const int frames = self.effective_buffer_size; assert(out_chan > 0); + assert(frames > 0); - // Note: the amount of data asked for here is *not* guaranteed to match the - // SDL_AudioSpec we got when opening the device. SDL keeps its own copy of - // that spec and backends are free to rewrite it while the stream runs: on - // Windows, WASAPI resizes the callback buffer to the device period every - // time the device is reset (default device changed, format changed in the - // control panel, endpoint invalidated...), see UpdateAudioStream() in - // SDL_wasapi.c. Our m_obtained is a stale copy at that point, so the byte - // count we are given is the only thing we can trust. - const int frames = bytes / int(sizeof(float) * out_chan); - - if(self.stop_processing || frames <= 0) - { - self.tick_clear(); - memset(data, 0, bytes); - return; - } + const int block_bytes = int(frames * out_chan * sizeof(float)); + while(additional_amount > 0) { - const std::size_t samples = std::size_t(frames) * out_chan; - if(self.m_scratch.size() < samples) - self.m_scratch.resize(samples); + self.tick_start(); + if(!self.m_start) + self.m_start = std::chrono::steady_clock::now(); - float* const float_data = self.m_scratch.data(); - memset(float_data, 0, sizeof(float) * samples); + if(self.stop_processing) + { + self.tick_clear(); + std::fill(self.m_interleaved.begin(), self.m_interleaved.end(), 0.f); + SDL_PutAudioStreamData(stream, self.m_interleaved.data(), block_bytes); + return; + } + float* const float_data = self.m_planar.data(); float** const float_output = self.m_channels.data(); + std::fill_n(float_data, std::size_t(frames) * out_chan, 0.f); + for(int c = 0; c < out_chan; c++) - { float_output[c] = float_data + c * frames; - } // if one day there's input... samples[j++] / 32768.; @@ -137,25 +133,25 @@ class sdl_protocol final : public audio_engine out_chan, (uint64_t)frames, nsecs}; self.audio_tick(ts); + float* audio_out = self.m_interleaved.data(); for(int j = 0; j < frames; j++) for(int c = 0; c < out_chan; c++) *audio_out++ = float_output[c][j]; - // Zero the trailing partial frame if SDL asked for a size which is not a - // multiple of the frame size - const int written = int(sizeof(float) * samples); - if(written < bytes) - memset(data + written, 0, bytes - written); + SDL_PutAudioStreamData(stream, self.m_interleaved.data(), block_bytes); self.tick_end(); self.m_total_frames += frames; + + additional_amount -= block_bytes; } } - SDL_AudioDeviceID m_deviceId{}; - SDL_AudioSpec m_desired, m_obtained; - ossia::float_vector m_scratch; - ossia::pod_vector m_channels; + SDL_AudioStream* m_stream{}; + SDL_AudioSpec m_spec{}; + std::vector m_interleaved; + std::vector m_planar; + std::vector m_channels; uint64_t m_total_frames{}; std::optional m_start; bool m_activated{}; @@ -164,4 +160,3 @@ class sdl_protocol final : public audio_engine #endif #endif -#endif diff --git a/src/ossia/protocols/joystick/game_controller_protocol.cpp b/src/ossia/protocols/joystick/game_controller_protocol.cpp index cd11442ca1e..b105321b664 100644 --- a/src/ossia/protocols/joystick/game_controller_protocol.cpp +++ b/src/ossia/protocols/joystick/game_controller_protocol.cpp @@ -14,11 +14,11 @@ game_controller_protocol::game_controller_protocol( , m_processor{joystick_event_processor::instance(m_manager)} , m_ctx{ptr} , m_joystick_id{joystick_id} - , m_joystick_index{joystick_index} { // Check That (ID, Index) is a valid combination // Could happen if a joystick is unplugged between settings and here - if(joystick_id != SDL_JoystickGetDeviceInstanceID(joystick_index)) + const SDL_JoystickID sdl_id = sdl_joystick_ids{}[joystick_index]; + if(sdl_id == 0 || joystick_id != static_cast(sdl_id)) throw std::runtime_error("Invalid Settings"); // Check that this ID is not already registered @@ -26,10 +26,10 @@ game_controller_protocol::game_controller_protocol( throw std::runtime_error("This Joystick is already open"); // Open The Joystick - if(!SDL_IsGameController(joystick_index)) + if(!SDL_IsGamepad(sdl_id)) throw std::runtime_error("This Joystick is not a game controller"); - m_joystick = SDL_GameControllerOpen(joystick_index); + m_joystick = SDL_OpenGamepad(sdl_id); if(m_joystick == nullptr) throw std::runtime_error("Failed to open Joystick"); @@ -49,17 +49,15 @@ void game_controller_protocol::set_device(ossia::net::device_base& dev) { m_device = &dev; auto& root = dev.get_root_node(); - switch(SDL_GameControllerTypeForIndex(m_joystick_index)) + switch(SDL_GetGamepadTypeForID(static_cast(m_joystick_id))) { - case SDL_CONTROLLER_TYPE_XBOX360: - case SDL_CONTROLLER_TYPE_XBOXONE: + case SDL_GAMEPAD_TYPE_XBOX360: + case SDL_GAMEPAD_TYPE_XBOXONE: SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "1"); break; - case SDL_CONTROLLER_TYPE_PS4: - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); - break; - case SDL_CONTROLLER_TYPE_PS5: - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); + case SDL_GAMEPAD_TYPE_PS4: + case SDL_GAMEPAD_TYPE_PS5: + SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "1"); break; default: // TODO @@ -102,24 +100,22 @@ void game_controller_protocol::set_device(ossia::net::device_base& dev) // clang-format on using namespace std::literals; - for(int i = SDL_CONTROLLER_AXIS_LEFTX; i <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT; i++) + for(int i = SDL_GAMEPAD_AXIS_LEFTX; i <= SDL_GAMEPAD_AXIS_RIGHT_TRIGGER; i++) { - if(SDL_GameControllerHasAxis(m_joystick, static_cast(i)) - == SDL_TRUE) + if(SDL_GamepadHasAxis(m_joystick, static_cast(i))) { // Triggers range from 0 to 1, while sticks range from -1 to 1. - const bool is_trigger = (i == SDL_CONTROLLER_AXIS_TRIGGERLEFT - || i == SDL_CONTROLLER_AXIS_TRIGGERRIGHT); + const bool is_trigger = (i == SDL_GAMEPAD_AXIS_LEFT_TRIGGER + || i == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER); m_axis_parameters[i] = device_parameter::create_device_parameter( root, axes[i], 0.0, val_type::FLOAT, bounding_mode::CLIP, access_mode::GET, is_trigger ? make_domain(0.0f, 1.0f) : make_domain(-1.0f, 1.0f)); } } - for(int i = SDL_CONTROLLER_BUTTON_A; i <= SDL_CONTROLLER_BUTTON_TOUCHPAD; i++) + for(int i = SDL_GAMEPAD_BUTTON_SOUTH; i <= SDL_GAMEPAD_BUTTON_TOUCHPAD; i++) { - if(SDL_GameControllerHasButton(m_joystick, static_cast(i)) - == SDL_TRUE) + if(SDL_GamepadHasButton(m_joystick, static_cast(i))) { m_button_parameters[i] = device_parameter::create_device_parameter( root, buttons[i], false, val_type::BOOL, bounding_mode::CLIP, access_mode::GET, @@ -127,32 +123,21 @@ void game_controller_protocol::set_device(ossia::net::device_base& dev) } } - const int max_sensor = -#if SDL_VERSION_ATLEAST(2, 26, 0) - SDL_SENSOR_GYRO_R -#else - SDL_SENSOR_GYRO -#endif - ; - - for(int i = SDL_SENSOR_UNKNOWN; i <= max_sensor; i++) + for(int i = SDL_SENSOR_UNKNOWN; i <= SDL_SENSOR_GYRO_R; i++) { - if(SDL_GameControllerHasSensor(m_joystick, static_cast(i)) - == SDL_TRUE) + if(SDL_GamepadHasSensor(m_joystick, static_cast(i))) { - if(SDL_GameControllerSetSensorEnabled( - m_joystick, static_cast(i), SDL_TRUE) - >= 0) + if(SDL_SetGamepadSensorEnabled(m_joystick, static_cast(i), true)) m_sensor_parameters[i] = device_parameter::create_device_parameter( root, sensors[i], ossia::vec3f{}, val_type::VEC3F, bounding_mode::CLIP, access_mode::GET, make_domain(-1.0f, 1.0f)); } } - for(int i = 0, N = SDL_GameControllerGetNumTouchpads(m_joystick); i < N; i++) + for(int i = 0, N = SDL_GetNumGamepadTouchpads(m_joystick); i < N; i++) { touchpad tp; - const int fingers = SDL_GameControllerGetNumTouchpadFingers(m_joystick, i); + const int fingers = SDL_GetNumGamepadTouchpadFingers(m_joystick, i); tp.fingers.resize(fingers); for(int f = 0; f < fingers; f++) { @@ -171,7 +156,9 @@ void game_controller_protocol::set_device(ossia::net::device_base& dev) m_touchpads[i] = tp; } - if(SDL_GameControllerHasRumble(m_joystick)) + const SDL_PropertiesID props = SDL_GetGamepadProperties(m_joystick); + + if(SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false)) { // Gamepads have two motors : a low-frequency one (left) and a high-frequency one (right) rumble.lo_freq = device_parameter::create_device_parameter( @@ -185,7 +172,7 @@ void game_controller_protocol::set_device(ossia::net::device_base& dev) access_mode::SET, make_domain(10.0f, 1000000.0f)); } - if(SDL_GameControllerHasRumbleTriggers(m_joystick)) + if(SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false)) { rumble_triggers.left = device_parameter::create_device_parameter( root, "/rumble/triggers/left", 0.0f, val_type::FLOAT, bounding_mode::CLIP, @@ -216,7 +203,7 @@ bool game_controller_protocol::push( uint16_t hi = std::clamp(ossia::convert(rumble.hi_freq->value()), 0.f, 1.f) * 65535; uint32_t dur = ossia::convert(rumble.duration->value()); - SDL_GameControllerRumble(m_joystick, lo, hi, dur); + SDL_RumbleGamepad(m_joystick, lo, hi, dur); } if(¶m == rumble_triggers.duration) @@ -228,7 +215,7 @@ bool game_controller_protocol::push( = std::clamp(ossia::convert(rumble_triggers.right->value()), 0.f, 1.f) * 65535; uint32_t dur = ossia::convert(rumble_triggers.duration->value()); - SDL_GameControllerRumbleTriggers(m_joystick, left, right, dur); + SDL_RumbleGamepadTriggers(m_joystick, left, right, dur); } return true; } @@ -253,7 +240,7 @@ void game_controller_protocol::stop() if(m_joystick != nullptr) { m_manager.unregister_protocol(*this); - SDL_GameControllerClose(m_joystick); + SDL_CloseGamepad(m_joystick); m_joystick = nullptr; } } diff --git a/src/ossia/protocols/joystick/game_controller_protocol.hpp b/src/ossia/protocols/joystick/game_controller_protocol.hpp index 3eefd4ca6cd..fa54312aeba 100644 --- a/src/ossia/protocols/joystick/game_controller_protocol.hpp +++ b/src/ossia/protocols/joystick/game_controller_protocol.hpp @@ -7,7 +7,7 @@ #include #include -typedef struct _SDL_GameController SDL_GameController; +typedef struct SDL_Gamepad SDL_Gamepad; namespace ossia::net { class joystick_protocol_manager; @@ -75,9 +75,8 @@ class OSSIA_EXPORT game_controller_protocol final : public ossia::net::protocol_ ossia::small_flat_map m_touchpads; int32_t m_joystick_id{}; - int32_t m_joystick_index{}; - SDL_GameController* m_joystick{}; + SDL_Gamepad* m_joystick{}; }; } diff --git a/src/ossia/protocols/joystick/joystick_manager.hpp b/src/ossia/protocols/joystick/joystick_manager.hpp index 3fc7529a6eb..73e61149b09 100644 --- a/src/ossia/protocols/joystick/joystick_manager.hpp +++ b/src/ossia/protocols/joystick/joystick_manager.hpp @@ -6,8 +6,8 @@ #include #include -#if __has_include() -#include +#if __has_include() +#include #else #include #endif @@ -25,17 +25,17 @@ struct sdl_joystick_context // Prevent SDL from setting SIGINT handler on Posix Systems SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); - if(int ret = SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER); ret < 0) + if(!SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) throw std::runtime_error(fmt::format("SDL Init failure: {}", SDL_GetError())); // Optional: absent from some SDL builds (the Emscripten port has no haptic // support at all), and no joystick API here needs them to be up. Sensors - // are read through SDL_GameControllerHasSensor, which simply reports none. + // are read through SDL_GamepadHasSensor, which simply reports none. SDL_InitSubSystem(SDL_INIT_HAPTIC); SDL_InitSubSystem(SDL_INIT_SENSOR); - SDL_JoystickEventState(SDL_ENABLE); - SDL_GameControllerEventState(SDL_ENABLE); + SDL_SetJoystickEventsEnabled(true); + SDL_SetGamepadEventsEnabled(true); } static sdl_joystick_context& instance() @@ -48,13 +48,32 @@ struct sdl_joystick_context { // To be sure to quit the event loop SDL_Event ev; - ev.type = SDL_FIRSTEVENT; + ev.type = SDL_EVENT_FIRST; SDL_PushEvent(&ev); SDL_Quit(); } }; +struct sdl_joystick_ids +{ + sdl_joystick_ids() { m_ids = SDL_GetJoysticks(&m_count); } + sdl_joystick_ids(const sdl_joystick_ids&) = delete; + sdl_joystick_ids& operator=(const sdl_joystick_ids&) = delete; + ~sdl_joystick_ids() { SDL_free(m_ids); } + + int count() const noexcept { return m_ids ? m_count : 0; } + + SDL_JoystickID operator[](int index) const noexcept + { + return (m_ids && index >= 0 && index < m_count) ? m_ids[index] : 0; + } + +private: + SDL_JoystickID* m_ids{}; + int m_count{}; +}; + class joystick_protocol_manager { public: @@ -216,7 +235,7 @@ struct joystick_event_processor using namespace std::literals; // To be sure to quit the event loop SDL_Event ev; - ev.type = SDL_FIRSTEVENT; + ev.type = SDL_EVENT_FIRST; SDL_PushEvent(&ev); for(auto& tm : m_timers) @@ -238,7 +257,7 @@ struct joystick_event_processor } } - void push_axis(const SDL_ControllerAxisEvent& ev) + void push_axis(const SDL_GamepadAxisEvent& ev) { if(auto p = m_manager.get_protocol_by_id(ev.which)) { @@ -251,19 +270,19 @@ struct joystick_event_processor { if(auto p = m_manager.get_protocol_by_id(ev.which)) { - push(p, p->m_button_parameters[ev.button], bool(ev.state == SDL_PRESSED)); + push(p, p->m_button_parameters[ev.button], bool(ev.down)); } } - void push_button(const SDL_ControllerButtonEvent& ev) + void push_button(const SDL_GamepadButtonEvent& ev) { if(auto p = m_manager.get_protocol_by_id(ev.which)) { - push(p, p->m_button_parameters[ev.button], bool(ev.state == SDL_PRESSED)); + push(p, p->m_button_parameters[ev.button], bool(ev.down)); } } - void push_sensor(const SDL_ControllerSensorEvent& ev) + void push_sensor(const SDL_GamepadSensorEvent& ev) { if(auto p = m_manager.get_protocol_by_id(ev.which)) { @@ -273,7 +292,7 @@ struct joystick_event_processor } } - void push_touchpad(const SDL_ControllerTouchpadEvent& ev) + void push_touchpad(const SDL_GamepadTouchpadEvent& ev) { if(auto p = m_manager.get_protocol_by_id(ev.which)) { @@ -317,36 +336,34 @@ struct joystick_event_processor { switch(ev.type) { - case SDL_JOYAXISMOTION: + case SDL_EVENT_JOYSTICK_AXIS_MOTION: push_axis(ev.jaxis); break; - case SDL_JOYBUTTONDOWN: - case SDL_JOYBUTTONUP: + case SDL_EVENT_JOYSTICK_BUTTON_DOWN: + case SDL_EVENT_JOYSTICK_BUTTON_UP: push_button(ev.jbutton); break; - case SDL_JOYHATMOTION: + case SDL_EVENT_JOYSTICK_HAT_MOTION: push_hat(ev.jhat); break; - case SDL_CONTROLLERAXISMOTION: - push_axis(ev.caxis); + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + push_axis(ev.gaxis); break; - case SDL_CONTROLLERBUTTONDOWN: - case SDL_CONTROLLERBUTTONUP: - push_button(ev.cbutton); + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + push_button(ev.gbutton); break; - case SDL_CONTROLLERTOUCHPADDOWN: - case SDL_CONTROLLERTOUCHPADMOTION: - case SDL_CONTROLLERTOUCHPADUP: - push_touchpad(ev.ctouchpad); + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + push_touchpad(ev.gtouchpad); break; - case SDL_CONTROLLERSENSORUPDATE: - push_sensor(ev.csensor); + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: + push_sensor(ev.gsensor); break; - // case SDL_CONTROLLERUPDATECOMPLETE_RESERVED_FOR_SDL3: - // case SDL_CONTROLLERSTEAMHANDLEUPDATED: default: break; diff --git a/src/ossia/protocols/joystick/joystick_protocol.cpp b/src/ossia/protocols/joystick/joystick_protocol.cpp index 4e4969d3bcc..4eb922669b5 100644 --- a/src/ossia/protocols/joystick/joystick_protocol.cpp +++ b/src/ossia/protocols/joystick/joystick_protocol.cpp @@ -17,7 +17,8 @@ joystick_protocol::joystick_protocol( { // Check That (ID, Index) is a valid combination // Could happen if a joystick is unplugged between settings and here - if(joystick_id != SDL_JoystickGetDeviceInstanceID(joystick_index)) + const SDL_JoystickID sdl_id = sdl_joystick_ids{}[joystick_index]; + if(sdl_id == 0 || joystick_id != static_cast(sdl_id)) throw std::runtime_error("Invalid Settings"); // Check that this ID is not already registered @@ -25,7 +26,7 @@ joystick_protocol::joystick_protocol( throw std::runtime_error("This Joystick is already open"); // Open The Joystick - m_joystick = SDL_JoystickOpen(joystick_index); + m_joystick = SDL_OpenJoystick(sdl_id); if(m_joystick == nullptr) throw std::runtime_error("Failed to open Joystick"); @@ -47,10 +48,10 @@ void joystick_protocol::set_device(ossia::net::device_base& dev) auto& root = dev.get_root_node(); // Retrieve Joystick Info - const int axis_count = SDL_JoystickNumAxes(m_joystick); - // const int ball_count = SDL_JoystickNumBalls(m_joystick); - const int hat_count = SDL_JoystickNumHats(m_joystick); - const int button_count = SDL_JoystickNumButtons(m_joystick); + const int axis_count = SDL_GetNumJoystickAxes(m_joystick); + // const int ball_count = SDL_GetNumJoystickBalls(m_joystick); + const int hat_count = SDL_GetNumJoystickHats(m_joystick); + const int button_count = SDL_GetNumJoystickButtons(m_joystick); // Build Parameters Tree @@ -112,7 +113,7 @@ void joystick_protocol::stop() if(m_joystick != nullptr) { m_manager.unregister_protocol(*this); - SDL_JoystickClose(m_joystick); + SDL_CloseJoystick(m_joystick); m_joystick = nullptr; } } @@ -120,27 +121,27 @@ void joystick_protocol::stop() unsigned int joystick_info::get_joystick_count() { sdl_joystick_context::instance(); - SDL_JoystickUpdate(); - return static_cast(SDL_NumJoysticks()); + SDL_UpdateJoysticks(); + return static_cast(sdl_joystick_ids{}.count()); } const char* joystick_info::get_joystick_name(const int index) { sdl_joystick_context::instance(); - return SDL_JoystickNameForIndex(index); + return SDL_GetJoystickNameForID(sdl_joystick_ids{}[index]); } bool joystick_info::get_joystick_is_gamepad(const int index) { sdl_joystick_context::instance(); - return SDL_IsGameController(index); + return SDL_IsGamepad(sdl_joystick_ids{}[index]); } bool joystick_info::get_joystick_is_available(const int index) { sdl_joystick_context::instance(); - const auto id = SDL_JoystickGetDeviceInstanceID(index); - if(id != -1) + const auto id = sdl_joystick_ids{}[index]; + if(id != 0) return !joystick_protocol_manager::instance().joystick_is_registered(id); return false; } @@ -148,13 +149,14 @@ bool joystick_info::get_joystick_is_available(const int index) int32_t joystick_info::get_joystick_id(const int index) { sdl_joystick_context::instance(); - return SDL_JoystickGetDeviceInstanceID(index); + const auto id = sdl_joystick_ids{}[index]; + return id != 0 ? static_cast(id) : -1; } void joystick_info::write_joystick_uuid(const int index, uint8_t* dst) { sdl_joystick_context::instance(); - const auto uid = SDL_JoystickGetDeviceGUID(index); + const auto uid = SDL_GetJoystickGUIDForID(sdl_joystick_ids{}[index]); std::copy_n(&uid.data[0], 16, dst); } @@ -162,17 +164,18 @@ std::pair joystick_info::get_available_id_for_uid(const uint8_t* request) { auto& mgr = joystick_protocol_manager::instance(); - SDL_JoystickUpdate(); - for(int i = 0, N = SDL_NumJoysticks(); i < N; i++) + SDL_UpdateJoysticks(); + const sdl_joystick_ids ids; + for(int i = 0, N = ids.count(); i < N; i++) { - const auto uid = SDL_JoystickGetDeviceGUID(i); + const auto uid = SDL_GetJoystickGUIDForID(ids[i]); if(std::equal(std::begin(uid.data), std::end(uid.data), request)) { - auto id = SDL_JoystickGetDeviceInstanceID(i); + const auto id = ids[i]; if(mgr.joystick_is_registered(id)) continue; - return {id, i}; + return {static_cast(id), i}; } } return {-1, -1}; diff --git a/src/ossia/protocols/joystick/joystick_protocol.hpp b/src/ossia/protocols/joystick/joystick_protocol.hpp index b6eb44bd45b..0f48f45d123 100644 --- a/src/ossia/protocols/joystick/joystick_protocol.hpp +++ b/src/ossia/protocols/joystick/joystick_protocol.hpp @@ -8,7 +8,7 @@ #include -typedef struct _SDL_Joystick SDL_Joystick; +typedef struct SDL_Joystick SDL_Joystick; namespace ossia::net { diff --git a/src/ossia_features.cmake b/src/ossia_features.cmake index 23ff00b2491..1b79946674f 100644 --- a/src/ossia_features.cmake +++ b/src/ossia_features.cmake @@ -134,7 +134,7 @@ endif() if(OSSIA_PROTOCOL_JOYSTICK) target_sources(ossia PRIVATE ${OSSIA_JOYSTICK_SRCS} ${OSSIA_JOYSTICK_HEADERS}) - target_link_libraries(ossia PRIVATE $) + target_link_libraries(ossia PRIVATE $) set(OSSIA_PROTOCOLS ${OSSIA_PROTOCOLS} Joystick) endif() @@ -273,7 +273,7 @@ if(OSSIA_DATAFLOW) #SDL support if(OSSIA_ENABLE_SDL) - target_link_libraries(ossia PRIVATE $) + target_link_libraries(ossia PRIVATE $) endif() # MiniAudio (WASAPI on Windows, CoreAudio on macOS, ALSA on Linux, Web Audio