2018-09-29 23:18:46 +00:00
# include "session.h"
2018-06-28 08:44:43 +00:00
# include "settings/streamingpreferences.h"
2018-09-04 04:21:37 +00:00
# include "streaming/streamutils.h"
2019-06-30 00:40:30 +00:00
# include "backend/richpresencemanager.h"
2018-06-28 08:44:43 +00:00
# include <Limelight.h>
# include <SDL.h>
2018-07-08 04:52:20 +00:00
# include "utils.h"
2018-06-28 08:44:43 +00:00
2018-07-22 00:32:00 +00:00
# ifdef HAVE_FFMPEG
2018-07-18 03:00:16 +00:00
# include "video/ffmpeg.h"
2018-07-22 00:32:00 +00:00
# endif
2018-07-18 03:00:16 +00:00
2018-07-22 03:22:00 +00:00
# ifdef HAVE_SLVIDEO
2019-03-23 05:51:08 +00:00
# include "video/slvid.h"
2018-07-22 03:22:00 +00:00
# endif
2018-07-23 00:42:31 +00:00
# ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
2018-07-23 00:07:45 +00:00
# define ICON_SIZE 32
2018-07-23 00:42:31 +00:00
# else
# define ICON_SIZE 64
# endif
2018-07-23 00:07:45 +00:00
2021-01-09 23:51:25 +00:00
# define SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER 100
2018-07-22 01:47:41 +00:00
# include <openssl/rand.h>
2018-06-28 08:44:43 +00:00
# include <QtEndian>
# include <QCoreApplication>
2018-07-09 07:09:06 +00:00
# include <QThreadPool>
2018-07-23 00:07:45 +00:00
# include <QSvgRenderer>
# include <QPainter>
# include <QImage>
2020-01-11 10:06:08 +00:00
# include <QGuiApplication>
# include <QCursor>
2018-06-28 08:44:43 +00:00
2020-08-11 05:21:54 +00:00
# define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
2018-06-28 08:44:43 +00:00
CONNECTION_LISTENER_CALLBACKS Session : : k_ConnCallbacks = {
Session : : clStageStarting ,
nullptr ,
Session : : clStageFailed ,
nullptr ,
Session : : clConnectionTerminated ,
2019-02-12 05:39:55 +00:00
Session : : clLogMessage ,
2019-03-17 22:08:21 +00:00
Session : : clRumble ,
Session : : clConnectionStatusUpdate
2018-06-28 08:44:43 +00:00
} ;
Session * Session : : s_ActiveSession ;
2018-07-09 07:09:06 +00:00
QSemaphore Session : : s_ActiveSessionSemaphore ( 1 ) ;
2018-06-28 08:44:43 +00:00
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.
2018-07-07 23:30:26 +00:00
emit s_ActiveSession - > stageStarting ( QString : : fromLocal8Bit ( LiGetStageName ( stage ) ) ) ;
2018-06-28 08:44:43 +00:00
}
2020-02-25 07:03:34 +00:00
void Session : : clStageFailed ( int stage , int errorCode )
2018-06-28 08:44:43 +00:00
{
2020-08-11 05:21:54 +00:00
// Perform the port test now, while we're on the async connection thread and not blocking the UI.
2020-12-23 19:56:15 +00:00
unsigned int portFlags = LiGetPortFlagsFromStage ( stage ) ;
s_ActiveSession - > m_PortTestResults = LiTestClientConnectivity ( CONN_TEST_SERVER , 443 , portFlags ) ;
2020-08-11 05:21:54 +00:00
2020-12-23 19:56:15 +00:00
char failingPorts [ 128 ] ;
LiStringifyPortFlags ( portFlags , " , " , failingPorts , sizeof ( failingPorts ) ) ;
emit s_ActiveSession - > stageFailed ( QString : : fromLocal8Bit ( LiGetStageName ( stage ) ) , errorCode , QString ( failingPorts ) ) ;
2018-06-28 08:44:43 +00:00
}
2020-02-25 07:03:34 +00:00
void Session : : clConnectionTerminated ( int errorCode )
2018-06-28 08:44:43 +00:00
{
2020-12-23 19:56:15 +00:00
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode ( errorCode ) ;
s_ActiveSession - > m_PortTestResults = LiTestClientConnectivity ( CONN_TEST_SERVER , 443 , portFlags ) ;
2020-08-09 00:59:26 +00:00
2019-02-10 10:16:05 +00:00
// Display the termination dialog if this was not intended
2020-05-02 04:28:48 +00:00
switch ( errorCode ) {
case ML_ERROR_GRACEFUL_TERMINATION :
break ;
case ML_ERROR_NO_VIDEO_TRAFFIC :
s_ActiveSession - > m_UnexpectedTermination = true ;
2020-12-23 19:56:15 +00:00
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 ) ) ;
2020-05-02 04:28:48 +00:00
break ;
2020-08-30 04:02:22 +00:00
case ML_ERROR_NO_VIDEO_FRAME :
s_ActiveSession - > m_UnexpectedTermination = true ;
2020-11-21 19:15:54 +00:00
emit s_ActiveSession - > displayLaunchError ( tr ( " Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection. " ) ) ;
2020-08-30 04:02:22 +00:00
break ;
2020-12-24 17:24:01 +00:00
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 ;
2020-05-02 04:28:48 +00:00
default :
2019-02-10 10:16:05 +00:00
s_ActiveSession - > m_UnexpectedTermination = true ;
2020-11-21 19:15:54 +00:00
emit s_ActiveSession - > displayLaunchError ( tr ( " Connection terminated " ) ) ;
2020-05-02 04:28:48 +00:00
break ;
2019-02-10 10:16:05 +00:00
}
2018-09-02 22:34:10 +00:00
2018-06-28 08:44:43 +00:00
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
2020-02-25 07:03:34 +00:00
" Connection terminated: %d " ,
2018-06-28 08:44:43 +00:00
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 ) ;
}
2019-02-12 05:39:55 +00:00
void Session : : clRumble ( unsigned short controllerNumber , unsigned short lowFreqMotor , unsigned short highFreqMotor )
{
2019-02-13 03:30:02 +00:00
// The input handler can be closed during the stream if LiStopConnection() hasn't completed yet
// but the stream has been stopped by the user. In this case, just discard the rumble.
SDL_AtomicLock ( & s_ActiveSession - > m_InputHandlerLock ) ;
if ( s_ActiveSession - > m_InputHandler ! = nullptr ) {
s_ActiveSession - > m_InputHandler - > rumble ( controllerNumber , lowFreqMotor , highFreqMotor ) ;
}
SDL_AtomicUnlock ( & s_ActiveSession - > m_InputHandlerLock ) ;
2019-02-12 05:39:55 +00:00
}
2019-03-17 22:08:21 +00:00
void Session : : clConnectionStatusUpdate ( int connectionStatus )
{
2019-03-19 03:21:52 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Connection status update: %d " ,
connectionStatus ) ;
if ( ! s_ActiveSession - > m_Preferences - > connectionWarnings ) {
return ;
}
2019-05-19 20:10:42 +00:00
if ( s_ActiveSession - > m_MouseEmulationRefCount > 0 ) {
// Don't display the overlay if mouse emulation is already using it
return ;
}
2019-03-17 22:08:21 +00:00
switch ( connectionStatus )
{
case CONN_STATUS_POOR :
2019-03-19 03:21:52 +00:00
if ( s_ActiveSession - > m_StreamConfig . bitrate > 5000 ) {
2019-04-06 19:25:35 +00:00
strcpy ( s_ActiveSession - > m_OverlayManager . getOverlayText ( Overlay : : OverlayStatusUpdate ) , " Slow connection to PC \n Reduce your bitrate " ) ;
2019-03-19 03:21:52 +00:00
}
else {
strcpy ( s_ActiveSession - > m_OverlayManager . getOverlayText ( Overlay : : OverlayStatusUpdate ) , " Poor connection to PC " ) ;
}
2019-03-17 22:08:21 +00:00
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 ;
}
}
2018-07-18 03:00:16 +00:00
bool Session : : chooseDecoder ( StreamingPreferences : : VideoDecoderSelection vds ,
SDL_Window * window , int videoFormat , int width , int height ,
2019-02-13 02:42:53 +00:00
int frameRate , bool enableVsync , bool enableFramePacing , bool testOnly , IVideoDecoder * & chosenDecoder )
2018-07-18 03:00:16 +00:00
{
2019-04-12 05:27:20 +00:00
DECODER_PARAMETERS params ;
2021-04-30 23:12:56 +00:00
// 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 ) ;
2019-04-12 05:27:20 +00:00
params . width = width ;
params . height = height ;
params . frameRate = frameRate ;
params . videoFormat = videoFormat ;
params . window = window ;
params . enableVsync = enableVsync ;
params . enableFramePacing = enableFramePacing ;
params . vds = vds ;
2018-12-26 06:19:23 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" V-sync %s " ,
enableVsync ? " enabled " : " disabled " ) ;
2018-07-22 03:22:00 +00:00
# ifdef HAVE_SLVIDEO
2019-02-13 02:42:53 +00:00
chosenDecoder = new SLVideoDecoder ( testOnly ) ;
2019-04-12 05:27:20 +00:00
if ( chosenDecoder - > initialize ( & params ) ) {
2018-07-22 03:22:00 +00:00
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
2018-07-22 00:00:09 +00:00
# ifdef HAVE_FFMPEG
2019-02-13 02:42:53 +00:00
chosenDecoder = new FFmpegVideoDecoder ( testOnly ) ;
2019-04-12 05:27:20 +00:00
if ( chosenDecoder - > initialize ( & params ) ) {
2018-07-18 03:00:16 +00:00
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 ;
}
2018-07-22 00:00:09 +00:00
# endif
2018-07-18 03:00:16 +00:00
2018-08-03 09:11:44 +00:00
# if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO)
# error No video decoding libraries available!
# endif
2018-07-18 03:00:16 +00:00
// 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 )
{
2018-07-20 22:31:57 +00:00
s_ActiveSession - > m_ActiveVideoFormat = videoFormat ;
s_ActiveSession - > m_ActiveVideoWidth = width ;
s_ActiveSession - > m_ActiveVideoHeight = height ;
s_ActiveSession - > m_ActiveVideoFrameRate = frameRate ;
2018-07-21 02:55:07 +00:00
// 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).
2018-07-18 03:00:16 +00:00
2018-07-28 23:06:26 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION , " Video stream is %dx%dx%d (format 0x%x) " ,
width , height , frameRate , videoFormat ) ;
2018-07-18 03:00:16 +00:00
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
2018-09-23 03:03:41 +00:00
// 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 ( SDL_AtomicTryLock ( & s_ActiveSession - > m_DecoderLock ) ) {
if ( s_ActiveSession - > m_NeedsIdr ) {
// If we reset our decoder, we'll need to request an IDR frame
s_ActiveSession - > m_NeedsIdr = false ;
SDL_AtomicUnlock ( & s_ActiveSession - > m_DecoderLock ) ;
return DR_NEED_IDR ;
}
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 ;
}
2018-07-18 03:00:16 +00:00
}
else {
2018-09-23 03:03:41 +00:00
// Decoder is going away. Ignore anything coming in until
// the lock is released.
2018-07-18 03:00:16 +00:00
return DR_OK ;
}
}
2020-02-09 19:35:05 +00:00
void Session : : getDecoderInfo ( SDL_Window * window ,
2020-02-23 08:43:43 +00:00
bool & isHardwareAccelerated , bool & isFullScreenOnly , QSize & maxResolution )
2020-02-09 19:35:05 +00:00
{
IVideoDecoder * decoder ;
if ( ! chooseDecoder ( StreamingPreferences : : VDS_AUTO ,
window , VIDEO_FORMAT_H264 , 1920 , 1080 , 60 ,
2021-04-30 23:12:56 +00:00
false , false , true , decoder ) ) {
2020-02-09 19:35:05 +00:00
isHardwareAccelerated = isFullScreenOnly = false ;
return ;
}
isHardwareAccelerated = decoder - > isHardwareAccelerated ( ) ;
isFullScreenOnly = decoder - > isAlwaysFullScreen ( ) ;
2020-02-23 08:43:43 +00:00
maxResolution = decoder - > getDecoderMaxResolution ( ) ;
2020-02-09 19:35:05 +00:00
delete decoder ;
}
2019-03-24 00:46:42 +00:00
bool Session : : isHardwareDecodeAvailable ( SDL_Window * window ,
StreamingPreferences : : VideoDecoderSelection vds ,
2018-07-18 03:00:16 +00:00
int videoFormat , int width , int height , int frameRate )
{
IVideoDecoder * decoder ;
2021-04-30 23:12:56 +00:00
if ( ! chooseDecoder ( vds , window , videoFormat , width , height , frameRate , false , false , true , decoder ) ) {
2018-07-18 03:00:16 +00:00
return false ;
}
bool ret = decoder - > isHardwareAccelerated ( ) ;
2018-07-21 01:15:46 +00:00
2018-07-18 03:00:16 +00:00
delete decoder ;
2018-11-01 01:20:39 +00:00
2018-07-18 03:00:16 +00:00
return ret ;
}
2019-12-14 23:20:44 +00:00
bool Session : : populateDecoderProperties ( SDL_Window * window )
2018-08-25 20:36:54 +00:00
{
IVideoDecoder * decoder ;
2019-12-14 23:20:44 +00:00
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 ,
2021-04-30 23:12:56 +00:00
false , false , true , decoder ) ) {
2018-08-25 20:36:54 +00:00
return false ;
}
2019-12-14 23:34:48 +00:00
m_VideoCallbacks . capabilities = decoder - > getDecoderCapabilities ( ) ;
2018-08-25 20:36:54 +00:00
2019-12-14 23:25:56 +00:00
m_StreamConfig . colorSpace = decoder - > getDecoderColorspace ( ) ;
2021-02-07 16:49:29 +00:00
if ( decoder - > isAlwaysFullScreen ( ) ) {
m_IsFullScreen = true ;
}
2018-11-01 01:49:37 +00:00
delete decoder ;
2019-12-14 23:20:44 +00:00
return true ;
2018-08-25 20:36:54 +00:00
}
2018-09-29 21:06:55 +00:00
Session : : Session ( NvComputer * computer , NvApp & app , StreamingPreferences * preferences )
: m_Preferences ( preferences ? preferences : new StreamingPreferences ( this ) ) ,
2021-03-13 21:20:58 +00:00
m_IsFullScreen ( m_Preferences - > windowMode ! = StreamingPreferences : : WM_WINDOWED | | ! WMUtils : : isRunningDesktopEnvironment ( ) ) ,
2018-09-29 21:06:55 +00:00
m_Computer ( computer ) ,
2018-07-09 07:09:06 +00:00
m_App ( app ) ,
2018-07-18 03:00:16 +00:00
m_Window ( nullptr ) ,
m_VideoDecoder ( nullptr ) ,
2018-07-20 22:31:57 +00:00
m_DecoderLock ( 0 ) ,
2018-08-31 04:09:31 +00:00
m_NeedsIdr ( false ) ,
2018-09-05 22:15:53 +00:00
m_AudioDisabled ( false ) ,
2020-12-25 21:32:11 +00:00
m_AudioMuted ( false ) ,
2018-09-05 22:15:53 +00:00
m_DisplayOriginX ( 0 ) ,
2018-09-08 23:01:35 +00:00
m_DisplayOriginY ( 0 ) ,
2018-09-13 13:23:06 +00:00
m_PendingWindowedTransition ( false ) ,
2019-02-13 03:30:02 +00:00
m_UnexpectedTermination ( true ) , // Failure prior to streaming is unexpected
2019-02-12 05:39:55 +00:00
m_InputHandler ( nullptr ) ,
2019-02-13 03:30:02 +00:00
m_InputHandlerLock ( 0 ) ,
2019-05-19 20:10:42 +00:00
m_MouseEmulationRefCount ( 0 ) ,
2021-01-15 01:28:21 +00:00
m_FlushingWindowEventsRef ( 0 ) ,
2020-08-11 05:21:54 +00:00
m_AsyncConnectionSuccess ( false ) ,
m_PortTestResults ( 0 ) ,
2018-09-13 13:23:06 +00:00
m_OpusDecoder ( nullptr ) ,
2018-10-02 08:21:42 +00:00
m_AudioRenderer ( nullptr ) ,
2019-02-15 06:32:54 +00:00
m_AudioSampleCount ( 0 ) ,
m_DropAudioEndTime ( 0 )
2018-09-09 17:17:32 +00:00
{
}
2019-02-13 03:30:02 +00:00
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
// Use Session::exec() or DeferredSessionCleanupTask instead.
2018-12-06 03:49:06 +00:00
Session : : ~ Session ( )
{
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
// and the object is deallocated.
s_ActiveSessionSemaphore . acquire ( ) ;
s_ActiveSessionSemaphore . release ( ) ;
}
2019-03-24 00:46:42 +00:00
bool Session : : initialize ( )
2018-06-28 08:44:43 +00:00
{
2019-04-22 00:43:38 +00:00
if ( SDL_InitSubSystem ( SDL_INIT_VIDEO ) ! = 0 ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s " ,
SDL_GetError ( ) ) ;
return false ;
}
2019-03-24 00:46:42 +00:00
// Create a hidden window to use for decoder initialization tests
2020-09-05 21:06:56 +00:00
SDL_Window * testWindow = SDL_CreateWindow ( " " , 0 , 0 , 1280 , 720 ,
SDL_WINDOW_HIDDEN | StreamUtils : : getPlatformWindowFlags ( ) ) ;
2019-03-24 00:46:42 +00:00
if ( ! testWindow ) {
2020-09-05 21:06:56 +00:00
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 ;
}
2019-03-24 00:46:42 +00:00
}
2018-09-15 01:35:28 +00:00
qInfo ( ) < < " Server GPU: " < < m_Computer - > gpuModel ;
qInfo ( ) < < " Server GFE version: " < < m_Computer - > gfeVersion ;
2018-08-05 20:32:04 +00:00
2018-07-08 04:52:20 +00:00
LiInitializeVideoCallbacks ( & m_VideoCallbacks ) ;
m_VideoCallbacks . setup = drSetup ;
m_VideoCallbacks . submitDecodeUnit = drSubmitDecodeUnit ;
2018-07-18 03:00:16 +00:00
2018-06-28 08:44:43 +00:00
LiInitializeStreamConfiguration ( & m_StreamConfig ) ;
2018-09-29 21:06:55 +00:00
m_StreamConfig . width = m_Preferences - > width ;
m_StreamConfig . height = m_Preferences - > height ;
m_StreamConfig . fps = m_Preferences - > fps ;
m_StreamConfig . bitrate = m_Preferences - > bitrateKbps ;
2018-06-28 08:44:43 +00:00
m_StreamConfig . hevcBitratePercentageMultiplier = 75 ;
2021-04-24 00:49:03 +00:00
# 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
2018-08-05 20:32:04 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Video bitrate: %d kbps " ,
m_StreamConfig . bitrate ) ;
2018-07-22 01:47:41 +00:00
RAND_bytes ( reinterpret_cast < unsigned char * > ( m_StreamConfig . remoteInputAesKey ) ,
sizeof ( m_StreamConfig . remoteInputAesKey ) ) ;
// Only the first 4 bytes are populated in the RI key IV
RAND_bytes ( reinterpret_cast < unsigned char * > ( m_StreamConfig . remoteInputAesIv ) , 4 ) ;
2018-09-29 21:06:55 +00:00
switch ( m_Preferences - > audioConfig )
2018-06-28 08:44:43 +00:00
{
2018-10-02 22:30:22 +00:00
case StreamingPreferences : : AC_STEREO :
2020-04-03 07:12:52 +00:00
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_STEREO ;
2018-06-28 08:44:43 +00:00
break ;
2018-10-02 22:30:22 +00:00
case StreamingPreferences : : AC_51_SURROUND :
2020-04-03 07:12:52 +00:00
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND ;
break ;
case StreamingPreferences : : AC_71_SURROUND :
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND ;
2018-06-28 08:44:43 +00:00
break ;
}
2018-07-13 09:28:10 +00:00
2019-07-26 16:50:45 +00:00
LiInitializeAudioCallbacks ( & m_AudioCallbacks ) ;
m_AudioCallbacks . init = arInit ;
m_AudioCallbacks . cleanup = arCleanup ;
m_AudioCallbacks . decodeAndPlaySample = arDecodeAndPlaySample ;
m_AudioCallbacks . capabilities = getAudioRendererCapabilities ( m_StreamConfig . audioConfiguration ) ;
2018-07-28 23:06:26 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
2020-04-04 19:53:03 +00:00
" 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 ) ) ;
2018-07-28 23:06:26 +00:00
2018-09-29 21:06:55 +00:00
switch ( m_Preferences - > videoCodecConfig )
2018-06-28 08:44:43 +00:00
{
case StreamingPreferences : : VCC_AUTO :
// TODO: Determine if HEVC is better depending on the decoder
2018-07-13 09:28:10 +00:00
m_StreamConfig . supportsHevc =
2019-03-24 00:46:42 +00:00
isHardwareDecodeAvailable ( testWindow ,
m_Preferences - > videoDecoderSelection ,
2018-07-13 09:28:10 +00:00
VIDEO_FORMAT_H265 ,
m_StreamConfig . width ,
2018-07-18 03:00:16 +00:00
m_StreamConfig . height ,
m_StreamConfig . fps ) ;
2018-10-14 18:28:52 +00:00
# ifdef Q_OS_DARWIN
2018-10-13 00:59:53 +00:00
{
// 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 < int > 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 ;
}
}
2018-10-14 18:28:52 +00:00
# endif
2018-06-28 08:44:43 +00:00
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 ;
}
2018-07-16 08:12:53 +00:00
2018-09-29 21:06:55 +00:00
switch ( m_Preferences - > windowMode )
2018-09-04 02:17:34 +00:00
{
2019-06-23 20:29:57 +00:00
default :
2018-09-04 02:17:34 +00:00
case StreamingPreferences : : WM_FULLSCREEN_DESKTOP :
2021-03-13 21:20:58 +00:00
// Only use full-screen desktop mode if we're running a desktop environment
if ( WMUtils : : isRunningDesktopEnvironment ( ) ) {
2021-02-07 16:49:29 +00:00
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP ;
break ;
}
// Fall-through
2018-09-04 02:17:34 +00:00
case StreamingPreferences : : WM_FULLSCREEN :
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN ;
break ;
}
2019-03-24 00:46:42 +00:00
2020-02-25 03:55:16 +00:00
# if !SDL_VERSION_ATLEAST(2, 0, 11)
2020-02-09 20:00:30 +00:00
// 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 ;
}
2020-02-25 03:55:16 +00:00
# endif
2020-02-09 20:00:30 +00:00
2019-03-24 00:46:42 +00:00
// Check for validation errors/warnings and emit
// signals for them, if appropriate
bool ret = validateLaunch ( testWindow ) ;
2019-12-14 23:20:44 +00:00
if ( ret ) {
// Populate decoder-dependent properties.
// Must be done after validateLaunch() since m_StreamConfig is finalized.
ret = populateDecoderProperties ( testWindow ) ;
}
2019-03-24 00:46:42 +00:00
SDL_DestroyWindow ( testWindow ) ;
2019-04-22 00:43:38 +00:00
if ( ! ret ) {
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
return false ;
}
return true ;
2018-06-28 08:44:43 +00:00
}
2018-08-04 23:05:37 +00:00
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 ) ;
2021-03-07 16:06:12 +00:00
if ( ! m_ThreadedExec ) {
// Pump the UI loop while we wait if we're on the main thread
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
QCoreApplication : : sendPostedEvents ( ) ;
}
2018-08-04 23:05:37 +00:00
}
}
2019-03-24 00:46:42 +00:00
bool Session : : validateLaunch ( SDL_Window * testWindow )
2018-06-28 08:44:43 +00:00
{
QStringList warningList ;
2021-05-01 01:05:38 +00:00
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 ;
}
2020-04-26 06:50:12 +00:00
if ( m_Preferences - > absoluteMouseMode & & ! m_App . isAppCollectorGame ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Your selection to enable remote desktop mouse mode may cause problems in games. " ) ) ;
2020-04-26 06:50:12 +00:00
}
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > videoDecoderSelection = = StreamingPreferences : : VDS_FORCE_SOFTWARE ) {
2019-12-31 00:05:43 +00:00
if ( m_Preferences - > videoCodecConfig = = StreamingPreferences : : VCC_FORCE_HEVC_HDR ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " HDR is not supported with software decoding. " ) ) ;
2019-12-31 00:05:43 +00:00
m_StreamConfig . enableHdr = false ;
}
else {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Your settings selection to force software decoding may cause poor streaming performance. " ) ) ;
2019-12-31 00:05:43 +00:00
}
2018-08-04 23:15:13 +00:00
}
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > unsupportedFps & & m_StreamConfig . fps > 60 ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Using unsupported FPS options may cause stuttering or lag. " ) ) ;
2018-09-08 22:09:46 +00:00
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > enableVsync ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " V-sync will be disabled when streaming at a higher frame rate than the display. " ) ) ;
2018-09-08 22:09:46 +00:00
}
2018-09-08 21:33:34 +00:00
}
2018-07-09 03:53:24 +00:00
if ( m_StreamConfig . supportsHevc ) {
2018-09-29 21:06:55 +00:00
bool hevcForced = m_Preferences - > videoCodecConfig = = StreamingPreferences : : VCC_FORCE_HEVC | |
m_Preferences - > videoCodecConfig = = StreamingPreferences : : VCC_FORCE_HEVC_HDR ;
2018-07-27 02:26:22 +00:00
2019-12-14 23:20:44 +00:00
if ( m_Preferences - > videoDecoderSelection = = StreamingPreferences : : VDS_AUTO & & // Force hardware decoding checked below
m_Preferences - > videoCodecConfig ! = StreamingPreferences : : VCC_AUTO & & // Already checked in initialize()
! isHardwareDecodeAvailable ( testWindow ,
m_Preferences - > videoDecoderSelection ,
VIDEO_FORMAT_H265 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
m_StreamConfig . fps ) ) {
2018-07-27 02:26:22 +00:00
if ( hevcForced ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance. " ) ) ;
2018-08-04 23:45:31 +00:00
}
else {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " This PC's GPU doesn't support HEVC decoding. " ) ) ;
2018-08-04 23:45:31 +00:00
m_StreamConfig . supportsHevc = false ;
2018-07-27 02:26:22 +00:00
}
}
if ( hevcForced ) {
if ( m_Computer - > maxLumaPixelsHEVC = = 0 ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Your host PC GPU doesn't support HEVC. "
" A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming. " ) ) ;
2018-08-10 02:37:49 +00:00
// 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 ;
2018-07-27 02:26:22 +00:00
}
2018-07-13 09:28:10 +00:00
}
2018-07-09 03:53:24 +00:00
}
2018-06-28 08:44:43 +00:00
if ( m_StreamConfig . enableHdr ) {
// Turn HDR back off unless all criteria are met.
m_StreamConfig . enableHdr = false ;
// Check that the app supports HDR
if ( ! m_App . hdrSupported ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " %1 doesn't support HDR10. " ) . arg ( m_App . name ) ) ;
2018-06-28 08:44:43 +00:00
}
// Check that the server GPU supports HDR
else if ( ! ( m_Computer - > serverCodecModeSupport & 0x200 ) ) {
2020-11-21 19:15:54 +00:00
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. " ) ) ;
2018-06-28 08:44:43 +00:00
}
2019-03-24 00:46:42 +00:00
else if ( ! isHardwareDecodeAvailable ( testWindow ,
m_Preferences - > videoDecoderSelection ,
2018-07-13 09:28:10 +00:00
VIDEO_FORMAT_H265_MAIN10 ,
m_StreamConfig . width ,
2018-07-18 03:00:16 +00:00
m_StreamConfig . height ,
m_StreamConfig . fps ) ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming. " ) ) ;
2018-07-13 09:28:10 +00:00
}
2018-06-28 08:44:43 +00:00
else {
2018-07-13 09:28:10 +00:00
// TODO: Also validate display capabilites
2018-06-28 08:44:43 +00:00
// Validation successful so HDR is good to go
m_StreamConfig . enableHdr = true ;
}
}
if ( m_StreamConfig . width > = 3840 ) {
2018-07-09 03:53:24 +00:00
// Only allow 4K on GFE 3.x+
2018-09-15 01:36:15 +00:00
if ( m_Computer - > gfeVersion . isEmpty ( ) | | m_Computer - > gfeVersion . startsWith ( " 2. " ) ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " GeForce Experience 3.0 or higher is required for 4K streaming. " ) ) ;
2018-07-09 03:53:24 +00:00
m_StreamConfig . width = 1920 ;
m_StreamConfig . height = 1080 ;
2018-06-28 08:44:43 +00:00
}
}
2018-09-22 23:04:36 +00:00
// Test if audio works at the specified audio configuration
bool audioTestPassed = testAudio ( m_StreamConfig . audioConfiguration ) ;
2020-04-01 04:36:16 +00:00
// Gracefully degrade to stereo if surround sound doesn't work
if ( ! audioTestPassed & & CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION ( m_StreamConfig . audioConfiguration ) > 2 ) {
2020-04-03 07:12:52 +00:00
audioTestPassed = testAudio ( AUDIO_CONFIGURATION_STEREO ) ;
2018-09-22 23:04:36 +00:00
if ( audioTestPassed ) {
2020-04-03 07:12:52 +00:00
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_STEREO ;
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Your selected surround sound setting is not supported by the current audio device. " ) ) ;
2018-09-22 23:04:36 +00:00
}
}
// If nothing worked, warn the user that audio will not work
m_AudioDisabled = ! audioTestPassed ;
2018-08-31 04:09:31 +00:00
if ( m_AudioDisabled ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " Failed to open audio device. Audio will be unavailable during this session. " ) ) ;
2018-08-31 04:09:31 +00:00
}
2018-09-30 02:14:52 +00:00
// Check for unmapped gamepads
if ( ! SdlInputHandler : : getUnmappedGamepads ( ) . isEmpty ( ) ) {
2020-11-21 19:15:54 +00:00
emitLaunchWarning ( tr ( " An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this. " ) ) ;
2018-09-30 02:14:52 +00:00
}
2020-04-04 19:47:44 +00:00
// NVENC will fail to initialize when using dimensions over 4096 and H.264.
if ( m_StreamConfig . width > 4096 | | m_StreamConfig . height > 4096 ) {
2020-12-23 00:57:45 +00:00
// 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 ) ) {
2020-11-21 19:15:54 +00:00
emit displayLaunchError ( tr ( " Your host PC's GPU doesn't support streaming video resolutions over 4K. " ) ) ;
2020-04-04 19:47:44 +00:00
return false ;
}
else if ( ! m_StreamConfig . supportsHevc ) {
2020-11-21 19:15:54 +00:00
emit displayLaunchError ( tr ( " Video resolutions over 4K are only supported by the HEVC codec. " ) ) ;
2020-04-04 19:47:44 +00:00
return false ;
}
}
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > videoDecoderSelection = = StreamingPreferences : : VDS_FORCE_HARDWARE & &
2019-12-14 23:20:44 +00:00
! m_StreamConfig . enableHdr & & // HEVC Main10 was already checked for hardware decode support above
2019-03-24 00:46:42 +00:00
! isHardwareDecodeAvailable ( testWindow ,
m_Preferences - > videoDecoderSelection ,
2018-08-10 02:37:49 +00:00
m_StreamConfig . supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
m_StreamConfig . fps ) ) {
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > videoCodecConfig = = StreamingPreferences : : VCC_AUTO ) {
2020-11-21 19:15:54 +00:00
emit displayLaunchError ( tr ( " Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU. " ) ) ;
2018-08-10 02:37:49 +00:00
}
else {
2020-11-21 19:15:54 +00:00
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. " ) ) ;
2018-08-10 02:37:49 +00:00
}
// Fail the launch, because we won't manage to get a decoder for the actual stream
return false ;
}
2018-07-07 23:30:26 +00:00
return true ;
2018-06-28 08:44:43 +00:00
}
2018-07-09 07:09:06 +00:00
class DeferredSessionCleanupTask : public QRunnable
{
2018-12-06 03:49:06 +00:00
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 ( ) ;
}
2018-07-09 07:09:06 +00:00
void run ( ) override
{
2018-12-06 04:17:00 +00:00
// Only quit the running app if our session terminated gracefully
bool shouldQuit =
2018-12-06 06:17:26 +00:00
! m_Session - > m_UnexpectedTermination & &
2018-12-06 04:17:00 +00:00
m_Session - > m_Preferences - > quitAppAfter ;
2018-12-06 03:49:06 +00:00
// Notify the UI
2018-12-06 04:17:00 +00:00
if ( shouldQuit ) {
emit m_Session - > quitStarting ( ) ;
2018-12-06 03:49:06 +00:00
}
else {
2020-08-11 05:21:54 +00:00
emit m_Session - > sessionFinished ( m_Session - > m_PortTestResults ) ;
2018-12-06 03:49:06 +00:00
}
2018-07-09 07:09:06 +00:00
// Finish cleanup of the connection state
LiStopConnection ( ) ;
2018-12-06 03:49:06 +00:00
// Perform a best-effort app quit
2018-12-06 04:17:00 +00:00
if ( shouldQuit ) {
2018-12-22 02:08:07 +00:00
NvHTTP http ( m_Session - > m_Computer - > activeAddress , m_Session - > m_Computer - > serverCert ) ;
2018-12-06 03:49:06 +00:00
// Logging is already done inside NvHTTP
try {
http . quitApp ( ) ;
} catch ( const GfeHttpResponseException & ) {
} catch ( const QtNetworkReplyException & ) {
}
// Session is finished now
2020-08-11 05:21:54 +00:00
emit m_Session - > sessionFinished ( m_Session - > m_PortTestResults ) ;
2018-12-06 03:49:06 +00:00
}
2018-07-09 07:09:06 +00:00
}
2018-12-06 03:49:06 +00:00
Session * m_Session ;
2018-07-09 07:09:06 +00:00
} ;
2018-09-05 22:45:36 +00:00
void Session : : getWindowDimensions ( int & x , int & y ,
2018-07-20 23:01:22 +00:00
int & width , int & height )
{
int displayIndex = 0 ;
2018-11-19 07:10:30 +00:00
bool fullScreen ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
if ( m_Window ! = nullptr ) {
displayIndex = SDL_GetWindowDisplayIndex ( m_Window ) ;
SDL_assert ( displayIndex > = 0 ) ;
2018-11-19 07:10:30 +00:00
fullScreen = ( SDL_GetWindowFlags ( m_Window ) & SDL_WINDOW_FULLSCREEN ) ;
2018-08-17 05:25:14 +00:00
}
2018-09-05 22:15:53 +00:00
// 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 ) ;
2018-08-05 21:55:26 +00:00
for ( int i = 0 ; i < SDL_GetNumVideoDisplays ( ) ; i + + ) {
2018-09-05 22:15:53 +00:00
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 ( ) ) ;
2018-08-05 21:55:26 +00:00
}
}
2018-11-19 07:10:30 +00:00
2021-02-07 16:49:29 +00:00
fullScreen = m_IsFullScreen ;
2018-08-05 21:55:26 +00:00
}
2018-08-17 05:25:14 +00:00
SDL_Rect usableBounds ;
2018-11-19 07:10:30 +00:00
if ( fullScreen & & SDL_GetDisplayBounds ( displayIndex , & usableBounds ) = = 0 ) {
width = usableBounds . w ;
height = usableBounds . h ;
}
else if ( SDL_GetDisplayUsableBounds ( displayIndex , & usableBounds ) = = 0 ) {
2018-08-17 05:25:14 +00:00
width = usableBounds . w ;
height = usableBounds . h ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
if ( m_Window ! = nullptr ) {
int top , left , bottom , right ;
2018-07-20 23:01:22 +00:00
2018-09-05 22:15:53 +00:00
if ( SDL_GetWindowBordersSize ( m_Window , & top , & left , & bottom , & right ) = = 0 ) {
width - = left + right ;
height - = top + bottom ;
}
else {
2018-08-17 05:25:14 +00:00
SDL_LogWarn ( SDL_LOG_CATEGORY_APPLICATION ,
2018-09-05 22:15:53 +00:00
" Unable to get window border size: %s " ,
SDL_GetError ( ) ) ;
2018-08-17 05:25:14 +00:00
}
2018-07-23 01:28:17 +00:00
2018-08-17 05:25:14 +00:00
// 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 ;
2018-07-21 02:18:55 +00:00
}
2018-07-20 23:01:22 +00:00
}
2018-08-17 05:25:14 +00:00
}
else {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_GetDisplayUsableBounds() failed: %s " ,
SDL_GetError ( ) ) ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
width = m_StreamConfig . width ;
height = m_StreamConfig . height ;
2018-07-20 23:01:22 +00:00
}
2018-09-07 01:53:00 +00:00
x = y = SDL_WINDOWPOS_CENTERED_DISPLAY ( displayIndex ) ;
2018-07-20 23:01:22 +00:00
}
2018-09-04 02:54:41 +00:00
void Session : : updateOptimalWindowDisplayMode ( )
{
SDL_DisplayMode desktopMode , bestMode , mode ;
int displayIndex = SDL_GetWindowDisplayIndex ( m_Window ) ;
2018-09-07 08:12:18 +00:00
// 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 ;
2018-09-04 02:54:41 +00:00
}
}
}
2018-09-07 08:12:18 +00:00
}
2018-09-04 02:54:41 +00:00
2018-09-07 08:12:18 +00:00
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 ;
}
2018-09-07 02:16:59 +00:00
2018-10-13 03:02:54 +00:00
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 ) ;
}
2018-09-04 02:54:41 +00:00
2018-09-07 08:12:18 +00:00
SDL_SetWindowDisplayMode ( m_Window , & bestMode ) ;
2018-09-04 02:54:41 +00:00
}
2018-07-20 23:01:22 +00:00
void Session : : toggleFullscreen ( )
{
2018-09-04 02:17:34 +00:00
bool fullScreen = ! ( SDL_GetWindowFlags ( m_Window ) & m_FullScreenFlag ) ;
2018-07-20 23:01:22 +00:00
2018-07-21 02:18:55 +00:00
if ( fullScreen ) {
2021-02-27 22:47:38 +00:00
if ( m_FullScreenFlag = = SDL_WINDOW_FULLSCREEN & & m_InputHandler - > isCaptureActive ( ) ) {
2021-01-16 01:32:38 +00:00
// Confine the cursor to the window if we're capturing input while transitioning to full screen.
2020-04-25 20:15:14 +00:00
SDL_SetWindowGrab ( m_Window , SDL_TRUE ) ;
}
2018-08-04 22:30:44 +00:00
SDL_SetWindowResizable ( m_Window , SDL_FALSE ) ;
2018-09-04 02:17:34 +00:00
SDL_SetWindowFullscreen ( m_Window , m_FullScreenFlag ) ;
2018-07-21 02:18:55 +00:00
}
2018-09-08 23:01:35 +00:00
else {
2020-04-25 20:15:14 +00:00
// Unconfine the cursor
SDL_SetWindowGrab ( m_Window , SDL_FALSE ) ;
2018-09-08 23:01:35 +00:00
SDL_SetWindowFullscreen ( m_Window , 0 ) ;
SDL_SetWindowResizable ( m_Window , SDL_TRUE ) ;
// Reposition the window when the resize is complete
m_PendingWindowedTransition = true ;
}
2021-02-27 22:47:38 +00:00
// Input handler might need to start/stop keyboard grab after changing modes
m_InputHandler - > updateKeyboardGrabState ( ) ;
2018-07-20 23:01:22 +00:00
}
2019-05-19 20:10:42 +00:00
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 \n Long press Start to deactivate " ) ;
m_OverlayManager . setOverlayTextUpdated ( Overlay : : OverlayStatusUpdate ) ;
m_OverlayManager . setOverlayState ( Overlay : : OverlayStatusUpdate , true ) ;
}
else {
m_OverlayManager . setOverlayState ( Overlay : : OverlayStatusUpdate , false ) ;
}
}
2020-08-11 05:21:54 +00:00
class AsyncConnectionStartThread : public QThread
2018-06-28 08:44:43 +00:00
{
2020-08-11 05:21:54 +00:00
public :
AsyncConnectionStartThread ( Session * session ) :
QThread ( nullptr ) ,
m_Session ( session ) { }
2018-07-07 23:30:26 +00:00
2020-08-11 05:21:54 +00:00
void run ( ) override
{
m_Session - > m_AsyncConnectionSuccess = m_Session - > startConnectionAsync ( ) ;
2018-09-15 02:11:06 +00:00
}
2018-07-07 23:30:26 +00:00
2020-08-11 05:21:54 +00:00
Session * m_Session ;
} ;
2018-06-28 08:44:43 +00:00
2020-08-11 05:21:54 +00:00
// 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 ) ;
2018-06-28 08:44:43 +00:00
// 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 ) ;
2019-02-10 04:37:11 +00:00
// 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 ) ;
2020-08-30 08:38:26 +00:00
enableGameOptimizations = m_Preferences - > gameOptimizations ;
2019-02-10 04:37:11 +00:00
break ;
}
}
2018-06-28 09:10:31 +00:00
try {
2018-12-22 02:08:07 +00:00
NvHTTP http ( m_Computer - > activeAddress , m_Computer - > serverCert ) ;
2018-06-28 09:10:31 +00:00
if ( m_Computer - > currentGameId ! = 0 ) {
http . resumeApp ( & m_StreamConfig ) ;
}
else {
http . launchApp ( m_App . id , & m_StreamConfig ,
2019-02-10 04:37:11 +00:00
enableGameOptimizations ,
2020-08-30 08:38:26 +00:00
m_Preferences - > playAudioOnHost ,
2019-02-12 05:39:55 +00:00
m_InputHandler - > getAttachedGamepadMask ( ) ) ;
2018-06-28 09:10:31 +00:00
}
} catch ( const GfeHttpResponseException & e ) {
2020-11-21 19:15:54 +00:00
emit displayLaunchError ( tr ( " GeForce Experience returned error: %1 " ) . arg ( e . toQString ( ) ) ) ;
2020-08-11 05:21:54 +00:00
return false ;
2018-09-29 10:01:49 +00:00
} catch ( const QtNetworkReplyException & e ) {
emit displayLaunchError ( e . toQString ( ) ) ;
2020-08-11 05:21:54 +00:00
return false ;
2018-06-28 08:44:43 +00:00
}
QByteArray hostnameStr = m_Computer - > activeAddress . 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 ;
2018-09-15 01:36:15 +00:00
if ( ! m_Computer - > gfeVersion . isEmpty ( ) ) {
2018-06-28 08:44:43 +00:00
siGfeVersion = m_Computer - > gfeVersion . toLatin1 ( ) ;
}
2018-09-15 01:36:15 +00:00
if ( ! siGfeVersion . isEmpty ( ) ) {
2018-06-28 08:44:43 +00:00
hostInfo . serverInfoGfeVersion = siGfeVersion . data ( ) ;
}
2020-01-22 03:10:10 +00:00
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 ) ;
2019-12-02 05:50:36 +00:00
}
else {
2020-01-22 03:10:10 +00:00
// 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 ;
}
2019-12-02 05:50:36 +00:00
}
2018-06-28 08:44:43 +00:00
int err = LiStartConnection ( & hostInfo , & m_StreamConfig , & k_ConnCallbacks ,
2018-08-31 04:09:31 +00:00
& m_VideoCallbacks ,
2019-06-23 19:49:37 +00:00
m_AudioDisabled ? nullptr : & m_AudioCallbacks ,
2018-06-28 08:44:43 +00:00
NULL , 0 , NULL , 0 ) ;
if ( err ! = 0 ) {
// We already displayed an error dialog in the stage failure
// listener.
2020-08-11 05:21:54 +00:00
return false ;
}
emit connectionStarted ( ) ;
return true ;
}
2021-01-09 23:51:25 +00:00
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.
2021-01-15 01:28:21 +00:00
m_FlushingWindowEventsRef + + ;
2021-01-09 23:51:25 +00:00
// 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 ) ;
}
2021-03-07 16:06:12 +00:00
class ExecThread : public QThread
{
public :
ExecThread ( Session * session ) :
QThread ( nullptr ) ,
m_Session ( session ) { }
void run ( ) override
{
m_Session - > execInternal ( ) ;
}
Session * m_Session ;
} ;
2020-08-11 05:21:54 +00:00
void Session : : exec ( int displayOriginX , int displayOriginY )
{
m_DisplayOriginX = displayOriginX ;
m_DisplayOriginY = displayOriginY ;
2021-03-07 16:06:12 +00:00
// 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 ( )
{
2020-08-11 05:21:54 +00:00
// 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 ) ;
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.
2020-08-30 08:38:26 +00:00
m_InputHandler = new SdlInputHandler ( * m_Preferences , m_Computer ,
2020-08-11 05:21:54 +00:00
m_StreamConfig . width ,
m_StreamConfig . height ) ;
AsyncConnectionStartThread asyncConnThread ( this ) ;
2021-03-07 16:06:12 +00:00
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().
2020-08-11 05:21:54 +00:00
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
2020-09-05 05:38:54 +00:00
QCoreApplication : : sendPostedEvents ( ) ;
2020-08-11 05:21:54 +00:00
}
2021-03-07 16:06:12 +00:00
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 ( ) ;
}
2020-09-05 05:38:54 +00:00
2020-08-11 05:21:54 +00:00
// If the connection failed, clean up and abort the connection.
if ( ! m_AsyncConnectionSuccess ) {
2019-02-16 09:39:31 +00:00
delete m_InputHandler ;
m_InputHandler = nullptr ;
2019-04-22 00:43:38 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-12-06 06:17:26 +00:00
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( this ) ) ;
2018-06-28 08:44:43 +00:00
return ;
}
2018-07-21 02:55:07 +00:00
int x , y , width , height ;
2018-09-05 22:45:36 +00:00
getWindowDimensions ( x , y , width , height ) ;
2018-07-21 02:55:07 +00:00
2020-03-19 05:26:15 +00:00
# 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
2018-07-21 02:55:07 +00:00
m_Window = SDL_CreateWindow ( " Moonlight " ,
x ,
y ,
width ,
height ,
2020-09-05 21:06:56 +00:00
SDL_WINDOW_ALLOW_HIGHDPI | StreamUtils : : getPlatformWindowFlags ( ) ) ;
2018-07-21 02:55:07 +00:00
if ( ! m_Window ) {
2020-09-05 21:06:56 +00:00
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 ;
}
2018-07-21 02:55:07 +00:00
}
2020-04-29 03:24:23 +00:00
m_InputHandler - > setWindow ( m_Window ) ;
2018-10-13 03:34:58 +00:00
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
2018-07-21 02:55:07 +00:00
// For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account
// for window chrome size.
2021-02-07 16:49:29 +00:00
if ( ! m_IsFullScreen ) {
2018-09-05 22:45:36 +00:00
getWindowDimensions ( x , y , width , height ) ;
2018-07-21 02:55:07 +00:00
2018-09-08 23:01:35 +00:00
// We must set the size before the position because centering
// won't work unless it knows the final size of the window.
2018-07-21 02:55:07 +00:00
SDL_SetWindowSize ( m_Window , width , height ) ;
2018-09-08 23:01:35 +00:00
SDL_SetWindowPosition ( m_Window , x , y ) ;
2018-08-04 22:30:44 +00:00
// 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 ) ;
2018-07-21 02:55:07 +00:00
}
2018-08-17 05:25:14 +00:00
else {
// Update the window display mode based on our current monitor
2018-09-04 02:54:41 +00:00
updateOptimalWindowDisplayMode ( ) ;
2018-08-17 05:25:14 +00:00
// Enter full screen
2018-09-04 02:17:34 +00:00
SDL_SetWindowFullscreen ( m_Window , m_FullScreenFlag ) ;
2018-08-17 05:25:14 +00:00
}
2018-07-08 04:52:20 +00:00
2020-03-14 07:33:53 +00:00
bool needsFirstEnterCapture = false ;
2020-05-10 02:48:13 +00:00
bool needsPostDecoderCreationCapture = false ;
2020-03-14 07:33:53 +00:00
2020-05-02 01:43:54 +00:00
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
2020-05-10 02:48:13 +00:00
// 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 ) {
2020-05-02 01:43:54 +00:00
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
needsFirstEnterCapture = true ;
}
2020-05-10 02:48:13 +00:00
else {
// X11/XWayland: Capture after decoder creation
needsPostDecoderCreationCapture = true ;
}
2018-06-28 08:44:43 +00:00
2018-07-31 05:44:19 +00:00
// 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 ( ) ;
2018-07-13 09:28:10 +00:00
// Disable the screen saver
SDL_DisableScreenSaver ( ) ;
2020-01-16 02:23:02 +00:00
// Hide Qt's fake mouse cursor on EGLFS systems
if ( QGuiApplication : : platformName ( ) = = " eglfs " ) {
QGuiApplication : : setOverrideCursor ( QCursor ( Qt : : BlankCursor ) ) ;
}
2020-01-11 10:06:08 +00:00
2018-08-14 05:23:05 +00:00
// Set timer resolution to 1 ms on Windows for greater
// sleep precision and more accurate callback timing.
SDL_SetHint ( SDL_HINT_TIMER_RESOLUTION , " 1 " ) ;
2018-09-07 02:42:53 +00:00
int currentDisplayIndex = SDL_GetWindowDisplayIndex ( m_Window ) ;
2018-12-06 06:17:26 +00:00
// 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 ;
2019-06-30 00:40:30 +00:00
// Start rich presence to indicate we're in game
2020-08-30 08:38:26 +00:00
RichPresenceManager presence ( * m_Preferences , m_App . name ) ;
2019-06-30 00:40:30 +00:00
2018-09-13 19:14:56 +00:00
// 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.
2018-06-28 08:44:43 +00:00
SDL_Event event ;
2018-08-02 01:26:50 +00:00
for ( ; ; ) {
2019-01-22 04:10:13 +00:00
// 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 ) ) {
2019-04-28 20:04:58 +00:00
# ifndef STEAM_LINK
2019-01-22 04:10:13 +00:00
SDL_Delay ( 1 ) ;
2019-04-28 20:04:58 +00:00
# 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
2019-06-30 00:40:30 +00:00
presence . runCallbacks ( ) ;
2019-01-22 04:10:13 +00:00
continue ;
}
2018-06-28 08:44:43 +00:00
switch ( event . type ) {
case SDL_QUIT :
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Quit event received " ) ;
2018-07-09 07:09:06 +00:00
goto DispatchDeferredCleanup ;
2018-07-08 04:52:20 +00:00
2019-04-10 04:46:14 +00:00
case SDL_USEREVENT :
2020-07-12 22:06:36 +00:00
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 ;
2021-02-07 18:38:57 +00:00
case SDL_CODE_UNCAPTURE_MOUSE :
SDL_CaptureMouse ( SDL_FALSE ) ;
break ;
2021-01-09 23:51:25 +00:00
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER :
2021-01-15 01:28:21 +00:00
m_FlushingWindowEventsRef - - ;
2021-01-09 23:51:25 +00:00
break ;
2020-07-12 22:06:36 +00:00
default :
SDL_assert ( false ) ;
}
2018-07-08 04:52:20 +00:00
break ;
2018-07-20 22:31:57 +00:00
2018-07-21 02:37:54 +00:00
case SDL_WINDOWEVENT :
2020-12-25 21:32:11 +00:00
// Early handling of some events
switch ( event . window . event ) {
case SDL_WINDOWEVENT_FOCUS_LOST :
2021-01-09 23:56:23 +00:00
if ( m_Preferences - > muteOnFocusLoss ) {
2020-12-25 21:32:11 +00:00
m_AudioMuted = true ;
}
2021-01-09 23:56:23 +00:00
m_InputHandler - > notifyFocusLost ( ) ;
2020-12-25 21:32:11 +00:00
break ;
2021-01-09 23:56:23 +00:00
case SDL_WINDOWEVENT_FOCUS_GAINED :
if ( m_Preferences - > muteOnFocusLoss ) {
2020-12-25 21:32:11 +00:00
m_AudioMuted = false ;
}
2021-02-27 03:43:47 +00:00
m_InputHandler - > notifyFocusGained ( ) ;
2020-12-25 21:32:11 +00:00
break ;
2021-01-09 23:56:23 +00:00
case SDL_WINDOWEVENT_LEAVE :
m_InputHandler - > notifyMouseLeave ( ) ;
break ;
2020-05-08 02:26:02 +00:00
}
2018-09-01 21:31:37 +00:00
2020-03-14 07:33:53 +00:00
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
if ( needsFirstEnterCapture & & event . window . event = = SDL_WINDOWEVENT_ENTER ) {
m_InputHandler - > setCaptureActive ( true ) ;
needsFirstEnterCapture = false ;
}
2018-07-21 07:16:03 +00:00
// 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 ) {
2018-09-07 02:42:53 +00:00
// 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 ;
}
2018-07-21 02:37:54 +00:00
}
2018-09-08 23:01:35 +00:00
// 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 ) ;
}
2021-01-15 01:28:21 +00:00
if ( m_FlushingWindowEventsRef > 0 ) {
2021-01-09 23:51:25 +00:00
// Ignore window events for renderer reset if flushing
2021-01-15 01:28:21 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Dropping window event during flush: %d (%d %d) " ,
event . window . event ,
event . window . data1 ,
event . window . data2 ) ;
2021-01-09 23:51:25 +00:00
break ;
}
2020-05-13 01:41:36 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Recreating renderer for window event: %d (%d %d) " ,
event . window . event ,
event . window . data1 ,
event . window . data2 ) ;
2018-07-21 02:55:07 +00:00
// Fall through
2018-07-20 22:31:57 +00:00
case SDL_RENDER_DEVICE_RESET :
case SDL_RENDER_TARGETS_RESET :
2018-08-17 05:25:14 +00:00
2018-08-19 08:19:23 +00:00
SDL_AtomicLock ( & m_DecoderLock ) ;
// Destroy the old decoder
delete m_VideoDecoder ;
2021-01-09 23:51:25 +00:00
// 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 ( ) ;
2018-08-19 08:19:23 +00:00
2018-08-17 05:25:14 +00:00
// Update the window display mode based on our current monitor
2018-09-07 02:42:53 +00:00
currentDisplayIndex = SDL_GetWindowDisplayIndex ( m_Window ) ;
2018-09-04 02:54:41 +00:00
updateOptimalWindowDisplayMode ( ) ;
2018-08-17 05:25:14 +00:00
2018-08-19 08:19:23 +00:00
// 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 ) ;
2018-07-20 22:31:57 +00:00
2018-09-08 22:09:46 +00:00
{
// 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 ) ;
2018-09-29 21:06:55 +00:00
bool enableVsync = m_Preferences - > enableVsync ;
2018-09-08 22:09:46 +00:00
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).
2018-09-29 21:06:55 +00:00
if ( ! chooseDecoder ( m_Preferences - > videoDecoderSelection ,
2018-09-08 22:09:46 +00:00
m_Window , m_ActiveVideoFormat , m_ActiveVideoWidth ,
m_ActiveVideoHeight , m_ActiveVideoFrameRate ,
enableVsync ,
2018-12-25 20:57:00 +00:00
enableVsync & & m_Preferences - > framePacing ,
2019-02-13 02:42:53 +00:00
false ,
2018-09-08 22:09:46 +00:00
s_ActiveSession - > m_VideoDecoder ) ) {
SDL_AtomicUnlock ( & m_DecoderLock ) ;
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Failed to recreate decoder after reset " ) ;
2020-11-21 19:15:54 +00:00
emit displayLaunchError ( tr ( " Unable to initialize video decoder. Please check your streaming settings and try again. " ) ) ;
2018-09-08 22:09:46 +00:00
goto DispatchDeferredCleanup ;
}
2020-05-10 02:48:13 +00:00
// 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 ) ;
2021-01-15 01:30:30 +00:00
needsPostDecoderCreationCapture = false ;
2020-05-10 02:48:13 +00:00
}
2018-07-20 22:31:57 +00:00
}
// Request an IDR frame to complete the reset
m_NeedsIdr = true ;
SDL_AtomicUnlock ( & m_DecoderLock ) ;
break ;
2018-06-28 08:44:43 +00:00
case SDL_KEYUP :
case SDL_KEYDOWN :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleKeyEvent ( & event . key ) ;
2018-06-28 08:44:43 +00:00
break ;
case SDL_MOUSEBUTTONDOWN :
case SDL_MOUSEBUTTONUP :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleMouseButtonEvent ( & event . button ) ;
2018-06-28 08:44:43 +00:00
break ;
case SDL_MOUSEMOTION :
2020-04-29 03:24:23 +00:00
m_InputHandler - > handleMouseMotionEvent ( & event . motion ) ;
2018-06-28 08:44:43 +00:00
break ;
case SDL_MOUSEWHEEL :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleMouseWheelEvent ( & event . wheel ) ;
2018-06-28 08:44:43 +00:00
break ;
case SDL_CONTROLLERAXISMOTION :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleControllerAxisEvent ( & event . caxis ) ;
2018-06-28 08:44:43 +00:00
break ;
case SDL_CONTROLLERBUTTONDOWN :
case SDL_CONTROLLERBUTTONUP :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleControllerButtonEvent ( & event . cbutton ) ;
2018-06-28 08:44:43 +00:00
break ;
case SDL_CONTROLLERDEVICEADDED :
case SDL_CONTROLLERDEVICEREMOVED :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleControllerDeviceEvent ( & event . cdevice ) ;
2018-06-28 08:44:43 +00:00
break ;
2018-09-30 02:14:52 +00:00
case SDL_JOYDEVICEADDED :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleJoystickArrivalEvent ( & event . jdevice ) ;
2018-09-30 02:14:52 +00:00
break ;
2018-09-30 05:43:28 +00:00
case SDL_FINGERDOWN :
case SDL_FINGERMOTION :
case SDL_FINGERUP :
2020-04-29 03:24:23 +00:00
m_InputHandler - > handleTouchFingerEvent ( & event . tfinger ) ;
2018-09-30 05:43:28 +00:00
break ;
2018-06-28 08:44:43 +00:00
}
}
2018-07-09 07:09:06 +00:00
DispatchDeferredCleanup :
// Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP.
2019-07-03 05:17:18 +00:00
m_InputHandler - > setCaptureActive ( false ) ;
2018-07-13 09:28:10 +00:00
SDL_EnableScreenSaver ( ) ;
2018-08-14 05:23:05 +00:00
SDL_SetHint ( SDL_HINT_TIMER_RESOLUTION , " 0 " ) ;
2020-01-16 02:23:02 +00:00
if ( QGuiApplication : : platformName ( ) = = " eglfs " ) {
QGuiApplication : : restoreOverrideCursor ( ) ;
}
2018-07-09 07:09:06 +00:00
2018-10-04 01:27:12 +00:00
// Raise any keys that are still down
2019-02-12 05:39:55 +00:00
m_InputHandler - > raiseAllKeys ( ) ;
2018-10-04 01:27:12 +00:00
2019-02-13 03:30:02 +00:00
// Destroy the input handler now. Any rumble callbacks that
// occur after this point will be discarded. This must be destroyed
// before allow the UI to continue execution or it could interfere
// with SDLGamepadKeyNavigation.
SDL_AtomicLock ( & m_InputHandlerLock ) ;
delete m_InputHandler ;
m_InputHandler = nullptr ;
SDL_AtomicUnlock ( & m_InputHandlerLock ) ;
2018-07-18 03:00:16 +00:00
// Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock ( & m_DecoderLock ) ;
delete m_VideoDecoder ;
m_VideoDecoder = nullptr ;
SDL_AtomicUnlock ( & m_DecoderLock ) ;
2018-11-01 01:20:39 +00:00
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
2018-07-21 01:15:46 +00:00
SDL_DestroyWindow ( m_Window ) ;
2018-11-01 01:20:39 +00:00
2018-07-23 00:07:45 +00:00
if ( iconSurface ! = nullptr ) {
SDL_FreeSurface ( iconSurface ) ;
}
2018-07-21 01:15:46 +00:00
2019-04-22 00:43:38 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-07-09 07:09:06 +00:00
// Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore
// reference.
2018-12-06 03:49:06 +00:00
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( this ) ) ;
2018-06-28 08:44:43 +00:00
}
2018-07-09 07:09:06 +00:00