diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca4e5e175..0d01f3ad7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,9 @@ jobs: sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_BASE}/ llvm-toolchain-${UBUNTU_BASE}-21 main" sudo apt-get -y update - sudo apt-get -y install libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libx11-xcb-dev clang-21 lld-21 ninja-build cmake libwxgtk3.2-dev libfontconfig1-dev libxtst-dev + sudo apt-get -y install libc++-dev libc++abi-dev libgtk-3-dev libx11-xcb-dev clang-21 lld-21 ninja-build cmake libfontconfig1-dev libxtst-dev \ + libasound2-dev libpulse-dev libudev-dev libdbus-1-dev \ + libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxss-dev libxkbcommon-dev libxfixes-dev # Download linuxdeploy if not cached. if [ '${{ steps.cache-linuxdeploy.outputs.cache-hit }}' != 'true' ]; then diff --git a/.gitmodules b/.gitmodules index a53b25774..940eaa603 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,9 +29,6 @@ [submodule "third_party/cxxopts"] path = third_party/cxxopts url = https://github.com/jarro2783/cxxopts.git -[submodule "third_party/SDL2"] - path = third_party/SDL2 - url = https://github.com/libsdl-org/SDL.git [submodule "third_party/utfcpp"] path = third_party/utfcpp url = https://github.com/nemtrif/utfcpp.git @@ -129,3 +126,7 @@ [submodule "third_party/cereal"] path = third_party/cereal url = https://github.com/USCiLab/cereal.git +[submodule "third_party/SDL3"] + path = third_party/SDL3 + url = https://github.com/libsdl-org/SDL.git + ignore = dirty diff --git a/CMakeLists.txt b/CMakeLists.txt index 743f7d760..e5dd4d3f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(APPLE) endif() if(APPLE) - # OBJC needed for SDL2's .m sources (Cocoa video, CoreAudio, MFi joystick). + # OBJC needed for SDL3's .m sources (Cocoa, CoreAudio, MFi joystick). project(xenia LANGUAGES C CXX OBJC OBJCXX) set(CMAKE_OBJCXX_STANDARD 20) set(CMAKE_OBJCXX_STANDARD_REQUIRED ON) diff --git a/docs/building.md b/docs/building.md index 9cfae2f51..68d686db3 100644 --- a/docs/building.md +++ b/docs/building.md @@ -94,7 +94,9 @@ The build script uses Clang 21. You will also need some development libraries. To get them on an Ubuntu system: ```sh -sudo apt-get install build-essential mesa-vulkan-drivers libc++-dev libc++abi-dev liblz4-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-21 llvm-21 ninja-build libwxgtk3.2-dev libfontconfig1-dev +sudo apt-get install build-essential mesa-vulkan-drivers libc++-dev libc++abi-dev liblz4-dev libvulkan-dev libx11-xcb-dev clang-21 llvm-21 ninja-build libfontconfig1-dev \ + libasound2-dev libpulse-dev libudev-dev libdbus-1-dev \ + libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxss-dev libxkbcommon-dev libxfixes-dev ``` In addition, you will need up to date Vulkan libraries and drivers for your hardware, which most distributions have in their standard repositories nowadays. diff --git a/src/xenia/app/CMakeLists.txt b/src/xenia/app/CMakeLists.txt index c6493d87f..6359918ff 100644 --- a/src/xenia/app/CMakeLists.txt +++ b/src/xenia/app/CMakeLists.txt @@ -118,7 +118,7 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") X11 xcb X11-xcb - SDL2 + SDL3 ) elseif(APPLE) target_link_libraries(xenia-app PRIVATE diff --git a/src/xenia/apu/CMakeLists.txt b/src/xenia/apu/CMakeLists.txt index 456c68fb0..b7dc3f719 100644 --- a/src/xenia/apu/CMakeLists.txt +++ b/src/xenia/apu/CMakeLists.txt @@ -8,6 +8,6 @@ target_link_libraries(xenia-apu PUBLIC libavcodec libavutil libavformat xenia-ba # AudioMediaPlayer instantiates SDLAudioDriver directly on Linux, so the # SDL chain is a transitive dependency of xenia-apu there. if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_libraries(xenia-apu PUBLIC xenia-helper-sdl SDL2) + target_link_libraries(xenia-apu PUBLIC xenia-helper-sdl SDL3) endif() xe_target_defaults(xenia-apu) diff --git a/src/xenia/apu/sdl/CMakeLists.txt b/src/xenia/apu/sdl/CMakeLists.txt index 3052fde6c..112ec0435 100644 --- a/src/xenia/apu/sdl/CMakeLists.txt +++ b/src/xenia/apu/sdl/CMakeLists.txt @@ -1,4 +1,4 @@ add_library(xenia-apu-sdl STATIC) xe_platform_sources(xenia-apu-sdl ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(xenia-apu-sdl PUBLIC xenia-apu xenia-base xenia-helper-sdl SDL2) +target_link_libraries(xenia-apu-sdl PUBLIC xenia-apu xenia-base xenia-helper-sdl SDL3) xe_target_defaults(xenia-apu-sdl) diff --git a/src/xenia/apu/sdl/sdl_audio_driver.cc b/src/xenia/apu/sdl/sdl_audio_driver.cc index 56318f8d7..a0289210c 100644 --- a/src/xenia/apu/sdl/sdl_audio_driver.cc +++ b/src/xenia/apu/sdl/sdl_audio_driver.cc @@ -50,57 +50,30 @@ SDLAudioDriver::~SDLAudioDriver() { }; bool SDLAudioDriver::Initialize() { - SDL_version ver = {}; - SDL_GetVersion(&ver); - if ((ver.major < 2) || (ver.major == 2 && ver.minor == 0 && ver.patch < 8)) { - XELOGW( - "SDL library version {}.{}.{} is outdated. " - "You may experience choppy audio.", - ver.major, ver.minor, ver.patch); - } - if (!xe::helper::sdl::SDLHelper::Prepare()) { return false; } - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { return false; } sdl_initialized_ = true; - SDL_AudioSpec desired_spec = {}; - SDL_AudioSpec obtained_spec; - desired_spec.freq = frame_frequency_; - desired_spec.format = AUDIO_F32; - desired_spec.channels = frame_channels_; - desired_spec.samples = channel_samples_; - desired_spec.callback = SDLCallback; - desired_spec.userdata = this; - // Allow the hardware to decide between 5.1 and stereo, - // unless the input is stereo - int allowed_change = - frame_channels_ != 2 ? SDL_AUDIO_ALLOW_CHANNELS_CHANGE : 0; - for (int i = 0; i < 2; i++) { - sdl_device_id_ = SDL_OpenAudioDevice(nullptr, 0, &desired_spec, - &obtained_spec, allowed_change); - if (sdl_device_id_ <= 0) { - XELOGE("SDL_OpenAudioDevice() failed."); - return false; - } - if (obtained_spec.channels == 2 || obtained_spec.channels == 6) { - break; - } - // If the system is 4 or 7.1, let SDL convert - allowed_change = 0; - SDL_CloseAudioDevice(sdl_device_id_); - sdl_device_id_ = -1; - } - if (sdl_device_id_ <= 0) { - XELOGE("Failed to get a compatible SDL Audio Device."); + // SDL3 always converts between the stream spec and the device spec, so we + // just request our native channel count and let SDL3 downmix/upmix to the + // hardware's actual configuration. + SDL_AudioSpec spec = {}; + spec.format = SDL_AUDIO_F32; + spec.channels = static_cast(frame_channels_); + spec.freq = static_cast(frame_frequency_); + + sdl_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, + &spec, SDLCallback, this); + if (!sdl_stream_) { + XELOGE("SDL_OpenAudioDeviceStream() failed: {}", SDL_GetError()); return false; } - sdl_device_channels_ = obtained_spec.channels; - SDL_PauseAudioDevice(sdl_device_id_, 0); + SDL_ResumeAudioStreamDevice(sdl_stream_); return true; } @@ -125,14 +98,24 @@ void SDLAudioDriver::SubmitFrame(float* frame) { } } -void SDLAudioDriver::Pause() { SDL_PauseAudioDevice(sdl_device_id_, 1); } +void SDLAudioDriver::Pause() { + if (sdl_stream_) { + SDL_PauseAudioStreamDevice(sdl_stream_); + } +} -void SDLAudioDriver::Resume() { SDL_PauseAudioDevice(sdl_device_id_, 0); } +void SDLAudioDriver::Resume() { + if (sdl_stream_) { + SDL_ResumeAudioStreamDevice(sdl_stream_); + } +} void SDLAudioDriver::Shutdown() { - if (sdl_device_id_ > 0) { - SDL_CloseAudioDevice(sdl_device_id_); - sdl_device_id_ = -1; + if (sdl_stream_) { + // SDL_OpenAudioDeviceStream-created streams own the underlying logical + // device and close it on destroy. + SDL_DestroyAudioStream(sdl_stream_); + sdl_stream_ = nullptr; } if (sdl_initialized_) { SDL_QuitSubSystem(SDL_INIT_AUDIO); @@ -149,58 +132,63 @@ void SDLAudioDriver::Shutdown() { }; } -void SDLAudioDriver::SDLCallback(void* userdata, Uint8* stream, int len) { +void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, + int additional_amount, int total_amount) { SCOPE_profile_cpu_f("apu"); - if (!userdata || !stream) { - XELOGE("SDLAudioDriver::sdl_callback called with nullptr."); + if (!userdata || !stream || additional_amount <= 0) { return; } const auto driver = static_cast(userdata); - assert_true(len == sizeof(float) * driver->channel_samples_ * - driver->sdl_device_channels_); + const int frame_bytes = static_cast(driver->frame_size_); - std::unique_lock guard(driver->frames_mutex_); - if (driver->frames_queued_.empty()) { - std::memset(stream, 0, len); - } else { - auto buffer = driver->frames_queued_.front(); - driver->frames_queued_.pop(); - if (driver->need_format_conversion_) { - switch (driver->sdl_device_channels_) { - case 2: - conversion::sequential_6_BE_to_interleaved_2_LE( - reinterpret_cast(stream), buffer, - driver->channel_samples_); - break; - case 6: - conversion::sequential_6_BE_to_interleaved_6_LE( - reinterpret_cast(stream), buffer, - driver->channel_samples_); - break; - default: - assert_unhandled_case(driver->sdl_device_channels_); - break; + // SDL3 pulls until additional_amount is satisfied or we run out of frames. + // If the queue empties, returning early lets SDL3 fill the remainder with + // silence — the producer will catch up on the next callback. + while (additional_amount > 0) { + float* buffer; + { + std::unique_lock guard(driver->frames_mutex_); + if (driver->frames_queued_.empty()) { + return; } - } else { - assert_true(driver->sdl_device_channels_ == driver->frame_channels_); - std::memcpy(stream, buffer, len); + buffer = driver->frames_queued_.front(); + driver->frames_queued_.pop(); } + + // Convert into a scratch buffer in the format SDL3 expects (interleaved + // little-endian floats at our requested channel count). SDL3 will then + // matrix-mix this to whatever the hardware actually wants. + float scratch[kFrameSizeMax / sizeof(float)]; + if (driver->need_format_conversion_) { + // need_format_conversion implies 6-channel BE-sequential input. + conversion::sequential_6_BE_to_interleaved_6_LE(scratch, buffer, + driver->channel_samples_); + } else { + std::memcpy(scratch, buffer, frame_bytes); + } + // Scale by master (cvar) and per-driver volume. const uint32_t mv = cvars::volume > 100 ? 100 : cvars::volume; const float volume = driver->volume_ * (mv / 100.0f); if (volume != 1.0f) { - float* samples = reinterpret_cast(stream); - const size_t count = len / sizeof(float); + const size_t count = frame_bytes / sizeof(float); for (size_t i = 0; i < count; ++i) { - samples[i] *= volume; + scratch[i] *= volume; } } - driver->frames_unused_.push(buffer); + + SDL_PutAudioStreamData(stream, scratch, frame_bytes); + additional_amount -= frame_bytes; + + { + std::unique_lock guard(driver->frames_mutex_); + driver->frames_unused_.push(buffer); + } auto ret = driver->semaphore_->Release(1, nullptr); assert_true(ret); } -}; +} } // namespace sdl } // namespace apu } // namespace xe diff --git a/src/xenia/apu/sdl/sdl_audio_driver.h b/src/xenia/apu/sdl/sdl_audio_driver.h index ec805ef7e..93b4c32dc 100644 --- a/src/xenia/apu/sdl/sdl_audio_driver.h +++ b/src/xenia/apu/sdl/sdl_audio_driver.h @@ -14,7 +14,7 @@ #include #include -#include "SDL.h" +#include #include "xenia/apu/audio_driver.h" #include "xenia/base/threading.h" @@ -38,13 +38,13 @@ class SDLAudioDriver : public AudioDriver { void Shutdown() override; protected: - static void SDLCallback(void* userdata, Uint8* stream, int len); + static void SDLCallback(void* userdata, SDL_AudioStream* stream, + int additional_amount, int total_amount); xe::threading::Semaphore* semaphore_ = nullptr; - SDL_AudioDeviceID sdl_device_id_ = -1; + SDL_AudioStream* sdl_stream_ = nullptr; bool sdl_initialized_ = false; - uint8_t sdl_device_channels_ = 0; float volume_ = 1.0f; diff --git a/src/xenia/base/system_gnulinux.cc b/src/xenia/base/system_gnulinux.cc index e45d82747..aa956e559 100644 --- a/src/xenia/base/system_gnulinux.cc +++ b/src/xenia/base/system_gnulinux.cc @@ -11,6 +11,7 @@ #include #include +#include #include #include "xenia/base/assert.h" @@ -18,8 +19,16 @@ #include "xenia/base/string.h" #include "xenia/base/system.h" -// Use headers in third party to not depend on system sdl headers for building -#include "third_party/SDL2/include/SDL.h" +// Forward-declared instead of pulling in — xenia-base must not +// drag SDL onto its include path, and only the dlsym() cast needs the type. +// Flags and signature are stable across SDL2/3. +namespace { +using SDL_ShowSimpleMessageBox_fn = int (*)(uint32_t flags, const char* title, + const char* message, void* window); +constexpr uint32_t kSDL_MessageBoxError = 0x00000010; +constexpr uint32_t kSDL_MessageBoxWarning = 0x00000020; +constexpr uint32_t kSDL_MessageBoxInformation = 0x00000040; +} // namespace namespace xe { @@ -37,15 +46,18 @@ void LaunchFileExplorer(const std::filesystem::path& path) { } void ShowSimpleMessageBox(SimpleMessageBoxType type, std::string_view message) { - void* libsdl2 = dlopen("libSDL2.so", RTLD_LAZY | RTLD_LOCAL); - assert_not_null(libsdl2); - if (libsdl2) { - auto* pSDL_ShowSimpleMessageBox = - reinterpret_cast( - dlsym(libsdl2, "SDL_ShowSimpleMessageBox")); + void* libsdl = dlopen("libSDL3.so.0", RTLD_LAZY | RTLD_LOCAL); + if (!libsdl) { + libsdl = dlopen("libSDL3.so", RTLD_LAZY | RTLD_LOCAL); + } + assert_not_null(libsdl); + if (libsdl) { + auto pSDL_ShowSimpleMessageBox = + reinterpret_cast( + dlsym(libsdl, "SDL_ShowSimpleMessageBox")); assert_not_null(pSDL_ShowSimpleMessageBox); if (pSDL_ShowSimpleMessageBox) { - Uint32 flags; + uint32_t flags; const char* title; char* message_copy = reinterpret_cast(alloca(message.size() + 1)); std::memcpy(message_copy, message.data(), message.size()); @@ -55,20 +67,20 @@ void ShowSimpleMessageBox(SimpleMessageBoxType type, std::string_view message) { default: case SimpleMessageBoxType::Help: title = "Xenia Help"; - flags = SDL_MESSAGEBOX_INFORMATION; + flags = kSDL_MessageBoxInformation; break; case SimpleMessageBoxType::Warning: title = "Xenia Warning"; - flags = SDL_MESSAGEBOX_WARNING; + flags = kSDL_MessageBoxWarning; break; case SimpleMessageBoxType::Error: title = "Xenia Error"; - flags = SDL_MESSAGEBOX_ERROR; + flags = kSDL_MessageBoxError; break; } pSDL_ShowSimpleMessageBox(flags, title, message_copy, NULL); } - dlclose(libsdl2); + dlclose(libsdl); } } diff --git a/src/xenia/helper/sdl/CMakeLists.txt b/src/xenia/helper/sdl/CMakeLists.txt index e072735c9..7dd6429e0 100644 --- a/src/xenia/helper/sdl/CMakeLists.txt +++ b/src/xenia/helper/sdl/CMakeLists.txt @@ -1,4 +1,4 @@ add_library(xenia-helper-sdl STATIC) xe_platform_sources(xenia-helper-sdl ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(xenia-helper-sdl PUBLIC SDL2) +target_link_libraries(xenia-helper-sdl PUBLIC SDL3) xe_target_defaults(xenia-helper-sdl) diff --git a/src/xenia/helper/sdl/sdl_helper.cc b/src/xenia/helper/sdl/sdl_helper.cc index 4bfd4243f..b31f4725d 100644 --- a/src/xenia/helper/sdl/sdl_helper.cc +++ b/src/xenia/helper/sdl/sdl_helper.cc @@ -9,13 +9,6 @@ #include "xenia/helper/sdl/sdl_helper.h" -// On linux we likely build on an "outdated" system but still want to control -// these features when available on a newer system. -#if !SDL_VERSION_ATLEAST(2, 0, 14) -#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME" -#define SDL_HINT_JOYSTICK_RAWINPUT "SDL_JOYSTICK_RAWINPUT" -#endif - #include "xenia/base/assert.h" #include "xenia/base/logging.h" @@ -33,9 +26,9 @@ bool SDLHelper::Prepare() { is_prepared_ &= SetHints(); is_prepared_ &= RedirectLog(); - SDL_version ver = {}; - SDL_GetVersion(&ver); - XELOGI("SDL Version {}.{}.{} initialized.", ver.major, ver.minor, ver.patch); + const int ver = SDL_GetVersion(); + XELOGI("SDL Version {}.{}.{} initialized.", SDL_VERSIONNUM_MAJOR(ver), + SDL_VERSIONNUM_MINOR(ver), SDL_VERSIONNUM_MICRO(ver)); return is_prepared_; } @@ -48,8 +41,7 @@ bool SDLHelper::SetHints() { // Setting hints with normal priority fails when the hint is set via env // vars or a hint with override priority is set, which does not conclude a // failure. - if (SDL_FALSE == - SDL_SetHintWithPriority( + if (!SDL_SetHintWithPriority( name, value, override_ ? SDL_HINT_OVERRIDE : SDL_HINT_NORMAL)) { const char* msg_fmt = "SDLHelper: Unable to set hint \"{}\" to value \"{}\"."; @@ -63,15 +55,12 @@ bool SDLHelper::SetHints() { return true; }; - // SDL calls timeBeginPeriod(1) but xenia sets this to a lower value before - // using NtSetTimerResolution(). Having that value overwritten causes overall - // fps drops. Use override priority as timer resolution should always be - // managed by xenia. https://bugzilla.libsdl.org/show_bug.cgi?id=5104 - suc &= setHint(SDL_HINT_TIMER_RESOLUTION, "0", true); - suc &= setHint(SDL_HINT_AUDIO_CATEGORY, "playback"); - suc &= setHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "xenia emulator"); + // SDL3 replaced SDL_HINT_AUDIO_DEVICE_APP_NAME with app metadata. Backends + // (PulseAudio, PipeWire, WASAPI session names, etc.) read this for the + // user-facing device label. + SDL_SetAppMetadata("xenia emulator", nullptr, nullptr); // On Windows, SDL's joystick subsystem creates a hidden device-notification // window (WM_DEVICECHANGE + raw-input dispatch) on whichever thread inits @@ -92,7 +81,7 @@ bool SDLHelper::SetHints() { bool SDLHelper::RedirectLog() { // Redirect SDL_Log* output (internal library stuff) to our log system. - SDL_LogSetOutputFunction( + SDL_SetLogOutputFunction( [](void* userdata, int category, SDL_LogPriority priority, const char* message) { const char* msg_fmt = "SDL: {}"; @@ -121,7 +110,7 @@ bool SDLHelper::RedirectLog() { // SDL itself isn't that talkative. Additionally to this settings there are // hints that can be switched on to output additional internal logging // information. - SDL_LogSetAllPriority(SDL_LogPriority::SDL_LOG_PRIORITY_VERBOSE); + SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); return true; } } // namespace sdl diff --git a/src/xenia/helper/sdl/sdl_helper.h b/src/xenia/helper/sdl/sdl_helper.h index ce4ffefc4..7bf633c13 100644 --- a/src/xenia/helper/sdl/sdl_helper.h +++ b/src/xenia/helper/sdl/sdl_helper.h @@ -10,7 +10,7 @@ #ifndef XENIA_HELPER_SDL_SDL_HELPER_H_ #define XENIA_HELPER_SDL_SDL_HELPER_H_ -#include "SDL.h" +#include namespace xe { namespace helper { diff --git a/src/xenia/hid/sdl/CMakeLists.txt b/src/xenia/hid/sdl/CMakeLists.txt index 0f8a7abca..e1e4e8192 100644 --- a/src/xenia/hid/sdl/CMakeLists.txt +++ b/src/xenia/hid/sdl/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(xenia-hid-sdl STATIC) xe_platform_sources(xenia-hid-sdl ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(xenia-hid-sdl PUBLIC xenia-base xenia-hid xenia-ui SDL2) +target_link_libraries(xenia-hid-sdl PUBLIC xenia-base xenia-hid xenia-ui SDL3) xe_target_defaults(xenia-hid-sdl) # Bake the bundled controller mappings. diff --git a/src/xenia/hid/sdl/sdl_input_driver.cc b/src/xenia/hid/sdl/sdl_input_driver.cc index b2d8acbab..172c9d9a7 100644 --- a/src/xenia/hid/sdl/sdl_input_driver.cc +++ b/src/xenia/hid/sdl/sdl_input_driver.cc @@ -45,7 +45,7 @@ namespace sdl { SDLInputDriver::SDLInputDriver(xe::ui::Window* window, size_t window_z_order) : InputDriver(window, window_z_order), sdl_events_initialized_(false), - sdl_gamecontroller_initialized_(false), + sdl_gamepad_initialized_(false), sdl_events_unflushed_(0), sdl_thread_should_exit_(false), controllers_(), @@ -59,10 +59,6 @@ SDLInputDriver::~SDLInputDriver() { } X_STATUS SDLInputDriver::Setup() { - if (!TestSDLVersion()) { - return X_STATUS_UNSUCCESSFUL; - } - std::promise init_promise; auto init_future = init_promise.get_future(); sdl_thread_ = std::thread(&SDLInputDriver::SDLEventThread, this, @@ -88,7 +84,7 @@ void SDLInputDriver::SDLEventThread(std::promise init_result) { // Initialize the event system early, so we catch device events for already // connected controllers. - if (SDL_InitSubSystem(SDL_INIT_EVENTS) < 0) { + if (!SDL_InitSubSystem(SDL_INIT_EVENTS)) { init_result.set_value(X_STATUS_UNSUCCESSFUL); return; } @@ -97,15 +93,16 @@ void SDLInputDriver::SDLEventThread(std::promise init_result) { // With an event watch we will always get notified, even if the event queue // is full, which can happen if another subsystem does not clear its events. SDL_AddEventWatch( - [](void* userdata, SDL_Event* event) -> int { + [](void* userdata, SDL_Event* event) -> bool { if (!userdata || !event) { assert_always(); - return 0; + return false; } const auto type = event->type; - if (type < SDL_JOYAXISMOTION || type >= SDL_FINGERDOWN) { - return 0; + if (type < SDL_EVENT_JOYSTICK_AXIS_MOTION || + type >= SDL_EVENT_FINGER_DOWN) { + return false; } // If another part of xenia uses another SDL subsystem that generates @@ -114,17 +111,17 @@ void SDLInputDriver::SDLEventThread(std::promise init_result) { const auto driver = static_cast(userdata); driver->HandleEvent(*event); - return 0; + return false; }, this); - if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) { + if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { SDL_QuitSubSystem(SDL_INIT_EVENTS); sdl_events_initialized_ = false; init_result.set_value(X_STATUS_UNSUCCESSFUL); return; } - sdl_gamecontroller_initialized_ = true; + sdl_gamepad_initialized_ = true; LoadGameControllerDB(); @@ -138,12 +135,12 @@ void SDLInputDriver::SDLEventThread(std::promise init_result) { // Tear down on the same thread that initialized SDL. for (size_t i = 0; i < controllers_.size(); i++) { if (controllers_.at(i).sdl) { - SDL_GameControllerClose(controllers_.at(i).sdl); + SDL_CloseGamepad(controllers_.at(i).sdl); controllers_.at(i) = {}; } } - SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); - sdl_gamecontroller_initialized_ = false; + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); + sdl_gamepad_initialized_ = false; SDL_QuitSubSystem(SDL_INIT_EVENTS); sdl_events_initialized_ = false; } @@ -212,7 +209,7 @@ void SDLInputDriver::LoadMappingsFromStream(std::istream& stream) { std::string mapping_str = std::accumulate(row.begin(), row.end(), std::string{}, format); - int updated = SDL_GameControllerAddMapping(mapping_str.c_str()); + int updated = SDL_AddGamepadMapping(mapping_str.c_str()); switch (updated) { case 0: { @@ -233,7 +230,7 @@ void SDLInputDriver::LoadMappingsFromStream(std::istream& stream) { if (controller) { XELOGI("SDL Controller {}: {}", i, - SDL_GameControllerMapping(controller->sdl)); + SDL_GetGamepadMapping(controller->sdl)); } } @@ -243,7 +240,7 @@ void SDLInputDriver::LoadMappingsFromStream(std::istream& stream) { X_RESULT SDLInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags, X_INPUT_CAPABILITIES* out_caps) { - assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_); + assert(sdl_events_initialized_ && sdl_gamepad_initialized_); if (user_index >= HID_SDL_USER_COUNT || !out_caps) { return X_ERROR_BAD_ARGUMENTS; } @@ -264,7 +261,7 @@ X_RESULT SDLInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags, X_RESULT SDLInputDriver::GetState(uint32_t user_index, X_INPUT_STATE* out_state) { - assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_); + assert(sdl_events_initialized_ && sdl_gamepad_initialized_); if (user_index >= HID_SDL_USER_COUNT) { return X_ERROR_BAD_ARGUMENTS; } @@ -284,7 +281,7 @@ X_RESULT SDLInputDriver::GetState(uint32_t user_index, X_RESULT SDLInputDriver::SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) { - assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_); + assert(sdl_events_initialized_ && sdl_gamepad_initialized_); if (user_index >= HID_SDL_USER_COUNT) { return X_ERROR_BAD_ARGUMENTS; } @@ -294,23 +291,18 @@ X_RESULT SDLInputDriver::SetState(uint32_t user_index, return X_ERROR_DEVICE_NOT_CONNECTED; } -#if SDL_VERSION_ATLEAST(2, 0, 9) - if (SDL_GameControllerRumble(controller->sdl, vibration->left_motor_speed, - vibration->right_motor_speed, 0)) { + if (!SDL_RumbleGamepad(controller->sdl, vibration->left_motor_speed, + vibration->right_motor_speed, 0)) { return X_ERROR_FUNCTION_FAILED; - } else { - return X_ERROR_SUCCESS; } -#else return X_ERROR_SUCCESS; -#endif } X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags, X_INPUT_KEYSTROKE* out_keystroke) { // TODO(JoelLinn): Figure out the flags // https://github.com/evilC/UCR/blob/0489929e2a8e39caa3484c67f3993d3fba39e46f/Libraries/XInput.ahk#L85-L98 - assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_); + assert(sdl_events_initialized_ && sdl_gamepad_initialized_); bool user_any = users == XUserIndexAny; if (users >= HID_SDL_USER_COUNT && !user_any) { return X_ERROR_BAD_ARGUMENTS; @@ -457,8 +449,8 @@ static const char* JoystickTypeName(SDL_JoystickType t) { switch (t) { case SDL_JOYSTICK_TYPE_UNKNOWN: return "Unknown"; - case SDL_JOYSTICK_TYPE_GAMECONTROLLER: - return "GameController"; + case SDL_JOYSTICK_TYPE_GAMEPAD: + return "Gamepad"; case SDL_JOYSTICK_TYPE_WHEEL: return "Wheel"; case SDL_JOYSTICK_TYPE_ARCADE_STICK: @@ -521,10 +513,10 @@ static bool NameContainsCI(const char* name, std::string_view needle) { } // SDL_JoystickType numbering diverges from XINPUT_DEVSUBTYPE_* past value 6. -// GAMECONTROLLER doesn't expose form factor; fall back to name keywords. +// GAMEPAD doesn't expose form factor; fall back to name keywords. static uint8_t SdlTypeToXInputSubType(SDL_JoystickType t, const char* name) { switch (t) { - case SDL_JOYSTICK_TYPE_GAMECONTROLLER: + case SDL_JOYSTICK_TYPE_GAMEPAD: // SDL's XInput backend bakes the SubType into the device name (e.g. // "XInput Guitar #1", "XInput DrumKit #1"), so a name keyword recovers // the form factor for free. @@ -578,35 +570,30 @@ void SDLInputDriver::HandleEvent(const SDL_Event& event) { // may be a dedicated thread SDL has created for the joystick subsystem. // Event queue should never be (this) full - assert(SDL_PeepEvents(nullptr, 0, SDL_PEEKEVENT, SDL_FIRSTEVENT, - SDL_LASTEVENT) < 0xFFFF); + assert(SDL_PeepEvents(nullptr, 0, SDL_PEEKEVENT, SDL_EVENT_FIRST, + SDL_EVENT_LAST) < 0xFFFF); // The queue could grow up to 3.5MB since it is never polled. if (++sdl_events_unflushed_ > 64) { - SDL_FlushEvents(SDL_JOYAXISMOTION, SDL_FINGERDOWN - 1); + SDL_FlushEvents(SDL_EVENT_JOYSTICK_AXIS_MOTION, SDL_EVENT_FINGER_DOWN - 1); sdl_events_unflushed_ = 0; } switch (event.type) { - case SDL_JOYDEVICEADDED: { + case SDL_EVENT_JOYSTICK_ADDED: { // Logged before the controller event (if any) so unmapped devices // (some guitars/drums) leave a paper trail even when SDL can't promote // them to a game controller. - const int idx = event.jdevice.which; - const char* name = SDL_JoystickNameForIndex(idx); + const SDL_JoystickID id = event.jdevice.which; + const char* name = SDL_GetJoystickNameForID(id); char guid_str[33] = {}; - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(idx), guid_str, - sizeof(guid_str)); - const bool is_controller = SDL_IsGameController(idx) == SDL_TRUE; -#if SDL_VERSION_ATLEAST(2, 0, 6) + SDL_GUIDToString(SDL_GetJoystickGUIDForID(id), guid_str, + sizeof(guid_str)); + const bool is_controller = SDL_IsGamepad(id); XELOGI( "SDL JoystickAdded: \"{}\", VendorID(0x{:04X}), " "ProductID(0x{:04X}), GUID({}), HasControllerMapping({})", - name ? name : "?", SDL_JoystickGetDeviceVendor(idx), - SDL_JoystickGetDeviceProduct(idx), guid_str, is_controller); -#else - XELOGI("SDL JoystickAdded: \"{}\", GUID({}), HasControllerMapping({})", - name ? name : "?", guid_str, is_controller); -#endif + name ? name : "?", SDL_GetJoystickVendorForID(id), + SDL_GetJoystickProductForID(id), guid_str, is_controller); if (!is_controller) { XELOGW( "SDL JoystickAdded: \"{}\" has no game controller mapping; " @@ -615,17 +602,17 @@ void SDLInputDriver::HandleEvent(const SDL_Event& event) { } break; } - case SDL_CONTROLLERDEVICEADDED: + case SDL_EVENT_GAMEPAD_ADDED: OnControllerDeviceAdded(event); break; - case SDL_CONTROLLERDEVICEREMOVED: + case SDL_EVENT_GAMEPAD_REMOVED: OnControllerDeviceRemoved(event); break; - case SDL_CONTROLLERAXISMOTION: + case SDL_EVENT_GAMEPAD_AXIS_MOTION: OnControllerDeviceAxisMotion(event); break; - case SDL_CONTROLLERBUTTONDOWN: - case SDL_CONTROLLERBUTTONUP: + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: OnControllerDeviceButtonChanged(event); break; default: @@ -636,7 +623,7 @@ void SDLInputDriver::HandleEvent(const SDL_Event& event) { void SDLInputDriver::OnControllerDeviceAdded(const SDL_Event& event) { // Open the controller. - const auto controller = SDL_GameControllerOpen(event.cdevice.which); + const auto controller = SDL_OpenGamepad(event.gdevice.which); if (!controller) { assert_always(); return; @@ -644,13 +631,12 @@ void SDLInputDriver::OnControllerDeviceAdded(const SDL_Event& event) { char guid_str[33]; - SDL_JoystickGetGUIDString( - SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(controller)), guid_str, - 33); + SDL_GUIDToString(SDL_GetJoystickGUID(SDL_GetGamepadJoystick(controller)), + guid_str, 33); const SDL_JoystickType joy_type = - SDL_JoystickGetType(SDL_GameControllerGetJoystick(controller)); - const char* controller_name = SDL_GameControllerName(controller); + SDL_GetJoystickType(SDL_GetGamepadJoystick(controller)); + const char* controller_name = SDL_GetGamepadName(controller); const uint8_t xinput_subtype = SdlTypeToXInputSubType(joy_type, controller_name); XELOGI( @@ -662,37 +648,23 @@ void SDLInputDriver::OnControllerDeviceAdded(const SDL_Event& event) { "ProductID(0x{:04X}), " "GUID({})", controller_name ? controller_name : "?", JoystickTypeName(joy_type), -#if SDL_VERSION_ATLEAST(2, 0, 12) - static_cast(SDL_GameControllerGetType(controller)), -#else - "?", -#endif + static_cast(SDL_GetGamepadType(controller)), XInputSubTypeName(xinput_subtype), xinput_subtype, -#if SDL_VERSION_ATLEAST(2, 0, 6) - SDL_GameControllerGetVendor(controller), - SDL_GameControllerGetProduct(controller), -#else - "?", "?", -#endif + SDL_GetGamepadVendor(controller), SDL_GetGamepadProduct(controller), guid_str); - int user_id = -1; -#if SDL_VERSION_ATLEAST(2, 0, 9) // Check if the controller has a player index LED. - user_id = SDL_GameControllerGetPlayerIndex(controller); + int user_id = SDL_GetGamepadPlayerIndex(controller); // Is that id already taken? - if (user_id < 0 || user_id >= controllers_.size() || + if (user_id < 0 || user_id >= static_cast(controllers_.size()) || controllers_.at(user_id).sdl) { user_id = -1; } -#endif // No player index or already taken, just take the first free slot. if (user_id < 0) { for (size_t i = 0; i < controllers_.size(); i++) { if (!controllers_.at(i).sdl) { user_id = static_cast(i); -#if SDL_VERSION_ATLEAST(2, 0, 12) - SDL_GameControllerSetPlayerIndex(controller, user_id); -#endif + SDL_SetGamepadPlayerIndex(controller, user_id); break; } } @@ -705,29 +677,27 @@ void SDLInputDriver::OnControllerDeviceAdded(const SDL_Event& event) { UpdateXCapabilities(state); XELOGI("SDL OnControllerDeviceAdded: Added at index {}.", user_id); - XELOGI("SDL Controller {}: {}", user_id, - SDL_GameControllerMapping(controller)); + XELOGI("SDL Controller {}: {}", user_id, SDL_GetGamepadMapping(controller)); NotifyDevicesChanged(); } else { // No more controllers needed, close it. - SDL_GameControllerClose(controller); + SDL_CloseGamepad(controller); XELOGW("SDL OnControllerDeviceAdded: Ignored. No free slots."); } } void SDLInputDriver::OnControllerDeviceRemoved(const SDL_Event& event) { - // Find the disconnected gamecontroller and close it. - auto idx = GetControllerIndexFromInstanceID(event.cdevice.which); + // Find the disconnected gamepad and close it. + auto idx = GetControllerIndexFromInstanceID(event.gdevice.which); if (idx) { auto* sdl = controllers_.at(*idx).sdl; - const char* name = SDL_GameControllerName(sdl); + const char* name = SDL_GetGamepadName(sdl); char guid_str[33] = {}; - SDL_JoystickGetGUIDString( - SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(sdl)), guid_str, - sizeof(guid_str)); + SDL_GUIDToString(SDL_GetJoystickGUID(SDL_GetGamepadJoystick(sdl)), guid_str, + sizeof(guid_str)); XELOGI("SDL OnControllerDeviceRemoved: \"{}\", GUID({}), driver_slot({}).", name ? name : "?", guid_str, *idx); - SDL_GameControllerClose(sdl); + SDL_CloseGamepad(sdl); controllers_.at(*idx) = {}; keystroke_states_.at(*idx) = {}; NotifyDevicesChanged(); @@ -735,32 +705,32 @@ void SDLInputDriver::OnControllerDeviceRemoved(const SDL_Event& event) { // Can happen in case all slots where full previously. XELOGW( "SDL OnControllerDeviceRemoved: Ignored, instance_id({}) not in use.", - event.cdevice.which); + event.gdevice.which); } } void SDLInputDriver::OnControllerDeviceAxisMotion(const SDL_Event& event) { - auto idx = GetControllerIndexFromInstanceID(event.caxis.which); + auto idx = GetControllerIndexFromInstanceID(event.gaxis.which); assert(idx); auto& pad = controllers_.at(*idx).state.gamepad; - switch (event.caxis.axis) { - case SDL_CONTROLLER_AXIS_LEFTX: - pad.thumb_lx = event.caxis.value; + switch (event.gaxis.axis) { + case SDL_GAMEPAD_AXIS_LEFTX: + pad.thumb_lx = event.gaxis.value; break; - case SDL_CONTROLLER_AXIS_LEFTY: - pad.thumb_ly = ~event.caxis.value; + case SDL_GAMEPAD_AXIS_LEFTY: + pad.thumb_ly = ~event.gaxis.value; break; - case SDL_CONTROLLER_AXIS_RIGHTX: - pad.thumb_rx = event.caxis.value; + case SDL_GAMEPAD_AXIS_RIGHTX: + pad.thumb_rx = event.gaxis.value; break; - case SDL_CONTROLLER_AXIS_RIGHTY: - pad.thumb_ry = ~event.caxis.value; + case SDL_GAMEPAD_AXIS_RIGHTY: + pad.thumb_ry = ~event.gaxis.value; break; - case SDL_CONTROLLER_AXIS_TRIGGERLEFT: - pad.left_trigger = static_cast(event.caxis.value >> 7); + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + pad.left_trigger = static_cast(event.gaxis.value >> 7); break; - case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: - pad.right_trigger = static_cast(event.caxis.value >> 7); + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + pad.right_trigger = static_cast(event.gaxis.value >> 7); break; default: assert_always(); @@ -771,11 +741,12 @@ void SDLInputDriver::OnControllerDeviceAxisMotion(const SDL_Event& event) { void SDLInputDriver::OnControllerDeviceButtonChanged(const SDL_Event& event) { // Define a lookup table to map between SDL and XInput button codes. - // These need to be in the order of the SDL_GameControllerButton enum. + // These need to be in the order of the SDL_GamepadButton enum. static constexpr std::array< std::underlying_type::type, 21> xbutton_lookup = { - // Standard buttons: + // Standard buttons (SDL3 uses position-based names: SOUTH/EAST/ + // WEST/NORTH = the Xbox A/B/X/Y physical positions): X_INPUT_GAMEPAD_A, X_INPUT_GAMEPAD_B, X_INPUT_GAMEPAD_X, @@ -795,7 +766,9 @@ void SDLInputDriver::OnControllerDeviceButtonChanged(const SDL_Event& event) { // For now just assign sensible defaults // Misc: X_INPUT_GAMEPAD_GUIDE, - // Xbox Elite paddles: + // Xbox Elite paddles (SDL3 indices preserve physical position even + // though the SDL2 PADDLE1..PADDLE4 names became RIGHT_PADDLE1, + // LEFT_PADDLE1, RIGHT_PADDLE2, LEFT_PADDLE2): X_INPUT_GAMEPAD_Y, X_INPUT_GAMEPAD_B, X_INPUT_GAMEPAD_X, @@ -803,23 +776,23 @@ void SDLInputDriver::OnControllerDeviceButtonChanged(const SDL_Event& event) { // PS touchpad button X_INPUT_GAMEPAD_GUIDE, }; - static_assert(SDL_CONTROLLER_BUTTON_A == 0); - static_assert(SDL_CONTROLLER_BUTTON_DPAD_RIGHT == 14); + static_assert(SDL_GAMEPAD_BUTTON_SOUTH == 0); + static_assert(SDL_GAMEPAD_BUTTON_DPAD_RIGHT == 14); - auto idx = GetControllerIndexFromInstanceID(event.cbutton.which); + auto idx = GetControllerIndexFromInstanceID(event.gbutton.which); assert(idx); auto& controller = controllers_.at(*idx); uint16_t xbuttons = controller.state.gamepad.buttons; // Lookup the XInput button code. - if (event.cbutton.button >= xbutton_lookup.size()) { + if (event.gbutton.button >= xbutton_lookup.size()) { // A newer SDL Version may have added new buttons. - XELOGI("SDL HID: Unknown button was pressed: {}.", event.cbutton.button); + XELOGI("SDL HID: Unknown button was pressed: {}.", event.gbutton.button); return; } - auto xbutton = xbutton_lookup.at(event.cbutton.button); + auto xbutton = xbutton_lookup.at(event.gbutton.button); // Pressed or released? - if (event.cbutton.state == SDL_PRESSED) { + if (event.gbutton.down) { if (xbutton == X_INPUT_GAMEPAD_GUIDE && !cvars::guide_button) { return; } @@ -839,10 +812,9 @@ std::optional SDLInputDriver::GetControllerIndexFromInstanceID( if (!controller) { continue; } - auto joystick = SDL_GameControllerGetJoystick(controller); + auto joystick = SDL_GetGamepadJoystick(controller); assert(joystick); - auto joy_instance_id = SDL_JoystickInstanceID(joystick); - assert(joy_instance_id >= 0); + auto joy_instance_id = SDL_GetJoystickID(joystick); if (joy_instance_id == instance_id) { return i; } @@ -862,26 +834,6 @@ SDLInputDriver::ControllerState* SDLInputDriver::GetControllerState( return controller; } -bool SDLInputDriver::TestSDLVersion() const { -#if SDL_VERSION_ATLEAST(2, 0, 9) - // SDL 2.0.9 or newer is required for simple rumble support and player - // index. - constexpr Uint8 min_patchlevel = 9; -#else - // SDL 2.0.4 or newer is required to read game controller mappings from - // file. - constexpr Uint8 min_patchlevel = 4; -#endif - - SDL_version ver = {}; - SDL_GetVersion(&ver); - if ((ver.major < 2) || - (ver.major == 2 && ver.minor == 0 && ver.patch < min_patchlevel)) { - return false; - } - return true; -} - void SDLInputDriver::UpdateXCapabilities(ControllerState& state) { assert(state.sdl); uint16_t cap_flags = 0x0; @@ -893,23 +845,24 @@ void SDLInputDriver::UpdateXCapabilities(ControllerState& state) { // should not be a problem, when in doubt disable the RAWINPUT driver via hint // (env var). - // Guess if we are wireless - auto power_level = - SDL_JoystickCurrentPowerLevel(SDL_GameControllerGetJoystick(state.sdl)); - if (power_level >= SDL_JOYSTICK_POWER_EMPTY && - power_level <= SDL_JOYSTICK_POWER_FULL) { + // Guess if we are wireless: a battery-backed power state means the device + // is running on its own power. + const SDL_PowerState power_state = + SDL_GetJoystickPowerInfo(SDL_GetGamepadJoystick(state.sdl), nullptr); + if (power_state == SDL_POWERSTATE_ON_BATTERY || + power_state == SDL_POWERSTATE_CHARGING || + power_state == SDL_POWERSTATE_CHARGED) { cap_flags |= X_INPUT_CAPS_WIRELESS; } // Check if all navigational buttons are present - static constexpr std::array nav_buttons = { - SDL_CONTROLLER_BUTTON_START, SDL_CONTROLLER_BUTTON_BACK, - SDL_CONTROLLER_BUTTON_DPAD_UP, SDL_CONTROLLER_BUTTON_DPAD_DOWN, - SDL_CONTROLLER_BUTTON_DPAD_LEFT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT, + static constexpr std::array nav_buttons = { + SDL_GAMEPAD_BUTTON_START, SDL_GAMEPAD_BUTTON_BACK, + SDL_GAMEPAD_BUTTON_DPAD_UP, SDL_GAMEPAD_BUTTON_DPAD_DOWN, + SDL_GAMEPAD_BUTTON_DPAD_LEFT, SDL_GAMEPAD_BUTTON_DPAD_RIGHT, }; for (auto it = nav_buttons.begin(); it < nav_buttons.end(); it++) { - auto bind = SDL_GameControllerGetBindForButton(state.sdl, *it); - if (bind.bindType == SDL_CONTROLLER_BINDTYPE_NONE) { + if (!SDL_GamepadHasButton(state.sdl, *it)) { cap_flags |= X_INPUT_CAPS_NO_NAVIGATION; break; } @@ -918,8 +871,8 @@ void SDLInputDriver::UpdateXCapabilities(ControllerState& state) { auto& c = state.caps; c.type = 0x01; // XINPUT_DEVTYPE_GAMEPAD c.sub_type = SdlTypeToXInputSubType( - SDL_JoystickGetType(SDL_GameControllerGetJoystick(state.sdl)), - SDL_GameControllerName(state.sdl)); + SDL_GetJoystickType(SDL_GetGamepadJoystick(state.sdl)), + SDL_GetGamepadName(state.sdl)); c.flags = cap_flags; c.gamepad.buttons = 0xF3FF | (cvars::guide_button ? X_INPUT_GAMEPAD_GUIDE : 0x0); @@ -942,15 +895,15 @@ std::vector SDLInputDriver::EnumerateDevices() { } InputDeviceInfo info{}; info.driver_slot = static_cast(i); - const char* name = SDL_GameControllerName(sdl); - auto* joystick = SDL_GameControllerGetJoystick(sdl); + const char* name = SDL_GetGamepadName(sdl); + auto* joystick = SDL_GetGamepadJoystick(sdl); if (joystick) { char guid_buf[33] = {}; - SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(joystick), guid_buf, - sizeof(guid_buf)); + SDL_GUIDToString(SDL_GetJoystickGUID(joystick), guid_buf, + sizeof(guid_buf)); info.stable_id = guid_buf; info.subtype = - SdlTypeToXInputSubType(SDL_JoystickGetType(joystick), name); + SdlTypeToXInputSubType(SDL_GetJoystickType(joystick), name); } info.display_name = name ? name : "Controller"; out.push_back(std::move(info)); diff --git a/src/xenia/hid/sdl/sdl_input_driver.h b/src/xenia/hid/sdl/sdl_input_driver.h index cf9f7a541..172f979f5 100644 --- a/src/xenia/hid/sdl/sdl_input_driver.h +++ b/src/xenia/hid/sdl/sdl_input_driver.h @@ -18,7 +18,7 @@ #include #include -#include "SDL.h" +#include #include "third_party/rapidcsv/src/rapidcsv.h" #include "xenia/hid/input_driver.h" #include "xenia/xbox.h" @@ -54,7 +54,7 @@ class SDLInputDriver final : public InputDriver { private: struct ControllerState { - SDL_GameController* sdl; + SDL_Gamepad* sdl; X_INPUT_CAPABILITIES caps; X_INPUT_STATE state; bool state_changed; @@ -84,7 +84,6 @@ class SDLInputDriver final : public InputDriver { std::optional GetControllerIndexFromInstanceID( SDL_JoystickID instance_id); ControllerState* GetControllerState(uint32_t user_index); - bool TestSDLVersion() const; void UpdateXCapabilities(ControllerState& state); // Owns SDL init, the event pump, and teardown so the UI thread's modal @@ -92,7 +91,7 @@ class SDLInputDriver final : public InputDriver { void SDLEventThread(std::promise init_result); bool sdl_events_initialized_; - bool sdl_gamecontroller_initialized_; + bool sdl_gamepad_initialized_; int sdl_events_unflushed_; std::thread sdl_thread_; std::atomic sdl_thread_should_exit_; diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index f1944bfa7..28ba46ce8 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -677,165 +677,68 @@ if(WIN32) endif() # ============================================================================== -# SDL2 +# SDL3 # ============================================================================== -if(WIN32) - # Build SDL2 from source on Windows - file(GLOB _sdl2_sources - "SDL2/src/*.c" - "SDL2/src/atomic/*.c" - "SDL2/src/audio/*.c" - "SDL2/src/audio/directsound/*.c" - "SDL2/src/audio/disk/*.c" - "SDL2/src/audio/dummy/*.c" - "SDL2/src/audio/wasapi/*.c" - "SDL2/src/audio/winmm/*.c" - "SDL2/src/core/windows/*.c" - "SDL2/src/cpuinfo/*.c" - "SDL2/src/dynapi/*.c" - "SDL2/src/events/*.c" - "SDL2/src/file/*.c" - "SDL2/src/filesystem/windows/*.c" - "SDL2/src/haptic/*.c" - "SDL2/src/haptic/dummy/*.c" - "SDL2/src/haptic/windows/*.c" - "SDL2/src/hidapi/*.c" - "SDL2/src/joystick/*.c" - "SDL2/src/joystick/dummy/*.c" - "SDL2/src/joystick/hidapi/*.c" - "SDL2/src/joystick/virtual/*.c" - "SDL2/src/joystick/windows/*.c" - "SDL2/src/libm/*.c" - "SDL2/src/loadso/windows/*.c" - "SDL2/src/locale/*.c" - "SDL2/src/locale/windows/*.c" - "SDL2/src/misc/*.c" - "SDL2/src/misc/windows/*.c" - "SDL2/src/power/*.c" - "SDL2/src/power/windows/*.c" - "SDL2/src/render/*.c" - "SDL2/src/render/direct3d/*.c" - "SDL2/src/render/direct3d11/*.c" - "SDL2/src/render/direct3d12/*.c" - "SDL2/src/render/opengl/*.c" - "SDL2/src/render/opengles2/*.c" - "SDL2/src/render/software/*.c" - "SDL2/src/sensor/*.c" - "SDL2/src/sensor/dummy/*.c" - "SDL2/src/sensor/windows/*.c" - "SDL2/src/stdlib/*.c" - "SDL2/src/thread/*.c" - "SDL2/src/thread/generic/SDL_syscond.c" - "SDL2/src/thread/windows/*.c" - "SDL2/src/timer/*.c" - "SDL2/src/timer/windows/*.c" - "SDL2/src/video/*.c" - "SDL2/src/video/dummy/*.c" - "SDL2/src/video/windows/*.c" - "SDL2/src/video/yuv2rgb/*.c" - ) - add_library(SDL2 STATIC ${_sdl2_sources}) - target_compile_definitions(SDL2 PRIVATE - HAVE_LIBC - SDL_LEAN_AND_MEAN - SDL_RENDER_DISABLED - ) - target_include_directories(SDL2 PUBLIC SDL2/include) - target_link_libraries(SDL2 PRIVATE setupapi winmm imm32 version) -elseif(APPLE) - # Build SDL2 from source on macOS. SDL2's auto-detected SDL_config_macosx.h - # selects the CoreAudio audio, Cocoa video, IOKit joystick/haptic, MFi - # controller, pthread, and dlopen subsystems wired up below; SDL_RENDER_DISABLED - # keeps the renderer subsystem out (xenia drives Metal directly). - file(GLOB _sdl2_sources - "SDL2/src/*.c" - "SDL2/src/atomic/*.c" - "SDL2/src/audio/*.c" - "SDL2/src/audio/coreaudio/*.m" - "SDL2/src/audio/disk/*.c" - "SDL2/src/audio/dummy/*.c" - "SDL2/src/cpuinfo/*.c" - "SDL2/src/dynapi/*.c" - "SDL2/src/events/*.c" - "SDL2/src/file/*.c" - "SDL2/src/file/cocoa/*.m" - "SDL2/src/filesystem/cocoa/*.m" - "SDL2/src/haptic/*.c" - "SDL2/src/haptic/darwin/*.c" - "SDL2/src/hidapi/*.c" - "SDL2/src/joystick/*.c" - "SDL2/src/joystick/darwin/*.c" - "SDL2/src/joystick/dummy/*.c" - "SDL2/src/joystick/hidapi/*.c" - "SDL2/src/joystick/iphoneos/*.m" - "SDL2/src/joystick/virtual/*.c" - "SDL2/src/libm/*.c" - "SDL2/src/loadso/dlopen/*.c" - "SDL2/src/locale/*.c" - "SDL2/src/locale/macosx/*.m" - "SDL2/src/misc/*.c" - "SDL2/src/misc/macosx/*.m" - "SDL2/src/power/*.c" - "SDL2/src/power/macosx/*.c" - "SDL2/src/render/*.c" - "SDL2/src/sensor/*.c" - "SDL2/src/sensor/dummy/*.c" - "SDL2/src/stdlib/*.c" - "SDL2/src/thread/*.c" - "SDL2/src/thread/pthread/*.c" - "SDL2/src/timer/*.c" - "SDL2/src/timer/unix/*.c" - "SDL2/src/video/*.c" - "SDL2/src/video/cocoa/*.m" - "SDL2/src/video/dummy/*.c" - "SDL2/src/video/yuv2rgb/*.c" - ) - add_library(SDL2 STATIC ${_sdl2_sources}) - target_compile_definitions(SDL2 PRIVATE - HAVE_LIBC - SDL_LEAN_AND_MEAN - SDL_RENDER_DISABLED - # Apple ships CGL but not . SDL_USE_BUILTIN_OPENGL_DEFINITIONS - # makes SDL_egl.h provide stub EGL typedefs, which is enough for the - # whole EGL family (SDL_egl.c, SDL_cocoaopengles.m) to compile — the - # actual EGL entry points are runtime-loaded via dlopen("libEGL.so") and - # only fail if an app explicitly asks for SDL_GL_CONTEXT_PROFILE_ES. - SDL_USE_BUILTIN_OPENGL_DEFINITIONS - ) - # SDL_mfijoystick.m uses __weak — requires ARC. Other .m files don't rely - # on explicit retain/release, so ARC across the whole OBJC set is safe. - target_compile_options(SDL2 PRIVATE - $<$:-fobjc-arc> - ) - target_include_directories(SDL2 PUBLIC SDL2/include) - target_link_libraries(SDL2 PRIVATE - "-framework AVFoundation" - "-framework AudioToolbox" - "-framework Carbon" - "-framework Cocoa" - "-framework CoreAudio" - "-framework CoreFoundation" - "-framework CoreGraphics" - "-framework CoreHaptics" - "-framework CoreServices" - "-framework CoreVideo" - "-framework ForceFeedback" - "-framework Foundation" - "-framework GameController" - "-framework IOKit" - "-framework Metal" - "-framework OpenGL" - "-framework QuartzCore" - iconv - ) -elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") - # Use system SDL2 on Linux - add_library(SDL2 INTERFACE) - find_package(PkgConfig REQUIRED) - pkg_check_modules(SDL2_PKG REQUIRED sdl2) - target_include_directories(SDL2 INTERFACE ${SDL2_PKG_INCLUDE_DIRS}) - target_link_libraries(SDL2 INTERFACE ${SDL2_PKG_LDFLAGS}) - target_compile_options(SDL2 INTERFACE ${SDL2_PKG_CFLAGS_OTHER}) +# Build SDL3 from source on every platform using upstream's own CMake. Xenia +# drives windowing/rendering/GPU directly, so the SDL subsystems we don't +# consume are switched off. Only audio + joystick/gamepad/haptic + events are +# exercised by xenia-apu-sdl / xenia-hid-sdl. Linux builds shared so the +# runtime dlopen("libSDL3.so.0") path in system_gnulinux.cc can resolve and so +# linuxdeploy can bundle the .so into the AppImage; Windows/macOS build static. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(SDL_SHARED ON CACHE INTERNAL "") + set(SDL_STATIC OFF CACHE INTERNAL "") +else() + set(SDL_SHARED OFF CACHE INTERNAL "") + set(SDL_STATIC ON CACHE INTERNAL "") +endif() +set(SDL_TEST_LIBRARY OFF CACHE INTERNAL "") +set(SDL_INSTALL OFF CACHE INTERNAL "") +set(SDL_DISABLE_INSTALL ON CACHE INTERNAL "") +set(SDL_CAMERA OFF CACHE INTERNAL "") +set(SDL_DIALOG OFF CACHE INTERNAL "") +set(SDL_GPU OFF CACHE INTERNAL "") +set(SDL_RENDER OFF CACHE INTERNAL "") +set(SDL_SENSOR OFF CACHE INTERNAL "") +set(SDL_HIDAPI_LIBUSB OFF CACHE INTERNAL "") + +# SDL3's sdlcommands.cmake uses file(GLOB CONFIGURE_DEPENDS ...) for its +# source lists. CMake honours that by emitting a "Re-checking globbed +# directories" probe on every build — slow and pointless for a submodule +# pinned to a release tag. Strip the flag in place at configure time; +# .gitmodules marks SDL3 as ignore=dirty so the in-tree edit doesn't pollute +# git status. Same pattern as snappy and zlib-ng. +set(_sdl3_cmd "${CMAKE_CURRENT_SOURCE_DIR}/SDL3/cmake/sdlcommands.cmake") +if(EXISTS "${_sdl3_cmd}") + file(READ "${_sdl3_cmd}" _sdl3_cmd_content) + string(REPLACE " CONFIGURE_DEPENDS " " " _sdl3_cmd_patched "${_sdl3_cmd_content}") + if(NOT "${_sdl3_cmd_patched}" STREQUAL "${_sdl3_cmd_content}") + file(WRITE "${_sdl3_cmd}" "${_sdl3_cmd_patched}") + endif() +endif() + +add_subdirectory(SDL3 EXCLUDE_FROM_ALL) +# Consistent target name for downstream link calls. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(SDL3 ALIAS SDL3-shared) +else() + add_library(SDL3 ALIAS SDL3-static) +endif() + +# SDL3 explicitly bakes /W3 into its target COMPILE_OPTIONS, which clashes +# with the directory-level /W0 above and produces D9025 noise on every TU. +# Strip the /W* and /WX flags it added so only the directory-level /W0 +# survives. Mirrors the same workaround we apply to SPIRV-Tools. +if(MSVC) + foreach(_sdl3_target SDL3-static SDL_uclibc) + if(TARGET ${_sdl3_target}) + get_target_property(_opts ${_sdl3_target} COMPILE_OPTIONS) + if(_opts) + list(FILTER _opts EXCLUDE REGEX "^/[Ww]([0-9]|all|X)$") + set_target_properties(${_sdl3_target} PROPERTIES COMPILE_OPTIONS "${_opts}") + endif() + endif() + endforeach() endif() # ============================================================================== diff --git a/third_party/SDL2 b/third_party/SDL2 deleted file mode 160000 index e11183ea6..000000000 --- a/third_party/SDL2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e11183ea6caa3ae4895f4bc54cad2bbb0e365417 diff --git a/third_party/SDL3 b/third_party/SDL3 new file mode 160000 index 000000000..d9d553670 --- /dev/null +++ b/third_party/SDL3 @@ -0,0 +1 @@ +Subproject commit d9d5536704d585616d4db3c8ba3c4ff6fc2757e1