#include "session.h" #include "settings/streamingpreferences.h" #include "streaming/streamutils.h" #include "backend/richpresencemanager.h" #include #include #include "utils.h" #ifdef HAVE_FFMPEG #include "video/ffmpeg.h" #endif #ifdef HAVE_SLVIDEO #include "video/slvid.h" #endif #ifdef Q_OS_WIN32 // Scaling the icon down on Win32 looks dreadful, so render at lower res #define ICON_SIZE 32 #else #define ICON_SIZE 64 #endif #define SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER 100 #define SDL_CODE_GAMECONTROLLER_RUMBLE 101 #include #include #include #include #include #include #include #include #include #define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org" CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { Session::clStageStarting, nullptr, Session::clStageFailed, nullptr, Session::clConnectionTerminated, Session::clLogMessage, Session::clRumble, Session::clConnectionStatusUpdate, Session::clSetHdrMode, }; Session* Session::s_ActiveSession; QSemaphore Session::s_ActiveSessionSemaphore(1); void Session::clStageStarting(int stage) { // We know this is called on the same thread as LiStartConnection() // which happens to be the main thread, so it's cool to interact // with the GUI in these callbacks. emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage))); } void Session::clStageFailed(int stage, int errorCode) { // Perform the port test now, while we're on the async connection thread and not blocking the UI. unsigned int portFlags = LiGetPortFlagsFromStage(stage); s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags); char failingPorts[128]; LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts)); emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts)); } void Session::clConnectionTerminated(int errorCode) { unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode); s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags); // Display the termination dialog if this was not intended switch (errorCode) { case ML_ERROR_GRACEFUL_TERMINATION: break; case ML_ERROR_NO_VIDEO_TRAFFIC: s_ActiveSession->m_UnexpectedTermination = true; char ports[128]; SDL_assert(portFlags != 0); LiStringifyPortFlags(portFlags, ", ", ports, sizeof(ports)); emit s_ActiveSession->displayLaunchError(tr("No video received from host.") + "\n\n"+ tr("Check your firewall and port forwarding rules for port(s): %1").arg(ports)); break; case ML_ERROR_NO_VIDEO_FRAME: s_ActiveSession->m_UnexpectedTermination = true; emit s_ActiveSession->displayLaunchError(tr("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.")); break; case ML_ERROR_UNEXPECTED_EARLY_TERMINATION: s_ActiveSession->m_UnexpectedTermination = true; emit s_ActiveSession->displayLaunchError(tr("Something went wrong on your host PC when starting the stream.") + "\n\n" + tr("Make sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.") + "\n\n" + tr("If the issue persists, try reinstalling your GPU drivers and GeForce Experience.")); break; default: s_ActiveSession->m_UnexpectedTermination = true; emit s_ActiveSession->displayLaunchError(tr("Connection terminated")); break; } SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Connection terminated: %d", errorCode); // Push a quit event to the main loop SDL_Event event; event.type = SDL_QUIT; event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); } void Session::clLogMessage(const char* format, ...) { va_list ap; va_start(ap, format); SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, format, ap); va_end(ap); } void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { // We push an event for the main thread to handle in order to properly synchronize // with the removal of game controllers that could result in our game controller // going away during this callback. SDL_Event rumbleEvent = {}; rumbleEvent.type = SDL_USEREVENT; rumbleEvent.user.code = SDL_CODE_GAMECONTROLLER_RUMBLE; rumbleEvent.user.data1 = (void*)(uintptr_t)controllerNumber; rumbleEvent.user.data2 = (void*)(uintptr_t)((lowFreqMotor << 16) | highFreqMotor); SDL_PushEvent(&rumbleEvent); } void Session::clConnectionStatusUpdate(int connectionStatus) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Connection status update: %d", connectionStatus); if (!s_ActiveSession->m_Preferences->connectionWarnings) { return; } if (s_ActiveSession->m_MouseEmulationRefCount > 0) { // Don't display the overlay if mouse emulation is already using it return; } switch (connectionStatus) { case CONN_STATUS_POOR: if (s_ActiveSession->m_StreamConfig.bitrate > 5000) { strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Slow connection to PC\nReduce your bitrate"); } else { strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Poor connection to PC"); } s_ActiveSession->m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate); s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); break; case CONN_STATUS_OKAY: s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); break; } } 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_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) { IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder; if (decoder != nullptr) { decoder->setHdrMode(enabled); } SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock); } } bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, SDL_Window* window, int videoFormat, int width, int height, int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder) { DECODER_PARAMETERS params; // We should never have vsync enabled for test-mode. // It introduces unnecessary delay for renderers that may // block while waiting for a backbuffer swap. SDL_assert(!enableVsync || !testOnly); params.width = width; params.height = height; params.frameRate = frameRate; params.videoFormat = videoFormat; params.window = window; params.enableVsync = enableVsync; params.enableFramePacing = enableFramePacing; params.vds = vds; memset(¶ms.hdrMetadata, 0, sizeof(params.hdrMetadata)); params.hdrMetadata.eotf = 2; // SMPTE ST 2084 params.hdrMetadata.staticMetadataDescriptorId = 0; // Static Metadata Type 1 SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "V-sync %s", enableVsync ? "enabled" : "disabled"); #ifdef HAVE_SLVIDEO chosenDecoder = new SLVideoDecoder(testOnly); if (chosenDecoder->initialize(¶ms)) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "SLVideo video decoder chosen"); return true; } else { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to load SLVideo decoder"); delete chosenDecoder; chosenDecoder = nullptr; } #endif #ifdef HAVE_FFMPEG chosenDecoder = new FFmpegVideoDecoder(testOnly); if (chosenDecoder->initialize(¶ms)) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "FFmpeg-based video decoder chosen"); return true; } else { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to load FFmpeg decoder"); delete chosenDecoder; chosenDecoder = nullptr; } #endif #if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO) #error No video decoding libraries available! #endif // If we reach this, we didn't initialize any decoders successfully return false; } int Session::drSetup(int videoFormat, int width, int height, int frameRate, void *, int) { s_ActiveSession->m_ActiveVideoFormat = videoFormat; s_ActiveSession->m_ActiveVideoWidth = width; s_ActiveSession->m_ActiveVideoHeight = height; s_ActiveSession->m_ActiveVideoFrameRate = frameRate; // Defer decoder setup until we've started streaming so we // don't have to hide and show the SDL window (which seems to // cause pointer hiding to break on Windows). SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Video stream is %dx%dx%d (format 0x%x)", width, height, frameRate, videoFormat); return 0; } int Session::drSubmitDecodeUnit(PDECODE_UNIT du) { // Use a lock since we'll be yanking this decoder out // from underneath the session when we initiate destruction. // We need to destroy the decoder on the main thread to satisfy // some API constraints (like DXVA2). If we can't acquire it, // that means the decoder is about to be destroyed, so we can // safely return DR_OK and wait for m_NeedsIdr to be set by // the decoder reinitialization code. if (s_ActiveSession->getAndClearPendingIdrFrameStatus()) { // If we reset our decoder, we'll need to request an IDR frame return DR_NEED_IDR; } if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) { IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder; if (decoder != nullptr) { int ret = decoder->submitDecodeUnit(du); SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock); return ret; } else { SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock); return DR_OK; } } else { // Decoder is going away. Ignore anything coming in until // the lock is released. return DR_OK; } } void Session::getDecoderInfo(SDL_Window* window, bool& isHardwareAccelerated, bool& isFullScreenOnly, bool& isHdrSupported, QSize& maxResolution) { IVideoDecoder* decoder; // Try an HEVC Main10 decoder first to see if we have HDR support if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE, window, VIDEO_FORMAT_H265_MAIN10, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); isFullScreenOnly = decoder->isAlwaysFullScreen(); isHdrSupported = decoder->isHdrSupported(); maxResolution = decoder->getDecoderMaxResolution(); delete decoder; return; } // HDR can only be supported by a hardware codec that can handle HEVC Main10. // If we made it this far, we don't have one, so HDR will not be available. isHdrSupported = false; // Try a regular hardware accelerated HEVC decoder now if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE, window, VIDEO_FORMAT_H265, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); isFullScreenOnly = decoder->isAlwaysFullScreen(); maxResolution = decoder->getDecoderMaxResolution(); delete decoder; return; } // If we still didn't find a hardware decoder, try H.264 now. // This will fall back to software decoding, so it should always work. if (chooseDecoder(StreamingPreferences::VDS_AUTO, window, VIDEO_FORMAT_H264, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); isFullScreenOnly = decoder->isAlwaysFullScreen(); maxResolution = decoder->getDecoderMaxResolution(); delete decoder; return; } SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to find ANY working H.264 or HEVC decoder!"); } bool Session::isHardwareDecodeAvailable(SDL_Window* window, StreamingPreferences::VideoDecoderSelection vds, int videoFormat, int width, int height, int frameRate) { IVideoDecoder* decoder; if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) { return false; } bool ret = decoder->isHardwareAccelerated(); delete decoder; return ret; } bool Session::populateDecoderProperties(SDL_Window* window) { IVideoDecoder* decoder; if (!chooseDecoder(m_Preferences->videoDecoderSelection, window, m_StreamConfig.enableHdr ? VIDEO_FORMAT_H265_MAIN10 : (m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264), m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps, false, false, true, decoder)) { return false; } m_VideoCallbacks.capabilities = decoder->getDecoderCapabilities(); if (m_VideoCallbacks.capabilities & CAPABILITY_PULL_RENDERER) { // It is an error to pass a push callback when in pull mode m_VideoCallbacks.submitDecodeUnit = nullptr; } else { m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit; } { bool ok; m_StreamConfig.colorSpace = qEnvironmentVariableIntValue("COLOR_SPACE_OVERRIDE", &ok); if (ok) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Using colorspace override: %d", m_StreamConfig.colorSpace); } else { m_StreamConfig.colorSpace = decoder->getDecoderColorspace(); } m_StreamConfig.colorRange = qEnvironmentVariableIntValue("COLOR_RANGE_OVERRIDE", &ok); if (ok) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Using color range override: %d", m_StreamConfig.colorRange); } else { // Limited is the default for GFE m_StreamConfig.colorRange = COLOR_RANGE_LIMITED; } } if (decoder->isAlwaysFullScreen()) { m_IsFullScreen = true; } delete decoder; return true; } Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences) : m_Preferences(preferences ? preferences : new StreamingPreferences(this)), m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()), m_Computer(computer), m_App(app), m_Window(nullptr), m_VideoDecoder(nullptr), m_DecoderLock(0), m_AudioDisabled(false), m_AudioMuted(false), m_DisplayOriginX(0), m_DisplayOriginY(0), m_PendingWindowedTransition(false), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected m_InputHandler(nullptr), m_MouseEmulationRefCount(0), m_FlushingWindowEventsRef(0), m_AsyncConnectionSuccess(false), m_PortTestResults(0), m_OpusDecoder(nullptr), m_AudioRenderer(nullptr), m_AudioSampleCount(0), m_DropAudioEndTime(0) { SDL_AtomicSet(&m_NeedsIdr, 0); } bool Session::initialize() { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", SDL_GetError()); return false; } // Create a hidden window to use for decoder initialization tests SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags()); if (!testWindow) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to create test window with platform flags: %s", SDL_GetError()); testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN); if (!testWindow) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create window for hardware decode test: %s", SDL_GetError()); SDL_QuitSubSystem(SDL_INIT_VIDEO); return false; } } qInfo() << "Server GPU:" << m_Computer->gpuModel; qInfo() << "Server GFE version:" << m_Computer->gfeVersion; LiInitializeVideoCallbacks(&m_VideoCallbacks); m_VideoCallbacks.setup = drSetup; LiInitializeStreamConfiguration(&m_StreamConfig); m_StreamConfig.width = m_Preferences->width; m_StreamConfig.height = m_Preferences->height; m_StreamConfig.fps = m_Preferences->fps; m_StreamConfig.bitrate = m_Preferences->bitrateKbps; m_StreamConfig.hevcBitratePercentageMultiplier = 75; #ifndef STEAM_LINK // Enable audio encryption as long as we're not on Steam Link. // That hardware can hardly handle Opus decoding at all. m_StreamConfig.encryptionFlags = ENCFLG_AUDIO; #endif SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Video bitrate: %d kbps", m_StreamConfig.bitrate); RAND_bytes(reinterpret_cast(m_StreamConfig.remoteInputAesKey), sizeof(m_StreamConfig.remoteInputAesKey)); // Only the first 4 bytes are populated in the RI key IV RAND_bytes(reinterpret_cast(m_StreamConfig.remoteInputAesIv), 4); switch (m_Preferences->audioConfig) { case StreamingPreferences::AC_STEREO: m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO; break; case StreamingPreferences::AC_51_SURROUND: m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND; break; case StreamingPreferences::AC_71_SURROUND: m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND; break; } LiInitializeAudioCallbacks(&m_AudioCallbacks); m_AudioCallbacks.init = arInit; m_AudioCallbacks.cleanup = arCleanup; m_AudioCallbacks.decodeAndPlaySample = arDecodeAndPlaySample; m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Audio channel count: %d", CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration)); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Audio channel mask: %X", CHANNEL_MASK_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration)); switch (m_Preferences->videoCodecConfig) { case StreamingPreferences::VCC_AUTO: // TODO: Determine if HEVC is better depending on the decoder m_StreamConfig.supportsHevc = isHardwareDecodeAvailable(testWindow, m_Preferences->videoDecoderSelection, VIDEO_FORMAT_H265, m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps); #ifdef Q_OS_DARWIN { // Prior to GFE 3.11, GFE did not allow us to constrain // the number of reference frames, so we have to fixup the SPS // to allow decoding via VideoToolbox on macOS. Since we don't // have fixup code for HEVC, just avoid it if GFE is too old. QVector gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion); if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all gfeVersion[0] < 3 || (gfeVersion[0] == 3 && gfeVersion[1] < 11)) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Disabling HEVC on macOS due to old GFE version"); m_StreamConfig.supportsHevc = false; } } #endif m_StreamConfig.enableHdr = false; break; case StreamingPreferences::VCC_FORCE_H264: m_StreamConfig.supportsHevc = false; m_StreamConfig.enableHdr = false; break; case StreamingPreferences::VCC_FORCE_HEVC: m_StreamConfig.supportsHevc = true; m_StreamConfig.enableHdr = false; break; case StreamingPreferences::VCC_FORCE_HEVC_HDR: m_StreamConfig.supportsHevc = true; m_StreamConfig.enableHdr = true; break; } switch (m_Preferences->windowMode) { default: case StreamingPreferences::WM_FULLSCREEN_DESKTOP: // Only use full-screen desktop mode if we're running a desktop environment if (WMUtils::isRunningDesktopEnvironment()) { m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP; break; } // Fall-through case StreamingPreferences::WM_FULLSCREEN: m_FullScreenFlag = SDL_WINDOW_FULLSCREEN; break; } #if !SDL_VERSION_ATLEAST(2, 0, 11) // HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE // GUI environment. Force the session to use windowed mode (which won't // really matter anyway because the MMAL renderer always draws full-screen). if (qgetenv("DESKTOP_SESSION") == "LXDE-pi") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Forcing windowed mode on LXDE-Pi"); m_FullScreenFlag = 0; } #endif // Check for validation errors/warnings and emit // signals for them, if appropriate bool ret = validateLaunch(testWindow); if (ret) { // Populate decoder-dependent properties. // Must be done after validateLaunch() since m_StreamConfig is finalized. ret = populateDecoderProperties(testWindow); } SDL_DestroyWindow(testWindow); if (!ret) { SDL_QuitSubSystem(SDL_INIT_VIDEO); return false; } return true; } void Session::emitLaunchWarning(QString text) { // Emit the warning to the UI emit displayLaunchWarning(text); // Wait a little bit so the user can actually read what we just said. // This wait is a little longer than the actual toast timeout (3 seconds) // to allow it to transition off the screen before continuing. uint32_t start = SDL_GetTicks(); while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) { SDL_Delay(5); if (!m_ThreadedExec) { // Pump the UI loop while we wait if we're on the main thread QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); QCoreApplication::sendPostedEvents(); } } } bool Session::validateLaunch(SDL_Window* testWindow) { if (!m_Computer->isSupportedServerVersion) { emit displayLaunchError(tr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(m_Computer->name)); return false; } if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) { emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games.")); } if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) { if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) { emitLaunchWarning(tr("HDR is not supported with software decoding.")); m_StreamConfig.enableHdr = false; } else { emitLaunchWarning(tr("Your settings selection to force software decoding may cause poor streaming performance.")); } } if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) { emitLaunchWarning(tr("Using unsupported FPS options may cause stuttering or lag.")); if (m_Preferences->enableVsync) { emitLaunchWarning(tr("V-sync will be disabled when streaming at a higher frame rate than the display.")); } } if (m_StreamConfig.supportsHevc) { bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC || m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR; if (m_Computer->maxLumaPixelsHEVC == 0) { if (hevcForced) { emitLaunchWarning(tr("Your host PC GPU doesn't support HEVC. " "A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.")); } // Moonlight-common-c will handle this case already, but we want // to set this explicitly here so we can do our hardware acceleration // check below. m_StreamConfig.supportsHevc = false; } else if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && // Force hardware decoding checked below hevcForced && // Auto VCC is already checked in initialize() !isHardwareDecodeAvailable(testWindow, m_Preferences->videoDecoderSelection, VIDEO_FORMAT_H265, m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps)) { emitLaunchWarning(tr("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.")); } } if (!m_StreamConfig.supportsHevc && m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && !isHardwareDecodeAvailable(testWindow, m_Preferences->videoDecoderSelection, VIDEO_FORMAT_H264, m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps)) { if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_H264) { emitLaunchWarning(tr("Using software decoding due to your selection to force H.264 without GPU support. This may cause poor streaming performance.")); } else { if (m_Computer->maxLumaPixelsHEVC == 0 && isHardwareDecodeAvailable(testWindow, m_Preferences->videoDecoderSelection, VIDEO_FORMAT_H265, m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps)) { emitLaunchWarning(tr("Your host PC and client PC don't support the same video codecs. This may cause poor streaming performance.")); } else { emitLaunchWarning(tr("Your client GPU doesn't support H.264 decoding. This may cause poor streaming performance.")); } } } if (m_StreamConfig.enableHdr) { // Turn HDR back off unless all criteria are met. m_StreamConfig.enableHdr = false; // Check that the server GPU supports HDR if (!(m_Computer->serverCodecModeSupport & 0x200)) { emitLaunchWarning(tr("Your host PC GPU doesn't support HDR streaming. " "A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.")); } else if (!isHardwareDecodeAvailable(testWindow, m_Preferences->videoDecoderSelection, VIDEO_FORMAT_H265_MAIN10, m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps)) { emitLaunchWarning(tr("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.")); } else { // TODO: Also validate display capabilites // Validation successful so HDR is good to go m_StreamConfig.enableHdr = true; } } if (m_StreamConfig.width >= 3840) { // Only allow 4K on GFE 3.x+ if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) { emitLaunchWarning(tr("GeForce Experience 3.0 or higher is required for 4K streaming.")); m_StreamConfig.width = 1920; m_StreamConfig.height = 1080; } } // Test if audio works at the specified audio configuration bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration); // Gracefully degrade to stereo if surround sound doesn't work if (!audioTestPassed && CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration) > 2) { audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO); if (audioTestPassed) { m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO; emitLaunchWarning(tr("Your selected surround sound setting is not supported by the current audio device.")); } } // If nothing worked, warn the user that audio will not work m_AudioDisabled = !audioTestPassed; if (m_AudioDisabled) { emitLaunchWarning(tr("Failed to open audio device. Audio will be unavailable during this session.")); } // Check for unmapped gamepads if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) { emitLaunchWarning(tr("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.")); } // NVENC will fail to initialize when using dimensions over 4096 and H.264. if (m_StreamConfig.width > 4096 || m_StreamConfig.height > 4096) { // Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K. // We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation. if (m_Computer->maxLumaPixelsHEVC == 0 || !(m_Computer->serverCodecModeSupport & 0x200)) { emit displayLaunchError(tr("Your host PC's GPU doesn't support streaming video resolutions over 4K.")); return false; } else if (!m_StreamConfig.supportsHevc) { emit displayLaunchError(tr("Video resolutions over 4K are only supported by the HEVC codec.")); return false; } } if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE && !m_StreamConfig.enableHdr && // HEVC Main10 was already checked for hardware decode support above !isHardwareDecodeAvailable(testWindow, m_Preferences->videoDecoderSelection, m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264, m_StreamConfig.width, m_StreamConfig.height, m_StreamConfig.fps)) { if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) { emit displayLaunchError(tr("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.")); } else { emit displayLaunchError(tr("Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec.")); } // Fail the launch, because we won't manage to get a decoder for the actual stream return false; } return true; } class DeferredSessionCleanupTask : public QRunnable { public: DeferredSessionCleanupTask(Session* session) : m_Session(session) {} private: virtual ~DeferredSessionCleanupTask() override { // Allow another session to start now that we're cleaned up Session::s_ActiveSession = nullptr; Session::s_ActiveSessionSemaphore.release(); // Notify that the session is ready to be cleaned up emit m_Session->readyForDeletion(); } void run() override { // Only quit the running app if our session terminated gracefully bool shouldQuit = !m_Session->m_UnexpectedTermination && m_Session->m_Preferences->quitAppAfter; // Notify the UI if (shouldQuit) { emit m_Session->quitStarting(); } else { emit m_Session->sessionFinished(m_Session->m_PortTestResults); } // The video decoder must already be destroyed, since it could // try to interact with APIs that can only be called between // LiStartConnection() and LiStopConnection(). SDL_assert(m_Session->m_VideoDecoder == nullptr); // Finish cleanup of the connection state LiStopConnection(); // Perform a best-effort app quit if (shouldQuit) { NvHTTP http(m_Session->m_Computer); // Logging is already done inside NvHTTP try { http.quitApp(); } catch (const GfeHttpResponseException&) { } catch (const QtNetworkReplyException&) { } // Session is finished now emit m_Session->sessionFinished(m_Session->m_PortTestResults); } } Session* m_Session; }; void Session::getWindowDimensions(int& x, int& y, int& width, int& height) { int displayIndex = 0; bool fullScreen; if (m_Window != nullptr) { displayIndex = SDL_GetWindowDisplayIndex(m_Window); SDL_assert(displayIndex >= 0); fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN); } // Create our window on the same display that Qt's UI // was being displayed on. else { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Qt UI screen is at (%d,%d)", m_DisplayOriginX, m_DisplayOriginY); for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) { SDL_Rect displayBounds; if (SDL_GetDisplayBounds(i, &displayBounds) == 0) { if (displayBounds.x == m_DisplayOriginX && displayBounds.y == m_DisplayOriginY) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "SDL found matching display %d", i); displayIndex = i; break; } } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "SDL_GetDisplayBounds(%d) failed: %s", i, SDL_GetError()); } } fullScreen = m_IsFullScreen; } SDL_Rect usableBounds; if (fullScreen && SDL_GetDisplayBounds(displayIndex, &usableBounds) == 0) { width = usableBounds.w; height = usableBounds.h; } else if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) { width = usableBounds.w; height = usableBounds.h; if (m_Window != nullptr) { int top, left, bottom, right; if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) { width -= left + right; height -= top + bottom; } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get window border size: %s", SDL_GetError()); } // If the stream window can fit within the usable drawing area with 1:1 // scaling, do that rather than filling the screen. if (m_StreamConfig.width < width && m_StreamConfig.height < height) { width = m_StreamConfig.width; height = m_StreamConfig.height; } } } else { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GetDisplayUsableBounds() failed: %s", SDL_GetError()); width = m_StreamConfig.width; height = m_StreamConfig.height; } x = y = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex); } void Session::updateOptimalWindowDisplayMode() { SDL_DisplayMode desktopMode, bestMode, mode; int displayIndex = SDL_GetWindowDisplayIndex(m_Window); // Try the current display mode first. On macOS, this will be the normal // scaled desktop resolution setting. if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) { // 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::getRealDesktopMode(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; for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) { if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) { if (mode.w == desktopMode.w && mode.h == desktopMode.h && mode.refresh_rate % m_StreamConfig.fps == 0) { if (mode.refresh_rate > bestMode.refresh_rate) { bestMode = mode; } } } } // For KMSDRM backends where we exclusively own the display, we should opt // to change the resolution if that allows us to hit the desired frame rate if (bestMode.refresh_rate == 0 && strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) { for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) { if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected display mode: %dx%dx%d", mode.w, mode.h, mode.refresh_rate); if (mode.w >= m_ActiveVideoWidth && mode.h >= m_ActiveVideoHeight && mode.refresh_rate % m_StreamConfig.fps == 0) { if (mode.refresh_rate > bestMode.refresh_rate) { bestMode = mode; } } } } } if (bestMode.refresh_rate == 0) { // 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 refresh rate found; using desktop mode"); bestMode = desktopMode; } if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { // 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%d", bestMode.w, bestMode.h, bestMode.refresh_rate); } SDL_SetWindowDisplayMode(m_Window, &bestMode); } void Session::toggleFullscreen() { bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag); if (fullScreen) { if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) { // Confine the cursor to the window if we're capturing input while transitioning to full screen. SDL_SetWindowGrab(m_Window, SDL_TRUE); } SDL_SetWindowResizable(m_Window, SDL_FALSE); SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag); } else { // Unconfine the cursor SDL_SetWindowGrab(m_Window, SDL_FALSE); SDL_SetWindowFullscreen(m_Window, 0); SDL_SetWindowResizable(m_Window, SDL_TRUE); // Reposition the window when the resize is complete m_PendingWindowedTransition = true; } // Input handler might need to start/stop keyboard grab after changing modes m_InputHandler->updateKeyboardGrabState(); } void Session::notifyMouseEmulationMode(bool enabled) { m_MouseEmulationRefCount += enabled ? 1 : -1; SDL_assert(m_MouseEmulationRefCount >= 0); // We re-use the status update overlay for mouse mode notification if (m_MouseEmulationRefCount > 0) { strcpy(m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Gamepad mouse mode active\nLong press Start to deactivate"); m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate); m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); } else { m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); } } class AsyncConnectionStartThread : public QThread { public: AsyncConnectionStartThread(Session* session) : QThread(nullptr), m_Session(session) { setObjectName("Async Conn Start"); } void run() override { m_Session->m_AsyncConnectionSuccess = m_Session->startConnectionAsync(); } Session* m_Session; }; // Called in a non-main thread bool Session::startConnectionAsync() { // Wait 1.5 seconds before connecting to let the user // have time to read any messages present on the segue SDL_Delay(1500); // The UI should have ensured the old game was already quit // if we decide to stream a different game. Q_ASSERT(m_Computer->currentGameId == 0 || m_Computer->currentGameId == m_App.id); // SOPS will set all settings to 720p60 if it doesn't recognize // the chosen resolution. Avoid that by disabling SOPS when it // is not streaming a supported resolution. bool enableGameOptimizations = false; for (const NvDisplayMode &mode : m_Computer->displayModes) { if (mode.width == m_StreamConfig.width && mode.height == m_StreamConfig.height) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Found host supported resolution: %dx%d", mode.width, mode.height); enableGameOptimizations = m_Preferences->gameOptimizations; break; } } QString rtspSessionUrl; try { NvHTTP http(m_Computer); if (m_Computer->currentGameId != 0) { http.resumeApp(&m_StreamConfig, rtspSessionUrl); } else { http.launchApp(m_App.id, &m_StreamConfig, enableGameOptimizations, m_Preferences->playAudioOnHost, m_InputHandler->getAttachedGamepadMask(), rtspSessionUrl); } } catch (const GfeHttpResponseException& e) { emit displayLaunchError(tr("GeForce Experience returned error: %1").arg(e.toQString())); return false; } catch (const QtNetworkReplyException& e) { emit displayLaunchError(e.toQString()); return false; } QByteArray hostnameStr = m_Computer->activeAddress.address().toLatin1(); QByteArray siAppVersion = m_Computer->appVersion.toLatin1(); SERVER_INFORMATION hostInfo; hostInfo.address = hostnameStr.data(); hostInfo.serverInfoAppVersion = siAppVersion.data(); // Older GFE versions didn't have this field QByteArray siGfeVersion; if (!m_Computer->gfeVersion.isEmpty()) { siGfeVersion = m_Computer->gfeVersion.toLatin1(); } if (!siGfeVersion.isEmpty()) { hostInfo.serverInfoGfeVersion = siGfeVersion.data(); } // Older GFE and Sunshine versions didn't have this field QByteArray rtspSessionUrlStr; if (!rtspSessionUrl.isEmpty()) { rtspSessionUrlStr = rtspSessionUrl.toLatin1(); hostInfo.rtspSessionUrl = rtspSessionUrlStr.data(); } if (m_Preferences->packetSize != 0) { // Override default packet size and remote streaming detection // NB: Using STREAM_CFG_AUTO will cap our packet size at 1024 for remote hosts. m_StreamConfig.streamingRemotely = STREAM_CFG_LOCAL; m_StreamConfig.packetSize = m_Preferences->packetSize; SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Using custom packet size: %d bytes", m_Preferences->packetSize); } else { // isReachableOverVpn() does network I/O, so we only attempt to check // VPN reachability if we've already contacted the PC successfully if (m_Computer->isReachableOverVpn()) { // It looks like our route to this PC is over a VPN. // Treat it as remote even if the target address is in RFC 1918 address space. m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE; m_StreamConfig.packetSize = 1024; } else { m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO; m_StreamConfig.packetSize = 1392; } } int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks, &m_VideoCallbacks, m_AudioDisabled ? nullptr : &m_AudioCallbacks, NULL, 0, NULL, 0); if (err != 0) { // We already displayed an error dialog in the stage failure // listener. return false; } emit connectionStarted(); return true; } void Session::flushWindowEvents() { // Pump events to ensure all pending OS events are posted SDL_PumpEvents(); // Insert a barrier to discard any additional window events. // We don't use SDL_FlushEvent() here because it could cause // important events to be lost. m_FlushingWindowEventsRef++; // This event will cause us to set m_FlushingWindowEvents back to false. SDL_Event flushEvent = {}; flushEvent.type = SDL_USEREVENT; flushEvent.user.code = SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER; SDL_PushEvent(&flushEvent); } bool Session::getAndClearPendingIdrFrameStatus() { return SDL_AtomicSet(&m_NeedsIdr, 0); } class ExecThread : public QThread { public: ExecThread(Session* session) : QThread(nullptr), m_Session(session) { setObjectName("Session Exec"); } void run() override { m_Session->execInternal(); } Session* m_Session; }; void Session::exec(int displayOriginX, int displayOriginY) { m_DisplayOriginX = displayOriginX; m_DisplayOriginY = displayOriginY; // Use a separate thread for the streaming session on X11 or Wayland // to ensure we don't stomp on Qt's GL context. This breaks when using // the Qt EGLFS backend, so we will restrict this to X11 m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland(); if (m_ThreadedExec) { // Run the streaming session on a separate thread for Linux/BSD ExecThread execThread(this); execThread.start(); // Until the SDL streaming window is created, we should continue // to update the Qt UI to allow warning messages to display and // make sure that the Qt window can hide itself. while (!execThread.wait(10) && m_Window == nullptr) { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); QCoreApplication::sendPostedEvents(); } QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); QCoreApplication::sendPostedEvents(); // SDL is in charge now. Wait until the streaming thread exits // to further update the Qt window. execThread.wait(); } else { // Run the streaming session on the main thread for Windows and macOS execInternal(); } } void Session::execInternal() { // Complete initialization in this deferred context to avoid // calling expensive functions in the constructor (during the // process of loading the StreamSegue). // // NB: This initializes the SDL video subsystem, so it must be // called on the main thread. if (!initialize()) { emit sessionFinished(0); emit readyForDeletion(); return; } // Wait for any old session to finish cleanup s_ActiveSessionSemaphore.acquire(); // We're now active s_ActiveSession = this; // Initialize the gamepad code with our preferences // NB: m_InputHandler must be initialize before starting the connection. m_InputHandler = new SdlInputHandler(*m_Preferences, m_Computer, m_StreamConfig.width, m_StreamConfig.height); AsyncConnectionStartThread asyncConnThread(this); if (!m_ThreadedExec) { // Kick off the async connection thread while we sit here and pump the event loop asyncConnThread.start(); while (!asyncConnThread.wait(10)) { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); QCoreApplication::sendPostedEvents(); } // Pump the event loop one last time to ensure we pick up any events from // the thread that happened while it was in the final successful QThread::wait(). QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); QCoreApplication::sendPostedEvents(); } else { // We're already in a separate thread so run the connection operations // synchronously and don't pump the event loop. The main thread is already // pumping the event loop for us. asyncConnThread.run(); } // If the connection failed, clean up and abort the connection. if (!m_AsyncConnectionSuccess) { delete m_InputHandler; m_InputHandler = nullptr; SDL_QuitSubSystem(SDL_INIT_VIDEO); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this)); return; } int x, y, width, height; getWindowDimensions(x, y, width, height); #ifdef STEAM_LINK // We need a little delay before creating the window or we will trigger some kind // of graphics driver bug on Steam Link that causes a jagged overlay to appear in // the top right corner randomly. SDL_Delay(500); #endif // Request at least 8 bits per color for GL SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); m_Window = SDL_CreateWindow("Moonlight", x, y, width, height, SDL_WINDOW_ALLOW_HIGHDPI | StreamUtils::getPlatformWindowFlags()); if (!m_Window) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow() failed with platform flags: %s", SDL_GetError()); m_Window = SDL_CreateWindow("Moonlight", x, y, width, height, SDL_WINDOW_ALLOW_HIGHDPI); if (!m_Window) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow() failed: %s", SDL_GetError()); delete m_InputHandler; m_InputHandler = nullptr; SDL_QuitSubSystem(SDL_INIT_VIDEO); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this)); return; } } m_InputHandler->setWindow(m_Window); QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg")); QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888); svgImage.fill(0); QPainter svgPainter(&svgImage); svgIconRenderer.render(&svgPainter); SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(), svgImage.width(), svgImage.height(), 32, 4 * svgImage.width(), SDL_PIXELFORMAT_RGBA32); #ifndef Q_OS_DARWIN // Other platforms seem to preserve our Qt icon when creating a new window. if (iconSurface != nullptr) { // This must be called before entering full-screen mode on Windows // or our icon will not persist when toggling to windowed mode SDL_SetWindowIcon(m_Window, iconSurface); } #endif // For non-full screen windows, call getWindowDimensions() // again after creating a window to allow it to account // for window chrome size. if (!m_IsFullScreen) { getWindowDimensions(x, y, width, height); // We must set the size before the position because centering // won't work unless it knows the final size of the window. SDL_SetWindowSize(m_Window, width, height); SDL_SetWindowPosition(m_Window, x, y); // Passing SDL_WINDOW_RESIZABLE to set this during window // creation causes our window to be full screen for some reason SDL_SetWindowResizable(m_Window, SDL_TRUE); } else { // Update the window display mode based on our current monitor updateOptimalWindowDisplayMode(); // Enter full screen SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag); } bool needsFirstEnterCapture = false; bool needsPostDecoderCreationCapture = false; // HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER // event where it seems to work consistently on GNOME. For other platforms, // especially where SDL may call SDL_RecreateWindow(), we must only capture // after the decoder is created. if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) { // Native Wayland: Capture on SDL_WINDOWEVENT_ENTER needsFirstEnterCapture = true; } else { // X11/XWayland: Capture after decoder creation needsPostDecoderCreationCapture = true; } // Stop text input. SDL enables it by default // when we initialize the video subsystem, but this // causes an IME popup when certain keys are held down // on macOS. SDL_StopTextInput(); // Disable the screen saver if requested if (m_Preferences->keepAwake) { SDL_DisableScreenSaver(); } // Hide Qt's fake mouse cursor on EGLFS systems if (QGuiApplication::platformName() == "eglfs") { QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); } // Set timer resolution to 1 ms on Windows for greater // sleep precision and more accurate callback timing. SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1"); int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); // Now that we're about to stream, any SDL_QUIT event is expected // unless it comes from the connection termination callback where // (m_UnexpectedTermination is set back to true). m_UnexpectedTermination = false; // Start rich presence to indicate we're in game RichPresenceManager presence(*m_Preferences, m_App.name); // Hijack this thread to be the SDL main thread. We have to do this // because we want to suspend all Qt processing until the stream is over. SDL_Event event; for (;;) { #if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(STEAM_LINK) // SDL 2.0.18 has a proper wait event implementation that uses platform // support to block on events rather than polling on Windows, macOS, X11, // and Wayland. It will fall back to 1 ms polling if a joystick is // connected, so we don't use it for STEAM_LINK to ensure we only poll // every 10 ms. // // NB: This behavior was introduced in SDL 2.0.16, but had a few critical // issues that could cause indefinite timeouts, delayed joystick detection, // and other problems. if (!SDL_WaitEventTimeout(&event, 1000)) { presence.runCallbacks(); continue; } #else // We explicitly use SDL_PollEvent() and SDL_Delay() because // SDL_WaitEvent() has an internal SDL_Delay(10) inside which // blocks this thread too long for high polling rate mice and high // refresh rate displays. if (!SDL_PollEvent(&event)) { #ifndef STEAM_LINK SDL_Delay(1); #else // Waking every 1 ms to process input is too much for the low performance // ARM core in the Steam Link, so we will wait 10 ms instead. SDL_Delay(10); #endif presence.runCallbacks(); continue; } #endif switch (event.type) { case SDL_QUIT: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Quit event received"); goto DispatchDeferredCleanup; case SDL_USEREVENT: switch (event.user.code) { case SDL_CODE_FRAME_READY: m_VideoDecoder->renderFrameOnMainThread(); break; case SDL_CODE_HIDE_CURSOR: SDL_ShowCursor(SDL_DISABLE); break; case SDL_CODE_SHOW_CURSOR: SDL_ShowCursor(SDL_ENABLE); break; case SDL_CODE_UNCAPTURE_MOUSE: SDL_CaptureMouse(SDL_FALSE); break; case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER: m_FlushingWindowEventsRef--; break; case SDL_CODE_GAMECONTROLLER_RUMBLE: m_InputHandler->rumble((unsigned short)(uintptr_t)event.user.data1, (unsigned short)((uintptr_t)event.user.data2 >> 16), (unsigned short)((uintptr_t)event.user.data2 & 0xFFFF)); break; default: SDL_assert(false); } break; case SDL_WINDOWEVENT: // Early handling of some events switch (event.window.event) { case SDL_WINDOWEVENT_FOCUS_LOST: if (m_Preferences->muteOnFocusLoss) { m_AudioMuted = true; } m_InputHandler->notifyFocusLost(); break; case SDL_WINDOWEVENT_FOCUS_GAINED: if (m_Preferences->muteOnFocusLoss) { m_AudioMuted = false; } break; case SDL_WINDOWEVENT_LEAVE: m_InputHandler->notifyMouseLeave(); break; } presence.runCallbacks(); // Capture the mouse on SDL_WINDOWEVENT_ENTER if needed if (needsFirstEnterCapture && event.window.event == SDL_WINDOWEVENT_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_WINDOWEVENT_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.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_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. if (SDL_GetWindowDisplayIndex(m_Window) == currentDisplayIndex) { 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 // Complete any repositioning that was deferred until // the resize from full-screen to windowed had completed. // If we try to do this immediately, the resize won't take effect // properly on Windows. if (m_PendingWindowedTransition) { m_PendingWindowedTransition = false; int x, y, width, height; getWindowDimensions(x, y, width, height); SDL_SetWindowSize(m_Window, width, height); SDL_SetWindowPosition(m_Window, x, y); } 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.event, event.window.data1, event.window.data2); break; } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Recreating renderer for window event: %d (%d %d)", event.window.event, event.window.data1, event.window.data2); // Fall through case SDL_RENDER_DEVICE_RESET: case SDL_RENDER_TARGETS_RESET: SDL_AtomicLock(&m_DecoderLock); // Destroy the old decoder delete m_VideoDecoder; // Insert a barrier to discard any additional window events // that could cause the renderer to be and recreated again. // We don't use SDL_FlushEvent() here because it could cause // important events to be lost. flushWindowEvents(); // Update the window display mode based on our current monitor currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); updateOptimalWindowDisplayMode(); // 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(); SDL_FlushEvent(SDL_RENDER_DEVICE_RESET); SDL_FlushEvent(SDL_RENDER_TARGETS_RESET); { // If the stream exceeds the display refresh rate (plus some slack), // forcefully disable V-sync to allow the stream to render faster // than the display. int displayHz = StreamUtils::getDisplayRefreshRate(m_Window); bool enableVsync = m_Preferences->enableVsync; if (displayHz + 5 < m_StreamConfig.fps) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Disabling V-sync because refresh rate limit exceeded"); enableVsync = false; } // Choose a new decoder (hopefully the same one, but possibly // not if a GPU was removed or something). if (!chooseDecoder(m_Preferences->videoDecoderSelection, m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth, m_ActiveVideoHeight, m_ActiveVideoFrameRate, enableVsync, enableVsync && m_Preferences->framePacing, false, s_ActiveSession->m_VideoDecoder)) { SDL_AtomicUnlock(&m_DecoderLock); 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.")); goto DispatchDeferredCleanup; } // As of SDL 2.0.12, SDL_RecreateWindow() doesn't carry over mouse capture // or mouse hiding state to the new window. By capturing after the decoder // is set up, this ensures the window re-creation is already done. if (needsPostDecoderCreationCapture) { m_InputHandler->setCaptureActive(true); needsPostDecoderCreationCapture = false; } } // Request an IDR frame to complete the reset SDL_AtomicSet(&m_NeedsIdr, 1); // Set HDR mode. We may miss the callback if we're in the middle // of recreating our decoder at the time the HDR transition happens. m_VideoDecoder->setHdrMode(LiGetCurrentHostDisplayHdrMode()); // After a window resize, we need to reset the pointer lock region m_InputHandler->updatePointerRegionLock(); SDL_AtomicUnlock(&m_DecoderLock); break; case SDL_KEYUP: case SDL_KEYDOWN: presence.runCallbacks(); m_InputHandler->handleKeyEvent(&event.key); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: presence.runCallbacks(); m_InputHandler->handleMouseButtonEvent(&event.button); break; case SDL_MOUSEMOTION: m_InputHandler->handleMouseMotionEvent(&event.motion); break; case SDL_MOUSEWHEEL: m_InputHandler->handleMouseWheelEvent(&event.wheel); break; case SDL_CONTROLLERAXISMOTION: m_InputHandler->handleControllerAxisEvent(&event.caxis); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: presence.runCallbacks(); m_InputHandler->handleControllerButtonEvent(&event.cbutton); break; case SDL_CONTROLLERDEVICEADDED: case SDL_CONTROLLERDEVICEREMOVED: m_InputHandler->handleControllerDeviceEvent(&event.cdevice); break; case SDL_JOYDEVICEADDED: m_InputHandler->handleJoystickArrivalEvent(&event.jdevice); break; case SDL_FINGERDOWN: case SDL_FINGERMOTION: case SDL_FINGERUP: m_InputHandler->handleTouchFingerEvent(&event.tfinger); break; } } DispatchDeferredCleanup: // Uncapture the mouse and hide the window immediately, // so we can return to the Qt GUI ASAP. m_InputHandler->setCaptureActive(false); SDL_EnableScreenSaver(); SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0"); if (QGuiApplication::platformName() == "eglfs") { QGuiApplication::restoreOverrideCursor(); } // Raise any keys that are still down m_InputHandler->raiseAllKeys(); // Destroy the input handler now. This must be destroyed // before allowwing the UI to continue execution or it could // interfere with SDLGamepadKeyNavigation. delete m_InputHandler; m_InputHandler = nullptr; // Destroy the decoder, since this must be done on the main thread // NB: This must happen before LiStopConnection() for pull-based // decoders. SDL_AtomicLock(&m_DecoderLock); delete m_VideoDecoder; m_VideoDecoder = nullptr; SDL_AtomicUnlock(&m_DecoderLock); // This must be called after the decoder is deleted, because // the renderer may want to interact with the window SDL_DestroyWindow(m_Window); if (iconSurface != nullptr) { SDL_FreeSurface(iconSurface); } SDL_QuitSubSystem(SDL_INIT_VIDEO); // Cleanup can take a while, so dispatch it to a worker thread. // When it is complete, it will release our s_ActiveSessionSemaphore // reference. QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this)); }