#include #include #include #include #include #include #include #include #include #include #include #include #include // Don't let SDL hook our main function, since Qt is already // doing the same thing. This needs to be before any headers // that might include SDL.h themselves. #define SDL_MAIN_HANDLED #include #ifdef HAVE_FFMPEG #include "streaming/video/ffmpeg.h" #endif #if defined(Q_OS_WIN32) #include "antihookingprotection.h" #elif defined(Q_OS_LINUX) #include #endif #include "cli/quitstream.h" #include "cli/startstream.h" #include "cli/commandlineparser.h" #include "path.h" #include "utils.h" #include "gui/computermodel.h" #include "gui/appmodel.h" #include "backend/autoupdatechecker.h" #include "backend/systemproperties.h" #include "streaming/session.h" #include "settings/streamingpreferences.h" #include "gui/sdlgamepadkeynavigation.h" #if !defined(QT_DEBUG) && defined(Q_OS_WIN32) // Log to file for release Windows builds #define USE_CUSTOM_LOGGER #define LOG_TO_FILE #elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) // Use stdout logger on all Linux/BSD builds #define USE_CUSTOM_LOGGER #elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN) // Log to file for release Mac builds #define USE_CUSTOM_LOGGER #define LOG_TO_FILE #else // For debug Windows and Mac builds, use default logger #endif #ifdef USE_CUSTOM_LOGGER static QElapsedTimer s_LoggerTime; static QTextStream s_LoggerStream(stdout); static QMutex s_LoggerLock; #ifdef LOG_TO_FILE #define MAX_LOG_LINES 10000 static int s_LogLinesWritten = 0; static bool s_LogLimitReached = false; static QFile* s_LoggerFile; #endif void logToLoggerStream(QString& message) { QMutexLocker lock(&s_LoggerLock); #ifdef LOG_TO_FILE if (s_LogLimitReached) { return; } else if (s_LogLinesWritten == MAX_LOG_LINES) { s_LoggerStream << "Log size limit reached!"; #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) s_LoggerStream << Qt::endl; #else s_LoggerStream << endl; #endif s_LogLimitReached = true; return; } else { s_LogLinesWritten++; } #endif s_LoggerStream << message; s_LoggerStream.flush(); } void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message) { QString priorityTxt; switch (priority) { case SDL_LOG_PRIORITY_VERBOSE: priorityTxt = "Verbose"; break; case SDL_LOG_PRIORITY_DEBUG: priorityTxt = "Debug"; break; case SDL_LOG_PRIORITY_INFO: priorityTxt = "Info"; break; case SDL_LOG_PRIORITY_WARN: priorityTxt = "Warn"; break; case SDL_LOG_PRIORITY_ERROR: priorityTxt = "Error"; break; case SDL_LOG_PRIORITY_CRITICAL: priorityTxt = "Critical"; break; default: priorityTxt = "Unknown"; break; } QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed()); QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message); logToLoggerStream(txt); } void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg) { QString typeTxt; switch (type) { case QtDebugMsg: typeTxt = "Debug"; break; case QtInfoMsg: typeTxt = "Info"; break; case QtWarningMsg: typeTxt = "Warning"; break; case QtCriticalMsg: typeTxt = "Critical"; break; case QtFatalMsg: typeTxt = "Fatal"; break; } QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed()); QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg); logToLoggerStream(txt); } #ifdef HAVE_FFMPEG void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl) { char lineBuffer[1024]; static int printPrefix = 1; if ((level & 0xFF) > av_log_get_level()) { return; } av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix); QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed()); QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer); logToLoggerStream(txt); } #endif #endif #ifdef Q_OS_WIN32 #define WIN32_LEAN_AND_MEAN #include #include static UINT s_HitUnhandledException = 0; LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo) { // Only write a dump for the first unhandled exception if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) { return EXCEPTION_CONTINUE_SEARCH; } WCHAR dmpFileName[MAX_PATH]; swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp", (PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch()); QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName); HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (dumpHandle != INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION info; info.ThreadId = GetCurrentThreadId(); info.ExceptionPointers = ExceptionInfo; info.ClientPointers = FALSE; DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory | MiniDumpIgnoreInaccessibleMemory | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo; if (MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dumpHandle, (MINIDUMP_TYPE)typeFlags, &info, nullptr, nullptr)) { qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName; } else { qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError(); } CloseHandle(dumpHandle); } else { qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError(); } // Let the program crash and WER collect a dump return EXCEPTION_CONTINUE_SEARCH; } #endif int main(int argc, char *argv[]) { SDL_SetMainReady(); // Set the app version for the QCommandLineParser's showVersion() command QCoreApplication::setApplicationVersion(VERSION_STR); // Set these here to allow us to use the default QSettings constructor. // These also ensure that our cache directory is named correctly. As such, // it is critical that these be called before Path::initialize(). QCoreApplication::setOrganizationName("Moonlight Game Streaming Project"); QCoreApplication::setOrganizationDomain("moonlight-stream.com"); QCoreApplication::setApplicationName("Moonlight"); if (QFile(QDir::currentPath() + "/portable.dat").exists()) { qInfo() << "Running in portable mode from:" << QDir::currentPath(); QSettings::setDefaultFormat(QSettings::IniFormat); QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath()); QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath()); // Initialize paths for portable mode Path::initialize(true); } else { // Initialize paths for standard installation Path::initialize(false); } #ifdef USE_CUSTOM_LOGGER #ifdef LOG_TO_FILE QDir tempDir(Path::getLogDir()); s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch()))); if (s_LoggerFile->open(QIODevice::WriteOnly)) { qInfo() << "Redirecting log output to " << s_LoggerFile->fileName(); s_LoggerStream.setDevice(s_LoggerFile); } #endif s_LoggerTime.start(); qInstallMessageHandler(qtLogToDiskHandler); SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr); #ifdef HAVE_FFMPEG av_log_set_callback(ffmpegLogToDiskHandler); #endif #endif #ifdef Q_OS_WIN32 // Create a crash dump when we crash on Windows SetUnhandledExceptionFilter(UnhandledExceptionHandler); #endif #if defined(Q_OS_WIN32) // Force AntiHooking.dll to be statically imported and loaded // by ntdll by calling a dummy function. AntiHookingDummyImport(); #elif defined(Q_OS_LINUX) // Force libssl.so to be directly linked to our binary, so // linuxdeployqt can find it and include it in our AppImage. // QtNetwork will pull it in via dlopen(). SSL_free(nullptr); #endif // Avoid using High DPI on EGLFS. It breaks font rendering. // https://bugreports.qt.io/browse/QTBUG-64377 // // NB: We can't use QGuiApplication::platformName() here because it is only // set once the QGuiApplication is created, which is too late to enable High DPI :( if (WMUtils::isRunningWindowManager()) { // Enable High DPI support QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) // Enable fractional High DPI scaling on Qt 5.14 and later QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif } else { #ifndef STEAM_LINK if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) { qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this."; qputenv("QT_QPA_PLATFORM", "eglfs"); if (!QFile("/dev/dri").exists()) { qWarning() << "Unable to find a KMSDRM display device!"; qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment."; } } #endif } // This avoids using the default keychain for SSL, which may cause // password prompts on macOS. qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); #ifdef Q_OS_WIN32 if (!qEnvironmentVariableIsSet("QT_OPENGL")) { // On Windows, use ANGLE so we don't have to load OpenGL // user-mode drivers into our app. OGL drivers (especially Intel) // seem to crash Moonlight far more often than DirectX. qputenv("QT_OPENGL", "angle"); } #endif // We don't want system proxies to apply to us QNetworkProxyFactory::setUseSystemConfiguration(false); // Clear any default application proxy QNetworkProxy noProxy(QNetworkProxy::NoProxy); QNetworkProxy::setApplicationProxy(noProxy); // Register custom metatypes for use in signals qRegisterMetaType("NvApp"); SDL_version compileVersion; SDL_VERSION(&compileVersion); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Compiled with SDL %d.%d.%d", compileVersion.major, compileVersion.minor, compileVersion.patch); SDL_version runtimeVersion; SDL_GetVersion(&runtimeVersion); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Running with SDL %d.%d.%d", runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch); // Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver() // and SDL_EnableScreenSaver() when appropriate. This hint must be set before // initializing the SDL video subsystem to have any effect. SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); // For SDL backends that support it, use double buffering instead of triple buffering // to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they // are drawing directly to the screen without involving SDL, but it may matter for other // future KMSDRM platforms that use SDL for rendering. SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1"); if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s", SDL_GetError()); return -1; } #ifdef STEAM_LINK // Steam Link requires that we initialize video before creating our // QGuiApplication in order to configure the framebuffer correctly. if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", SDL_GetError()); return -1; } #endif // Use atexit() to ensure SDL_Quit() is called. This avoids // racing with object destruction where SDL may be used. atexit(SDL_Quit); // Avoid the default behavior of changing the timer resolution to 1 ms. // We don't want this all the time that Moonlight is open. We will set // it manually when we start streaming. SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0"); // Disable minimize on focus loss by default. Users seem to want this off by default. SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); // SDL 2.0.12 changes the default behavior to use the button label rather than the button // position as most other software does. Set this back to 0 to stay consistent with prior // releases of Moonlight. SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0"); #ifdef QT_DEBUG // Allow thread naming using exceptions on debug builds. SDL doesn't use SEH // when throwing the exceptions, so we don't enable it for release builds out // of caution. SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0"); #endif QGuiApplication app(argc, argv); // After the QGuiApplication is created, the platform stuff will be initialized // and we can set the SDL video driver to match Qt. if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11."); } else if (QGuiApplication::platformName().startsWith("wayland")) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland. Performance may be worse than X11."); qputenv("SDL_VIDEODRIVER", "wayland"); } #ifdef STEAM_LINK // Qt 5.9 from the Steam Link SDK is not able to load any fonts // since the Steam Link doesn't include any of the ones it looks // for. We know it has NotoSans so we will explicitly ask for that. if (app.font().family().isEmpty()) { qWarning() << "SL HACK: No default font - using NotoSans"; QFont fon("NotoSans"); app.setFont(fon); } // Move the mouse to the bottom right so it's invisible when using // gamepad-only navigation. QCursor().setPos(0xFFFF, 0xFFFF); #elif !SDL_VERSION_ATLEAST(2, 0, 11) && defined(Q_OS_LINUX) && (defined(__arm__) || defined(__aarch64__)) if (qgetenv("SDL_VIDEO_GL_DRIVER").isEmpty() && QGuiApplication::platformName() == "eglfs") { // Look for Raspberry Pi GLES libraries. SDL 2.0.10 and earlier needs some help finding // the correct libraries for the KMSDRM backend if not compiled with the RPI backend enabled. if (SDL_LoadObject("libbrcmGLESv2.so") != nullptr) { qputenv("SDL_VIDEO_GL_DRIVER", "libbrcmGLESv2.so"); } else if (SDL_LoadObject("/opt/vc/lib/libbrcmGLESv2.so") != nullptr) { qputenv("SDL_VIDEO_GL_DRIVER", "/opt/vc/lib/libbrcmGLESv2.so"); } } #endif app.setWindowIcon(QIcon(":/res/moonlight.svg")); // Register our C++ types for QML qmlRegisterType("ComputerModel", 1, 0, "ComputerModel"); qmlRegisterType("AppModel", 1, 0, "AppModel"); qmlRegisterUncreatableType("Session", 1, 0, "Session", "Session cannot be created from QML"); qmlRegisterSingletonType("ComputerManager", 1, 0, "ComputerManager", [](QQmlEngine*, QJSEngine*) -> QObject* { return new ComputerManager(); }); qmlRegisterSingletonType("AutoUpdateChecker", 1, 0, "AutoUpdateChecker", [](QQmlEngine*, QJSEngine*) -> QObject* { return new AutoUpdateChecker(); }); qmlRegisterSingletonType("SystemProperties", 1, 0, "SystemProperties", [](QQmlEngine*, QJSEngine*) -> QObject* { return new SystemProperties(); }); qmlRegisterSingletonType("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation", [](QQmlEngine*, QJSEngine*) -> QObject* { return new SdlGamepadKeyNavigation(); }); qmlRegisterSingletonType("StreamingPreferences", 1, 0, "StreamingPreferences", [](QQmlEngine*, QJSEngine*) -> QObject* { return new StreamingPreferences(); }); // Create the identity manager on the main thread IdentityManager::get(); #ifndef Q_OS_WINRT // Use the dense material dark theme by default if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) { qputenv("QT_QUICK_CONTROLS_STYLE", "Material"); } #else // Use universal dark on WinRT if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) { qputenv("QT_QUICK_CONTROLS_STYLE", "Universal"); } #endif if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) { qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark"); } if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) { qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple"); } if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) { qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense"); } if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) { qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark"); } QQmlApplicationEngine engine; QString initialView; GlobalCommandLineParser parser; switch (parser.parse(app.arguments())) { case GlobalCommandLineParser::NormalStartRequested: initialView = "qrc:/gui/PcView.qml"; break; case GlobalCommandLineParser::StreamRequested: { initialView = "qrc:/gui/CliStartStreamSegue.qml"; StreamingPreferences* preferences = new StreamingPreferences(&app); StreamCommandLineParser streamParser; streamParser.parse(app.arguments(), preferences); QString host = streamParser.getHost(); QString appName = streamParser.getAppName(); auto launcher = new CliStartStream::Launcher(host, appName, preferences, &app); engine.rootContext()->setContextProperty("launcher", launcher); break; } case GlobalCommandLineParser::QuitRequested: { initialView = "qrc:/gui/CliQuitStreamSegue.qml"; QuitCommandLineParser quitParser; quitParser.parse(app.arguments()); auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app); engine.rootContext()->setContextProperty("launcher", launcher); break; } } engine.rootContext()->setContextProperty("initialView", initialView); // Load the main.qml file engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; int err = app.exec(); // Give worker tasks time to properly exit. Fixes PendingQuitTask // sometimes freezing and blocking process exit. QThreadPool::globalInstance()->waitForDone(30000); return err; }