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"
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
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>
2018-06-28 08:44:43 +00:00
CONNECTION_LISTENER_CALLBACKS Session : : k_ConnCallbacks = {
Session : : clStageStarting ,
nullptr ,
Session : : clStageFailed ,
nullptr ,
Session : : clConnectionTerminated ,
nullptr ,
nullptr ,
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
} ;
AUDIO_RENDERER_CALLBACKS Session : : k_AudioCallbacks = {
2018-09-13 13:23:06 +00:00
Session : : arInit ,
nullptr ,
nullptr ,
Session : : arCleanup ,
Session : : arDecodeAndPlaySample ,
2018-06-28 08:44:43 +00:00
CAPABILITY_DIRECT_SUBMIT
} ;
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
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
void Session : : clStageFailed ( int stage , long errorCode )
{
// 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 - > stageFailed ( QString : : fromLocal8Bit ( LiGetStageName ( stage ) ) , errorCode ) ;
2018-06-28 08:44:43 +00:00
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
void Session : : clConnectionTerminated ( long errorCode )
{
2019-02-10 10:16:05 +00:00
// Display the termination dialog if this was not intended
if ( errorCode ! = 0 ) {
s_ActiveSession - > m_UnexpectedTermination = true ;
emit s_ActiveSession - > displayLaunchError ( " Connection terminated " ) ;
}
2018-09-02 22:34:10 +00:00
2018-06-28 08:44:43 +00:00
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Connection terminated: %ld " ,
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-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 ) {
strcpy ( s_ActiveSession - > m_OverlayManager . getOverlayText ( Overlay : : OverlayStatusUpdate ) , " Slow connection to PC \n Reduce bitrate " ) ;
}
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-12-25 20:57:00 +00:00
# define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
2018-08-31 14:40:25 +00:00
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
{
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 ) ;
2018-08-31 14:40:25 +00:00
if ( CALL_INITIALIZE ( chosenDecoder ) ) {
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 ) ;
2018-08-31 14:40:25 +00:00
if ( CALL_INITIALIZE ( chosenDecoder ) ) {
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 ;
}
}
bool Session : : isHardwareDecodeAvailable ( StreamingPreferences : : VideoDecoderSelection vds ,
int videoFormat , int width , int height , int frameRate )
{
IVideoDecoder * decoder ;
2018-07-21 01:15:46 +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 ;
}
2018-07-18 03:00:16 +00:00
SDL_Window * window = SDL_CreateWindow ( " " , 0 , 0 , width , height , SDL_WINDOW_HIDDEN ) ;
if ( ! window ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Failed to create window for hardware decode test: %s " ,
SDL_GetError ( ) ) ;
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-07-18 03:00:16 +00:00
return false ;
}
2019-02-13 02:42:53 +00:00
if ( ! chooseDecoder ( vds , window , videoFormat , width , height , frameRate , true , false , true , decoder ) ) {
2018-07-18 03:00:16 +00:00
SDL_DestroyWindow ( window ) ;
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
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
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow ( window ) ;
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-07-18 03:00:16 +00:00
return ret ;
}
2018-08-25 20:36:54 +00:00
int Session : : getDecoderCapabilities ( StreamingPreferences : : VideoDecoderSelection vds ,
int videoFormat , int width , int height , int frameRate )
{
IVideoDecoder * decoder ;
if ( SDL_InitSubSystem ( SDL_INIT_VIDEO ) ! = 0 ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s " ,
SDL_GetError ( ) ) ;
return false ;
}
SDL_Window * window = SDL_CreateWindow ( " " , 0 , 0 , width , height , SDL_WINDOW_HIDDEN ) ;
if ( ! window ) {
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-02-13 02:42:53 +00:00
if ( ! chooseDecoder ( vds , window , videoFormat , width , height , frameRate , true , false , true , decoder ) ) {
2018-08-25 20:36:54 +00:00
SDL_DestroyWindow ( window ) ;
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
return false ;
}
int caps = decoder - > getDecoderCapabilities ( ) ;
2018-11-01 01:49:37 +00:00
delete decoder ;
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
SDL_DestroyWindow ( window ) ;
2018-08-25 20:36:54 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
return caps ;
}
2018-09-29 21:06:55 +00:00
Session : : Session ( NvComputer * computer , NvApp & app , StreamingPreferences * preferences )
: m_Preferences ( preferences ? preferences : new StreamingPreferences ( this ) ) ,
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 ) ,
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 ) ,
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 ( ) ;
}
2018-09-09 17:17:32 +00:00
void Session : : initialize ( )
2018-06-28 08:44:43 +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
// Slice up to 4 times for parallel decode, once slice per core
2018-08-05 20:32:04 +00:00
int slices = qMin ( MAX_SLICES , SDL_GetCPUCount ( ) ) ;
m_VideoCallbacks . capabilities | = CAPABILITY_SLICES_PER_FRAME ( slices ) ;
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Encoder configured for %d slices per frame " ,
slices ) ;
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 ;
2018-11-22 04:55:25 +00:00
m_StreamConfig . streamingRemotely = STREAM_CFG_AUTO ;
m_StreamConfig . packetSize = 1392 ;
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 :
2018-06-28 08:44:43 +00:00
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_STEREO ;
break ;
2018-10-02 22:30:22 +00:00
case StreamingPreferences : : AC_51_SURROUND :
2018-06-28 08:44:43 +00:00
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND ;
break ;
}
2018-07-13 09:28:10 +00:00
2018-07-28 23:06:26 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Audio configuration: %d " ,
m_StreamConfig . audioConfiguration ) ;
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 =
2018-09-29 21:06:55 +00:00
isHardwareDecodeAvailable ( 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
{
case StreamingPreferences : : WM_FULLSCREEN_DESKTOP :
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP ;
break ;
case StreamingPreferences : : WM_FULLSCREEN :
default :
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN ;
break ;
}
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 ) ) {
// Pump the UI loop while we wait
SDL_Delay ( 5 ) ;
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
}
2018-07-07 23:30:26 +00:00
bool Session : : validateLaunch ( )
2018-06-28 08:44:43 +00:00
{
QStringList warningList ;
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > videoDecoderSelection = = StreamingPreferences : : VDS_FORCE_SOFTWARE ) {
2018-08-04 23:45:31 +00:00
emitLaunchWarning ( " Your settings selection to force software decoding may cause poor streaming performance. " ) ;
2018-08-04 23:15:13 +00:00
}
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > unsupportedFps & & m_StreamConfig . fps > 60 ) {
2018-09-08 21:33:34 +00:00
emitLaunchWarning ( " 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 ) {
2018-09-08 22:09:46 +00:00
emitLaunchWarning ( " V-sync will be disabled when streaming at a higher frame rate than the display. " ) ;
}
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
2018-09-29 21:06:55 +00:00
if ( ! isHardwareDecodeAvailable ( m_Preferences - > videoDecoderSelection ,
2018-07-27 02:26:22 +00:00
VIDEO_FORMAT_H265 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
2018-08-04 23:45:31 +00:00
m_StreamConfig . fps ) & &
2018-09-29 21:06:55 +00:00
m_Preferences - > videoDecoderSelection = = StreamingPreferences : : VDS_AUTO ) {
2018-07-27 02:26:22 +00:00
if ( hevcForced ) {
2018-08-10 02:37:49 +00:00
emitLaunchWarning ( " 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 {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " 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 ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " 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 ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( m_App . name + " doesn't support HDR10. " ) ;
2018-06-28 08:44:43 +00:00
}
// Check that the server GPU supports HDR
else if ( ! ( m_Computer - > serverCodecModeSupport & 0x200 ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " 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
}
2018-09-29 21:06:55 +00:00
else if ( ! isHardwareDecodeAvailable ( 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 ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " 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. " ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " 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 ) ;
// Gracefully degrade to stereo if 5.1 doesn't work
if ( ! audioTestPassed & & m_StreamConfig . audioConfiguration = = AUDIO_CONFIGURATION_51_SURROUND ) {
audioTestPassed = testAudio ( AUDIO_CONFIGURATION_STEREO ) ;
if ( audioTestPassed ) {
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_STEREO ;
emitLaunchWarning ( " 5.1 surround sound is not supported by the current audio device. " ) ;
}
}
// 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 ) {
emitLaunchWarning ( " Failed to open audio device. Audio will be unavailable during this session. " ) ;
}
2018-09-30 02:14:52 +00:00
// Check for unmapped gamepads
if ( ! SdlInputHandler : : getUnmappedGamepads ( ) . isEmpty ( ) ) {
emitLaunchWarning ( " An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this. " ) ;
}
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > videoDecoderSelection = = StreamingPreferences : : VDS_FORCE_HARDWARE & &
! isHardwareDecodeAvailable ( 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 ) {
2018-08-10 02:37:49 +00:00
emit displayLaunchError ( " Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU. " ) ;
}
else {
emit displayLaunchError ( " 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 ;
}
2018-08-25 20:36:54 +00:00
// Add the capability flags from the chosen decoder/renderer
2018-09-29 21:06:55 +00:00
m_VideoCallbacks . capabilities | = getDecoderCapabilities ( m_Preferences - > videoDecoderSelection ,
2018-08-25 20:36:54 +00:00
m_StreamConfig . supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
m_StreamConfig . fps ) ;
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 {
2018-12-06 04:17:00 +00:00
emit m_Session - > sessionFinished ( ) ;
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
emit m_Session - > sessionFinished ( ) ;
}
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
fullScreen = ( m_Preferences - > windowMode ! = StreamingPreferences : : WM_WINDOWED ) ;
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 ) {
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 {
SDL_SetWindowFullscreen ( m_Window , 0 ) ;
SDL_SetWindowResizable ( m_Window , SDL_TRUE ) ;
// Reposition the window when the resize is complete
m_PendingWindowedTransition = true ;
}
2018-07-20 23:01:22 +00:00
}
2018-09-05 22:15:53 +00:00
void Session : : exec ( int displayOriginX , int displayOriginY )
2018-06-28 08:44:43 +00:00
{
2018-09-05 22:15:53 +00:00
m_DisplayOriginX = displayOriginX ;
m_DisplayOriginY = displayOriginY ;
2018-09-09 17:17:32 +00:00
// Complete initialization in this deferred context to avoid
// calling expensive functions in the constructor (during the
// process of loading the StreamSegue).
initialize ( ) ;
2018-07-07 23:30:26 +00:00
// Check for validation errors/warnings and emit
// signals for them, if appropriate
if ( ! validateLaunch ( ) ) {
2018-12-06 06:17:26 +00:00
emit sessionFinished ( ) ;
2018-07-07 23:30:26 +00:00
return ;
}
2018-09-15 02:11:06 +00:00
// Wait 1.5 seconds before connecting to let the user
// have time to read any messages present on the segue
uint32_t start = SDL_GetTicks ( ) ;
while ( ! SDL_TICKS_PASSED ( SDL_GetTicks ( ) , start + 1500 ) ) {
// Pump the UI loop while we wait
SDL_Delay ( 5 ) ;
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
2018-07-07 23:30:26 +00:00
2018-07-09 07:09:06 +00:00
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore . acquire ( ) ;
2018-06-28 08:44:43 +00:00
// We're now active
s_ActiveSession = this ;
// Initialize the gamepad code with our preferences
StreamingPreferences prefs ;
2019-02-12 05:39:55 +00:00
m_InputHandler = new SdlInputHandler ( prefs , m_Computer ,
m_StreamConfig . width ,
m_StreamConfig . height ) ;
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 ) ;
enableGameOptimizations = prefs . gameOptimizations ;
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 ,
2018-06-28 09:10:31 +00:00
prefs . playAudioOnHost ,
2019-02-12 05:39:55 +00:00
m_InputHandler - > getAttachedGamepadMask ( ) ) ;
2018-06-28 09:10:31 +00:00
}
} catch ( const GfeHttpResponseException & e ) {
2019-02-16 09:39:31 +00:00
delete m_InputHandler ;
m_InputHandler = nullptr ;
2018-07-07 23:30:26 +00:00
emit displayLaunchError ( e . toQString ( ) ) ;
2018-12-06 06:17:26 +00:00
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( this ) ) ;
2018-06-28 09:10:31 +00:00
return ;
2018-09-29 10:01:49 +00:00
} catch ( const QtNetworkReplyException & e ) {
2019-02-16 09:39:31 +00:00
delete m_InputHandler ;
m_InputHandler = nullptr ;
2018-09-29 10:01:49 +00:00
emit displayLaunchError ( e . toQString ( ) ) ;
2018-12-06 06:17:26 +00:00
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( this ) ) ;
2018-09-29 10:01:49 +00:00
return ;
2018-06-28 08:44:43 +00:00
}
2018-07-21 01:15:46 +00:00
SDL_assert ( ! SDL_WasInit ( SDL_INIT_VIDEO ) ) ;
if ( SDL_InitSubSystem ( SDL_INIT_VIDEO ) ! = 0 ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s " ,
SDL_GetError ( ) ) ;
2019-02-16 09:39:31 +00:00
delete m_InputHandler ;
m_InputHandler = nullptr ;
2018-07-21 01:15:46 +00:00
emit displayLaunchError ( QString : : fromLocal8Bit ( SDL_GetError ( ) ) ) ;
2018-12-06 06:17:26 +00:00
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( this ) ) ;
2018-07-21 01:15:46 +00:00
return ;
}
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 ( ) ;
}
int err = LiStartConnection ( & hostInfo , & m_StreamConfig , & k_ConnCallbacks ,
2018-08-31 04:09:31 +00:00
& m_VideoCallbacks ,
m_AudioDisabled ? nullptr : & k_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.
2019-02-16 09:39:31 +00:00
delete m_InputHandler ;
m_InputHandler = nullptr ;
2018-07-21 01:15:46 +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-07 23:30:26 +00:00
// Pump the message loop to update the UI
emit connectionStarted ( ) ;
2018-06-28 08:44:43 +00:00
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
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
m_Window = SDL_CreateWindow ( " Moonlight " ,
x ,
y ,
width ,
height ,
2018-09-04 04:21:37 +00:00
SDL_WINDOW_ALLOW_HIGHDPI ) ;
2018-07-21 02:55:07 +00:00
if ( ! m_Window ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_CreateWindow() failed: %s " ,
SDL_GetError ( ) ) ;
2019-02-16 09:39:31 +00:00
delete m_InputHandler ;
m_InputHandler = nullptr ;
2018-07-21 02:55:07 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-12-06 06:17:26 +00:00
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( this ) ) ;
2018-07-21 02:55:07 +00:00
return ;
}
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.
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > windowMode = = StreamingPreferences : : WM_WINDOWED ) {
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
2018-07-22 23:21:15 +00:00
# ifndef QT_DEBUG
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
2018-09-29 21:06:55 +00:00
if ( m_Preferences - > windowMode ! = StreamingPreferences : : WM_WINDOWED ) {
2018-08-11 21:19:42 +00:00
SDL_SetRelativeMouseMode ( SDL_TRUE ) ;
}
2018-07-22 23:21:15 +00:00
# endif
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 ( ) ;
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 ;
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 ) ) {
SDL_Delay ( 1 ) ;
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
case SDL_USEREVENT : {
SDL_Event nextEvent ;
SDL_assert ( event . user . code = = SDL_CODE_FRAME_READY ) ;
// Drop any earlier frames
while ( SDL_PeepEvents ( & nextEvent ,
1 ,
SDL_GETEVENT ,
SDL_USEREVENT ,
SDL_USEREVENT ) = = 1 ) {
2018-07-18 03:00:16 +00:00
m_VideoDecoder - > dropFrame ( & event . user ) ;
2018-07-08 04:52:20 +00:00
event = nextEvent ;
}
// Render the last frame
2018-07-18 03:00:16 +00:00
m_VideoDecoder - > renderFrame ( & event . user ) ;
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 :
2018-09-08 13:39:20 +00:00
// Capture mouse cursor when user actives the window by clicking on
// window's client area (borders and title bar excluded).
// Without this you would have to click the window twice (once to
2018-09-08 21:05:42 +00:00
// activate it, second time to enable capture). With this you need to
2018-09-08 13:39:20 +00:00
// click it only once.
2018-09-08 21:05:42 +00:00
// On Linux, the button press event is delivered after the focus gain
// so this is not neccessary (and leads to a click sent to the host
// when focusing the window by clicking).
2018-09-08 13:39:20 +00:00
// By excluding window's borders and title bar out, lets user still
// interact with them without mouse capture kicking in.
2018-09-08 21:05:42 +00:00
# if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
2018-09-08 13:39:20 +00:00
if ( event . window . event = = SDL_WINDOWEVENT_FOCUS_GAINED ) {
int mouseX , mouseY ;
Uint32 mouseState = SDL_GetGlobalMouseState ( & mouseX , & mouseY ) ;
if ( mouseState & SDL_BUTTON ( SDL_BUTTON_LEFT ) ) {
int x , y , width , height ;
SDL_GetWindowPosition ( m_Window , & x , & y ) ;
SDL_GetWindowSize ( m_Window , & width , & height ) ;
if ( mouseX > x & & mouseX < x + width & & mouseY > y & & mouseY < y + height ) {
SDL_SetRelativeMouseMode ( SDL_TRUE ) ;
}
}
}
2018-09-08 21:05:42 +00:00
# endif
2018-09-08 13:39:20 +00:00
2018-10-04 01:27:12 +00:00
if ( event . window . event = = SDL_WINDOWEVENT_FOCUS_LOST ) {
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
// Doing this while the window is full-screen breaks the transition out of FS
// (desktop and exclusive), so we must check for that before releasing mouse capture.
if ( ! ( SDL_GetWindowFlags ( m_Window ) & SDL_WINDOW_FULLSCREEN ) ) {
SDL_SetRelativeMouseMode ( SDL_FALSE ) ;
}
// Raise all keys that are currently pressed. If we don't do this, certain keys
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
2019-02-12 05:39:55 +00:00
m_InputHandler - > raiseAllKeys ( ) ;
2018-09-01 21:31:37 +00:00
}
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 ) ;
}
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 ;
// Flush any other pending window events that could
// send us back here immediately
SDL_PumpEvents ( ) ;
SDL_FlushEvent ( SDL_WINDOWEVENT ) ;
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 " ) ;
emit displayLaunchError ( " Unable to initialize video decoder. Please check your streaming settings and try again. " ) ;
goto DispatchDeferredCleanup ;
}
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 :
2019-02-12 05:39:55 +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-10-03 03:11:13 +00:00
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS.
# ifndef Q_OS_DARWIN
2018-09-30 05:43:28 +00:00
case SDL_FINGERDOWN :
case SDL_FINGERMOTION :
case SDL_FINGERUP :
2019-02-12 05:39:55 +00:00
m_InputHandler - > handleTouchFingerEvent ( & event . tfinger ) ;
2018-09-30 05:43:28 +00:00
break ;
2018-10-03 03:11:13 +00:00
# endif
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.
SDL_SetRelativeMouseMode ( SDL_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 " ) ;
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
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
SDL_assert ( ! SDL_WasInit ( 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