diff --git a/app/app.pro b/app/app.pro index ff0269e65..8fb8a6a50 100644 --- a/app/app.pro +++ b/app/app.pro @@ -1,6 +1,8 @@ QT += core quick network quickcontrols2 svg CONFIG += c++11 +INCLUDEPATH += /usr/local/include/ + unix:!macx { TARGET = moonlight } else { @@ -66,7 +68,14 @@ macx { unix:!macx { CONFIG += link_pkgconfig - PKGCONFIG += openssl sdl2 SDL2_ttf opus + PKGCONFIG += openssl opus + packagesExist(sdl3) { + DEFINES += HAVE_SDL3 + PKGCONFIG += sdl3 sdl3-ttf + } + else { + PKGCONFIG += sdl2 SDL2_ttf + } !disable-ffmpeg { packagesExist(libavcodec) { diff --git a/app/backend/systemproperties.cpp b/app/backend/systemproperties.cpp index 4c1593ee8..bb11bec4a 100644 --- a/app/backend/systemproperties.cpp +++ b/app/backend/systemproperties.cpp @@ -132,14 +132,22 @@ void SystemProperties::querySdlVideoInfoInternal() // We call the internal variant because we're already in a safe thread context. refreshDisplaysInternal(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Window* testWindow = SDL_CreateWindow("", 1280, 720, +#else SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, +#endif SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags()); if (!testWindow) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to create test window with platform flags: %s", SDL_GetError()); +#if SDL_VERSION_ATLEAST(3, 0, 0) + testWindow = SDL_CreateWindow("", 1280, 720, SDL_WINDOW_HIDDEN); +#else testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN); +#endif if (!testWindow) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create window for hardware decode test: %s", @@ -197,7 +205,13 @@ void SystemProperties::refreshDisplaysInternal() monitorNativeResolutions.clear(); SDL_DisplayMode bestMode; +#if SDL_VERSION_ATLEAST(3, 0, 0) + int numDisplays = 0; + SDL_DisplayID *displays = SDL_GetDisplays(&numDisplays); + for (int displayIndex = 0; displayIndex < numDisplays; displayIndex++) { +#else for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) { +#endif SDL_DisplayMode desktopMode; if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) { @@ -212,6 +226,20 @@ void SystemProperties::refreshDisplaysInternal() // Start at desktop mode and work our way up bestMode = desktopMode; +#if SDL_VERSION_ATLEAST(3, 0, 0) + int numModes = 0; + const SDL_DisplayMode **modes = SDL_GetFullscreenDisplayModes(displayIndex, &numModes); + for (int i = 0; i < numModes; i++) { + if (modes[i]->w == desktopMode.w && modes[i]->h == desktopMode.h) { + if (modes[i]->refresh_rate > bestMode.refresh_rate) { + bestMode = *modes[i]; + } + } + } + if (modes) { + SDL_free(modes); + } +#else for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) { SDL_DisplayMode mode; if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) { @@ -222,6 +250,7 @@ void SystemProperties::refreshDisplaysInternal() } } } +#endif // Try to normalize values around our our standard refresh rates. // Some displays/OSes report values that are slightly off. @@ -236,6 +265,11 @@ void SystemProperties::refreshDisplaysInternal() } } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (displays) { + SDL_free((void*)displays); + } +#endif SDL_QuitSubSystem(SDL_INIT_VIDEO); } diff --git a/app/gui/sdlgamepadkeynavigation.cpp b/app/gui/sdlgamepadkeynavigation.cpp index 4f457d1e8..4a61f729f 100644 --- a/app/gui/sdlgamepadkeynavigation.cpp +++ b/app/gui/sdlgamepadkeynavigation.cpp @@ -36,12 +36,21 @@ void SdlGamepadKeyNavigation::enable() // arrival events. Additionally, there's a race condition between // our QML objects being destroyed and SDL being deinitialized that // this solves too. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_InitSubSystem(SDL_INIT_GAMEPAD) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_InitSubSystem(SDL_INIT_GAMEPAD) failed: %s", + SDL_GetError()); + return; + } +#else if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", SDL_GetError()); return; } +#endif MappingManager mappingManager; mappingManager.applyMappings(); @@ -51,6 +60,22 @@ void SdlGamepadKeyNavigation::enable() // overlapping lifetimes of SdlGamepadKeyNavigation instances, so we // will attach ourselves. SDL_PumpEvents(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FlushEvent(SDL_EVENT_GAMEPAD_ADDED); + + // Open all currently attached game controllers + int numGamepads = 0; + SDL_JoystickID *gamepads = SDL_GetGamepads(&numGamepads); + for (int i = 0; i < numGamepads; i++) { + SDL_Gamepad* gp = SDL_OpenGamepad(gamepads[i]); + if (gp != nullptr) { + m_Gamepads.append(gp); + } + } + if (gamepads) { + SDL_free((void*)gamepads); + } +#else SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED); // Open all currently attached game controllers @@ -62,6 +87,7 @@ void SdlGamepadKeyNavigation::enable() } } } +#endif // Flush events on the first poll m_FirstPoll = true; @@ -80,12 +106,21 @@ void SdlGamepadKeyNavigation::disable() m_PollingTimer->stop(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + while (!m_Gamepads.isEmpty()) { + SDL_CloseGamepad(m_Gamepads[0]); + m_Gamepads.removeAt(0); + } + + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); +#else while (!m_Gamepads.isEmpty()) { SDL_GameControllerClose(m_Gamepads[0]); m_Gamepads.removeAt(0); } SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); +#endif m_Enabled = false; } @@ -99,14 +134,120 @@ void SdlGamepadKeyNavigation::onPollingTimerFired() // stale input data from the stream session (like the quit combo). if (m_FirstPoll) { SDL_PumpEvents(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FlushEvent(SDL_EVENT_GAMEPAD_BUTTON_DOWN); + SDL_FlushEvent(SDL_EVENT_GAMEPAD_BUTTON_UP); +#else SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN); SDL_FlushEvent(SDL_CONTROLLERBUTTONUP); +#endif m_FirstPoll = false; } while (SDL_PollEvent(&event)) { switch (event.type) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_EVENT_QUIT: + // SDL may send us a quit event since we initialize + // the video subsystem on startup. If we get one, + // forward it on for Qt to take care of. + QCoreApplication::instance()->quit(); + break; + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + { + QEvent::Type type = + event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ? + QEvent::Type::KeyPress : QEvent::Type::KeyRelease; + + // Swap face buttons if needed + if (prefs.swapFaceButtons) { + switch (event.button.button) { + case SDL_GAMEPAD_BUTTON_SOUTH: + event.button.button = SDL_GAMEPAD_BUTTON_EAST; + break; + case SDL_GAMEPAD_BUTTON_EAST: + event.button.button = SDL_GAMEPAD_BUTTON_SOUTH; + break; + case SDL_GAMEPAD_BUTTON_WEST: + event.button.button = SDL_GAMEPAD_BUTTON_NORTH; + break; + case SDL_GAMEPAD_BUTTON_NORTH: + event.button.button = SDL_GAMEPAD_BUTTON_WEST; + break; + } + } + + switch (event.button.button) { + case SDL_GAMEPAD_BUTTON_DPAD_UP: + if (m_UiNavMode) { + // Back-tab + sendKey(type, Qt::Key_Tab, Qt::ShiftModifier); + } + else { + sendKey(type, Qt::Key_Up); + } + break; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + if (m_UiNavMode) { + sendKey(type, Qt::Key_Tab); + } + else { + sendKey(type, Qt::Key_Down); + } + break; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + sendKey(type, Qt::Key_Left); + break; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + sendKey(type, Qt::Key_Right); + break; + case SDL_GAMEPAD_BUTTON_SOUTH: + if (m_UiNavMode) { + sendKey(type, Qt::Key_Space); + } + else { + sendKey(type, Qt::Key_Return); + } + break; + case SDL_GAMEPAD_BUTTON_EAST: + sendKey(type, Qt::Key_Escape); + break; + case SDL_GAMEPAD_BUTTON_WEST: + sendKey(type, Qt::Key_Menu); + break; + case SDL_GAMEPAD_BUTTON_NORTH: + case SDL_GAMEPAD_BUTTON_START: + // HACK: We use this keycode to inform main.qml + // to show the settings when Key_Menu is handled + // by the control in focus. + sendKey(type, Qt::Key_Hangup); + break; + default: + break; + } + break; + } + case SDL_EVENT_GAMEPAD_ADDED: + SDL_Gamepad* gp = SDL_OpenGamepad(event.jdevice.which); + if (gp != nullptr) { + // SDL_EVENT_GAMEPAD_ADDED can be reported multiple times for the same + // gamepad in rare cases, because SDL doesn't fixup the device index in + // the SDL_EVENT_GAMEPAD_ADDED event if an unopened gamepad disappears + // before we've processed the add event. + if (!m_Gamepads.contains(gp)) { + m_Gamepads.append(gp); + } + else { + // We already have this game controller open + SDL_CloseGamepad(gp); + } + } + break; + } + } +#else case SDL_QUIT: // SDL may send us a quit event since we initialize // the video subsystem on startup. If we get one, @@ -206,11 +347,18 @@ void SdlGamepadKeyNavigation::onPollingTimerFired() break; } } +#endif // Handle analog sticks by polling +#if SDL_VERSION_ATLEAST(3, 0, 0) + for (auto gp : m_Gamepads) { + short leftX = SDL_GetGamepadAxis(gp, SDL_GAMEPAD_AXIS_LEFTX); + short leftY = SDL_GetGamepadAxis(gp, SDL_GAMEPAD_AXIS_LEFTY); +#else for (auto gc : m_Gamepads) { short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX); short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY); +#endif if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) { // Do nothing } @@ -272,11 +420,18 @@ int SdlGamepadKeyNavigation::getConnectedGamepads() Q_ASSERT(m_Enabled); int count = 0; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_JoystickID *gamepads = SDL_GetGamepads(&count); + if (gamepads) { + SDL_free((void*)gamepads); + } +#else for (int i = 0; i < SDL_NumJoysticks(); i++) { if (SDL_IsGameController(i)) { count++; } } +#endif return count; } diff --git a/app/gui/sdlgamepadkeynavigation.h b/app/gui/sdlgamepadkeynavigation.h index 4fcb1c32f..3b52afafb 100644 --- a/app/gui/sdlgamepadkeynavigation.h +++ b/app/gui/sdlgamepadkeynavigation.h @@ -3,7 +3,11 @@ #include #include +#if HAVE_SDL3 +#include +#else #include +#endif class SdlGamepadKeyNavigation : public QObject { @@ -30,7 +34,11 @@ private slots: private: QTimer* m_PollingTimer; +#if SDL_VERSION_ATLEAST(3, 0, 0) + QList m_Gamepads; +#else QList m_Gamepads; +#endif bool m_Enabled; bool m_UiNavMode; bool m_FirstPoll; diff --git a/app/main.cpp b/app/main.cpp index 67eac1d77..6734d9177 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -16,7 +16,12 @@ // doing the same thing. This needs to be before any headers // that might include SDL.h themselves. #define SDL_MAIN_HANDLED +#if HAVE_SDL3 +#include +#include +#else #include +#endif #ifdef HAVE_FFMPEG #include "streaming/video/ffmpeg.h" diff --git a/app/masterhook.c b/app/masterhook.c index 323c8ff67..321805809 100644 --- a/app/masterhook.c +++ b/app/masterhook.c @@ -15,7 +15,11 @@ // redirection that happens when _FILE_OFFSET_BITS=64! // See masterhook_internal.c for details. +#if HAVE_SDL3 +#include +#else #include +#endif #include #include #include diff --git a/app/masterhook_internal.c b/app/masterhook_internal.c index 5cc08b578..0cc05e049 100644 --- a/app/masterhook_internal.c +++ b/app/masterhook_internal.c @@ -4,7 +4,11 @@ #define _GNU_SOURCE +#if HAVE_SDL3 +#include +#else #include +#endif #include #include #include diff --git a/app/settings/mappingmanager.cpp b/app/settings/mappingmanager.cpp index be9a1a8d1..1f7889ab4 100644 --- a/app/settings/mappingmanager.cpp +++ b/app/settings/mappingmanager.cpp @@ -3,7 +3,11 @@ #include +#if HAVE_SDL3 +#include +#else #include +#endif #define SER_GAMEPADMAPPING "gcmapping" @@ -70,7 +74,11 @@ void MappingManager::applyMappings() { QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt"); if (!mappingData.isEmpty()) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + int newMappings = SDL_AddGamepadMappingsFromRW( +#else int newMappings = SDL_GameControllerAddMappingsFromRW( +#endif SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1); if (newMappings > 0) { @@ -101,7 +109,11 @@ void MappingManager::applyMappings() QList mappings = m_Mappings.values(); for (const SdlGamepadMapping& mapping : mappings) { QString sdlMappingString = mapping.getSdlMappingString(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + int ret = SDL_AddGamepadMapping(qPrintable(sdlMappingString)); +#else int ret = SDL_GameControllerAddMapping(qPrintable(sdlMappingString)); +#endif if (ret < 0) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to add mapping: %s", diff --git a/app/streaming/audio/audio.cpp b/app/streaming/audio/audio.cpp index 7b650b60f..696625f73 100644 --- a/app/streaming/audio/audio.cpp +++ b/app/streaming/audio/audio.cpp @@ -11,6 +11,11 @@ #include "renderers/sdl.h" +// SDL_TICKS_PASSED is removed in SDL3 +#if SDL_VERSION_ATLEAST(3, 0, 0) +#define SDL_TICKS_PASSED(A, B) ((Sint32)((B) - (A)) <= 0) +#endif + #include #define TRY_INIT_RENDERER(renderer, opusConfig) \ diff --git a/app/streaming/audio/renderers/sdl.h b/app/streaming/audio/renderers/sdl.h index 3e8234a19..823472e73 100644 --- a/app/streaming/audio/renderers/sdl.h +++ b/app/streaming/audio/renderers/sdl.h @@ -1,7 +1,11 @@ #pragma once #include "renderer.h" +#if HAVE_SDL3 +#include +#else #include +#endif class SdlAudioRenderer : public IAudioRenderer { @@ -19,7 +23,11 @@ class SdlAudioRenderer : public IAudioRenderer virtual int getCapabilities(); private: +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_AudioStream *m_AudioStream; +#else SDL_AudioDeviceID m_AudioDevice; +#endif void* m_AudioBuffer; int m_FrameSize; }; diff --git a/app/streaming/audio/renderers/sdlaud.cpp b/app/streaming/audio/renderers/sdlaud.cpp index 7d49bc1f6..cd8b27ccd 100644 --- a/app/streaming/audio/renderers/sdlaud.cpp +++ b/app/streaming/audio/renderers/sdlaud.cpp @@ -1,10 +1,18 @@ #include "sdl.h" #include +#if HAVE_SDL3 +#include +#else #include +#endif SdlAudioRenderer::SdlAudioRenderer() +#if SDL_VERSION_ATLEAST(3, 0, 0) + : m_AudioStream(nullptr), +#else : m_AudioDevice(0), +#endif m_AudioBuffer(nullptr) { SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO)); @@ -19,6 +27,38 @@ SdlAudioRenderer::SdlAudioRenderer() bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_AudioSpec spec; + + SDL_zero(spec); + spec.freq = opusConfig->sampleRate; + spec.format = SDL_AUDIO_S16LE; + spec.channels = opusConfig->channelCount; + + m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount; + + m_AudioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_OUTPUT, &spec, NULL, NULL); + if (!m_AudioStream) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to open audio device: %s", + SDL_GetError()); + return false; + } + + m_AudioBuffer = SDL_malloc(m_FrameSize); + if (m_AudioBuffer == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to allocate audio buffer"); + return false; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "SDL audio driver: %s", + SDL_GetCurrentAudioDriver()); + + // Start playback + SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(m_AudioStream)); +#else SDL_AudioSpec want, have; SDL_zero(want); @@ -73,17 +113,26 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* // Start playback SDL_PauseAudioDevice(m_AudioDevice, 0); +#endif return true; } SdlAudioRenderer::~SdlAudioRenderer() { - if (m_AudioDevice != 0) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_AudioStream) { + // Stop and destroy audio stream + SDL_PauseAudioDevice(SDL_GetAudioStreamDevice(m_AudioStream)); + SDL_DestroyAudioStream(m_AudioStream); + } +#else + if (m_AudioDevice) { // Stop playback SDL_PauseAudioDevice(m_AudioDevice, 1); SDL_CloseAudioDevice(m_AudioDevice); } +#endif if (m_AudioBuffer != nullptr) { SDL_free(m_AudioBuffer); @@ -102,7 +151,11 @@ bool SdlAudioRenderer::submitAudio(int bytesWritten) { // Our device may enter a permanent error status upon removal, so we need // to recreate the audio device to pick up the new default audio device. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_AudioDevicePaused(SDL_GetAudioStreamDevice(m_AudioStream))) { +#else if (SDL_GetAudioDeviceStatus(m_AudioDevice) == SDL_AUDIO_STOPPED) { +#endif return false; } @@ -119,11 +172,22 @@ bool SdlAudioRenderer::submitAudio(int bytesWritten) // Provide backpressure on the queue to ensure too many frames don't build up // in SDL's audio queue. +#if SDL_VERSION_ATLEAST(3, 0, 0) + // TODO: Check to see if this is correct and doing what SDL2 was doing. + while (SDL_GetAudioStreamQueued(m_AudioStream) / m_FrameSize > 10) { + SDL_Delay(1); + } +#else while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) { SDL_Delay(1); } +#endif +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_PutAudioStreamData(m_AudioStream, m_AudioBuffer, bytesWritten) < 0) { +#else if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) { +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to queue audio sample: %s", SDL_GetError()); diff --git a/app/streaming/audio/renderers/slaud.cpp b/app/streaming/audio/renderers/slaud.cpp index 4f8006d01..26de39d8f 100644 --- a/app/streaming/audio/renderers/slaud.cpp +++ b/app/streaming/audio/renderers/slaud.cpp @@ -1,6 +1,10 @@ #include "slaud.h" +#if HAVE_SDL3 +#include +#else #include +#endif SLAudioRenderer::SLAudioRenderer() : m_AudioContext(nullptr), diff --git a/app/streaming/audio/renderers/soundioaudiorenderer.cpp b/app/streaming/audio/renderers/soundioaudiorenderer.cpp index c29d01771..9efc9b5ca 100644 --- a/app/streaming/audio/renderers/soundioaudiorenderer.cpp +++ b/app/streaming/audio/renderers/soundioaudiorenderer.cpp @@ -1,6 +1,10 @@ #include "soundioaudiorenderer.h" +#if HAVE_SDL3 +#include +#else #include +#endif #include diff --git a/app/streaming/input/abstouch.cpp b/app/streaming/input/abstouch.cpp index fff12af3f..b26c1e6b2 100644 --- a/app/streaming/input/abstouch.cpp +++ b/app/streaming/input/abstouch.cpp @@ -1,8 +1,12 @@ #include "input.h" #include +#if HAVE_SDL3 +#include +#else #include #include +#endif #include "streaming/streamutils.h" #include @@ -30,13 +34,19 @@ Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*) void SdlInputHandler::disableTouchFeedback() { +#ifdef Q_OS_WIN32 +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0) { + HWND hwnd = (HWND)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); SDL_GetWindowWMInfo(m_Window, &info); -#ifdef Q_OS_WIN32 if (info.subsystem == SDL_SYSWM_WINDOWS) { + HWND hwnd = info.info.win.window; +#endif auto fnSetWindowFeedbackSetting = (decltype(SetWindowFeedbackSetting)*)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowFeedbackSetting"); if (fnSetWindowFeedbackSetting) { constexpr FEEDBACK_TYPE feedbackTypes[] = { @@ -55,7 +65,7 @@ void SdlInputHandler::disableTouchFeedback() for (FEEDBACK_TYPE ft : feedbackTypes) { BOOL val = FALSE; - fnSetWindowFeedbackSetting(info.info.win.window, ft, 0, sizeof(val), &val); + fnSetWindowFeedbackSetting(hwnd, ft, 0, sizeof(val), &val); } } } @@ -84,6 +94,17 @@ void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event) uint8_t eventType; switch (event->type) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_EVENT_FINGER_DOWN: + eventType = LI_TOUCH_EVENT_DOWN; + break; + case SDL_EVENT_FINGER_MOTION: + eventType = LI_TOUCH_EVENT_MOVE; + break; + case SDL_EVENT_FINGER_UP: + eventType = LI_TOUCH_EVENT_UP; + break; +#else case SDL_FINGERDOWN: eventType = LI_TOUCH_EVENT_DOWN; break; @@ -93,6 +114,7 @@ void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event) case SDL_FINGERUP: eventType = LI_TOUCH_EVENT_UP; break; +#endif default: return; } @@ -115,7 +137,23 @@ void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event) // Try to send it as a native pen/touch event, otherwise fall back to our touch emulation if (LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS) { bool isPen = false; +#if SDL_VERSION_ATLEAST(3, 0, 0) + int numTouchDevices = 0; + SDL_TouchID *touchId = SDL_GetTouchDevices(&numTouchDevices); + for (int i = 0; i < numTouchDevices; i++) { + if (event->touchId == touchId[i]) { + const char* touchName = SDL_GetTouchDeviceName(touchId[i]); + // SDL will report "pen" as the name of pen input devices on Windows. + // https://github.com/libsdl-org/SDL/pull/5926 + isPen = touchName && SDL_strcmp(touchName, "pen") == 0; + break; + } + } + if (touchId != nullptr) { + SDL_free((void*)touchId); + } +#else int numTouchDevices = SDL_GetNumTouchDevices(); for (int i = 0; i < numTouchDevices; i++) { if (event->touchId == SDL_GetTouchDevice(i)) { @@ -127,6 +165,7 @@ void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event) break; } } +#endif if (isPen) { LiSendPenEvent(eventType, LI_TOOL_TYPE_PEN, 0, vidrelx / dst.w, vidrely / dst.h, event->pressure, @@ -156,6 +195,17 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) // leave the client area during a drag motion. // dx and dy are deltas from the last touch event, not the first touch down. +#if SDL_VERSION_ATLEAST(3, 0, 0) + // Ignore touch down events with more than one finger + if (event->type == SDL_EVENT_FINGER_DOWN && SDL_GetNumTouchFingers(event->touchId) > 1) { + return; + } + + // Ignore touch move and touch up events from the non-primary finger + if (event->type != SDL_EVENT_FINGER_DOWN && event->fingerId != m_LastTouchDownEvent.fingerId) { + return; + } +#else // Ignore touch down events with more than one finger if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) { return; @@ -165,6 +215,7 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) { return; } +#endif SDL_Rect src, dst; int windowWidth, windowHeight; @@ -189,7 +240,11 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) } // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type != SDL_EVENT_FINGER_DOWN || +#else if (event->type != SDL_FINGERDOWN || +#endif event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY || qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) { // Scale window-relative events to be video-relative and clamp to video region @@ -200,7 +255,11 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h); } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type == SDL_EVENT_FINGER_DOWN) { +#else if (event->type == SDL_FINGERDOWN) { +#endif m_LastTouchDownEvent = *event; // Start/restart the long press timer @@ -212,7 +271,11 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) // Left button down on finger down LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); } +#if SDL_VERSION_ATLEAST(3, 0, 0) + else if (event->type == SDL_EVENT_FINGER_UP) { +#else else if (event->type == SDL_FINGERUP) { +#endif m_LastTouchUpEvent = *event; // Cancel the long press timer diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 458666a79..87b857d3f 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -1,7 +1,11 @@ #include "streaming/session.h" #include +#if HAVE_SDL3 +#include +#else #include +#endif #include "settings/mappingmanager.h" #include @@ -23,6 +27,11 @@ #define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17) #define ML_HAPTIC_GC_TRIGGER_RUMBLE (1U << 18) +// SDL_TICKS_PASSED is removed in SDL3 +#if SDL_VERSION_ATLEAST(3, 0, 0) +#define SDL_TICKS_PASSED(A, B) ((Sint32)((B) - (A)) <= 0) +#endif + const int SdlInputHandler::k_ButtonMap[] = { A_FLAG, B_FLAG, X_FLAG, Y_FLAG, BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG, @@ -188,6 +197,71 @@ Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param return interval; } +#if SDL_VERSION_ATLEAST(3, 0, 0) +void SdlInputHandler::handleControllerAxisEvent(SDL_GamepadAxisEvent* event) +{ + SDL_JoystickID gameControllerId = event->which; + GamepadState* state = findStateForGamepad(gameControllerId); + if (state == NULL) { + return; + } + + // Batch all pending axis motion events for this gamepad to save CPU time + SDL_Event nextEvent; + for (;;) { + switch (event->axis) + { + case SDL_GAMEPAD_AXIS_LEFTX: + state->lsX = event->value; + break; + case SDL_GAMEPAD_AXIS_LEFTY: + // Signed values have one more negative value than + // positive value, so inverting the sign on -32768 + // could actually cause the value to overflow and + // wrap around to be negative again. Avoid that by + // capping the value at 32767. + state->lsY = -qMax(event->value, (short)-32767); + break; + case SDL_GAMEPAD_AXIS_RIGHTX: + state->rsX = event->value; + break; + case SDL_GAMEPAD_AXIS_RIGHTY: + state->rsY = -qMax(event->value, (short)-32767); + break; + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + state->lt = (unsigned char)(event->value * 255UL / 32767); + break; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + state->rt = (unsigned char)(event->value * 255UL / 32767); + break; + default: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unhandled controller axis: %d", + event->axis); + return; + } + + // Check for another event to batch with + if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_EVENT_GAMEPAD_AXIS_MOTION) <= 0) { + break; + } + + event = &nextEvent.gaxis; + if (event->which != gameControllerId) { + // Stop batching if a different gamepad interrupts us + break; + } + + // Remove the next event to batch + SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_EVENT_GAMEPAD_AXIS_MOTION); + } + + // Only send the gamepad state to the host if it's not in mouse emulation mode + if (state->mouseEmulationTimer == 0) { + sendGamepadState(state); + } +} +#else void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) { SDL_JoystickID gameControllerId = event->which; @@ -251,8 +325,13 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) sendGamepadState(state); } } +#endif +#if SDL_VERSION_ATLEAST(3, 0, 0) +void SdlInputHandler::handleControllerButtonEvent(SDL_GamepadButtonEvent* event) +#else void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event) +#endif { if (event->button >= SDL_arraysize(k_ButtonMap)) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, @@ -266,6 +345,104 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve return; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_SwapFaceButtons) { + switch (event->button) { + case SDL_GAMEPAD_BUTTON_SOUTH: + event->button = SDL_GAMEPAD_BUTTON_EAST; + break; + case SDL_GAMEPAD_BUTTON_EAST: + event->button = SDL_GAMEPAD_BUTTON_SOUTH; + break; + case SDL_GAMEPAD_BUTTON_WEST: + event->button = SDL_GAMEPAD_BUTTON_NORTH; + break; + case SDL_GAMEPAD_BUTTON_NORTH: + event->button = SDL_GAMEPAD_BUTTON_WEST; + break; + } + } + + if (event->state == SDL_PRESSED) { + state->buttons |= k_ButtonMap[event->button]; + + if (event->button == SDL_GAMEPAD_BUTTON_START) { + state->lastStartDownTime = SDL_GetTicks(); + } + else if (state->mouseEmulationTimer != 0) { + if (event->button == SDL_GAMEPAD_BUTTON_SOUTH) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } + else if (event->button == SDL_GAMEPAD_BUTTON_EAST) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + } + else if (event->button == SDL_GAMEPAD_BUTTON_WEST) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); + } + else if (event->button == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); + } + else if (event->button == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); + } + else if (event->button == SDL_GAMEPAD_BUTTON_DPAD_UP) { + LiSendScrollEvent(1); + } + else if (event->button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) { + LiSendScrollEvent(-1); + } + else if (event->button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) { + LiSendHScrollEvent(1); + } + else if (event->button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) { + LiSendHScrollEvent(-1); + } + } + } + else { + state->buttons &= ~k_ButtonMap[event->button]; + + if (event->button == SDL_GAMEPAD_BUTTON_START) { + if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) { + if (state->mouseEmulationTimer != 0) { + SDL_RemoveTimer(state->mouseEmulationTimer); + state->mouseEmulationTimer = 0; + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Mouse emulation deactivated"); + Session::get()->notifyMouseEmulationMode(false); + } + else if (m_GamepadMouse) { + // Send the start button up event to the host, since we won't do it below + sendGamepadState(state); + + state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Mouse emulation active"); + Session::get()->notifyMouseEmulationMode(true); + } + } + } + else if (state->mouseEmulationTimer != 0) { + if (event->button == SDL_GAMEPAD_BUTTON_SOUTH) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } + else if (event->button == SDL_GAMEPAD_BUTTON_EAST) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } + else if (event->button == SDL_GAMEPAD_BUTTON_WEST) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + } + else if (event->button == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + } + else if (event->button == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); + } + } + } +#else if (m_SwapFaceButtons) { switch (event->button) { case SDL_CONTROLLER_BUTTON_A: @@ -362,6 +539,7 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve } } } +#endif // Handle Start+Select+L1+R1 as a gamepad quit combo if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG) && qgetenv("NO_GAMEPAD_QUIT") != "1") { @@ -370,7 +548,11 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve // Push a quit event to the main loop SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_QUIT; +#else event.type = SDL_QUIT; +#endif event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); @@ -403,7 +585,11 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve #if SDL_VERSION_ATLEAST(2, 0, 14) +#if SDL_VERSION_ATLEAST(3, 0, 0) +void SdlInputHandler::handleControllerSensorEvent(SDL_GamepadSensorEvent* event) +#else void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* event) +#endif { GamepadState* state = findStateForGamepad(event->which); if (state == NULL) { @@ -438,6 +624,32 @@ void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* eve } } +#if SDL_VERSION_ATLEAST(3, 0, 0) +void SdlInputHandler::handleControllerTouchpadEvent(SDL_GamepadTouchpadEvent* event) +{ + GamepadState* state = findStateForGamepad(event->which); + if (state == NULL) { + return; + } + + uint8_t eventType; + switch (event->type) { + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + eventType = LI_TOUCH_EVENT_DOWN; + break; + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + eventType = LI_TOUCH_EVENT_UP; + break; + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + eventType = LI_TOUCH_EVENT_MOVE; + break; + default: + return; + } + + LiSendControllerTouchEvent((uint8_t)state->index, eventType, event->finger, event->x, event->y, event->pressure); +} +#else void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* event) { GamepadState* state = findStateForGamepad(event->which); @@ -462,6 +674,7 @@ void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* LiSendControllerTouchEvent((uint8_t)state->index, eventType, event->finger, event->x, event->y, event->pressure); } +#endif #endif @@ -479,6 +692,254 @@ void SdlInputHandler::handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event) #endif +#if SDL_VERSION_ATLEAST(3, 0, 0) +void SdlInputHandler::handleControllerDeviceEvent(SDL_GamepadDeviceEvent* event) +{ + GamepadState* state; + + if (event->type == SDL_EVENT_GAMEPAD_ADDED) { + int i; + const char* name; + SDL_Gamepad* controller; + const char* mapping; + char guidStr[33]; + uint32_t hapticCaps; + + controller = SDL_OpenGamepad(event->which); + if (controller == NULL) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to open gamepad: %s", + SDL_GetError()); + return; + } + + // SDL_EVENT_GAMEPAD_ADDED can be reported multiple times for the same + // gamepad in rare cases, because SDL doesn't fixup the device index in + // the SDL_EVENT_GAMEPAD_ADDED event if an unopened gamepad disappears + // before we've processed the add event. + for (int i = 0; i < MAX_GAMEPADS; i++) { + if (m_GamepadState[i].controller == controller) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Received duplicate add event for controller index: %d", + event->which); + SDL_CloseGamepad(controller); + return; + } + } + + // We used to use SDL_GetGamepadPlayerIndex() here but that + // can lead to strange issues due to bugs in Windows where an Xbox + // controller will join as player 2, even though no player 1 controller + // is connected at all. This pretty much screws any attempt to use + // the gamepad in single player games, so just assign them in order from 0. + i = 0; + + for (; i < MAX_GAMEPADS; i++) { + SDL_assert(m_GamepadState[i].controller != controller); + if (m_GamepadState[i].controller == NULL) { + // Found an empty slot + break; + } + } + + if (i == MAX_GAMEPADS) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "No open gamepad slots found!"); + SDL_CloseGamepad(controller); + return; + } + + SDL_GetJoystickGUIDString(SDL_GetJoystickGUID(SDL_GetGamepadJoystick(controller)), + guidStr, sizeof(guidStr)); + if (m_IgnoreDeviceGuids.contains(guidStr, Qt::CaseInsensitive)) + { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Skipping ignored device with GUID: %s", + guidStr); + SDL_CloseGamepad(controller); + return; + } + + state = &m_GamepadState[i]; + if (m_MultiController) { + state->index = i; + + // This will change indicators on the controller to show the assigned + // player index. For Xbox 360 controllers, that means updating the LED + // ring to light up the corresponding quadrant for this player. + SDL_SetGamepadPlayerIndex(controller, state->index); + } + else { + // Always player 1 in single controller mode + state->index = 0; + } + + state->controller = controller; + state->jsId = SDL_GetJoystickInstanceID(SDL_GetGamepadJoystick(state->controller)); + + SDL_PropertiesID propertiesId = SDL_GetGamepadProperties(state->controller); + hapticCaps = 0; + hapticCaps |= SDL_GetBooleanProperty(propertiesId, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false) ? ML_HAPTIC_GC_RUMBLE : 0; + hapticCaps |= SDL_GetBooleanProperty(propertiesId, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false) ? ML_HAPTIC_GC_TRIGGER_RUMBLE : 0; + + mapping = SDL_GetGamepadMapping(state->controller); + name = SDL_GetGamepadName(state->controller); + + uint16_t vendorId = SDL_GetGamepadVendor(state->controller); + uint16_t productId = SDL_GetGamepadProduct(state->controller); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Gamepad %d (player %d) is: %s (VID/PID: 0x%.4x/0x%.4x) (haptic capabilities: 0x%x) (mapping: %s -> %s)", + i, + state->index, + name != nullptr ? name : "", + vendorId, + productId, + hapticCaps, + guidStr, + mapping != nullptr ? mapping : ""); + if (mapping != nullptr) { + SDL_free((void*)mapping); + } + + // Add this gamepad to the gamepad mask + if (m_MultiController) { + // NB: Don't assert that it's unset here because we will already + // have the mask set for initially attached gamepads to avoid confusing + // apps running on the host. + m_GamepadMask |= (1 << state->index); + } + else { + SDL_assert(m_GamepadMask == 0x1); + } + + SDL_JoystickPowerLevel powerLevel = SDL_GetJoystickPowerLevel(SDL_GetGamepadJoystick(state->controller)); + + // On SDL 2.0.14 and later, we can provide enhanced controller information to the host PC + // for it to use as a hint for the type of controller to emulate. + uint32_t supportedButtonFlags = 0; + for (int i = 0; i < (int)SDL_arraysize(k_ButtonMap); i++) { + if (SDL_GamepadHasButton(state->controller, (SDL_GamepadButton)i)) { + supportedButtonFlags |= k_ButtonMap[i]; + } + } + + int numBindings = 0; + SDL_GamepadBinding **bindings = SDL_GetGamepadBindings(state->controller, &numBindings); + bool analogTriggers = false; + bool touchpadUnmapped = false; + for (int i = 0; i < numBindings; i++) { + // We assume these are analog triggers if the binding is to an axis rather than a button + if (!analogTriggers) { + if (bindings[i]->outputType == SDL_GAMEPAD_BINDTYPE_AXIS && bindings[i]->output.axis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { + analogTriggers = true; + continue; + } + else if (bindings[i]->outputType == SDL_GAMEPAD_BINDTYPE_AXIS && bindings[i]->output.axis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + analogTriggers = true; + continue; + } + } + // Get a touchpad that is unmapped + if (bindings[i]->outputType == SDL_GAMEPAD_BINDTYPE_NONE && bindings[i]->output.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) { + touchpadUnmapped = true; + } + } + if (bindings != nullptr) { + SDL_free((void*)bindings); + } + + uint32_t capabilities = 0; + + capabilities |= analogTriggers ? LI_CCAP_ANALOG_TRIGGERS : 0; + + if (hapticCaps & ML_HAPTIC_GC_RUMBLE) { + capabilities |= LI_CCAP_RUMBLE; + } + if (hapticCaps & ML_HAPTIC_GC_TRIGGER_RUMBLE) { + capabilities |= LI_CCAP_TRIGGER_RUMBLE; + } + if (SDL_GetNumGamepadTouchpads(state->controller) > 0) { + capabilities |= LI_CCAP_TOUCHPAD; + } + if (SDL_GamepadHasSensor(state->controller, SDL_SENSOR_ACCEL)) { + capabilities |= LI_CCAP_ACCEL; + } + if (SDL_GamepadHasSensor(state->controller, SDL_SENSOR_GYRO)) { + capabilities |= LI_CCAP_GYRO; + } + if (powerLevel != SDL_JOYSTICK_POWER_UNKNOWN) { + capabilities |= LI_CCAP_BATTERY_STATE; + } + if (SDL_GetBooleanProperty(propertiesId, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false)) { + capabilities |= LI_CCAP_RGB_LED; + } + + uint8_t type; + switch (SDL_GetGamepadType(state->controller)) { + case SDL_GAMEPAD_TYPE_XBOX360: + case SDL_GAMEPAD_TYPE_XBOXONE: + type = LI_CTYPE_XBOX; + break; + case SDL_GAMEPAD_TYPE_PS3: + case SDL_GAMEPAD_TYPE_PS4: + case SDL_GAMEPAD_TYPE_PS5: + type = LI_CTYPE_PS; + break; + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO: + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: + case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: + type = LI_CTYPE_NINTENDO; + break; + default: + type = LI_CTYPE_UNKNOWN; + break; + } + + // If this is a PlayStation controller that doesn't have a touchpad button mapped, + // we'll allow the Select+PS button combo to act as the touchpad. + state->clickpadButtonEmulationEnabled = touchpadUnmapped && type == LI_CTYPE_PS; + + LiSendControllerArrivalEvent(state->index, m_GamepadMask, type, supportedButtonFlags, capabilities); + + // Send a power level if it's known at this time + if (powerLevel != SDL_JOYSTICK_POWER_UNKNOWN) { + sendGamepadBatteryState(state, powerLevel); + } + } + else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) { + state = findStateForGamepad(event->which); + if (state != NULL) { + if (state->mouseEmulationTimer != 0) { + Session::get()->notifyMouseEmulationMode(false); + SDL_RemoveTimer(state->mouseEmulationTimer); + } + + SDL_CloseGamepad(state->controller); + + // Remove this from the gamepad mask in MC-mode + if (m_MultiController) { + SDL_assert(m_GamepadMask & (1 << state->index)); + m_GamepadMask &= ~(1 << state->index); + } + else { + SDL_assert(m_GamepadMask == 0x1); + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Gamepad %d is gone", + state->index); + + // Send a final event to let the PC know this gamepad is gone + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + + // Clear all remaining state from this slot + SDL_memset(state, 0, sizeof(*state)); + } + } +} +#else void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) { GamepadState* state; @@ -758,9 +1219,37 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve } } } +#endif void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_assert(event->type == SDL_EVENT_JOYSTICK_ADDED); + + if (!SDL_IsGamepad(event->which)) { + char guidStr[33]; + SDL_GetJoystickGUIDString(SDL_GetJoystickInstanceGUID(event->which), + guidStr, sizeof(guidStr)); + const char* name = SDL_GetJoystickInstanceName(event->which); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick discovered with no mapping: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_OpenJoystick(event->which); + if (joy != nullptr) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + SDL_GetNumJoystickAxes(joy), SDL_GetNumJoystickButtons(joy), + SDL_GetNumJoystickHats(joy)); + SDL_CloseJoystick(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } + } +#else SDL_assert(event->type == SDL_JOYDEVICEADDED); if (!SDL_IsGameController(event->which)) { @@ -786,6 +1275,7 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) SDL_GetError()); } } +#endif } void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) @@ -795,7 +1285,11 @@ void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short low return; } -#if SDL_VERSION_ATLEAST(2, 0, 9) +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_GamepadState[controllerNumber].controller != nullptr) { + SDL_RumbleGamepad(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000); + } +#elif SDL_VERSION_ATLEAST(2, 0, 9) if (m_GamepadState[controllerNumber].controller != nullptr) { SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000); } @@ -853,7 +1347,11 @@ void SdlInputHandler::rumbleTriggers(uint16_t controllerNumber, uint16_t leftTri return; } -#if SDL_VERSION_ATLEAST(2, 0, 14) +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_GamepadState[controllerNumber].controller != nullptr) { + SDL_RumbleGamepadTriggers(m_GamepadState[controllerNumber].controller, leftTrigger, rightTrigger, 30000); + } +#elif SDL_VERSION_ATLEAST(2, 0, 14) if (m_GamepadState[controllerNumber].controller != nullptr) { SDL_GameControllerRumbleTriggers(m_GamepadState[controllerNumber].controller, leftTrigger, rightTrigger, 30000); } @@ -867,7 +1365,23 @@ void SdlInputHandler::setMotionEventState(uint16_t controllerNumber, uint8_t mot return; } -#if SDL_VERSION_ATLEAST(2, 0, 14) +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_GamepadState[controllerNumber].controller != nullptr) { + uint8_t reportPeriodMs = reportRateHz ? (1000 / reportRateHz) : 0; + + switch (motionType) { + case LI_MOTION_TYPE_ACCEL: + m_GamepadState[controllerNumber].accelReportPeriodMs = reportPeriodMs; + SDL_SetGamepadSensorEnabled(m_GamepadState[controllerNumber].controller, SDL_SENSOR_ACCEL, reportRateHz ? SDL_TRUE : SDL_FALSE); + break; + + case LI_MOTION_TYPE_GYRO: + m_GamepadState[controllerNumber].gyroReportPeriodMs = reportPeriodMs; + SDL_SetGamepadSensorEnabled(m_GamepadState[controllerNumber].controller, SDL_SENSOR_GYRO, reportRateHz ? SDL_TRUE : SDL_FALSE); + break; + } + } +#elif SDL_VERSION_ATLEAST(2, 0, 14) if (m_GamepadState[controllerNumber].controller != nullptr) { uint8_t reportPeriodMs = reportRateHz ? (1000 / reportRateHz) : 0; @@ -893,7 +1407,11 @@ void SdlInputHandler::setControllerLED(uint16_t controllerNumber, uint8_t r, uin return; } -#if SDL_VERSION_ATLEAST(2, 0, 14) +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_GamepadState[controllerNumber].controller != nullptr) { + SDL_SetGamepadLED(m_GamepadState[controllerNumber].controller, r, g, b); + } +#elif SDL_VERSION_ATLEAST(2, 0, 14) if (m_GamepadState[controllerNumber].controller != nullptr) { SDL_GameControllerSetLED(m_GamepadState[controllerNumber].controller, r, g, b); } @@ -904,6 +1422,63 @@ QString SdlInputHandler::getUnmappedGamepads() { QString ret; +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_InitSubSystem(SDL_INIT_GAMEPAD) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_InitSubSystem(SDL_INIT_GAMEPAD) failed: %s", + SDL_GetError()); + } + + MappingManager mappingManager; + mappingManager.applyMappings(); + + int numJoysticks = 0; + SDL_JoystickID *joysticks = SDL_GetJoysticks(&numJoysticks); + for (int i = 0; i < numJoysticks; i++) { + if (!SDL_IsGamepad(i)) { + char guidStr[33]; + SDL_GetJoystickGUIDString(SDL_GetJoystickInstanceGUID(i), + guidStr, sizeof(guidStr)); + const char* name = SDL_GetJoystickInstanceName(i); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unmapped joystick: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_OpenJoystick(i); + if (joy != nullptr) { + int numButtons = SDL_GetNumJoystickButtons(joy); + int numHats = SDL_GetNumJoystickHats(joy); + int numAxes = SDL_GetNumJoystickAxes(joy); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + numAxes, numButtons, numHats); + + if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick likely to be an unmapped gamepad"); + if (!ret.isEmpty()) { + ret += ", "; + } + + ret += name; + } + + SDL_CloseJoystick(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } + } + } + + if (joysticks) { + SDL_free((void*)joysticks); + } + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); +#else if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", @@ -954,6 +1529,7 @@ QString SdlInputHandler::getUnmappedGamepads() } SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); +#endif return ret; } @@ -968,6 +1544,26 @@ int SdlInputHandler::getAttachedGamepadMask() return 0x1; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + count = mask = 0; + int numJoysticks = 0; + SDL_JoystickID *joysticks = SDL_GetJoysticks(&numJoysticks); + for (int i = 0; i < numJoysticks; i++) { + if (SDL_IsGamepad(i)) { + char guidStr[33]; + SDL_GetJoystickGUIDString(SDL_GetJoystickInstanceGUID(i), + guidStr, sizeof(guidStr)); + + if (!m_IgnoreDeviceGuids.contains(guidStr, Qt::CaseInsensitive)) + { + mask |= (1 << count++); + } + } + } + if (joysticks) { + SDL_free((void*)joysticks); + } +#else count = mask = 0; for (int i = 0; i < SDL_NumJoysticks(); i++) { if (SDL_IsGameController(i)) { @@ -981,6 +1577,7 @@ int SdlInputHandler::getAttachedGamepadMask() } } } +#endif return mask; } diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index fb11938bb..5685ae5f3 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -1,5 +1,9 @@ #include +#if HAVE_SDL3 +#include +#else #include +#endif #include "streaming/session.h" #include "settings/mappingmanager.h" #include "path.h" @@ -21,7 +25,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_PointerRegionLockToggledByUser(false), m_FakeCaptureActive(false), m_CaptureSystemKeysMode(prefs.captureSysKeysMode), - m_MouseCursorCapturedVisibilityState(SDL_DISABLE), + m_MouseCursorCapturedVisibilityState(0), m_LongPressTimer(0), m_StreamWidth(streamWidth), m_StreamHeight(streamHeight), @@ -168,6 +172,19 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i // Flush gamepad arrival and departure events which may be queued before // starting the gamecontroller subsystem again. This prevents us from // receiving duplicate arrival and departure events for the same gamepad. +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FlushEvent(SDL_EVENT_GAMEPAD_ADDED); + SDL_FlushEvent(SDL_EVENT_GAMEPAD_REMOVED); + + // We need to reinit this each time, since you only get + // an initial set of gamepad arrival events once per init. + SDL_assert(!SDL_WasInit(SDL_INIT_GAMEPAD)); + if (SDL_InitSubSystem(SDL_INIT_GAMEPAD) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_InitSubSystem(SDL_INIT_GAMEPAD) failed: %s", + SDL_GetError()); + } +#else SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED); SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED); @@ -179,6 +196,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", SDL_GetError()); } +#endif #if !SDL_VERSION_ATLEAST(2, 0, 9) SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC)); @@ -212,9 +230,15 @@ SdlInputHandler::~SdlInputHandler() SDL_HapticClose(m_GamepadState[i].haptic); } #endif +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_GamepadState[i].controller != nullptr) { + SDL_CloseGamepad(m_GamepadState[i].controller); + } +#else if (m_GamepadState[i].controller != nullptr) { SDL_GameControllerClose(m_GamepadState[i].controller); } +#endif } SDL_RemoveTimer(m_LongPressTimer); @@ -227,8 +251,13 @@ SdlInputHandler::~SdlInputHandler() SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC)); #endif +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); + SDL_assert(!SDL_WasInit(SDL_INIT_GAMEPAD)); +#else SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); +#endif SDL_QuitSubSystem(SDL_INIT_JOYSTICK); SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK)); @@ -376,13 +405,21 @@ void SdlInputHandler::setCaptureActive(bool active) // If we're in relative mode, try to activate SDL's relative mouse mode if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) { // Relative mouse mode didn't work or was disabled, so we'll just hide the cursor +#if SDL_VERSION_ATLEAST(3, 0, 0) + m_MouseCursorCapturedVisibilityState ? SDL_ShowCursor() : SDL_HideCursor(); +#else SDL_ShowCursor(m_MouseCursorCapturedVisibilityState); +#endif m_FakeCaptureActive = true; } // Synchronize the client and host cursor when activating absolute capture if (m_AbsoluteMouseMode) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + float mouseX, mouseY; +#else int mouseX, mouseY; +#endif int windowX, windowY; // We have to use SDL_GetGlobalMouseState() because macOS may not reflect @@ -391,13 +428,22 @@ void SdlInputHandler::setCaptureActive(bool active) // Convert global mouse state to window-relative SDL_GetWindowPosition(m_Window, &windowX, &windowY); +#if SDL_VERSION_ATLEAST(3, 0, 0) + mouseX -= (float)windowX; + mouseY -= (float)windowY; +#else mouseX -= windowX; mouseY -= windowY; +#endif - if (isMouseInVideoRegion(mouseX, mouseY)) { + if (isMouseInVideoRegion((int)mouseX, (int)mouseY)) { // Synthesize a mouse event to synchronize the cursor SDL_MouseMotionEvent motionEvent = {}; +#if SDL_VERSION_ATLEAST(3, 0, 0) + motionEvent.type = SDL_EVENT_MOUSE_MOTION; +#else motionEvent.type = SDL_MOUSEMOTION; +#endif motionEvent.timestamp = SDL_GetTicks(); motionEvent.windowID = SDL_GetWindowID(m_Window); motionEvent.x = mouseX; @@ -409,7 +455,11 @@ void SdlInputHandler::setCaptureActive(bool active) else { if (m_FakeCaptureActive) { // Display the cursor again +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_ShowCursor(); +#else SDL_ShowCursor(SDL_ENABLE); +#endif m_FakeCaptureActive = false; } else { diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index ec24070ad..108b96f92 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -3,10 +3,18 @@ #include "settings/streamingpreferences.h" #include "backend/computermanager.h" +#if HAVE_SDL3 +#include +#else #include +#endif struct GamepadState { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Gamepad* controller; +#else SDL_GameController* controller; +#endif SDL_JoystickID jsId; short index; @@ -24,11 +32,19 @@ struct GamepadState { #if SDL_VERSION_ATLEAST(2, 0, 14) uint8_t gyroReportPeriodMs; +#if SDL_VERSION_ATLEAST(3, 0, 0) + float lastGyroEventData[SDL_arraysize(SDL_GamepadSensorEvent::data)]; +#else float lastGyroEventData[SDL_arraysize(SDL_ControllerSensorEvent::data)]; +#endif uint32_t lastGyroEventTime; uint8_t accelReportPeriodMs; +#if SDL_VERSION_ATLEAST(3, 0, 0) + float lastAccelEventData[SDL_arraysize(SDL_GamepadSensorEvent::data)]; +#else float lastAccelEventData[SDL_arraysize(SDL_ControllerSensorEvent::data)]; +#endif uint32_t lastAccelEventTime; #endif @@ -67,6 +83,17 @@ class SdlInputHandler void handleMouseWheelEvent(SDL_MouseWheelEvent* event); +#if SDL_VERSION_ATLEAST(3, 0, 0) + void handleControllerAxisEvent(SDL_GamepadAxisEvent* event); + + void handleControllerButtonEvent(SDL_GamepadButtonEvent* event); + + void handleControllerDeviceEvent(SDL_GamepadDeviceEvent* event); + + void handleControllerSensorEvent(SDL_GamepadSensorEvent* event); + + void handleControllerTouchpadEvent(SDL_GamepadTouchpadEvent* event); +#else void handleControllerAxisEvent(SDL_ControllerAxisEvent* event); void handleControllerButtonEvent(SDL_ControllerButtonEvent* event); @@ -79,6 +106,8 @@ class SdlInputHandler void handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* event); #endif +#endif + #if SDL_VERSION_ATLEAST(2, 24, 0) void handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event); #endif diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index 5c6983cf0..fd18c2f46 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -1,7 +1,11 @@ #include "streaming/session.h" #include +#if HAVE_SDL3 +#include +#else #include +#endif #define VK_0 0x30 #define VK_A 0x41 @@ -22,7 +26,11 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) // Push a quit event to the main loop SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_QUIT; +#else event.type = SDL_QUIT; +#endif event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); break; @@ -78,7 +86,11 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) if (!SDL_GetRelativeMouseMode()) { m_MouseCursorCapturedVisibilityState = !m_MouseCursorCapturedVisibilityState; +#if SDL_VERSION_ATLEAST(3, 0, 0) + m_MouseCursorCapturedVisibilityState ? SDL_ShowCursor() : SDL_HideCursor(); +#else SDL_ShowCursor(m_MouseCursorCapturedVisibilityState); +#endif } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, @@ -157,9 +169,15 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) // Check for our special key combos if ((event->state == SDL_PRESSED) && +#if SDL_VERSION_ATLEAST(3, 0, 0) + (event->keysym.mod & SDL_KMOD_CTRL) && + (event->keysym.mod & SDL_KMOD_ALT) && + (event->keysym.mod & SDL_KMOD_SHIFT)) { +#else (event->keysym.mod & KMOD_CTRL) && (event->keysym.mod & KMOD_ALT) && (event->keysym.mod & KMOD_SHIFT)) { +#endif // First we test the SDLK combos for matches, // that way we ensure that latin keyboard users // can match to the key they see on their keyboards. @@ -188,6 +206,22 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) // Set modifier flags modifiers = 0; +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->keysym.mod & SDL_KMOD_CTRL) { + modifiers |= MODIFIER_CTRL; + } + if (event->keysym.mod & SDL_KMOD_ALT) { + modifiers |= MODIFIER_ALT; + } + if (event->keysym.mod & SDL_KMOD_SHIFT) { + modifiers |= MODIFIER_SHIFT; + } + if (event->keysym.mod & SDL_KMOD_GUI) { + if (isSystemKeyCaptureActive()) { + modifiers |= MODIFIER_META; + } + } +#else if (event->keysym.mod & KMOD_CTRL) { modifiers |= MODIFIER_CTRL; } @@ -202,6 +236,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) modifiers |= MODIFIER_META; } } +#endif // Set keycode. We explicitly use scancode here because GFE will try to correct // for AZERTY layouts on the host but it depends on receiving VK_ values matching diff --git a/app/streaming/input/mouse.cpp b/app/streaming/input/mouse.cpp index 5e90e2dcc..4eaeba28f 100644 --- a/app/streaming/input/mouse.cpp +++ b/app/streaming/input/mouse.cpp @@ -1,7 +1,11 @@ #include "input.h" #include +#if HAVE_SDL3 +#include +#else #include +#endif #include "streaming/streamutils.h" void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) @@ -97,14 +101,14 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) // Use the stream and window sizes to determine the video region StreamUtils::scaleSourceToDestinationSurface(&src, &dst); - mouseInVideoRegion = isMouseInVideoRegion(event->x, - event->y, + mouseInVideoRegion = isMouseInVideoRegion((int)event->x, + (int)event->y, windowWidth, windowHeight); // Clamp motion to the video region - short x = qMin(qMax(event->x - dst.x, 0), dst.w); - short y = qMin(qMax(event->y - dst.y, 0), dst.h); + short x = qMin(qMax((int)event->x - dst.x, 0), dst.w); + short y = qMin(qMax((int)event->y - dst.y, 0), dst.h); // Send the mouse position update if one of the following is true: // a) it is in the video region now @@ -124,7 +128,11 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) // Adjust the cursor visibility if applicable if (mouseInVideoRegion ^ m_MouseWasInVideoRegion) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + mouseInVideoRegion && !m_MouseCursorCapturedVisibilityState ? SDL_HideCursor() : SDL_ShowCursor(); +#else SDL_ShowCursor((mouseInVideoRegion && m_MouseCursorCapturedVisibilityState == SDL_DISABLE) ? SDL_DISABLE : SDL_ENABLE); +#endif if (!mouseInVideoRegion && buttonState != 0) { // If we still have a button pressed on leave, wait for that to come up // before we stop sending mouse position events. @@ -151,15 +159,49 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) } if (m_AbsoluteMouseMode) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + float mouseX, mouseY; + SDL_GetMouseState(&mouseX, &mouseY); +#else int mouseX, mouseY; SDL_GetMouseState(&mouseX, &mouseY); - if (!isMouseInVideoRegion(mouseX, mouseY)) { +#endif + if (!isMouseInVideoRegion((int)mouseX, (int)mouseY)) { // Ignore scroll events outside the video region return; } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->mouseY != 0.0f) { + // Invert the scroll direction if needed + if (m_ReverseScrollDirection) { + event->mouseY = -event->mouseY; + } -#if SDL_VERSION_ATLEAST(2, 0, 18) +#ifdef Q_OS_DARWIN + // HACK: Clamp the scroll values on macOS to prevent OS scroll acceleration + // from generating wild scroll deltas when scrolling quickly. + event->mouseY = SDL_clamp(event->mouseY, -1.0f, 1.0f); +#endif + + LiSendHighResScrollEvent((short)(event->mouseY * 120)); // WHEEL_DELTA + } + + if (event->mouseX != 0.0f) { + // Invert the scroll direction if needed + if (m_ReverseScrollDirection) { + event->mouseX = -event->mouseY; + } + +#ifdef Q_OS_DARWIN + // HACK: Clamp the scroll values on macOS to prevent OS scroll acceleration + // from generating wild scroll deltas when scrolling quickly. + event->mouseX = SDL_clamp(event->mouseX, -1.0f, 1.0f); +#endif + + LiSendHighResHScrollEvent((short)(event->mouseX * 120)); // WHEEL_DELTA + } +#elif SDL_VERSION_ATLEAST(2, 0, 18) if (event->preciseY != 0.0f) { // Invert the scroll direction if needed if (m_ReverseScrollDirection) { @@ -255,7 +297,11 @@ void SdlInputHandler::updatePointerRegionLock() // have full control over it and we don't touch it anymore. if (!m_PointerRegionLockToggledByUser) { // Lock the pointer in true full-screen mode and leave it unlocked in other modes +#if SDL_VERSION_ATLEAST(3, 0, 0) + m_PointerRegionLockActive = SDL_GetWindowFullscreenMode(m_Window) ? true : false; +#else m_PointerRegionLockActive = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN; +#endif } // If region lock is enabled, grab the cursor so it can't accidentally leave our window. diff --git a/app/streaming/input/reltouch.cpp b/app/streaming/input/reltouch.cpp index 6125e6364..3963dcaf9 100644 --- a/app/streaming/input/reltouch.cpp +++ b/app/streaming/input/reltouch.cpp @@ -1,7 +1,11 @@ #include "input.h" #include +#if HAVE_SDL3 +#include +#else #include +#endif #include @@ -59,7 +63,11 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) // This is also required to handle finger up which // where the finger will not be in SDL_GetTouchFinger() // anymore. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type != SDL_EVENT_FINGER_DOWN) { +#else if (event->type != SDL_FINGERDOWN) { +#endif for (int i = 0; i < MAX_FINGERS; i++) { if (event->fingerId == m_TouchDownEvent[i].fingerId) { fingerIndex = i; @@ -106,7 +114,11 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) // Start a drag timer when primary or secondary // fingers go down +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type == SDL_EVENT_FINGER_DOWN && +#else if (event->type == SDL_FINGERDOWN && +#endif (fingerIndex == 0 || fingerIndex == 1)) { SDL_RemoveTimer(m_DragTimer); m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY, @@ -114,7 +126,11 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) this); } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type == SDL_EVENT_FINGER_MOTION) { +#else if (event->type == SDL_FINGERMOTION) { +#endif // If it's outside the deadzone delta, cancel drags and taps if (qSqrt(qPow(event->x - m_TouchDownEvent[fingerIndex].x, 2) + qPow(event->y - m_TouchDownEvent[fingerIndex].y, 2)) > DEAD_ZONE_DELTA) { @@ -126,7 +142,11 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type == SDL_EVENT_FINGER_UP) { +#else if (event->type == SDL_FINGERUP) { +#endif // Cancel the drag timer on finger up SDL_RemoveTimer(m_DragTimer); m_DragTimer = 0; @@ -166,10 +186,19 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId); +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event->type == SDL_EVENT_FINGER_DOWN) { + m_TouchDownEvent[fingerIndex] = *event; + } + else if (event->type == SDL_EVENT_FINGER_UP) { + m_TouchDownEvent[fingerIndex] = {}; + } +#else if (event->type == SDL_FINGERDOWN) { m_TouchDownEvent[fingerIndex] = *event; } else if (event->type == SDL_FINGERUP) { m_TouchDownEvent[fingerIndex] = {}; } +#endif } diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 399da4e81..296f84cab 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -4,7 +4,11 @@ #include "backend/richpresencemanager.h" #include +#if HAVE_SDL3 +#include +#else #include +#endif #include "utils.h" #ifdef HAVE_FFMPEG @@ -24,7 +28,9 @@ // HACK: Remove once proper Dark Mode support lands in SDL #ifdef Q_OS_WIN32 +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #include #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE_OLD #define DWMWA_USE_IMMERSIVE_DARK_MODE_OLD 19 @@ -41,6 +47,11 @@ #define SDL_CODE_GAMECONTROLLER_SET_MOTION_EVENT_STATE 103 #define SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED 104 +// SDL_TICKS_PASSED is removed in SDL3 +#if SDL_VERSION_ATLEAST(3, 0, 0) +#define SDL_TICKS_PASSED(A, B) ((Sint32)((B) - (A)) <= 0) +#endif + #include #include @@ -146,7 +157,11 @@ void Session::clConnectionTerminated(int errorCode) // Push a quit event to the main loop SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_QUIT; +#else event.type = SDL_QUIT; +#endif event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); } @@ -169,7 +184,11 @@ void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMo // with the removal of game controllers that could result in our game controller // going away during this callback. SDL_Event rumbleEvent = {}; +#if SDL_VERSION_ATLEAST(3, 0, 0) + rumbleEvent.type = SDL_EVENT_USER; +#else rumbleEvent.type = SDL_USEREVENT; +#endif rumbleEvent.user.code = SDL_CODE_GAMECONTROLLER_RUMBLE; rumbleEvent.user.data1 = (void*)(uintptr_t)controllerNumber; rumbleEvent.user.data2 = (void*)(uintptr_t)((lowFreqMotor << 16) | highFreqMotor); @@ -210,6 +229,15 @@ void Session::clSetHdrMode(bool enabled) // If we're in the process of recreating our decoder when we get // this callback, we'll drop it. The main thread will make the // callback when it finishes creating the new decoder. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_TryLockSpinlock(&s_ActiveSession->m_DecoderLock)) { + IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder; + if (decoder != nullptr) { + decoder->setHdrMode(enabled); + } + SDL_UnlockSpinlock(&s_ActiveSession->m_DecoderLock); + } +#else if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) { IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder; if (decoder != nullptr) { @@ -217,6 +245,7 @@ void Session::clSetHdrMode(bool enabled) } SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock); } +#endif } void Session::clRumbleTriggers(uint16_t controllerNumber, uint16_t leftTrigger, uint16_t rightTrigger) @@ -225,7 +254,11 @@ void Session::clRumbleTriggers(uint16_t controllerNumber, uint16_t leftTrigger, // with the removal of game controllers that could result in our game controller // going away during this callback. SDL_Event rumbleEvent = {}; +#if SDL_VERSION_ATLEAST(3, 0, 0) + rumbleEvent.type = SDL_EVENT_USER; +#else rumbleEvent.type = SDL_USEREVENT; +#endif rumbleEvent.user.code = SDL_CODE_GAMECONTROLLER_RUMBLE_TRIGGERS; rumbleEvent.user.data1 = (void*)(uintptr_t)controllerNumber; rumbleEvent.user.data2 = (void*)(uintptr_t)((leftTrigger << 16) | rightTrigger); @@ -238,7 +271,11 @@ void Session::clSetMotionEventState(uint16_t controllerNumber, uint8_t motionTyp // with the removal of game controllers that could result in our game controller // going away during this callback. SDL_Event setMotionEventStateEvent = {}; +#if SDL_VERSION_ATLEAST(3, 0, 0) + setMotionEventStateEvent.type = SDL_EVENT_USER; +#else setMotionEventStateEvent.type = SDL_USEREVENT; +#endif setMotionEventStateEvent.user.code = SDL_CODE_GAMECONTROLLER_SET_MOTION_EVENT_STATE; setMotionEventStateEvent.user.data1 = (void*)(uintptr_t)controllerNumber; setMotionEventStateEvent.user.data2 = (void*)(uintptr_t)((motionType << 16) | reportRateHz); @@ -251,7 +288,11 @@ void Session::clSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g // with the removal of game controllers that could result in our game controller // going away during this callback. SDL_Event setControllerLEDEvent = {}; +#if SDL_VERSION_ATLEAST(3, 0, 0) + setControllerLEDEvent.type = SDL_EVENT_USER; +#else setControllerLEDEvent.type = SDL_USEREVENT; +#endif setControllerLEDEvent.user.code = SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED; setControllerLEDEvent.user.data1 = (void*)(uintptr_t)controllerNumber; setControllerLEDEvent.user.data2 = (void*)(uintptr_t)(r << 16 | g << 8 | b); @@ -348,6 +389,25 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du) // safely return DR_OK and wait for the IDR frame request by // the decoder reinitialization code. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_TryLockSpinlock(&s_ActiveSession->m_DecoderLock)) { + IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder; + if (decoder != nullptr) { + int ret = decoder->submitDecodeUnit(du); + SDL_UnlockSpinlock(&s_ActiveSession->m_DecoderLock); + return ret; + } + else { + SDL_UnlockSpinlock(&s_ActiveSession->m_DecoderLock); + return DR_OK; + } + } + else { + // Decoder is going away. Ignore anything coming in until + // the lock is released. + return DR_OK; + } +#else if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) { IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder; if (decoder != nullptr) { @@ -365,6 +425,7 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du) // the lock is released. return DR_OK; } +#endif } void Session::getDecoderInfo(SDL_Window* window, @@ -574,14 +635,22 @@ bool Session::initialize() } // Create a hidden window to use for decoder initialization tests +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Window* testWindow = SDL_CreateWindow("", 1280, 720, +#else SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, +#endif SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags()); if (!testWindow) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to create test window with platform flags: %s", SDL_GetError()); +#if SDL_VERSION_ATLEAST(3, 0, 0) + testWindow = SDL_CreateWindow("", 1280, 720, SDL_WINDOW_HIDDEN); +#else testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN); +#endif if (!testWindow) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create window for hardware decode test: %s", @@ -742,6 +811,14 @@ bool Session::initialize() switch (m_Preferences->windowMode) { default: +#if SDL_VERSION_ATLEAST(3, 0, 0) + case StreamingPreferences::WM_FULLSCREEN_DESKTOP: + case StreamingPreferences::WM_FULLSCREEN: + // We will use SDL_SetWindowFullscreenMode to set exclusive or borderless + m_FullScreenFlag = SDL_TRUE; + break; + } +#else case StreamingPreferences::WM_FULLSCREEN_DESKTOP: // Only use full-screen desktop mode if we're running a desktop environment if (WMUtils::isRunningDesktopEnvironment()) { @@ -753,6 +830,7 @@ bool Session::initialize() m_FullScreenFlag = SDL_WINDOW_FULLSCREEN; break; } +#endif #if !SDL_VERSION_ATLEAST(2, 0, 11) // HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE @@ -1091,7 +1169,11 @@ void Session::getWindowDimensions(int& x, int& y, int displayIndex = 0; if (m_Window != nullptr) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + displayIndex = SDL_GetDisplayForWindow(m_Window); +#else displayIndex = SDL_GetWindowDisplayIndex(m_Window); +#endif SDL_assert(displayIndex >= 0); } // Create our window on the same display that Qt's UI @@ -1100,7 +1182,13 @@ void Session::getWindowDimensions(int& x, int& y, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Qt UI screen is at (%d,%d)", m_DisplayOriginX, m_DisplayOriginY); +#if SDL_VERSION_ATLEAST(3, 0, 0) + int numDisplays = 0; + SDL_DisplayID *displays = SDL_GetDisplays(&numDisplays); + for (int i = 0; i < numDisplays; i++) { +#else for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) { +#endif SDL_Rect displayBounds; if (SDL_GetDisplayBounds(i, &displayBounds) == 0) { @@ -1119,6 +1207,11 @@ void Session::getWindowDimensions(int& x, int& y, i, SDL_GetError()); } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (displays) { + SDL_free((void*)displays); + } +#endif } SDL_Rect usableBounds; @@ -1161,6 +1254,106 @@ void Session::getWindowDimensions(int& x, int& y, void Session::updateOptimalWindowDisplayMode() { +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (WMUtils::isRunningDesktopEnvironment() && + m_Preferences->windowMode == StreamingPreferences::WM_FULLSCREEN_DESKTOP) { + // Borderless fullscreen + SDL_SetWindowFullscreenMode(m_Window, NULL); + return; + } + + SDL_DisplayMode desktopMode, bestMode; + SDL_DisplayID displayIndex = SDL_GetDisplayForWindow(m_Window); + + // Try the current display mode first. On macOS, this will be the normal + // scaled desktop resolution setting. + const SDL_DisplayMode *desktopModePtr = SDL_GetDesktopDisplayMode(displayIndex); + if (desktopModePtr) { + desktopMode = *desktopModePtr; + // If this doesn't fit the selected resolution, use the native + // resolution of the panel (unscaled). + if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) { + if (!StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) { + return; + } + } + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "SDL_GetDesktopDisplayMode() failed: %s", + SDL_GetError()); + return; + } + + // Start with the native desktop resolution and try to find + // the highest refresh rate that our stream FPS evenly divides. + bestMode = desktopMode; + bestMode.refresh_rate = 0.0f; + int numModes = 0; + const SDL_DisplayMode **modes = SDL_GetFullscreenDisplayModes(displayIndex, &numModes); + for (int i = 0; i < numModes; i++) { + // TODO: SDL3 changed refresh rate to a float, check if we need + // to support a threshold range when comparing to fps and not + // cast it to an int. + if (modes[i]->w == desktopMode.w && modes[i]->h == desktopMode.h && + (int)modes[i]->refresh_rate % m_StreamConfig.fps == 0) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Found display mode with desktop resolution: %dx%dx%.2f", + modes[i]->w, modes[i]->h, modes[i]->refresh_rate); + if (modes[i]->refresh_rate > bestMode.refresh_rate) { + bestMode = *modes[i]; + } + } + } + + // If we didn't find a mode that matched the current resolution and + // had a high enough refresh rate, start looking for lower resolution + // modes that can meet the required refresh rate and minimum video + // resolution. We will also try to pick a display mode that matches + // aspect ratio closest to the video stream. + if (bestMode.refresh_rate == 0.0f) { + float bestModeAspectRatio = 0; + float videoAspectRatio = (float)m_ActiveVideoWidth / (float)m_ActiveVideoHeight; + for (int i = 0; i < numModes; i++) { + float modeAspectRatio = (float)modes[i]->w / (float)modes[i]->h; + if (modes[i]->w >= m_ActiveVideoWidth && modes[i]->h >= m_ActiveVideoHeight && + (int)modes[i]->refresh_rate % m_StreamConfig.fps == 0) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Found display mode with video resolution: %dx%dx%.2f", + modes[i]->w, modes[i]->h, modes[i]->refresh_rate); + if (modes[i]->refresh_rate >= bestMode.refresh_rate && + (bestModeAspectRatio == 0 || fabs(videoAspectRatio - modeAspectRatio) <= fabs(videoAspectRatio - bestModeAspectRatio))) { + bestMode = *modes[i]; + bestModeAspectRatio = modeAspectRatio; + } + } + } + } + + if (modes) { + SDL_free((void*)modes); + } + + if (bestMode.refresh_rate == 0.0f) { + // We may find no match if the user has moved a 120 FPS + // stream onto a 60 Hz monitor (since no refresh rate can + // divide our FPS setting). We'll stick to the default in + // this case. + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "No matching display mode found; using desktop mode"); + bestMode = desktopMode; + } + + if (SDL_GetWindowFullscreenMode(m_Window)) { + // Only print when the window is actually in full-screen exclusive mode, + // otherwise we're not actually using the mode we've set here + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Chosen best display mode: %dx%dx%.2f", + bestMode.w, bestMode.h, bestMode.refresh_rate); + } + + SDL_SetWindowFullscreenMode(m_Window, &bestMode); +#else SDL_DisplayMode desktopMode, bestMode, mode; int displayIndex = SDL_GetWindowDisplayIndex(m_Window); @@ -1245,6 +1438,7 @@ void Session::updateOptimalWindowDisplayMode() } SDL_SetWindowDisplayMode(m_Window, &bestMode); +#endif } void Session::toggleFullscreen() @@ -1460,7 +1654,11 @@ void Session::flushWindowEvents() // This event will cause us to set m_FlushingWindowEvents back to false. SDL_Event flushEvent = {}; +#if SDL_VERSION_ATLEAST(3, 0, 0) + flushEvent.type = SDL_EVENT_USER; +#else flushEvent.type = SDL_USEREVENT; +#endif flushEvent.user.code = SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER; SDL_PushEvent(&flushEvent); } @@ -1588,7 +1786,11 @@ void Session::execInternal() SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); // We always want a resizable window with High DPI enabled +#if SDL_VERSION_ATLEAST(3, 0, 0) + Uint32 defaultWindowFlags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE; +#else Uint32 defaultWindowFlags = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; +#endif // If we're starting in windowed mode and the Moonlight GUI is maximized, // match that with the streaming window. @@ -1610,23 +1812,39 @@ void Session::execInternal() std::string windowName = QString(m_Computer->name + " - Moonlight").toStdString(); #endif +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, windowName.c_str()); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height); + SDL_SetNumberProperty(props, "flags", defaultWindowFlags | StreamUtils::getPlatformWindowFlags()); + m_Window = SDL_CreateWindowWithProperties(props); +#else m_Window = SDL_CreateWindow(windowName.c_str(), x, y, width, height, defaultWindowFlags | StreamUtils::getPlatformWindowFlags()); +#endif if (!m_Window) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow() failed with platform flags: %s", SDL_GetError()); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_SetNumberProperty(props, "flags", defaultWindowFlags); + m_Window = SDL_CreateWindowWithProperties(props); +#else m_Window = SDL_CreateWindow(windowName.c_str(), x, y, width, height, defaultWindowFlags); +#endif if (!m_Window) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow() failed: %s", @@ -1654,10 +1872,18 @@ void Session::execInternal() } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0) { + HWND hwnd = (HWND)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + HDC hdc = (HDC)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HDC_POINTER, NULL); +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); if (SDL_GetWindowWMInfo(m_Window, &info) && info.subsystem == SDL_SYSWM_WINDOWS) { + HWND hwnd = info.info.win.window; + HDC hdc = info.info.win.hdc; +#endif RECT clientRect; HBRUSH blackBrush; @@ -1665,22 +1891,22 @@ void Session::execInternal() // // TODO: Remove when SDL does this itself blackBrush = CreateSolidBrush(0); - GetClientRect(info.info.win.window, &clientRect); - FillRect(info.info.win.hdc, &clientRect, blackBrush); + GetClientRect(hwnd, &clientRect); + FillRect(hdc, &clientRect, blackBrush); DeleteObject(blackBrush); // If dark mode is enabled, propagate that to our SDL window if (darkModeEnabled) { - if (FAILED(DwmSetWindowAttribute(info.info.win.window, DWMWA_USE_IMMERSIVE_DARK_MODE, &darkModeEnabled, sizeof(darkModeEnabled)))) { - DwmSetWindowAttribute(info.info.win.window, DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, &darkModeEnabled, sizeof(darkModeEnabled)); + if (FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &darkModeEnabled, sizeof(darkModeEnabled)))) { + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, &darkModeEnabled, sizeof(darkModeEnabled)); } // Toggle non-client rendering off and back on to ensure dark mode takes effect on Windows 10. // DWM doesn't seem to correctly invalidate the non-client area after enabling dark mode. DWMNCRENDERINGPOLICY ncPolicy = DWMNCRP_DISABLED; - DwmSetWindowAttribute(info.info.win.window, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy)); + DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy)); ncPolicy = DWMNCRP_ENABLED; - DwmSetWindowAttribute(info.info.win.window, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy)); + DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy)); } } } @@ -1694,12 +1920,20 @@ void Session::execInternal() QPainter svgPainter(&svgImage); svgIconRenderer.render(&svgPainter); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Surface* iconSurface = SDL_CreateSurfaceFrom((void*)svgImage.constBits(), + svgImage.width(), + svgImage.height(), + 4 * svgImage.width(), + SDL_PIXELFORMAT_RGBA32); +#else SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(), svgImage.width(), svgImage.height(), 32, 4 * svgImage.width(), SDL_PIXELFORMAT_RGBA32); +#endif #ifndef Q_OS_DARWIN // Other platforms seem to preserve our Qt icon when creating a new window. if (iconSurface != nullptr) { @@ -1716,8 +1950,13 @@ void Session::execInternal() // Enter full screen if requested if (m_IsFullScreen) { SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag); - #ifdef Q_OS_WIN32 +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0) { + HWND hwnd = (HWND)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + HDC hdc = (HDC)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HDC_POINTER, NULL); +#else + SDL_SysWMinfo info; SDL_VERSION(&info.version); @@ -1726,12 +1965,15 @@ void Session::execInternal() // // TODO: Remove when SDL does this itself if (SDL_GetWindowWMInfo(m_Window, &info) && info.subsystem == SDL_SYSWM_WINDOWS) { + HWND hwnd = info.info.win.window; + HDC hdc = info.info.win.hdc; +#endif RECT clientRect; HBRUSH blackBrush; blackBrush = CreateSolidBrush(0); - GetClientRect(info.info.win.window, &clientRect); - FillRect(info.info.win.hdc, &clientRect, blackBrush); + GetClientRect(hwnd, &clientRect); + FillRect(hdc, &clientRect, blackBrush); DeleteObject(blackBrush); } #endif @@ -1773,7 +2015,11 @@ void Session::execInternal() // sleep precision and more accurate callback timing. SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1"); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DisplayID currentDisplayIndex = SDL_GetDisplayForWindow(m_Window); +#else int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); +#endif // Now that we're about to stream, any SDL_QUIT event is expected // unless it comes from the connection termination callback where @@ -1819,12 +2065,20 @@ void Session::execInternal() } #endif switch (event.type) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_EVENT_QUIT: +#else case SDL_QUIT: +#endif SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Quit event received"); goto DispatchDeferredCleanup; +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_EVENT_USER: +#else case SDL_USEREVENT: +#endif switch (event.user.code) { case SDL_CODE_FRAME_READY: if (m_VideoDecoder != nullptr) { @@ -1860,6 +2114,9 @@ void Session::execInternal() } break; +// Window events we will do at the end as the API now doesn't have +// SDL_WINDOWEVENT and we need to special-case it. +#if !SDL_VERSION_ATLEAST(3, 0, 0) case SDL_WINDOWEVENT: // Early handling of some events switch (event.window.event) { @@ -1977,18 +2234,30 @@ void Session::execInternal() event.window.event, event.window.data1, event.window.data2); +#endif // #if !SDL_VERSION_ATLEAST(3, 0, 0) // Fall through +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_EVENT_RENDER_DEVICE_RESET: + case SDL_EVENT_RENDER_TARGETS_RESET: + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Recreating renderer by internal request: %d", + event.type); +#else case SDL_RENDER_DEVICE_RESET: case SDL_RENDER_TARGETS_RESET: - if (event.type != SDL_WINDOWEVENT) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Recreating renderer by internal request: %d", event.type); } +#endif +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_LockSpinlock(&m_DecoderLock); +#else SDL_AtomicLock(&m_DecoderLock); +#endif // Destroy the old decoder delete m_VideoDecoder; @@ -2001,17 +2270,29 @@ void Session::execInternal() // Update the window display mode based on our current monitor // NB: Avoid a useless modeset by only doing this if it changed. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (currentDisplayIndex != SDL_GetDisplayForWindow(m_Window)) { + currentDisplayIndex = SDL_GetDisplayForWindow(m_Window); + updateOptimalWindowDisplayMode(); + } +#else if (currentDisplayIndex != SDL_GetWindowDisplayIndex(m_Window)) { currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); updateOptimalWindowDisplayMode(); } +#endif // Now that the old decoder is dead, flush any events it may // have queued to reset itself (if this reset was the result // of state loss). SDL_PumpEvents(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FlushEvent(SDL_EVENT_RENDER_DEVICE_RESET); + SDL_FlushEvent(SDL_EVENT_RENDER_TARGETS_RESET); +#else SDL_FlushEvent(SDL_RENDER_DEVICE_RESET); SDL_FlushEvent(SDL_RENDER_TARGETS_RESET); +#endif { // If the stream exceeds the display refresh rate (plus some slack), @@ -2034,7 +2315,11 @@ void Session::execInternal() enableVsync && m_Preferences->framePacing, false, s_ActiveSession->m_VideoDecoder)) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_UnlockSpinlock(&m_DecoderLock); +#else SDL_AtomicUnlock(&m_DecoderLock); +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to recreate decoder after reset"); emit displayLaunchError(tr("Unable to initialize video decoder. Please check your streaming settings and try again.")); @@ -2060,9 +2345,62 @@ void Session::execInternal() // After a window resize, we need to reset the pointer lock region m_InputHandler->updatePointerRegionLock(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_UnlockSpinlock(&m_DecoderLock); +#else SDL_AtomicUnlock(&m_DecoderLock); +#endif break; +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_EVENT_KEY_UP: + case SDL_EVENT_KEY_DOWN: + presence.runCallbacks(); + m_InputHandler->handleKeyEvent(&event.key); + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + presence.runCallbacks(); + m_InputHandler->handleMouseButtonEvent(&event.button); + break; + case SDL_EVENT_MOUSE_MOTION: + m_InputHandler->handleMouseMotionEvent(&event.motion); + break; + case SDL_EVENT_MOUSE_WHEEL: + m_InputHandler->handleMouseWheelEvent(&event.wheel); + break; + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + m_InputHandler->handleControllerAxisEvent(&event.gaxis); + break; + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + presence.runCallbacks(); + m_InputHandler->handleControllerButtonEvent(&event.gbutton); + break; + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: + m_InputHandler->handleControllerSensorEvent(&event.gsensor); + break; + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + m_InputHandler->handleControllerTouchpadEvent(&event.gtouchpad); + break; + case SDL_EVENT_JOYSTICK_BATTERY_UPDATED: + m_InputHandler->handleJoystickBatteryEvent(&event.jbattery); + break; + case SDL_EVENT_GAMEPAD_ADDED: + case SDL_EVENT_GAMEPAD_REMOVED: + m_InputHandler->handleControllerDeviceEvent(&event.gdevice); + break; + case SDL_EVENT_JOYSTICK_ADDED: + m_InputHandler->handleJoystickArrivalEvent(&event.jdevice); + break; + case SDL_EVENT_FINGER_DOWN: + case SDL_EVENT_FINGER_MOTION: + case SDL_EVENT_FINGER_UP: + m_InputHandler->handleTouchFingerEvent(&event.tfinger); + break; +#else case SDL_KEYUP: case SDL_KEYDOWN: presence.runCallbacks(); @@ -2114,7 +2452,120 @@ void Session::execInternal() case SDL_FINGERUP: m_InputHandler->handleTouchFingerEvent(&event.tfinger); break; +#endif + } // switch(event.type) +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (event.type >= SDL_EVENT_WINDOW_FIRST && event.type <= SDL_EVENT_WINDOW_LAST) { + // Early handling of some events + switch (event.type) { + case SDL_EVENT_WINDOW_FOCUS_LOST: + if (m_Preferences->muteOnFocusLoss) { + m_AudioMuted = true; + } + m_InputHandler->notifyFocusLost(); + break; + case SDL_EVENT_WINDOW_FOCUS_GAINED: + if (m_Preferences->muteOnFocusLoss) { + m_AudioMuted = false; + } + break; + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + m_InputHandler->notifyMouseLeave(); + break; + } + + presence.runCallbacks(); + + // Capture the mouse on SDL_WINDOWEVENT_ENTER if needed + if (needsFirstEnterCapture && event.type == SDL_EVENT_WINDOW_MOUSE_ENTER) { + m_InputHandler->setCaptureActive(true); + needsFirstEnterCapture = false; + } + + // We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event. + // We use SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't + // seem to fire when switching from windowed to full-screen on X11. + if (event.type != SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED && event.type != SDL_EVENT_WINDOW_SHOWN) { + // Check that the window display hasn't changed. If it has, we want + // to recreate the decoder to allow it to adapt to the new display. + // This will allow Pacer to pull the new display refresh rate. + // On SDL 2.0.18+, there's an event for this specific situation + if (event.type != SDL_EVENT_WINDOW_DISPLAY_CHANGED) { + break; + } + } +#ifdef Q_OS_WIN32 + // We can get a resize event after being minimized. Recreating the renderer at that time can cause + // us to start drawing on the screen even while our window is minimized. Minimizing on Windows also + // moves the window to -32000, -32000 which can cause a false window display index change. Avoid + // that whole mess by never recreating the decoder if we're minimized. + else if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_MINIMIZED) { + break; + } +#endif + + if (m_FlushingWindowEventsRef > 0) { + // Ignore window events for renderer reset if flushing + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Dropping window event during flush: %d (%d %d)", + event.window.type, + event.window.data1, + event.window.data2); + break; + } + + // Allow the renderer to handle the state change without being recreated + if (m_VideoDecoder) { + bool forceRecreation = false; + + WINDOW_STATE_CHANGE_INFO windowChangeInfo = {}; + windowChangeInfo.window = m_Window; + + if (event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) { + windowChangeInfo.stateChangeFlags |= WINDOW_STATE_CHANGE_SIZE; + + windowChangeInfo.width = event.window.data1; + windowChangeInfo.height = event.window.data2; + } + + SDL_DisplayID newDisplayIndex = SDL_GetDisplayForWindow(m_Window); + if (newDisplayIndex != currentDisplayIndex) { + windowChangeInfo.stateChangeFlags |= WINDOW_STATE_CHANGE_DISPLAY; + + windowChangeInfo.displayIndex = newDisplayIndex; + + // If the refresh rates have changed, we will need to go through the full + // decoder recreation path to ensure Pacer is switched to the new display + // and that we apply any V-Sync disablement rules that may be needed for + // this display. + const SDL_DisplayMode *oldMode = SDL_GetCurrentDisplayMode(currentDisplayIndex); + const SDL_DisplayMode *newMode = SDL_GetCurrentDisplayMode(newDisplayIndex); + if (oldMode && newMode && (oldMode->refresh_rate != newMode->refresh_rate)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Forcing renderer recreation due to refresh rate change between displays"); + forceRecreation = true; + } + } + + if (!forceRecreation && m_VideoDecoder->notifyWindowChanged(&windowChangeInfo)) { + // Update the window display mode based on our current monitor + // NB: Avoid a useless modeset by only doing this if it changed. + if (newDisplayIndex != currentDisplayIndex) { + currentDisplayIndex = newDisplayIndex; + updateOptimalWindowDisplayMode(); + } + + break; + } + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Recreating renderer for window event: %d (%d %d)", + event.window.type, + event.window.data1, + event.window.data2); } +#endif } DispatchDeferredCleanup: @@ -2139,18 +2590,31 @@ void Session::execInternal() // Destroy the decoder, since this must be done on the main thread // NB: This must happen before LiStopConnection() for pull-based // decoders. +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_LockSpinlock(&m_DecoderLock); + delete m_VideoDecoder; + m_VideoDecoder = nullptr; + SDL_UnlockSpinlock(&m_DecoderLock); +#else SDL_AtomicLock(&m_DecoderLock); delete m_VideoDecoder; m_VideoDecoder = nullptr; SDL_AtomicUnlock(&m_DecoderLock); +#endif // This must be called after the decoder is deleted, because // the renderer may want to interact with the window SDL_DestroyWindow(m_Window); +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (iconSurface != nullptr) { + SDL_DestroySurface(iconSurface); + } +#else if (iconSurface != nullptr) { SDL_FreeSurface(iconSurface); } +#endif SDL_QuitSubSystem(SDL_INIT_VIDEO); diff --git a/app/streaming/streamutils.cpp b/app/streaming/streamutils.cpp index f4f965979..ce8ecf615 100644 --- a/app/streaming/streamutils.cpp +++ b/app/streaming/streamutils.cpp @@ -105,8 +105,13 @@ void StreamUtils::screenSpaceToNormalizedDeviceCoords(SDL_Rect* src, SDL_FRect* int StreamUtils::getDisplayRefreshRate(SDL_Window* window) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DisplayID displayIndex = SDL_GetDisplayForWindow(window); + if (displayIndex == 0) { +#else int displayIndex = SDL_GetWindowDisplayIndex(window); if (displayIndex < 0) { +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get current display: %s", SDL_GetError()); @@ -115,6 +120,36 @@ int StreamUtils::getDisplayRefreshRate(SDL_Window* window) displayIndex = 0; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + const SDL_DisplayMode *fullscreenMode = SDL_GetWindowFullscreenMode(window); + const SDL_DisplayMode *currentMode = SDL_GetCurrentDisplayMode(displayIndex); + if (fullscreenMode) { + // Use the window display mode for full-screen exclusive mode + if (fullscreenMode->refresh_rate == 0.0f) { + return 60; + } + else { + return (int)fullscreenMode->refresh_rate; + } + } + else if (currentMode) { + // Use the current display mode for windowed and borderless + if (currentMode->refresh_rate == 0.0f) { + return 60; + } + else { + return (int)currentMode->refresh_rate; + } + } + else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_GetCurrentDisplayMode() failed: %s", + SDL_GetError()); + + // Assume 60 Hz + return 60; + } +#else SDL_DisplayMode mode; if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { // Use the window display mode for full-screen exclusive mode @@ -147,6 +182,7 @@ int StreamUtils::getDisplayRefreshRate(SDL_Window* window) } return mode.refresh_rate; +#endif } bool StreamUtils::hasFastAes() @@ -193,6 +229,23 @@ bool StreamUtils::hasFastAes() #endif } +#if SDL_VERSION_ATLEAST(3, 0, 0) +bool StreamUtils::getNativeDesktopMode(SDL_DisplayID displayID, SDL_DisplayMode* mode) +{ + // TODO: Check how to get the native resolution + // withoud DPI scaling for Wayland and Darwin + const SDL_DisplayMode *displayMode = SDL_GetDesktopDisplayMode(displayID); + if (!displayMode) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_GetDisplayMode() failed: %s", + SDL_GetError()); + return false; + } + SDL_memcpy(mode, displayMode, sizeof(SDL_DisplayMode)); + + return true; +} +#else bool StreamUtils::getNativeDesktopMode(int displayIndex, SDL_DisplayMode* mode) { #ifdef Q_OS_DARWIN @@ -262,3 +315,4 @@ bool StreamUtils::getNativeDesktopMode(int displayIndex, SDL_DisplayMode* mode) return true; } +#endif diff --git a/app/streaming/streamutils.h b/app/streaming/streamutils.h index 4d61a8658..f8b7c24a1 100644 --- a/app/streaming/streamutils.h +++ b/app/streaming/streamutils.h @@ -1,6 +1,10 @@ #pragma once +#if HAVE_SDL3 +#include +#else #include +#endif // SDL_FRect wasn't added until 2.0.10 #if !SDL_VERSION_ATLEAST(2, 0, 10) @@ -29,7 +33,11 @@ class StreamUtils void screenSpaceToNormalizedDeviceCoords(SDL_Rect* src, SDL_FRect* dst, int viewportWidth, int viewportHeight); static +#if SDL_VERSION_ATLEAST(3, 0, 0) + bool getNativeDesktopMode(SDL_DisplayID displayID, SDL_DisplayMode* mode); +#else bool getNativeDesktopMode(int displayIndex, SDL_DisplayMode* mode); +#endif static int getDisplayRefreshRate(SDL_Window* window); diff --git a/app/streaming/video/decoder.h b/app/streaming/video/decoder.h index 24708d828..6dfb4ff13 100644 --- a/app/streaming/video/decoder.h +++ b/app/streaming/video/decoder.h @@ -1,7 +1,11 @@ #pragma once #include +#if HAVE_SDL3 +#include +#else #include +#endif #include "settings/streamingpreferences.h" #define SDL_CODE_FRAME_READY 0 diff --git a/app/streaming/video/ffmpeg-renderers/cuda.cpp b/app/streaming/video/ffmpeg-renderers/cuda.cpp index 22a6c0924..b9080fdda 100644 --- a/app/streaming/video/ffmpeg-renderers/cuda.cpp +++ b/app/streaming/video/ffmpeg-renderers/cuda.cpp @@ -1,6 +1,10 @@ #include "cuda.h" +#if HAVE_SDL3 +#include +#else #include +#endif CUDARenderer::CUDARenderer() : m_HwContext(nullptr) diff --git a/app/streaming/video/ffmpeg-renderers/d3d11va.cpp b/app/streaming/video/ffmpeg-renderers/d3d11va.cpp index 1ac76d019..e24028ecd 100644 --- a/app/streaming/video/ffmpeg-renderers/d3d11va.cpp +++ b/app/streaming/video/ffmpeg-renderers/d3d11va.cpp @@ -8,7 +8,9 @@ #include "streaming/streamutils.h" #include "streaming/session.h" +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #include #include @@ -374,16 +376,23 @@ bool D3D11VARenderer::initialize(PDECODER_PARAMETERS params) } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_assert(SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0); + HWND hwnd = (HWND)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); SDL_GetWindowWMInfo(params->window, &info); SDL_assert(info.subsystem == SDL_SYSWM_WINDOWS); + HWND hwnd = info.info.win.window; +#endif + // Always use windowed or borderless windowed mode.. SDL does mode-setting for us in // full-screen exclusive mode (SDL_WINDOW_FULLSCREEN), so this actually works out okay. IDXGISwapChain1* swapChain; hr = m_Factory->CreateSwapChainForHwnd(m_Device, - info.info.win.window, + hwnd, &swapChainDesc, nullptr, nullptr, @@ -409,7 +418,7 @@ bool D3D11VARenderer::initialize(PDECODER_PARAMETERS params) // Disable Alt+Enter, PrintScreen, and window message snooping. This makes // it safe to run the renderer on a separate rendering thread rather than // requiring the main (message loop) thread. - hr = m_Factory->MakeWindowAssociation(info.info.win.window, DXGI_MWA_NO_WINDOW_CHANGES); + hr = m_Factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_WINDOW_CHANGES); if (FAILED(hr)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "IDXGIFactory::MakeWindowAssociation() failed: %x", diff --git a/app/streaming/video/ffmpeg-renderers/d3d11va.h b/app/streaming/video/ffmpeg-renderers/d3d11va.h index 8421fdb46..7dd19930e 100644 --- a/app/streaming/video/ffmpeg-renderers/d3d11va.h +++ b/app/streaming/video/ffmpeg-renderers/d3d11va.h @@ -41,7 +41,11 @@ class D3D11VARenderer : public IFFmpegRenderer IDXGISwapChain4* m_SwapChain; ID3D11DeviceContext* m_DeviceContext; ID3D11RenderTargetView* m_RenderTargetView; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Mutex* m_ContextLock; +#else SDL_mutex* m_ContextLock; +#endif DECODER_PARAMETERS m_DecoderParams; int m_DisplayWidth; diff --git a/app/streaming/video/ffmpeg-renderers/drm.cpp b/app/streaming/video/ffmpeg-renderers/drm.cpp index 15d575d44..cbe798aca 100644 --- a/app/streaming/video/ffmpeg-renderers/drm.cpp +++ b/app/streaming/video/ffmpeg-renderers/drm.cpp @@ -56,7 +56,9 @@ extern "C" { #undef SDL_VIDEO_DRIVER_X11 #endif +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #include @@ -186,7 +188,18 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS params) m_Main10Hdr = (params->videoFormat & VIDEO_FORMAT_MASK_10BIT); m_SwFrameMapper.setVideoFormat(params->videoFormat); -#if SDL_VERSION_ATLEAST(2, 0, 15) +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "kmsdrm") == 0) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Sharing DRM FD with SDL"); + + int drmFd = SDL_GetNumberProperty(SDL_GetWindowProperties(params->window), SDL_PROP_WINDOW_KMSDRM_DRM_FD_NUMBER, 0); + SDL_assert(drmFd >= 0); + m_DrmFd = drmFd; + m_SdlOwnsDrmFd = true; + } + else +#elif SDL_VERSION_ATLEAST(2, 0, 15) SDL_SysWMinfo info; SDL_VERSION(&info.version); @@ -315,7 +328,11 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS params) // operation that the KMSDRM backend keeps pending until the next // time we swap buffers. We have to do this before we enumerate // CRTC modes below. +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Renderer* renderer = SDL_CreateRenderer(params->window, NULL, SDL_RENDERER_SOFTWARE); +#else SDL_Renderer* renderer = SDL_CreateRenderer(params->window, -1, SDL_RENDERER_SOFTWARE); +#endif if (renderer != nullptr) { SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); SDL_RenderClear(renderer); diff --git a/app/streaming/video/ffmpeg-renderers/dxva2.cpp b/app/streaming/video/ffmpeg-renderers/dxva2.cpp index dda824074..3fdd462fa 100644 --- a/app/streaming/video/ffmpeg-renderers/dxva2.cpp +++ b/app/streaming/video/ffmpeg-renderers/dxva2.cpp @@ -9,7 +9,9 @@ #include #include +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #define WIN32_LEAN_AND_MEAN #include @@ -544,11 +546,17 @@ bool DXVA2Renderer::isDecoderBlacklisted() bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + HWND hwnd = (HWND)SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); SDL_GetWindowWMInfo(window, &info); + HWND hwnd = info.info.win.window; +#endif + IDirect3D9Ex* d3d9ex; HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex); if (FAILED(hr)) { @@ -604,7 +612,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync) d3d9ex->GetAdapterDisplayModeEx(adapterIndex, ¤tMode, nullptr); D3DPRESENT_PARAMETERS d3dpp = {}; - d3dpp.hDeviceWindow = info.info.win.window; + d3dpp.hDeviceWindow = hwnd; d3dpp.Flags = D3DPRESENTFLAG_VIDEO; if (m_VideoFormat & VIDEO_FORMAT_MASK_10BIT) { diff --git a/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp b/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp index f60684f8a..df1bdb2df 100644 --- a/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp +++ b/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp @@ -1,7 +1,11 @@ // vim: noai:ts=4:sw=4:softtabstop=4:expandtab #include "renderer.h" +#if HAVE_SDL3 +#include +#else #include +#endif static QStringList egl_get_extensions(EGLDisplay dpy) { const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS); diff --git a/app/streaming/video/ffmpeg-renderers/eglimagefactory.h b/app/streaming/video/ffmpeg-renderers/eglimagefactory.h index 3cd90e4a6..30fe41269 100644 --- a/app/streaming/video/ffmpeg-renderers/eglimagefactory.h +++ b/app/streaming/video/ffmpeg-renderers/eglimagefactory.h @@ -3,7 +3,11 @@ #include "renderer.h" #define SDL_USE_BUILTIN_OPENGL_DEFINITIONS 1 +#if HAVE_SDL3 +#include +#else #include +#endif #ifdef HAVE_LIBVA #include diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.cpp b/app/streaming/video/ffmpeg-renderers/eglvid.cpp index 91f9f523b..c09d6d536 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.cpp +++ b/app/streaming/video/ffmpeg-renderers/eglvid.cpp @@ -10,8 +10,12 @@ #include #include +#if HAVE_SDL3 +#include +#else #include #include +#endif // These are extensions, so some platform headers may not provide them #ifndef EGL_PLATFORM_WAYLAND_KHR @@ -484,6 +488,18 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params) SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#if SDL_VERSION_ATLEAST(3, 0, 0) + // TODO: Untested + m_DummyRenderer = SDL_CreateRenderer(m_Window, "opengles2", SDL_RENDERER_ACCELERATED); + if (m_DummyRenderer) { + // Not sure if this is needed, it'll probably fail creating + // anyways if the renderer is not accelerated. + SDL_RendererInfo info; + if (SDL_GetRendererInfo(m_DummyRenderer, &info)) { + SDL_assert(renderInfo.flags & SDL_RENDERER_ACCELERATED); + } + } +#else int renderIndex; int maxRenderers = SDL_GetNumRenderDrivers(); SDL_assert(maxRenderers >= 0); @@ -503,6 +519,7 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params) } m_DummyRenderer = SDL_CreateRenderer(m_Window, renderIndex, SDL_RENDERER_ACCELERATED); +#endif if (!m_DummyRenderer) { // Print the error here (before it gets clobbered), but ensure that we flush window // events just in case SDL re-created the window before eventually failing. @@ -523,7 +540,11 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params) else { // If we get here prior to the start of a session, just pump and flush ourselves. SDL_PumpEvents(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FlushEvents(SDL_EVENT_WINDOW_FIRST, SDL_EVENT_WINDOW_LAST); +#else SDL_FlushEvent(SDL_WINDOWEVENT); +#endif } // Now we finally bail if we failed during SDL_CreateRenderer() above. @@ -533,6 +554,35 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params) return false; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + bool displayOpened = false; + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) { + void *wldisplay = SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL); + if (!openDisplay(EGL_PLATFORM_WAYLAND_KHR, wldisplay)) { + return false; + } + displayOpened = true; + } + else if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) { + void *xdisplay = SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL); + if (!openDisplay(EGL_PLATFORM_X11_KHR, xdisplay)) { + return false; + } + displayOpened = true; + } + else if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "kmsdrm") == 0) { + void *gbmdevice = SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_KMSDRM_GBM_DEVICE_POINTER, NULL); + if (!openDisplay(EGL_PLATFORM_GBM_KHR, gbmdevice)) { + return false; + } + displayOpened = true; + } + + if (!displayOpened) { + EGL_LOG(Error, "not compatible with SYSWM"); + return false; + } +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); if (!SDL_GetWindowWMInfo(params->window, &info)) { @@ -565,6 +615,7 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params) EGL_LOG(Error, "not compatible with SYSWM"); return false; } +#endif if (m_EGLDisplay == EGL_NO_DISPLAY) { EGL_LOG(Error, "Cannot get EGL display: %d", eglGetError()); @@ -671,18 +722,26 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params) // to resize the window. This seems to happen significantly more often // with vsync enabled, so this also mitigates that problem too. if (params->enableVsync +#if SDL_VERSION_ATLEAST(3, 0, 0) + && (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) +#else #ifdef SDL_VIDEO_DRIVER_WAYLAND && info.subsystem != SDL_SYSWM_WAYLAND +#endif #endif ) { SDL_GL_SetSwapInterval(1); -#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM) // The SDL KMSDRM backend already enforces double buffering (due to // SDL_HINT_VIDEO_DOUBLE_BUFFER=1), so calling glFinish() after // SDL_GL_SwapWindow() will block an extra frame and lock rendering // at 1/2 the display refresh rate. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "kmsdrm") != 0) +#else +#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM) if (info.subsystem != SDL_SYSWM_KMSDRM) +#endif #endif { m_BlockingSwapBuffers = true; @@ -905,7 +964,11 @@ void EGLRenderer::renderFrame(AVFrame* frame) // XWayland. Other strategies like calling glGetError() don't seem // to be able to detect this situation for some reason. SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_RENDER_TARGETS_RESET; +#else event.type = SDL_RENDER_TARGETS_RESET; +#endif SDL_PushEvent(&event); return; @@ -924,7 +987,11 @@ void EGLRenderer::renderFrame(AVFrame* frame) glClear(GL_COLOR_BUFFER_BIT); int drawableWidth, drawableHeight; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_GetWindowSizeInPixels(m_Window, &drawableWidth, &drawableHeight); +#else SDL_GL_GetDrawableSize(m_Window, &drawableWidth, &drawableHeight); +#endif // Set the viewport to the size of the aspect-ratio-scaled video SDL_Rect src, dst; diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.h b/app/streaming/video/ffmpeg-renderers/eglvid.h index ec7b994f5..a19da6f02 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.h +++ b/app/streaming/video/ffmpeg-renderers/eglvid.h @@ -3,8 +3,13 @@ #include "renderer.h" #define SDL_USE_BUILTIN_OPENGL_DEFINITIONS 1 +#if HAVE_SDL3 +#include +#include +#else #include #include +#endif class EGLRenderer : public IFFmpegRenderer { public: @@ -37,7 +42,11 @@ class EGLRenderer : public IFFmpegRenderer { unsigned m_Textures[EGL_MAX_PLANES]; unsigned m_OverlayTextures[Overlay::OverlayMax]; unsigned m_OverlayVbos[Overlay::OverlayMax]; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_AtomicInt m_OverlayHasValidData[Overlay::OverlayMax]; +#else SDL_atomic_t m_OverlayHasValidData[Overlay::OverlayMax]; +#endif unsigned m_ShaderProgram; unsigned m_OverlayShaderProgram; SDL_GLContext m_Context; diff --git a/app/streaming/video/ffmpeg-renderers/mmal.cpp b/app/streaming/video/ffmpeg-renderers/mmal.cpp index 98cf663de..71172c111 100644 --- a/app/streaming/video/ffmpeg-renderers/mmal.cpp +++ b/app/streaming/video/ffmpeg-renderers/mmal.cpp @@ -10,7 +10,9 @@ #undef SDL_VIDEO_DRIVER_X11 #endif +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #include #include diff --git a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp index 2d30aadb8..cf347817f 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp @@ -47,6 +47,12 @@ bool DxVsyncSource::initialize(SDL_Window* window, int) return false; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + // Pacer should only create us on Win32 + SDL_assert(SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0); + + m_Window = (HWND)SDL_GetProperty(SDL_GetWindowProperties(m_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); @@ -62,6 +68,7 @@ bool DxVsyncSource::initialize(SDL_Window* window, int) SDL_assert(info.subsystem == SDL_SYSWM_WINDOWS); m_Window = info.info.win.window; +#endif return true; } diff --git a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h index f126df152..158b344d8 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h +++ b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h @@ -2,7 +2,11 @@ #include "pacer.h" +#if HAVE_SDL3 +#include +#else #include +#endif // from typedef LONG NTSTATUS; diff --git a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp index 2686c5495..109813baf 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp @@ -12,7 +12,9 @@ #include "waylandvsyncsource.h" #endif +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif // Limit the number of queued frames to prevent excessive memory consumption // if the V-Sync source or renderer is blocked for a while. It's important @@ -187,7 +189,11 @@ void Pacer::enqueueFrameForRenderingAndUnlock(AVFrame *frame) SDL_Event event; // For main thread rendering, we'll push an event to trigger a callback +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_USER; +#else event.type = SDL_USEREVENT; +#endif event.user.code = SDL_CODE_FRAME_READY; SDL_PushEvent(&event); } @@ -267,6 +273,20 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing) "Frame pacing: target %d Hz with %d FPS stream", m_DisplayFps, m_MaxVideoFps); +#if SDL_VERSION_ATLEAST(3, 0, 0) + #ifdef Q_OS_WIN32 + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0) { + if (IsWindows8OrGreater()) { + m_VsyncSource = new DxVsyncSource(this); + } + } + #endif + #ifdef HAS_WAYLAND + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) { + m_VsyncSource = new WaylandVsyncSource(this); + } + #endif +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); if (!SDL_GetWindowWMInfo(window, &info)) { @@ -298,6 +318,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing) // immediately like they used to. break; } +#endif SDL_assert(m_VsyncSource != nullptr || !(m_RendererAttributes & RENDERER_ATTRIBUTE_FORCE_PACING)); diff --git a/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp b/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp index 13e5e4d4f..65fa5ae3b 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp @@ -1,8 +1,10 @@ #include "waylandvsyncsource.h" +#ifndef HAVE_SDL3 #include +#endif -#ifndef SDL_VIDEO_DRIVER_WAYLAND +#if !defined(HAVE_SDL3) && !defined(SDL_VIDEO_DRIVER_WAYLAND) #warning Unable to use WaylandVsyncSource without SDL support #else @@ -29,6 +31,12 @@ WaylandVsyncSource::~WaylandVsyncSource() bool WaylandVsyncSource::initialize(SDL_Window* window, int) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + // Pacer should not create us for non-Wayland windows + SDL_assert(SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0); + m_Display = (struct wl_display *)SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL); + m_Surface = (struct wl_surface *)SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, NULL); +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); @@ -45,6 +53,7 @@ bool WaylandVsyncSource::initialize(SDL_Window* window, int) m_Display = info.info.wl.display; m_Surface = info.info.wl.surface; +#endif // Enqueue our first frame callback m_Callback = wl_surface_frame(m_Surface); diff --git a/app/streaming/video/ffmpeg-renderers/plvk.cpp b/app/streaming/video/ffmpeg-renderers/plvk.cpp index fd054e9c5..4ecbb4b25 100644 --- a/app/streaming/video/ffmpeg-renderers/plvk.cpp +++ b/app/streaming/video/ffmpeg-renderers/plvk.cpp @@ -7,13 +7,22 @@ #define PL_LIBAV_IMPLEMENTATION 0 #include +#if HAVE_SDL3 +#include +#else #include +#endif #include #include #include +// Rocky 9.3 Vulkan is a bit out of date +#ifndef VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME +#define VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME "VK_KHR_cooperative_matrix" +#endif + // Keep these in sync with hwcontext_vulkan.c static const char *k_OptionalDeviceExtensions[] = { /* Misc or required by other extensions */ @@ -83,7 +92,11 @@ void PlVkRenderer::unlockQueue(struct AVHWDeviceContext *dev_ctx, uint32_t queue void PlVkRenderer::overlayUploadComplete(void* opaque) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface((SDL_Surface*)opaque); +#else SDL_FreeSurface((SDL_Surface*)opaque); +#endif } PlVkRenderer::PlVkRenderer(IFFmpegRenderer* backendRenderer) : @@ -323,6 +336,16 @@ bool PlVkRenderer::initialize(PDECODER_PARAMETERS params) m_Window = params->window; unsigned int instanceExtensionCount = 0; + +#if SDL_VERSION_ATLEAST(3, 0, 0) + char const* const* extensions = SDL_Vulkan_GetInstanceExtensions(&instanceExtensionCount); + if (!extensions) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_Vulkan_GetInstanceExtensions() failed: %s", + SDL_GetError()); + return false; + } +#else if (!SDL_Vulkan_GetInstanceExtensions(params->window, &instanceExtensionCount, nullptr)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Vulkan_GetInstanceExtensions() #1 failed: %s", @@ -337,6 +360,7 @@ bool PlVkRenderer::initialize(PDECODER_PARAMETERS params) SDL_GetError()); return false; } +#endif pl_vk_inst_params vkInstParams = pl_vk_inst_default_params; { @@ -350,8 +374,13 @@ bool PlVkRenderer::initialize(PDECODER_PARAMETERS params) #endif } vkInstParams.get_proc_addr = (PFN_vkGetInstanceProcAddr)SDL_Vulkan_GetVkGetInstanceProcAddr(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + vkInstParams.extensions = extensions; + vkInstParams.num_extensions = instanceExtensionCount; +#else vkInstParams.extensions = instanceExtensions.data(); vkInstParams.num_extensions = (int)instanceExtensions.size(); +#endif m_PlVkInstance = pl_vk_inst_create(m_Log, &vkInstParams); if (m_PlVkInstance == nullptr) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, @@ -369,7 +398,11 @@ bool PlVkRenderer::initialize(PDECODER_PARAMETERS params) POPULATE_FUNCTION(vkGetPhysicalDeviceSurfaceSupportKHR); POPULATE_FUNCTION(vkEnumerateDeviceExtensionProperties); +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (!SDL_Vulkan_CreateSurface(params->window, m_PlVkInstance->instance, NULL, &m_VkSurface)) { +#else if (!SDL_Vulkan_CreateSurface(params->window, m_PlVkInstance->instance, &m_VkSurface)) { +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Vulkan_CreateSurface() failed: %s", SDL_GetError()); @@ -603,7 +636,11 @@ void PlVkRenderer::waitToRender() SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GPU is in failed state. Recreating renderer."); SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_RENDER_DEVICE_RESET; +#else event.type = SDL_RENDER_DEVICE_RESET; +#endif SDL_PushEvent(&event); return; } @@ -616,7 +653,11 @@ void PlVkRenderer::waitToRender() // Handle the swapchain being resized int vkDrawableW, vkDrawableH; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_GetWindowSizeInPixels(m_Window, &vkDrawableW, &vkDrawableH); +#else SDL_Vulkan_GetDrawableSize(m_Window, &vkDrawableW, &vkDrawableH); +#endif if (!pl_swapchain_resize(m_Swapchain, &vkDrawableW, &vkDrawableH)) { // Swapchain (re)creation can fail if the window is occluded return; @@ -675,7 +716,11 @@ void PlVkRenderer::renderFrame(AVFrame *frame) pl_frame_from_swapchain(&targetFrame, &m_SwapchainFrame); // We perform minimal processing under the overlay lock to avoid blocking threads updating the overlay +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_LockSpinlock(&m_OverlayLock); +#else SDL_AtomicLock(&m_OverlayLock); +#endif for (int i = 0; i < Overlay::OverlayMax; i++) { // If we have a staging overlay, we need to transfer ownership to us if (m_Overlays[i].hasStagingOverlay) { @@ -721,7 +766,11 @@ void PlVkRenderer::renderFrame(AVFrame *frame) overlays.push_back(m_Overlays[i].overlay); } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_UnlockSpinlock(&m_OverlayLock); +#else SDL_AtomicUnlock(&m_OverlayLock); +#endif SDL_Rect src; src.x = mappedFrame.crop.x0; @@ -760,7 +809,11 @@ void PlVkRenderer::renderFrame(AVFrame *frame) // Recreate the renderer SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_RENDER_TARGETS_RESET; +#else event.type = SDL_RENDER_TARGETS_RESET; +#endif SDL_PushEvent(&event); goto UnmapExit; } @@ -794,12 +847,20 @@ void PlVkRenderer::notifyOverlayUpdated(Overlay::OverlayType type) return; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_LockSpinlock(&m_OverlayLock); +#else SDL_AtomicLock(&m_OverlayLock); +#endif // We want to clear the staging overlay flag even if a staging overlay is still present, // since this ensures the render thread will not read from a partially initialized pl_tex // as we modify or recreate the staging overlay texture outside the overlay lock. m_Overlays[type].hasStagingOverlay = false; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_UnlockSpinlock(&m_OverlayLock); +#else SDL_AtomicUnlock(&m_OverlayLock); +#endif // If there's no new staging overlay, free the old staging overlay texture. // NB: This is safe to do outside the overlay lock because we're guaranteed @@ -814,7 +875,11 @@ void PlVkRenderer::notifyOverlayUpdated(Overlay::OverlayType type) SDL_assert(newSurface->format->format == SDL_PIXELFORMAT_ARGB8888); pl_fmt texFormat = pl_find_named_fmt(m_Vulkan->gpu, "bgra8"); if (!texFormat) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "pl_find_named_fmt(bgra8) failed"); return; @@ -834,7 +899,11 @@ void PlVkRenderer::notifyOverlayUpdated(Overlay::OverlayType type) if (!pl_tex_recreate(m_Vulkan->gpu, &m_Overlays[type].stagingOverlay.tex, &texParams)) { pl_tex_destroy(m_Vulkan->gpu, &m_Overlays[type].stagingOverlay.tex); SDL_zero(m_Overlays[type].stagingOverlay); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "pl_tex_recreate() failed"); return; @@ -851,7 +920,11 @@ void PlVkRenderer::notifyOverlayUpdated(Overlay::OverlayType type) if (!pl_tex_upload(m_Vulkan->gpu, &xferParams)) { pl_tex_destroy(m_Vulkan->gpu, &m_Overlays[type].stagingOverlay.tex); SDL_zero(m_Overlays[type].stagingOverlay); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "pl_tex_upload() failed"); return; @@ -867,10 +940,18 @@ void PlVkRenderer::notifyOverlayUpdated(Overlay::OverlayType type) m_Overlays[type].stagingOverlay.color = pl_color_space_srgb; // Make this staging overlay visible to the render thread +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_LockSpinlock(&m_OverlayLock); +#else SDL_AtomicLock(&m_OverlayLock); +#endif SDL_assert(!m_Overlays[type].hasStagingOverlay); m_Overlays[type].hasStagingOverlay = true; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_UnlockSpinlock(&m_OverlayLock); +#else SDL_AtomicUnlock(&m_OverlayLock); +#endif } bool PlVkRenderer::notifyWindowChanged(PWINDOW_STATE_CHANGE_INFO info) diff --git a/app/streaming/video/ffmpeg-renderers/renderer.h b/app/streaming/video/ffmpeg-renderers/renderer.h index f0540ae41..e0121186c 100644 --- a/app/streaming/video/ffmpeg-renderers/renderer.h +++ b/app/streaming/video/ffmpeg-renderers/renderer.h @@ -1,6 +1,10 @@ #pragma once +#if HAVE_SDL3 +#include +#else #include +#endif #include "streaming/video/decoder.h" #include "streaming/video/overlaymanager.h" @@ -16,7 +20,11 @@ extern "C" { #ifdef HAVE_EGL #define MESA_EGL_NO_X11_HEADERS #define EGL_NO_X11 +#if HAVE_SDL3 +#include +#else #include +#endif #ifndef EGL_VERSION_1_5 typedef intptr_t EGLAttrib; diff --git a/app/streaming/video/ffmpeg-renderers/sdlvid.cpp b/app/streaming/video/ffmpeg-renderers/sdlvid.cpp index 66c5c9e5d..79e87d784 100644 --- a/app/streaming/video/ffmpeg-renderers/sdlvid.cpp +++ b/app/streaming/video/ffmpeg-renderers/sdlvid.cpp @@ -5,7 +5,9 @@ #include +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif SdlRenderer::SdlRenderer() : m_VideoFormat(0), @@ -100,6 +102,25 @@ bool SdlRenderer::initialize(PDECODER_PARAMETERS params) return false; } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "win32") == 0) { + // DWM is always tear-free except in full-screen exclusive mode + if (SDL_GetWindowFullscreenMode(params->window)) { + if (params->enableVsync) { + rendererFlags |= SDL_RENDERER_PRESENTVSYNC; + } + } + } + else if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) { + // Wayland is always tear-free in all modes + } + else { + // For other subsystems, just set SDL_RENDERER_PRESENTVSYNC if asked + if (params->enableVsync) { + rendererFlags |= SDL_RENDERER_PRESENTVSYNC; + } + } +#else SDL_SysWMinfo info; SDL_VERSION(&info.version); if (!SDL_GetWindowWMInfo(params->window, &info)) { @@ -131,6 +152,7 @@ bool SdlRenderer::initialize(PDECODER_PARAMETERS params) } break; } +#endif #ifdef Q_OS_WIN32 // We render on a different thread than the main thread which is handling window @@ -140,7 +162,11 @@ bool SdlRenderer::initialize(PDECODER_PARAMETERS params) SDL_SetHintWithPriority(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1", SDL_HINT_OVERRIDE); #endif +#if SDL_VERSION_ATLEAST(3, 0, 0) + m_Renderer = SDL_CreateRenderer(params->window, NULL, rendererFlags); +#else m_Renderer = SDL_CreateRenderer(params->window, -1, rendererFlags); +#endif if (!m_Renderer) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer() failed: %s", @@ -162,7 +188,11 @@ bool SdlRenderer::initialize(PDECODER_PARAMETERS params) else { // If we get here prior to the start of a session, just pump and flush ourselves. SDL_PumpEvents(); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FlushEvents(SDL_EVENT_WINDOW_FIRST, SDL_EVENT_WINDOW_LAST); +#else SDL_FlushEvent(SDL_WINDOWEVENT); +#endif } if (!params->testOnly) { @@ -199,7 +229,11 @@ void SdlRenderer::renderOverlay(Overlay::OverlayType type) if (type == Overlay::OverlayStatusUpdate) { // Bottom Left SDL_Rect viewportRect; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_GetRenderViewport(m_Renderer, &viewportRect); +#else SDL_RenderGetViewport(m_Renderer, &viewportRect); +#endif m_OverlayRects[type].x = 0; m_OverlayRects[type].y = viewportRect.h - newSurface->h; } @@ -213,12 +247,20 @@ void SdlRenderer::renderOverlay(Overlay::OverlayType type) m_OverlayRects[type].h = newSurface->h; m_OverlayTextures[type] = SDL_CreateTextureFromSurface(m_Renderer, newSurface); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif } // If we have an overlay texture, render it too if (m_OverlayTextures[type] != nullptr) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_RenderTexture(m_Renderer, m_OverlayTextures[type], nullptr, &m_OverlayRects[type]); +#else SDL_RenderCopy(m_Renderer, m_OverlayTextures[type], nullptr, &m_OverlayRects[type]); +#endif } } } @@ -272,6 +314,61 @@ void SdlRenderer::renderFrame(AVFrame* frame) } } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (m_Texture == nullptr) { + SDL_PropertiesID props = SDL_CreateProperties(); + + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STREAMING); + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, frame->width); + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, frame->height); + + // Remember to keep this in sync with SdlRenderer::isPixelFormatSupported()! + switch (frame->format) + { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, SDL_PIXELFORMAT_YV12); + break; + case AV_PIX_FMT_CUDA: + case AV_PIX_FMT_NV12: + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, SDL_PIXELFORMAT_NV12); + break; + case AV_PIX_FMT_NV21: + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, SDL_PIXELFORMAT_NV21); + break; + default: + SDL_assert(false); + goto Exit; + } + + switch (colorspace) + { + case COLORSPACE_REC_709: + SDL_assert(!isFrameFullRange(frame)); + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, SDL_COLORSPACE_BT709_FULL); + break; + case COLORSPACE_REC_601: + if (isFrameFullRange(frame)) { + // SDL's JPEG mode is Rec 601 Full Range + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, SDL_COLORSPACE_JPEG); + } + else { + SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, SDL_COLORSPACE_BT601_FULL); + } + break; + default: + break; + } + + m_Texture = SDL_CreateTextureWithProperties(m_Renderer, props); + + if (!m_Texture) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_CreateTextureWithProperties() failed: %s", + SDL_GetError()); + goto Exit; + } +#else if (m_Texture == nullptr) { Uint32 sdlFormat; @@ -318,12 +415,14 @@ void SdlRenderer::renderFrame(AVFrame* frame) SDL_TEXTUREACCESS_STREAMING, frame->width, frame->height); + if (!m_Texture) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateTexture() failed: %s", SDL_GetError()); goto Exit; } +#endif #ifdef HAVE_CUDA if (frame->format == AV_PIX_FMT_CUDA) { @@ -430,10 +529,23 @@ void SdlRenderer::renderFrame(AVFrame* frame) src.w = frame->width; src.h = frame->height; dst.x = dst.y = 0; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_GetCurrentRenderOutputSize(m_Renderer, &dst.w, &dst.h); +#else SDL_GetRendererOutputSize(m_Renderer, &dst.w, &dst.h); +#endif StreamUtils::scaleSourceToDestinationSurface(&src, &dst); // Ensure the viewport is set to the desired video region +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_SetRenderViewport(m_Renderer, &dst); + + // Draw the video content itself + SDL_RenderTexture(m_Renderer, m_Texture, nullptr, nullptr); + + // Reset the viewport to the full window for overlay rendering + SDL_SetRenderViewport(m_Renderer, nullptr); +#else SDL_RenderSetViewport(m_Renderer, &dst); // Draw the video content itself @@ -441,6 +553,7 @@ void SdlRenderer::renderFrame(AVFrame* frame) // Reset the viewport to the full window for overlay rendering SDL_RenderSetViewport(m_Renderer, nullptr); +#endif // Draw the overlays for (int i = 0; i < Overlay::OverlayMax; i++) { diff --git a/app/streaming/video/ffmpeg-renderers/sdlvid.h b/app/streaming/video/ffmpeg-renderers/sdlvid.h index 9a450bf22..b98b61c4c 100644 --- a/app/streaming/video/ffmpeg-renderers/sdlvid.h +++ b/app/streaming/video/ffmpeg-renderers/sdlvid.h @@ -27,7 +27,11 @@ class SdlRenderer : public IFFmpegRenderer { SDL_Texture* m_Texture; int m_ColorSpace; SDL_Texture* m_OverlayTextures[Overlay::OverlayMax]; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_FRect m_OverlayRects[Overlay::OverlayMax]; +#else SDL_Rect m_OverlayRects[Overlay::OverlayMax]; +#endif SwFrameMapper m_SwFrameMapper; diff --git a/app/streaming/video/ffmpeg-renderers/vaapi.cpp b/app/streaming/video/ffmpeg-renderers/vaapi.cpp index ff76be39e..afa7e3547 100644 --- a/app/streaming/video/ffmpeg-renderers/vaapi.cpp +++ b/app/streaming/video/ffmpeg-renderers/vaapi.cpp @@ -11,7 +11,9 @@ #include #endif +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #include #include @@ -78,6 +80,106 @@ VAAPIRenderer::~VAAPIRenderer() VADisplay VAAPIRenderer::openDisplay(SDL_Window* window) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + VADisplay display; + + const char *videoDriver = SDL_GetCurrentVideoDriver(); + if (videoDriver == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unable to find SDL video driver for VAAPI"); + return nullptr; + } + + if (SDL_strcmp(videoDriver, "x11") == 0) { +#ifdef HAVE_LIBVA_X11 + m_XWindow = (Window)SDL_GetNumberProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); + Display *xdisplay = (Display *)SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL); + display = vaGetDisplay(xdisplay); + if (display == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open X11 display for VAAPI"); + return nullptr; + } +#else + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Moonlight not compiled with VAAPI X11 support!"); + return nullptr; +#endif + } + else if (SDL_strcmp(videoDriver, "wayland") == 0) { +#ifdef HAVE_LIBVA_WAYLAND + struct wl_display *wldisplay = (struct wl_display *)SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL); + display = vaGetDisplayWl(wldisplay); + if (display == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open Wayland display for VAAPI"); + return nullptr; + } +#else + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Moonlight not compiled with VAAPI Wayland support!"); + return nullptr; +#endif + } + else if (SDL_strcmp(videoDriver, "kmsdrm") == 0) { +#ifdef HAVE_LIBVA_DRM + int drmFd = SDL_GetNumberProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_KMSDRM_DRM_FD_NUMBER, 0); + SDL_assert(drmFd >= 0); + + // It's possible to enter this function several times as we're probing VA drivers. + // Make sure to only duplicate the DRM FD the first time through. + if (m_DrmFd < 0) { + // If the KMSDRM FD is not a render node FD, open the render node for libva to use. + // Since libva 2.20, using a primary node will fail in vaGetDriverNames(). + if (drmGetNodeTypeFromFd(drmFd) != DRM_NODE_RENDER) { + char* renderNodePath = drmGetRenderDeviceNameFromFd(drmFd); + if (renderNodePath) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Opening render node for VAAPI: %s", + renderNodePath); + m_DrmFd = open(renderNodePath, O_RDWR | O_CLOEXEC); + free(renderNodePath); + if (m_DrmFd < 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to open render node: %d", + errno); + return nullptr; + } + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Failed to get render node path. Using the SDL FD directly."); + m_DrmFd = dup(drmFd); + } + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "KMSDRM FD is already a render node. Using the SDL FD directly."); + m_DrmFd = dup(drmFd); + } + } + + display = vaGetDisplayDRM(m_DrmFd); + if (display == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open DRM display for VAAPI"); + return nullptr; + } +#else + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Moonlight not compiled with VAAPI DRM support!"); + return nullptr; +#endif + } + else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unsupported VAAPI rendering subsystem: %s", + videoDriver); + return nullptr; + } + + return display; +#else SDL_SysWMinfo info; VADisplay display; @@ -173,6 +275,7 @@ VAAPIRenderer::openDisplay(SDL_Window* window) } return display; +#endif } VAStatus @@ -256,7 +359,11 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params) status = tryVaInitialize(vaDeviceContext, params, &major, &minor); } +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (status != VA_STATUS_SUCCESS && ((SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) || m_DecoderSelectionPass > 0)) { +#else if (status != VA_STATUS_SUCCESS && (m_WindowSystem != SDL_SYSWM_X11 || m_DecoderSelectionPass > 0)) { +#endif // The unofficial nvidia VAAPI driver over NVDEC/CUDA works well on Wayland, // but we'd rather use CUDA for XWayland and VDPAU for regular X11. // NB: Remember to update the VA-API NVDEC condition below when modifying this! @@ -347,7 +454,11 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params) } // Prefer CUDA for XWayland and VDPAU for regular X11. +#if SDL_VERSION_ATLEAST(3, 0, 0) + if ((SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) && vendorStr.contains("VA-API NVDEC", Qt::CaseInsensitive)) { +#else if (m_WindowSystem == SDL_SYSWM_X11 && vendorStr.contains("VA-API NVDEC", Qt::CaseInsensitive)) { +#endif SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Deprioritizing VAAPI for NVIDIA driver on X11/XWayland. Set FORCE_VAAPI=1 to override."); return false; @@ -497,7 +608,11 @@ VAAPIRenderer::isDirectRenderingSupported() } // We only support direct rendering on X11 with VAEntrypointVideoProc support +#if SDL_VERSION_ATLEAST(3, 0, 0) + if ((SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) || m_BlacklistedForDirectRendering) { +#else if (m_WindowSystem != SDL_SYSWM_X11 || m_BlacklistedForDirectRendering) { +#endif SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Using indirect rendering due to WM or blacklist"); return false; @@ -601,7 +716,11 @@ void VAAPIRenderer::notifyOverlayUpdated(Overlay::OverlayType type) } if (!overlayEnabled) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif return; } @@ -615,7 +734,11 @@ void VAAPIRenderer::notifyOverlayUpdated(Overlay::OverlayType type) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "vaCreateImage() failed: %d", status); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif return; } @@ -625,7 +748,11 @@ void VAAPIRenderer::notifyOverlayUpdated(Overlay::OverlayType type) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "vaMapBuffer() failed: %d", status); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif vaDestroyImage(vaDeviceContext->display, newImage.image_id); return; } @@ -640,7 +767,11 @@ void VAAPIRenderer::notifyOverlayUpdated(Overlay::OverlayType type) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "vaUnmapBuffer() failed: %d", status); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif vaDestroyImage(vaDeviceContext->display, newImage.image_id); return; } @@ -662,7 +793,11 @@ void VAAPIRenderer::notifyOverlayUpdated(Overlay::OverlayType type) overlayRect.h = newSurface->h; // Surface data is no longer needed +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif VASubpictureID newSubpicture; status = vaCreateSubpicture(vaDeviceContext->display, newImage.image_id, &newSubpicture); @@ -708,7 +843,11 @@ VAAPIRenderer::renderFrame(AVFrame* frame) StreamUtils::scaleSourceToDestinationSurface(&src, &dst); +#if SDL_VERSION_ATLEAST(3, 0, 0) + if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) { +#else if (m_WindowSystem == SDL_SYSWM_X11) { +#endif #ifdef HAVE_LIBVA_X11 unsigned int flags = 0; @@ -844,7 +983,11 @@ VAAPIRenderer::renderFrame(AVFrame* frame) SDL_UnlockMutex(m_OverlayMutex); #endif } +#if SDL_VERSION_ATLEAST(3, 0, 0) + else if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) { +#else else if (m_WindowSystem == SDL_SYSWM_WAYLAND) { +#endif // We don't support direct rendering on Wayland, so we should // never get called there. Many common Wayland compositors don't // support YUV surfaces, so direct rendering would fail. diff --git a/app/streaming/video/ffmpeg-renderers/vaapi.h b/app/streaming/video/ffmpeg-renderers/vaapi.h index ad69744ef..9f41baaab 100644 --- a/app/streaming/video/ffmpeg-renderers/vaapi.h +++ b/app/streaming/video/ffmpeg-renderers/vaapi.h @@ -2,6 +2,8 @@ #include "renderer.h" +#ifndef HAVE_SDL3 + // Avoid X11 if SDL was built without it #if !defined(SDL_VIDEO_DRIVER_X11) && defined(HAVE_LIBVA_X11) #warning Unable to use libva-x11 without SDL X11 backend @@ -32,6 +34,8 @@ #undef HAVE_LIBVA_DRM #endif +#endif + #ifdef HAVE_EGL #include "eglimagefactory.h" #endif @@ -92,12 +96,18 @@ class VAAPIRenderer : public IFFmpegRenderer #endif int m_DecoderSelectionPass; +#if !SDL_VERSION_ATLEAST(3, 0, 0) int m_WindowSystem; +#endif AVBufferRef* m_HwContext; bool m_BlacklistedForDirectRendering; bool m_HasRfiLatencyBug; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Mutex* m_OverlayMutex; +#else SDL_mutex* m_OverlayMutex; +#endif VAImageFormat m_OverlayFormat; Uint32 m_OverlaySdlPixelFormat; VAImage m_OverlayImage[Overlay::OverlayMax]; diff --git a/app/streaming/video/ffmpeg-renderers/vdpau.cpp b/app/streaming/video/ffmpeg-renderers/vdpau.cpp index 68b43db94..b7e4bb254 100644 --- a/app/streaming/video/ffmpeg-renderers/vdpau.cpp +++ b/app/streaming/video/ffmpeg-renderers/vdpau.cpp @@ -3,7 +3,9 @@ #include #include +#if !SDL_VERSION_ATLEAST(3, 0, 0) #include +#endif #define BAIL_ON_FAIL(status, something) if ((status) != VDP_STATUS_OK) { \ SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \ @@ -77,7 +79,6 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params) { int err; VdpStatus status; - SDL_SysWMinfo info; static const char* driverPathsToTry[] = { #if Q_PROCESSOR_WORDSIZE == 8 "/usr/lib64", @@ -93,7 +94,30 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params) "/usr/lib/i386-linux-gnu/vdpau", // Ubuntu/Debian i386 #endif }; - +#if SDL_VERSION_ATLEAST(3, 0, 0) + const char *videoDriver = SDL_GetCurrentVideoDriver(); + if (SDL_strcmp(videoDriver, "wayland") == 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "VDPAU is not supported on Wayland"); + return false; + } + else if (SDL_strcmp(videoDriver, "x11") != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "VDPAU is not supported on the current subsystem: %s", + videoDriver); + return false; + } + else if (qgetenv("VDPAU_XWAYLAND") != "1" && WMUtils::isRunningWayland()) { + // VDPAU initialization causes Moonlight to crash when using XWayland in a Flatpak + // on a system with the Nvidia 495.44 driver. VDPAU won't work under XWayland anyway, + // so let's not risk trying it (unless the user wants to roll the dice). + // https://gitlab.freedesktop.org/vdpau/libvdpau/-/issues/2 + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "VDPAU is disabled on XWayland. Set VDPAU_XWAYLAND=1 to try your luck."); + return false; + } +#else + SDL_SysWMinfo info; SDL_VERSION(&info.version); if (!SDL_GetWindowWMInfo(params->window, &info)) { @@ -123,6 +147,7 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params) "VDPAU is disabled on XWayland. Set VDPAU_XWAYLAND=1 to try your luck."); return false; } +#endif m_VideoWidth = params->width; m_VideoHeight = params->height; @@ -189,12 +214,21 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params) SDL_GetWindowSize(params->window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_assert(SDL_strcmp(videoDriver, "x11") == 0); + Window xwindow = (Window)SDL_GetNumberProperty(SDL_GetWindowProperties(params->window), SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); +#else SDL_assert(info.subsystem == SDL_SYSWM_X11); +#endif GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11, &m_VdpPresentationQueueTargetCreateX11); status = m_VdpPresentationQueueTargetCreateX11(m_Device, +#if SDL_VERSION_ATLEAST(3, 0, 0) + xwindow, +#else info.info.x11.window, +#endif &m_PresentationQueueTarget); if (status != VDP_STATUS_OK) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, @@ -360,7 +394,11 @@ void VDPAURenderer::notifyOverlayUpdated(Overlay::OverlayType type) } if (!overlayEnabled) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif return; } @@ -379,7 +417,11 @@ void VDPAURenderer::notifyOverlayUpdated(Overlay::OverlayType type) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpBitmapSurfaceCreate() failed: %s", m_VdpGetErrorString(status)); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif return; } @@ -392,7 +434,11 @@ void VDPAURenderer::notifyOverlayUpdated(Overlay::OverlayType type) "VdpBitmapSurfacePutBitsNative() failed: %s", m_VdpGetErrorString(status)); m_VdpBitmapSurfaceDestroy(newBitmapSurface); +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif return; } @@ -413,7 +459,11 @@ void VDPAURenderer::notifyOverlayUpdated(Overlay::OverlayType type) overlayRect.y1 = overlayRect.y0 + newSurface->h; // Surface data is no longer needed +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(newSurface); +#else SDL_FreeSurface(newSurface); +#endif SDL_LockMutex(m_OverlayMutex); m_OverlaySurface[type] = newBitmapSurface; diff --git a/app/streaming/video/ffmpeg-renderers/vdpau.h b/app/streaming/video/ffmpeg-renderers/vdpau.h index 0e8fc957d..7364a492b 100644 --- a/app/streaming/video/ffmpeg-renderers/vdpau.h +++ b/app/streaming/video/ffmpeg-renderers/vdpau.h @@ -38,7 +38,11 @@ class VDPAURenderer : public IFFmpegRenderer // This is fine because the majority of time spent in the mutex // is by the render thread, which cannot contend with itself // because overlays are rendered sequentially. +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_Mutex* m_OverlayMutex; +#else SDL_mutex* m_OverlayMutex; +#endif VdpBitmapSurface m_OverlaySurface[Overlay::OverlayMax]; VdpRect m_OverlayRect[Overlay::OverlayMax]; VdpOutputSurfaceRenderBlendState m_OverlayBlendState; diff --git a/app/streaming/video/ffmpeg.cpp b/app/streaming/video/ffmpeg.cpp index 6ecc6c88f..6d4e7ca99 100644 --- a/app/streaming/video/ffmpeg.cpp +++ b/app/streaming/video/ffmpeg.cpp @@ -55,6 +55,11 @@ extern "C" { #define FAILED_DECODES_RESET_THRESHOLD 20 +// SDL_TICKS_PASSED is removed in SDL3 +#if SDL_VERSION_ATLEAST(3, 0, 0) +#define SDL_TICKS_PASSED(A, B) ((Sint32)((B) - (A)) <= 0) +#endif + // Note: This is NOT an exhaustive list of all decoders // that Moonlight could pick. It will pick any working // decoder that matches the codec ID and outputs one of @@ -1507,7 +1512,11 @@ void FFmpegVideoDecoder::decoderThreadProc() "Resetting decoder due to consistent failure"); SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_RENDER_DEVICE_RESET; +#else event.type = SDL_RENDER_DEVICE_RESET; +#endif SDL_PushEvent(&event); // Don't consume any additional data @@ -1633,7 +1642,11 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du) "Resetting decoder due to consistent failure"); SDL_Event event; +#if SDL_VERSION_ATLEAST(3, 0, 0) + event.type = SDL_EVENT_RENDER_DEVICE_RESET; +#else event.type = SDL_RENDER_DEVICE_RESET; +#endif SDL_PushEvent(&event); // Don't consume any additional data diff --git a/app/streaming/video/ffmpeg.h b/app/streaming/video/ffmpeg.h index eb3c1812b..ea35ae6fa 100644 --- a/app/streaming/video/ffmpeg.h +++ b/app/streaming/video/ffmpeg.h @@ -93,7 +93,11 @@ class FFmpegVideoDecoder : public IVideoDecoder { bool m_NeedsSpsFixup; bool m_TestOnly; SDL_Thread* m_DecoderThread; +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_AtomicInt m_DecoderThreadShouldQuit; +#else SDL_atomic_t m_DecoderThreadShouldQuit; +#endif // Data buffers in the queued DU are not valid QQueue m_FrameInfoQueue; diff --git a/app/streaming/video/overlaymanager.cpp b/app/streaming/video/overlaymanager.cpp index 168e52330..5211e3b8d 100644 --- a/app/streaming/video/overlaymanager.cpp +++ b/app/streaming/video/overlaymanager.cpp @@ -32,7 +32,11 @@ OverlayManager::~OverlayManager() { for (int i = 0; i < OverlayType::OverlayMax; i++) { if (m_Overlays[i].surface != nullptr) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(m_Overlays[i].surface); +#else SDL_FreeSurface(m_Overlays[i].surface); +#endif } if (m_Overlays[i].font != nullptr) { TTF_CloseFont(m_Overlays[i].font); @@ -150,7 +154,11 @@ void OverlayManager::notifyOverlayUpdated(OverlayType type) // Free the old surface if (oldSurface != nullptr) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_DestroySurface(oldSurface); +#else SDL_FreeSurface(oldSurface); +#endif } if (m_Overlays[type].enabled) { diff --git a/app/streaming/video/overlaymanager.h b/app/streaming/video/overlaymanager.h index 59c808b92..9bcb58a05 100644 --- a/app/streaming/video/overlaymanager.h +++ b/app/streaming/video/overlaymanager.h @@ -2,8 +2,13 @@ #include +#if HAVE_SDL3 +#include +#include +#else #include #include +#endif namespace Overlay { diff --git a/app/wm.cpp b/app/wm.cpp index c244a3770..9517711cd 100644 --- a/app/wm.cpp +++ b/app/wm.cpp @@ -2,7 +2,11 @@ #include "utils.h" +#if HAVE_SDL3 +#include +#else #include +#endif #ifdef HAS_X11 #include @@ -18,7 +22,11 @@ bool WMUtils::isRunningX11() { #ifdef HAS_X11 +#if SDL_VERSION_ATLEAST(3, 0, 0) + static SDL_AtomicInt isRunningOnX11; +#else static SDL_atomic_t isRunningOnX11; +#endif // If the value is not set yet, populate it now. int val = SDL_AtomicGet(&isRunningOnX11); @@ -44,7 +52,11 @@ bool WMUtils::isRunningX11() bool WMUtils::isRunningWayland() { #ifdef HAS_WAYLAND +#if SDL_VERSION_ATLEAST(3, 0, 0) + static SDL_AtomicInt isRunningOnWayland; +#else static SDL_atomic_t isRunningOnWayland; +#endif // If the value is not set yet, populate it now. int val = SDL_AtomicGet(&isRunningOnWayland); diff --git a/config.tests/EGL/main.cpp b/config.tests/EGL/main.cpp index 1ad9236d0..85a34be71 100644 --- a/config.tests/EGL/main.cpp +++ b/config.tests/EGL/main.cpp @@ -1,7 +1,12 @@ #define SDL_USE_BUILTIN_OPENGL_DEFINITIONS 1 +#if HAVE_SDL3 +#include +#include +#else #include #include +#endif #ifndef EGL_VERSION_1_4 #error EGLRenderer requires EGL 1.4