Compare commits

...
64 Commits
Author SHA1 Message Date
Cameron Gutman 103625fbdd Version v0.4.0 2018-09-08 16:58:03 -07:00
Cameron Gutman 4712080a1b Fix centering of streaming window and size after toggling full-screen on Windows 2018-09-08 16:01:35 -07:00
Cameron Gutman 93c683be3a Allow renderers that force V-sync on to run without it when streaming higher than the display refresh rate 2018-09-08 15:27:21 -07:00
Cameron Gutman c9a7c15f98 Improve unsupported FPS options and performance 2018-09-08 15:09:46 -07:00
Cameron Gutman 64a08f0533 Allow higher than display refresh rate streaming with unsupported FPS option 2018-09-08 14:34:11 -07:00
Cameron Gutman 52c6455ca5 Avoid sending a click event on focus gain on Linux 2018-09-08 14:05:42 -07:00
Cameron Gutman 238681d72f Merge pull request #67 from jhakonen/capture-on-activated-window
Capture mouse cursor when clicking inactive window
2018-09-08 14:01:27 -07:00
Cameron Gutman dbcc6feb3f Merge pull request #66 from jhakonen/fix-key-not-working
Fixed < key in keyboard not working
2018-09-08 11:05:34 -07:00
Janne Hakonen 300b53f28c Capture mouse cursor when clicking inactive window 2018-09-08 16:39:20 +03:00
Janne Hakonen 3dabb1c891 Fixed < key in keyboard not working 2018-09-08 14:16:27 +03:00
Cameron Gutman 4381f7a973 Apply changes to preferences when loading the settings page 2018-09-07 23:16:13 -07:00
Cameron Gutman 77b767ae5b Hide autodetect option for audio configuration since it's broken on macOS and Linux (and to some extent, Windows) 2018-09-07 22:51:56 -07:00
Cameron Gutman 2c4f78133f Avoid inflating MessageDialogs for each app in the grid. Fixes scrolling jank on app grid. 2018-09-07 17:33:34 -07:00
Cameron Gutman ec2c181f4e Use 4 threads for fetching box art 2018-09-07 17:30:43 -07:00
Cameron Gutman fae98eb13a Limit box art fetching to a single thread to improve UI responsiveness 2018-09-07 17:02:52 -07:00
Cameron Gutman f1d117d5d7 Ensure FFmpeg log messages are written to the debug log 2018-09-07 15:53:10 -07:00
Cameron Gutman 529ac8041a Only write a dump for the first unhandled exception 2018-09-07 15:45:35 -07:00
Cameron Gutman 391e84e0f4 Create a crash dump when Moonlight crashes on Windows 2018-09-07 14:16:59 -07:00
Cameron Gutman 6a2b717ff1 Run in native resolution on Retina displays when streaming above scaled resolution 2018-09-07 01:12:18 -07:00
Cameron Gutman 475a39cb73 Recreate the decoder when moving to a new display to allow Pacer to get the new refresh rate 2018-09-06 19:42:53 -07:00
Cameron Gutman 4dba74e2f9 Fix handling of undefined and non-matching refresh rate 2018-09-06 19:16:59 -07:00
Cameron Gutman 587d783955 Use SDL_WINDOWPOS_CENTERED_DISPLAY() to fix incorrect positioning on multi-display system on macOS 2018-09-06 18:53:00 -07:00
Cameron Gutman 3bfe87d516 Merge pull request #63 from jhakonen/optional-desktop-shortcut
Changed desktop shortcut as optional in Windows installer. Fixes #17
2018-09-06 16:20:50 -07:00
Janne Hakonen d3e43012fb Changed desktop shortcut as optional in installer. Fixes #17 2018-09-06 20:09:00 +03:00
Cameron Gutman 5da156b433 Prefer H.264 for higher decoding performance in very high load situations 2018-09-05 18:57:40 -07:00
Cameron Gutman b2eeb63b6f Mark the full-screen display mode as recommended due to lower latency and better frame pacing 2018-09-05 17:46:29 -07:00
Cameron Gutman a7517dace3 Switch to ItemDelegate for PCs and Apps for proper hover and click behavior 2018-09-05 17:08:27 -07:00
Cameron Gutman bd9f2d2009 Remove unused Menu items in ToolButtons 2018-09-05 16:38:52 -07:00
Cameron Gutman 78730f7235 Wake host on a separate worker thread to avoid hanging the UI for DNS resolution 2018-09-05 16:10:32 -07:00
Cameron Gutman 8a2491d955 Populate RC strings for Windows builds 2018-09-05 15:52:50 -07:00
Cameron Gutman 0d68faf40c Remove unused parameter 2018-09-05 15:45:36 -07:00
Cameron Gutman 957a2b2169 Fix box art caching location and cleanup of cache directory by the uninstaller 2018-09-05 15:45:10 -07:00
Cameron Gutman 856318f947 Start the SDL window on the same display as the Qt UI was displayed on 2018-09-05 15:15:53 -07:00
Cameron Gutman 4df9d6b20a Update common-c with FEC fixes 2018-09-05 14:49:07 -07:00
Cameron Gutman 8519d09141 Extract dSYM file for debugging Mac builds 2018-09-04 02:20:00 -07:00
Cameron Gutman 1b4fe83739 Fix AppVeyor symbol artifact name 2018-09-04 01:09:10 -07:00
Cameron Gutman 8fc825b2eb Produce PDBs for Windows builds 2018-09-04 01:03:01 -07:00
Cameron Gutman 6d0fc22ab5 Ensure symbols are always generated for debugging purposes 2018-09-04 00:18:02 -07:00
Cameron Gutman 9a71b4c237 Improve mouse batching to only batch if the last event was less than 1 millisecond ago 2018-09-04 00:09:35 -07:00
Cameron Gutman 9aa487418f Force V-Sync enabled for DXVA2 windowed with DWM composition enabled 2018-09-03 23:34:15 -07:00
Cameron Gutman 203b2fcaca Batch mouse move events for high DPI mice 2018-09-04 00:37:21 -04:00
Cameron Gutman dae21f2638 Detect native panel resolution on Retina displays. Fixes #59 2018-09-04 00:21:37 -04:00
Cameron Gutman af6e99cbac Select the highest full-screen refresh rate that our stream FPS evenly divides 2018-09-03 22:54:41 -04:00
Cameron Gutman 6d6acf4308 Fix release builds 2018-09-03 22:47:31 -04:00
Cameron Gutman 7dd4815edf Allow selection between full-screen exclusive mode and borderless windowed mode 2018-09-03 22:17:34 -04:00
Cameron Gutman 2c068a99a3 Allow FFmpeg renderers to enforce V-sync being enabled or disabled 2018-09-03 20:57:09 -04:00
Cameron Gutman fa93364ddd Ensure the CVDisplayLink is synchronized to the display that our window is on 2018-09-03 20:19:24 -04:00
Cameron Gutman 9b1eef3f63 Pass AV_HWACCEL_FLAG_IGNORE_LEVEL for VDPAU per FFmpeg documentation recommendation 2018-09-03 17:06:21 -04:00
Cameron Gutman ed6c07fd4b Display all frames even if they are corrupt or missing references (as will happen during reference frame invalidation) 2018-09-02 18:45:29 -04:00
Cameron Gutman 5ad96f5287 Display a dialog when the connection is terminated by the remote PC 2018-09-02 18:34:10 -04:00
Cameron Gutman fa8845c7e2 Merge pull request #58 from jhakonen/release-cursor
Release mouse cursor when window focus is lost
2018-09-02 16:51:56 -04:00
Janne Hakonen b96ace7f04 Release mouse cursor when window focus is lost 2018-09-02 00:31:37 +03:00
Cameron Gutman 234a8dc3d5 Fix build with HAVE_SLVIDEO 2018-08-31 10:40:25 -04:00
Cameron Gutman 0467c2f893 Don't rely on Qt 5.10 icon feature 2018-08-31 02:28:26 -04:00
Cameron Gutman 762fe92933 Allow streaming with no audio device. Fixes #52 2018-08-31 00:09:31 -04:00
Cameron Gutman 16d105d462 Avoid hardcoding VCRedist version 2018-08-30 20:47:44 -07:00
Cameron Gutman 8a8b624f47 Add logging for adding a PC and offline/online state transitions 2018-08-30 23:15:06 -04:00
Cameron Gutman 9470615c63 Use QtQuick.Dialogs 1.2 for Qt 5.9 support 2018-08-30 00:00:05 -04:00
Cameron Gutman 8f0ea700a5 Fix QML error when pairing successfully 2018-08-29 23:59:19 -04:00
Cameron Gutman b7cbec0b1c Pass RFI flags to decoder 2018-08-25 13:36:54 -07:00
Cameron Gutman 9be9934b8c Add reference frame invalidation for the software decoder 2018-08-25 12:38:04 -07:00
Cameron Gutman 55f0e1e1d5 Use QT_OPENGL to set ANGLE 2018-08-25 11:59:32 -07:00
Cameron Gutman bbd784fc85 Use ANGLE for rendering on Windows 2018-08-25 11:35:55 -07:00
Cameron Gutman 408f93732b Fix 'invalid VADisplay' errors on renderer destruction 2018-08-25 11:21:52 -07:00
54 changed files with 1557 additions and 449 deletions
+2 -2
View File
@@ -23,9 +23,9 @@
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleVersion</key>
<string>0.3.0</string>
<string>0.4.0</string>
<key>CFBundleShortVersionString</key>
<string>0.3.0</string>
<string>0.4.0</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
+11 -4
View File
@@ -11,6 +11,9 @@ unix:!macx {
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
@@ -34,7 +37,7 @@ win32 {
LIBS += -L$$PWD/../libs/windows/lib/x64
}
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
@@ -177,7 +180,7 @@ macx {
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.cpp
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
HEADERS += \
streaming/video/ffmpeg-renderers/vt.h \
@@ -231,6 +234,10 @@ unix:!macx: {
}
win32 {
RC_ICONS = moonlight.ico
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
QMAKE_TARGET_PRODUCT = Moonlight
CONFIG -= embed_manifest_exe
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
}
@@ -242,5 +249,5 @@ macx {
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
}
VERSION = 0.3.0
DEFINES += VERSION_STR=\\\"0.3.0\\\"
VERSION = 0.4.0
DEFINES += VERSION_STR=\\\"0.4.0\\\"
+8 -2
View File
@@ -6,8 +6,14 @@
BoxArtManager::BoxArtManager(QObject *parent) :
QObject(parent),
m_BoxArtDir(Path::getBoxArtCacheDir())
m_BoxArtDir(Path::getBoxArtCacheDir()),
m_ThreadPool(this)
{
// 4 is a good balance between fast loading for large
// app grids and not crushing GFE with tons of requests
// and causing UI jank from constantly stalling to decode
// new images.
m_ThreadPool.setMaxThreadCount(4);
if (!m_BoxArtDir.exists()) {
m_BoxArtDir.mkpath(".");
}
@@ -41,7 +47,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
// If we get here, we need to fetch asynchronously.
// Kick off a worker on our thread pool to do just that.
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
QThreadPool::globalInstance()->start(netLoadTask);
m_ThreadPool.start(netLoadTask);
// Return the placeholder then we can notify the caller
// later when the real image is ready.
+1
View File
@@ -36,6 +36,7 @@ private:
getFilePathForBoxArt(NvComputer* computer, int appId);
QDir m_BoxArtDir;
QThreadPool m_ThreadPool;
};
class NetworkBoxArtLoadTask : public QObject, public QRunnable
+5
View File
@@ -177,6 +177,11 @@ bool NvComputer::wake()
for (QString& addressString : addressList) {
QHostInfo hostInfo = QHostInfo::fromName(addressString);
if (hostInfo.error() != QHostInfo::NoError) {
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
continue;
}
// Try all IP addresses that this string resolves to
for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock;
+10 -2
View File
@@ -103,7 +103,7 @@ private:
QString serverInfo;
try {
serverInfo = http.getServerInfo();
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
} catch (...) {
return false;
}
@@ -154,6 +154,7 @@ private:
while (!isInterruptionRequested()) {
bool stateChanged = false;
bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
@@ -161,6 +162,9 @@ private:
}
if (tryPollComputer(address, stateChanged)) {
if (!wasOnline) {
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
}
online = true;
break;
}
@@ -171,6 +175,7 @@ private:
// Note: we don't need to acquire the read lock here,
// because we're on the writing thread.
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
qInfo() << m_Computer->name << "is now offline";
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
@@ -411,9 +416,11 @@ private:
{
NvHTTP http(m_Address);
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
QString serverInfo;
try {
serverInfo = http.getServerInfo();
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
} catch (...) {
if (!m_Mdns) {
emit computerAddCompleted(false);
@@ -449,6 +456,7 @@ private:
// Tell our client if something changed
if (changed) {
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
emit computerStateChanged(existingComputer);
}
}
+13 -12
View File
@@ -58,7 +58,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
}
QString
NvHTTP::getServerInfo()
NvHTTP::getServerInfo(NvLogLevel logLevel)
{
QString serverInfo;
@@ -70,7 +70,7 @@ NvHTTP::getServerInfo()
"serverinfo",
nullptr,
true,
true);
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
}
@@ -83,7 +83,7 @@ NvHTTP::getServerInfo()
"serverinfo",
nullptr,
true,
true);
logLevel);
verifyResponseStatus(serverInfo);
}
else
@@ -189,7 +189,7 @@ NvHTTP::quitApp()
// Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor.
if (getCurrentGame(getServerInfo()) != 0) {
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
// Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream.
throw GfeHttpResponseException(599, "");
@@ -230,7 +230,7 @@ NvHTTP::getAppList()
"applist",
nullptr,
true,
true);
NvLogLevel::ERROR);
verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml);
@@ -297,7 +297,8 @@ NvHTTP::getBoxArt(int appId)
"appasset",
"appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0",
true);
true,
NvLogLevel::VERBOSE);
QImage image = QImageReader(reply).read();
delete reply;
@@ -344,9 +345,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging)
NvLogLevel logLevel)
{
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
QString ret;
QTextStream stream(reply);
@@ -362,7 +363,7 @@ NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging)
NvLogLevel logLevel)
{
// Build a URL for the request
QUrl url(baseUrl);
@@ -388,7 +389,7 @@ NvHTTP::openConnection(QUrl baseUrl,
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
}
if (!suppressLogging) {
if (logLevel >= NvLogLevel::VERBOSE) {
qInfo() << "Executing request:" << url.toString();
}
loop.exec(QEventLoop::ExcludeUserInputEvents);
@@ -396,7 +397,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Abort the request if it timed out
if (!reply->isFinished())
{
if (!suppressLogging) {
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << "Aborting timed out request for" << url.toString();
}
reply->abort();
@@ -409,7 +410,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
if (!suppressLogging) {
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error();
}
GfeHttpResponseException exception(reply->error(), reply->errorString());
+9 -3
View File
@@ -80,6 +80,12 @@ private:
class NvHTTP
{
public:
enum NvLogLevel {
NONE,
ERROR,
VERBOSE
};
explicit NvHTTP(QString address);
static
@@ -87,7 +93,7 @@ public:
getCurrentGame(QString serverInfo);
QString
getServerInfo();
getServerInfo(NvLogLevel logLevel);
static
void
@@ -108,7 +114,7 @@ public:
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging = false);
NvLogLevel logLevel = NvLogLevel::VERBOSE);
static
QVector<int>
@@ -145,7 +151,7 @@ private:
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging = false);
NvLogLevel logLevel);
QString m_Address;
QNetworkAccessManager m_Nam;
@@ -33,6 +33,27 @@
</screenshots>
<releases>
<release version="0.4.0" date="2018-09-08">
<description>
<p>New features:</p>
<ul>
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
<li>The streaming window is now centered on the target monitor</li>
<li>Display a dialog when the connection is terminated by the remote PC</li>
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming with no audio device</li>
<li>Fixed mouse lag with some high polling rate mice</li>
<li>Improved UI performance on the app grid page</li>
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
<li>Autodetect audio configuration option removed because it was broken</li>
</ul>
</description>
</release>
<release version="0.3.0" date="2018-08-21">
<description>
<p>Bugfixes:</p>
+39 -44
View File
@@ -1,5 +1,5 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2
import AppModel 1.0
@@ -16,12 +16,7 @@ GridView {
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 350;
focus: true
// Cache delegates for 1000px in both directions to improve
// scrolling and resizing performance
cacheBuffer: 1000
cellWidth: 225; cellHeight: 385;
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
@@ -52,12 +47,13 @@ GridView {
model: appModel
delegate: Item {
width: 200; height: 300;
delegate: ItemDelegate {
width: 200; height: 335;
Image {
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
y: 20
source: model.boxart
sourceSize {
width: 150
@@ -104,46 +100,17 @@ GridView {
stackView.push(segue)
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : "";
property bool segueToStream : false
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
params.nextAppName = model.name
params.nextSession = appModel.createSessionForApp(index)
}
else {
params.nextAppName = null
params.nextSession = null
}
stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
onClicked: {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
else {
// Right click
appContextMenu.open()
}
// Right click
appContextMenu.open()
}
}
@@ -170,6 +137,34 @@ GridView {
}
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : "";
property bool segueToStream : false
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
params.nextAppName = model.name
params.nextSession = appModel.createSessionForApp(index)
}
else {
params.nextAppName = null
params.nextSession = null
}
stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
}
ScrollBar.vertical: ScrollBar {
parent: appGrid.parent
anchors {
+40 -42
View File
@@ -1,6 +1,6 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
@@ -19,7 +19,6 @@ GridView {
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
focus: true
objectName: "Computers"
// The StackView will trigger a visibility change when
@@ -49,7 +48,7 @@ GridView {
pairDialog.close()
// Display a failed dialog if we got an error
if (error !== null) {
if (error !== undefined) {
errorDialog.text = error
errorDialog.open()
}
@@ -73,7 +72,7 @@ GridView {
model: computerModel
delegate: Item {
delegate: ItemDelegate {
width: 300; height: 300;
Image {
@@ -142,47 +141,46 @@ GridView {
}
}
onClicked: {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
stackView.push(appView)
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
} else if (!model.online) {
pcContextMenu.open()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton;
acceptedButtons: Qt.RightButton;
onClicked: {
if(mouse.button === Qt.LeftButton) {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
stackView.push(appView)
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
} else if(!model.online) {
pcContextMenu.open()
}
}
else { // right click
if(!model.addPc) { // but only for actual PCs, not the add-pc option
pcContextMenu.open()
}
// right click
if (!model.addPc) { // but only for actual PCs, not the add-pc option
pcContextMenu.open()
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
+193 -81
View File
@@ -56,37 +56,51 @@ ScrollView {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// Add native resolutions for all attached displays
for (var displayIndex = 0;; displayIndex++) {
var screenRect = prefs.getDisplayResolution(displayIndex)
if (screenRect.width === 0) {
// Exceeded max count of displays
break
}
var done = false
for (var displayIndex = 0; !done; displayIndex++) {
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
var screenRect;
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
// Some platforms have different desktop resolutions
// and native resolutions (like macOS with Retina displays)
if (displayResIndex == 0) {
screenRect = prefs.getDesktopResolution(displayIndex)
}
else {
screenRect = prefs.getNativeResolution(displayIndex)
}
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
if (screenRect.width === 0) {
// Exceeded max count of displays
done = true
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height
})
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height
})
}
}
}
@@ -95,7 +109,7 @@ ScrollView {
var saved_width = prefs.width
var saved_height = prefs.height
currentIndex = 0
for (var i = 0; i < resolutionComboBox.count; i++) {
for (var i = 0; i < resolutionListModel.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
@@ -104,6 +118,9 @@ ScrollView {
currentIndex = i
}
}
// Persist the selected value
activated(currentIndex)
}
id: resolutionComboBox
@@ -136,59 +153,97 @@ ScrollView {
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
// Only modify the bitrate if the values actually changed
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
prefs.width = selectedWidth
prefs.height = selectedHeight
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
// Get the max supported FPS on this system
var max_fps = prefs.getMaximumStreamingFrameRate();
// Default entries
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
// Add unsupported FPS values that come before the display max FPS
if (prefs.unsupportedFps) {
if (max_fps > 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
if (max_fps > 120) {
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
}
}
// Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz.
if (max_fps > 64) {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
}
// Add unsupported FPS values that come after the display max FPS
if (prefs.unsupportedFps) {
if (max_fps < 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
if (max_fps < 120) {
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
}
}
return fpsListModel
}
function reinitialize() {
model = createModel()
var saved_fps = prefs.fps
currentIndex = 0
for (var i = 0; i < fpsComboBox.count; i++) {
var el_fps = parseInt(fpsListModel.get(i).video_fps);
for (var i = 0; i < model.count; i++) {
var el_fps = parseInt(model.get(i).video_fps);
// Pick the highest value lesser or equal to the saved FPS
if (saved_fps >= el_fps) {
currentIndex = i
}
}
// Persist the selected value
activated(currentIndex)
}
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
reinitialize()
}
id: fpsComboBox
textRole: "text"
model: ListModel {
id: fpsListModel
ListElement {
text: "30 FPS"
video_fps: "30"
}
ListElement {
text: "60 FPS"
video_fps: "60"
}
// A higher value may be added at runtime
// based on the attached display refresh rate
}
width: 250
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
var selectedFps = parseInt(model.get(currentIndex).video_fps)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
// Only modify the bitrate if the values actually changed
if (prefs.fps !== selectedFps) {
prefs.fps = selectedFps
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
}
}
@@ -230,26 +285,60 @@ ScrollView {
}
}
Row {
CheckBox {
id: fullScreenCheck
text: "<font color=\"white\">Full-screen</font>"
font.pointSize: 12
checked: prefs.fullScreen
onCheckedChanged: {
prefs.fullScreen = checked
Label {
width: parent.width
id: windowModeTitle
text: qsTr("Display mode")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var savedWm = prefs.windowMode
currentIndex = 0
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
break
}
}
activated(currentIndex)
}
CheckBox {
id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>"
font.pointSize: 12
visible: fullScreenCheck.checked
checked: prefs.enableVsync
onCheckedChanged: {
prefs.enableVsync = checked
id: windowModeComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: windowModeListModel
ListElement {
text: "Full-screen (Recommended)"
val: StreamingPreferences.WM_FULLSCREEN
}
ListElement {
text: "Borderless windowed"
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
}
ListElement {
text: "Windowed"
val: StreamingPreferences.WM_WINDOWED
}
}
onActivated: {
prefs.windowMode = windowModeListModel.get(currentIndex).val
}
}
CheckBox {
id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>"
font.pointSize: 12
checked: prefs.enableVsync
onCheckedChanged: {
prefs.enableVsync = checked
}
}
}
@@ -281,12 +370,14 @@ ScrollView {
Component.onCompleted: {
var saved_audio = prefs.audioConfig
currentIndex = 0
for(var i = 0; i < audioListModel.count; i++) {
var el_audio= audioListModel.get(i).val;
if(saved_audio === el_audio) {
currentIndex = i
}
for (var i = 0; i < audioListModel.count; i++) {
var el_audio = audioListModel.get(i).val;
if (saved_audio === el_audio) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: audioComboBox
@@ -294,10 +385,14 @@ ScrollView {
textRole: "text"
model: ListModel {
id: audioListModel
ListElement {
// Detection of audio channels only works on Windows
// and even so, it's still unreliable because audio devices
// lie about how many channels they can support. Hide the
// option to autodetect until this is resolved.
/*ListElement {
text: "Autodetect"
val: StreamingPreferences.AC_AUTO
}
}*/
ListElement {
text: "Stereo"
val: StreamingPreferences.AC_FORCE_STEREO
@@ -404,12 +499,14 @@ ScrollView {
Component.onCompleted: {
var saved_vds = prefs.videoDecoderSelection
currentIndex = 0
for(var i = 0; i < decoderListModel.count; i++) {
for (var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val;
if(saved_vds === el_vds) {
currentIndex = i
}
if (saved_vds === el_vds) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: decoderComboBox
@@ -452,10 +549,12 @@ ScrollView {
currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val;
if(saved_vcc === el_vcc) {
currentIndex = i
}
if (saved_vcc === el_vcc) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: codecComboBox
@@ -489,6 +588,19 @@ ScrollView {
}
}
CheckBox {
id: unlockUnsupportedFps
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
font.pointSize: 12
checked: prefs.unsupportedFps
onCheckedChanged: {
prefs.unsupportedFps = checked
// The selectable FPS values depend on whether
// this option is enabled or not
fpsComboBox.reinitialize()
}
}
}
}
}
+3 -2
View File
@@ -1,6 +1,7 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import Session 1.0
@@ -69,7 +70,7 @@ Item {
session.displayLaunchWarning.connect(displayLaunchWarning)
// Run the streaming session to completion
session.exec()
session.exec(Screen.virtualX, Screen.virtualY)
// Show the Qt window again after streaming
window.visible = true
+20 -2
View File
@@ -1,5 +1,7 @@
#include "computermodel.h"
#include <QThreadPool>
ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {}
@@ -119,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
endRemoveRows();
}
bool ComputerModel::wakeComputer(int computerIndex)
class DeferredWakeHostTask : public QRunnable
{
public:
DeferredWakeHostTask(NvComputer* computer)
: m_Computer(computer) {}
void run()
{
m_Computer->wake();
}
private:
NvComputer* m_Computer;
};
void ComputerModel::wakeComputer(int computerIndex)
{
Q_ASSERT(computerIndex < m_Computers.count());
return m_Computers[computerIndex]->wake();
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
QThreadPool::globalInstance()->start(wakeTask);
}
void ComputerModel::pairComputer(int computerIndex, QString pin)
+1 -1
View File
@@ -34,7 +34,7 @@ public:
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE bool wakeComputer(int computerIndex);
Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
+49 -32
View File
@@ -61,6 +61,8 @@ ApplicationWindow {
header: ToolBar {
id: toolBar
anchors.topMargin: 5
anchors.bottomMargin: 5
RowLayout {
spacing: 20
@@ -70,14 +72,19 @@ ApplicationWindow {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
icon.source: "qrc:/res/arrow_left.svg"
onClicked: stackView.pop()
Menu {
id: backButton
x: parent.width - width
transformOrigin: Menu.TopRight
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: stackView.pop()
}
Label {
@@ -94,8 +101,15 @@ ApplicationWindow {
property string browserUrl: ""
id: updateButton
icon.source: "qrc:/res/update.svg"
icon.color: "#0BF200"
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
@@ -108,11 +122,6 @@ ApplicationWindow {
onClicked: Qt.openUrlExternally(browserUrl);
Menu {
x: parent.width
transformOrigin: Menu.TopRight
}
function updateAvailable(url)
{
@@ -127,7 +136,14 @@ ApplicationWindow {
}
ToolButton {
icon.source: "qrc:/res/question_mark.svg"
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
@@ -136,11 +152,6 @@ ApplicationWindow {
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
Menu {
x: parent.width
transformOrigin: Menu.TopRight
}
}
ToolButton {
@@ -152,28 +163,34 @@ ApplicationWindow {
ToolTip.visible: hovered
ToolTip.text: "Gamepad Mapper"
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
Menu {
x: parent.width
transformOrigin: Menu.TopRight
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
}
ToolButton {
icon.source: "qrc:/res/settings.svg"
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Settings"
Menu {
x: parent.width
transformOrigin: Menu.TopRight
}
}
}
}
+107 -7
View File
@@ -11,6 +11,10 @@
#define SDL_MAIN_HANDLED
#include <SDL.h>
#ifdef HAVE_FFMPEG
#include "streaming/video/ffmpeg.h"
#endif
#include "path.h"
#include "gui/computermodel.h"
#include "gui/appmodel.h"
@@ -62,7 +66,8 @@ void logToLoggerStream(QString& message)
}
#endif
s_LoggerStream << message << endl;
s_LoggerStream << message;
s_LoggerStream.flush();
}
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
@@ -94,7 +99,7 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
logToLoggerStream(txt);
}
@@ -122,15 +127,99 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
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 <Windows.h>
#include <DbgHelp.h>
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[])
{
// 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);
@@ -158,6 +247,16 @@ int main(int argc, char *argv[])
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
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
@@ -166,10 +265,11 @@ int main(int argc, char *argv[])
// password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
// Set these here to allow us to use the default QSettings constructor
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
QCoreApplication::setApplicationName("Moonlight");
#ifdef Q_OS_WIN32
// On Windows, use ANGLE so we don't have to load both DX and OGL
// user-mode drivers into our app.
qputenv("QT_OPENGL", "angle");
#endif
// Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp");
+1 -1
View File
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
</svg>

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 499 B

+58 -10
View File
@@ -1,5 +1,6 @@
#include "streamingpreferences.h"
#include "streaming/session.hpp"
#include "streaming/streamutils.h"
#include <QSettings>
@@ -16,6 +17,8 @@
#define SER_AUDIOCFG "audiocfg"
#define SER_VIDEOCFG "videocfg"
#define SER_VIDEODEC "videodec"
#define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps"
StreamingPreferences::StreamingPreferences()
{
@@ -30,17 +33,21 @@ void StreamingPreferences::reload()
height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt();
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
fullScreen = settings.value(SER_FULLSCREEN, true).toBool();
enableVsync = settings.value(SER_VSYNC, true).toBool();
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
static_cast<int>(AudioConfig::AC_FORCE_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
}
void StreamingPreferences::save()
@@ -51,14 +58,15 @@ void StreamingPreferences::save()
settings.setValue(SER_HEIGHT, height);
settings.setValue(SER_FPS, fps);
settings.setValue(SER_BITRATE, bitrateKbps);
settings.setValue(SER_FULLSCREEN, fullScreen);
settings.setValue(SER_VSYNC, enableVsync);
settings.setValue(SER_GAMEOPTS, gameOptimizations);
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
@@ -87,12 +95,26 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
return maxFrameRate;
}
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
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) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
// Cap the frame rate at 120 FPS. Past this, the encoders start
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
}
}
@@ -101,7 +123,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
return maxFrameRate;
}
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -116,7 +138,7 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
}
SDL_DisplayMode mode;
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
@@ -124,12 +146,38 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetCurrentDisplayMode() failed: %s",
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
{
// This table prefers 16:10 resolutions because they are
+17 -4
View File
@@ -21,7 +21,9 @@ public:
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
void reload();
@@ -50,43 +52,54 @@ public:
};
Q_ENUM(VideoDecoderSelection)
enum WindowMode
{
WM_FULLSCREEN,
WM_FULLSCREEN_DESKTOP,
WM_WINDOWED
};
Q_ENUM(WindowMode)
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged)
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
// Directly accessible members for preferences
int width;
int height;
int fps;
int bitrateKbps;
bool fullScreen;
bool enableVsync;
bool gameOptimizations;
bool playAudioOnHost;
bool multiController;
bool unsupportedFps;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
signals:
void displayModeChanged();
void bitrateChanged();
void fullScreenChanged();
void enableVsyncChanged();
void gameOptimizationsChanged();
void playAudioOnHostChanged();
void multiControllerChanged();
void unsupportedFpsChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
void windowModeChanged();
};
+55 -1
View File
@@ -70,6 +70,60 @@ Exit:
return ret;
}
bool Session::testAudio(int audioConfiguration)
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
bool ret;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return false;
}
SDL_zero(want);
want.freq = 48000;
want.format = AUDIO_S16;
want.samples = SAMPLES_PER_FRAME;
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
want.channels = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
want.channels = 6;
break;
default:
SDL_assert(false);
ret = false;
goto Exit;
}
// Test audio device for functionality
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (dev == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - Failed to open audio device: %s",
SDL_GetError());
ret = false;
goto Exit;
}
SDL_CloseAudioDevice(dev);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - Successful with %d channels",
want.channels);
ret = true;
Exit:
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return ret;
}
int Session::sdlAudioInit(int /* audioConfiguration */,
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* /* arContext */, int /* arFlags */)
@@ -82,7 +136,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
return -1;
}
SDL_zero(want);
+35 -4
View File
@@ -23,7 +23,8 @@ const int SdlInputHandler::k_ButtonMap[] = {
};
SdlInputHandler::SdlInputHandler(bool multiController)
: m_MultiController(multiController)
: m_LastMouseMotionTime(0),
m_MultiController(multiController)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
@@ -324,6 +325,9 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE;
break;
case SDL_SCANCODE_NONUSBACKSLASH:
keyCode = 0xE2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
@@ -388,10 +392,37 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
// Not capturing
return;
}
short xdelta = (short)event->xrel;
short ydelta = (short)event->yrel;
if (event->xrel != 0 || event->yrel != 0) {
LiSendMouseMoveEvent((short)event->xrel,
(short)event->yrel);
// If we're sending more than one motion event per millisecond,
// delay for 1 ms to allow batching of mouse move events.
Uint32 currentTime = SDL_GetTicks();
if (!SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
SDL_Delay(1);
currentTime = SDL_GetTicks();
}
m_LastMouseMotionTime = currentTime;
// Pump even if we didn't delay since we might get some extra events
SDL_PumpEvents();
// Batch all of the pending mouse motion events
SDL_Event nextEvent;
while (SDL_PeepEvents(&nextEvent,
1,
SDL_GETEVENT,
SDL_MOUSEMOTION,
SDL_MOUSEMOTION) == 1) {
// In theory, these can overflow but in practice
// it should be highly unlikely since it would require
// moving 64K pixels in 1 ms.
xdelta += nextEvent.motion.xrel;
ydelta += nextEvent.motion.yrel;
}
if (xdelta != 0 || ydelta != 0) {
LiSendMouseMoveEvent(xdelta, ydelta);
}
}
+1
View File
@@ -44,6 +44,7 @@ private:
void sendGamepadState(GamepadState* state);
Uint32 m_LastMouseMotionTime;
bool m_MultiController;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
+283 -84
View File
@@ -1,5 +1,6 @@
#include "session.hpp"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
#include <Limelight.h>
#include <SDL.h>
@@ -19,13 +20,6 @@
#include "video/sl.h"
#endif
#if defined(Q_OS_DARWIN)
// Using full-screen desktop allows allows Spaces to work on macOS.
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
#else
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
#endif
#ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
#define ICON_SIZE 32
@@ -85,6 +79,8 @@ void Session::clStageFailed(int stage, long errorCode)
void Session::clConnectionTerminated(long errorCode)
{
emit s_ActiveSession->displayLaunchError("Connection terminated");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %ld",
errorCode);
@@ -108,13 +104,15 @@ void Session::clLogMessage(const char* format, ...)
va_end(ap);
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
{
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen");
return true;
@@ -129,7 +127,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)) {
if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen");
return true;
@@ -232,13 +230,54 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return ret;
}
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;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_DestroyWindow(window);
int caps = decoder->getDecoderCapabilities();
delete decoder;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return caps;
}
Session::Session(NvComputer* computer, NvApp& app)
: m_Computer(computer),
m_App(app),
m_Window(nullptr),
m_VideoDecoder(nullptr),
m_DecoderLock(0),
m_NeedsIdr(false)
m_NeedsIdr(false),
m_AudioDisabled(false),
m_DisplayOriginX(0),
m_DisplayOriginY(0),
m_PendingWindowedTransition(false)
{
qDebug() << "Server GPU:" << m_Computer->gpuModel;
@@ -301,6 +340,16 @@ Session::Session(NvComputer* computer, NvApp& app)
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
// Oddly enough, the H.264 encoder on my GTX 1080 is actually more
// performant than the HEVC encoder (especially because split frame encode
// is disabled for HEVC by moonlight-common-c). Use H.264 for 1440p 100 FPS and above.
if (m_StreamConfig.width * m_StreamConfig.height * m_StreamConfig.fps >= 2560 * 1440 * 100) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Preferring H.264 for higher performance");
m_StreamConfig.supportsHevc = false;
}
m_StreamConfig.enableHdr = false;
break;
case StreamingPreferences::VCC_FORCE_H264:
@@ -330,6 +379,17 @@ Session::Session(NvComputer* computer, NvApp& app)
else {
m_StreamConfig.packetSize = 1024;
}
switch (m_Preferences.windowMode)
{
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
break;
case StreamingPreferences::WM_FULLSCREEN:
default:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
break;
}
}
void Session::emitLaunchWarning(QString text)
@@ -356,6 +416,14 @@ bool Session::validateLaunch()
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
if (m_Preferences.unsupportedFps && m_StreamConfig.fps > 60) {
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
if (m_Preferences.enableVsync) {
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
}
}
if (m_StreamConfig.supportsHevc) {
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
@@ -435,6 +503,12 @@ bool Session::validateLaunch()
}
}
// Test that audio hardware is functional
m_AudioDisabled = !testAudio(m_StreamConfig.audioConfiguration);
if (m_AudioDisabled) {
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
}
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
@@ -452,6 +526,13 @@ bool Session::validateLaunch()
return false;
}
// Add the capability flags from the chosen decoder/renderer
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences.videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
return true;
}
@@ -469,8 +550,7 @@ class DeferredSessionCleanupTask : public QRunnable
}
};
void Session::getWindowDimensions(bool fullScreen,
int& x, int& y,
void Session::getWindowDimensions(int& x, int& y,
int& width, int& height)
{
int displayIndex = 0;
@@ -479,20 +559,29 @@ void Session::getWindowDimensions(bool fullScreen,
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
SDL_assert(displayIndex >= 0);
}
// If there's a display matching this exact resolution, pick that
// one (for native full-screen streaming). Otherwise, assume
// display 0 for now. TODO: Default to the screen that the Qt window is on
else if (fullScreen) {
// Create our window on the same display that Qt's UI
// was being displayed on.
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Qt UI screen is at (%d,%d)",
m_DisplayOriginX, m_DisplayOriginY);
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
m_ActiveVideoWidth == mode.w &&
m_ActiveVideoHeight == mode.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found exact resolution match on display: %d",
i);
displayIndex = i;
break;
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());
}
}
}
@@ -501,23 +590,19 @@ void Session::getWindowDimensions(bool fullScreen,
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
x = usableBounds.x;
y = usableBounds.y;
if (m_Window != nullptr) {
int top, left, bottom, right;
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) {
width -= left + right;
height -= top + bottom;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get window border size");
return;
"Unable to get window border size: %s",
SDL_GetError());
}
x += left;
y += top;
width -= left + right;
height -= top + bottom;
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
@@ -534,36 +619,88 @@ void Session::getWindowDimensions(bool fullScreen,
width = m_StreamConfig.width;
height = m_StreamConfig.height;
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
x = y = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex);
}
void Session::updateOptimalWindowDisplayMode()
{
SDL_DisplayMode desktopMode, bestMode, mode;
int displayIndex = SDL_GetWindowDisplayIndex(m_Window);
// Try the current display mode first. On macOS, this will be the normal
// scaled desktop resolution setting.
if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) {
// If this doesn't fit the selected resolution, use the native
// resolution of the panel (unscaled).
if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) {
if (!StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
return;
}
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return;
}
// Start with the native desktop resolution and try to find
// the highest refresh rate that our stream FPS evenly divides.
bestMode = desktopMode;
bestMode.refresh_rate = 0;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h &&
mode.refresh_rate % m_StreamConfig.fps == 0) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
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;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Chosen best display mode: %dx%dx%d",
bestMode.w, bestMode.h, bestMode.refresh_rate);
SDL_SetWindowDisplayMode(m_Window, &bestMode);
}
void Session::toggleFullscreen()
{
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
int x, y, width, height;
// If we're leaving full screen, toggle out before setting our size and position
// that way we have accurate display size metrics (not the size our stream changed it to).
if (!fullScreen) {
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
getWindowDimensions(fullScreen, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
if (fullScreen) {
SDL_SetWindowResizable(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
else {
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
// Reposition the window when the resize is complete
m_PendingWindowedTransition = true;
}
}
void Session::exec()
void Session::exec(int displayOriginX, int displayOriginY)
{
m_DisplayOriginX = displayOriginX;
m_DisplayOriginY = displayOriginY;
// Check for validation errors/warnings and emit
// signals for them, if appropriate
if (!validateLaunch()) {
@@ -644,7 +781,8 @@ void Session::exec()
}
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
&m_VideoCallbacks, &k_AudioCallbacks,
&m_VideoCallbacks,
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
NULL, 0, NULL, 0);
if (err != 0) {
// We already displayed an error dialog in the stage failure
@@ -659,15 +797,14 @@ void Session::exec()
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
int x, y, width, height;
getWindowDimensions(m_Preferences.fullScreen,
x, y, width, height);
getWindowDimensions(x, y, width, height);
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
width,
height,
0);
SDL_WINDOW_ALLOW_HIGHDPI);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
@@ -680,11 +817,13 @@ void Session::exec()
// For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account
// for window chrome size.
if (!m_Preferences.fullScreen) {
getWindowDimensions(false, x, y, width, height);
if (m_Preferences.windowMode == StreamingPreferences::WM_WINDOWED) {
getWindowDimensions(x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
// We must set the size before the position because centering
// won't work unless it knows the final size of the window.
SDL_SetWindowSize(m_Window, width, height);
SDL_SetWindowPosition(m_Window, x, y);
// Passing SDL_WINDOW_RESIZABLE to set this during window
// creation causes our window to be full screen for some reason
@@ -692,13 +831,10 @@ void Session::exec()
}
else {
// Update the window display mode based on our current monitor
SDL_DisplayMode mode;
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
SDL_SetWindowDisplayMode(m_Window, &mode);
}
updateOptimalWindowDisplayMode();
// Enter full screen
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
@@ -724,7 +860,7 @@ void Session::exec()
// 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.
if (m_Preferences.fullScreen) {
if (m_Preferences.windowMode != StreamingPreferences::WM_WINDOWED) {
SDL_SetRelativeMouseMode(SDL_TRUE);
}
#endif
@@ -757,6 +893,8 @@ void Session::exec()
SDL_GetError());
}
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event;
@@ -795,11 +933,61 @@ void Session::exec()
}
case SDL_WINDOWEVENT:
// 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
// activate it, second time to enable capture). With this you need to
// click it only once.
// 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).
// By excluding window's borders and title bar out, lets user still
// interact with them without mouse capture kicking in.
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
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);
}
}
}
#endif
// 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.
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
// seem to fire when switching from windowed to full-screen on X11.
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
break;
// 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;
}
}
// 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);
}
// Fall through
@@ -817,10 +1005,8 @@ void Session::exec()
SDL_FlushEvent(SDL_WINDOWEVENT);
// Update the window display mode based on our current monitor
SDL_DisplayMode mode;
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
SDL_SetWindowDisplayMode(m_Window, &mode);
}
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
updateOptimalWindowDisplayMode();
// Now that the old decoder is dead, flush any events it may
// have queued to reset itself (if this reset was the result
@@ -829,18 +1015,31 @@ void Session::exec()
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
// Choose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
m_Preferences.enableVsync,
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;
{
// If the stream exceeds the display refresh rate (plus some slack),
// forcefully disable V-sync to allow the stream to render faster
// than the display.
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
bool enableVsync = m_Preferences.enableVsync;
if (displayHz + 5 < m_StreamConfig.fps) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Disabling V-sync because refresh rate limit exceeded");
enableVsync = false;
}
// Choose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
enableVsync,
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;
}
}
// Request an IDR frame to complete the reset
+15 -3
View File
@@ -19,12 +19,16 @@ class Session : public QObject
public:
explicit Session(NvComputer* computer, NvApp& app);
Q_INVOKABLE void exec();
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
signals:
void stageStarting(QString stage);
@@ -45,12 +49,15 @@ private:
int sdlDetermineAudioConfiguration();
void getWindowDimensions(bool fullScreen,
int& x, int& y,
bool testAudio(int audioConfiguration);
void getWindowDimensions(int& x, int& y,
int& width, int& height);
void toggleFullscreen();
void updateOptimalWindowDisplayMode();
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
@@ -103,6 +110,11 @@ private:
IVideoDecoder* m_VideoDecoder;
SDL_SpinLock m_DecoderLock;
bool m_NeedsIdr;
bool m_AudioDisabled;
Uint32 m_FullScreenFlag;
int m_DisplayOriginX;
int m_DisplayOriginY;
bool m_PendingWindowedTransition;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
+107
View File
@@ -1,5 +1,11 @@
#include "streamutils.h"
#include <Qt>
#ifdef Q_OS_DARWIN
#include <ApplicationServices/ApplicationServices.h>
#endif
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
{
int dstH = dst->w * src->h / src->w;
@@ -18,3 +24,104 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
SDL_assert(dst->h * src->w / src->h <= dst->w);
}
}
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
{
int displayIndex = SDL_GetWindowDisplayIndex(window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get current display: %s",
SDL_GetError());
// Assume display 0 if it fails
displayIndex = 0;
}
SDL_DisplayMode mode;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// Use the window display mode for full-screen exclusive mode
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowDisplayMode() failed: %s",
SDL_GetError());
// Assume 60 Hz
return 60;
}
}
else {
// Use the current display mode for windowed and borderless
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetCurrentDisplayMode() failed: %s",
SDL_GetError());
// Assume 60 Hz
return 60;
}
}
// May be zero if undefined
if (mode.refresh_rate == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Refresh rate unknown; assuming 60 Hz");
mode.refresh_rate = 60;
}
return mode.refresh_rate;
}
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
{
#ifdef Q_OS_DARWIN
#define MAX_DISPLAYS 16
CGDirectDisplayID displayIds[MAX_DISPLAYS];
uint32_t displayCount = 0;
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
if (displayIndex >= displayCount) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Too many displays: %d vs %d",
displayIndex, displayCount);
return false;
}
SDL_zerop(mode);
// Retina displays have non-native resolutions both below and above (!) their
// native resolution, so it's impossible for us to figure out what's actually
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
// find the correct resolution and match it in our SDL list.
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
CFIndex count = CFArrayGetCount(modeList);
for (CFIndex i = 0; i < count; i++) {
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
break;
}
}
CFRelease(modeList);
// Now find the SDL mode that matches the CG native mode
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
SDL_DisplayMode thisMode;
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
if (thisMode.w == mode->w && thisMode.h == mode->h &&
thisMode.refresh_rate >= mode->refresh_rate) {
*mode = thisMode;
break;
}
}
}
#else
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return false;
}
#endif
return true;
}
+6
View File
@@ -7,4 +7,10 @@ class StreamUtils
public:
static
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
static
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
static
int getDisplayRefreshRate(SDL_Window* window);
};
+1
View File
@@ -19,6 +19,7 @@ public:
int frameRate,
bool enableVsync) = 0;
virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrame(SDL_UserEvent* event) = 0;
virtual void dropFrame(SDL_UserEvent* event) = 0;
+26 -5
View File
@@ -466,7 +466,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.hDeviceWindow = info.info.win.window;
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
d3dpp.Windowed = false;
@@ -489,10 +488,20 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
// to reduce latency by avoiding double v-syncing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
// continue while DWM is waiting to render the surface to the display.
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
d3dpp.BackBufferCount = 2;
// If V-sync is enabled (not rendering faster than display),
// we can use FlipEx for more efficient swapping.
if (enableVsync) {
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
// continue while DWM is waiting to render the surface to the display.
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
d3dpp.BackBufferCount = 2;
}
else {
// With V-sync off, we won't use FlipEx because that will block while
// DWM is waiting to render our surface (effectively behaving like V-Sync).
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed mode with DWM running");
@@ -501,6 +510,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
// We will enable V-sync in this scenario to avoid tearing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -510,6 +520,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
// We will allowing tearing for lowest latency.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -619,6 +630,16 @@ bool DXVA2Renderer::needsTestFrame()
return false;
}
int DXVA2Renderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
{
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
@@ -24,6 +24,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
bool initializeDecoder();
@@ -1,50 +0,0 @@
#include "displaylinkvsyncsource.h"
DisplayLinkVsyncSource::DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
DisplayLinkVsyncSource::~DisplayLinkVsyncSource()
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
CVReturn
DisplayLinkVsyncSource::displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
bool DisplayLinkVsyncSource::initialize(SDL_Window*, int displayFps)
{
m_DisplayFps = displayFps;
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
@@ -1,31 +1,11 @@
#pragma once
#include <CoreVideo/CoreVideo.h>
#include "pacer.h"
class DisplayLinkVsyncSource : public IVsyncSource
class DisplayLinkVsyncSourceFactory
{
public:
DisplayLinkVsyncSource(Pacer* pacer);
virtual ~DisplayLinkVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
private:
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* now,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext);
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
IVsyncSource* createVsyncSource(Pacer* pacer);
};
@@ -0,0 +1,132 @@
#include "displaylinkvsyncsource.h"
#include <SDL_syswm.h>
#include <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
class DisplayLinkVsyncSource : public IVsyncSource
{
public:
DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
virtual ~DisplayLinkVsyncSource() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
static
CGDirectDisplayID
getDisplayID(NSScreen* screen)
{
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
return [screenNumber unsignedIntValue];
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window, int displayFps) override
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
m_DisplayFps = displayFps;
NSScreen* screen = [info.info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId;
displayId = getDisplayID(screen);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
private:
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
};
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
return new DisplayLinkVsyncSource(pacer);
}
@@ -1,4 +1,5 @@
#include "pacer.h"
#include "streaming/streamutils.h"
#include "nullthreadedvsyncsource.h"
@@ -120,22 +121,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
{
m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
int displayIndex = SDL_GetWindowDisplayIndex(window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get current display: %s",
SDL_GetError());
// Assume display 0 if it fails
displayIndex = 0;
}
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) == 0) {
// May be zero if undefined
m_DisplayFps = mode.refresh_rate;
}
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -143,7 +129,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN)
m_VsyncSource = new DisplayLinkVsyncSource(this);
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
m_VsyncSource = new DxVsyncSource(this);
#else
@@ -8,6 +8,12 @@ extern "C" {
class IFFmpegRenderer {
public:
enum VSyncConstraint {
VSYNC_FORCE_OFF,
VSYNC_FORCE_ON,
VSYNC_ANY
};
virtual ~IFFmpegRenderer() {}
virtual bool initialize(SDL_Window* window,
int videoFormat,
@@ -18,6 +24,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0;
};
class SdlRenderer : public IFFmpegRenderer {
@@ -33,6 +41,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
SDL_Renderer* m_Renderer;
@@ -36,6 +36,18 @@ bool SdlRenderer::needsTestFrame()
return false;
}
int SdlRenderer::getDecoderCapabilities()
{
// The FFmpeg CPU decoder can handle reference frame invalidation,
// but only for H.264.
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
}
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
bool SdlRenderer::initialize(SDL_Window* window,
int,
int width,
@@ -149,6 +149,17 @@ VAAPIRenderer::needsTestFrame()
return true;
}
int
VAAPIRenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
{
@@ -39,6 +39,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
int m_WindowSystem;
@@ -217,6 +217,9 @@ bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
// This flag is recommended due to hardware underreporting supported levels
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VDPAU accelerated renderer");
@@ -230,6 +233,16 @@ bool VDPAURenderer::needsTestFrame()
return true;
}
int VDPAURenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
{
VdpStatus status;
@@ -22,6 +22,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
uint32_t m_VideoWidth, m_VideoHeight;
@@ -196,6 +196,19 @@ public:
return false;
}
virtual int getDecoderCapabilities() override
{
return 0;
}
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
{
// This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering.
return VSYNC_FORCE_ON;
}
private:
void setupDisplayLayer()
{
+33 -2
View File
@@ -1,5 +1,6 @@
#include <Limelight.h>
#include "ffmpeg.h"
#include "streaming/streamutils.h"
#ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h"
@@ -27,6 +28,11 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
return m_HwDecodeCfg != nullptr;
}
int FFmpegVideoDecoder::getDecoderCapabilities()
{
return m_Renderer->getDecoderCapabilities();
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts)
{
@@ -100,16 +106,37 @@ void FFmpegVideoDecoder::reset()
delete m_Pacer;
m_Pacer = nullptr;
// This must be called after deleting Pacer because it
// may be holding AVFrames to free in its destructor.
// However, it must be called before deleting the IFFmpegRenderer
// since the codec context may be referencing objects that we
// need to delete in the renderer destructor.
avcodec_free_context(&m_VideoDecoderCtx);
delete m_Renderer;
m_Renderer = nullptr;
avcodec_free_context(&m_VideoDecoderCtx);
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly)
{
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer");
enableVsync = false;
}
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
// FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully enabled by the active renderer");
enableVsync = true;
}
}
m_Pacer = new Pacer(m_Renderer);
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
return false;
@@ -125,6 +152,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
// Always request low delay decoding
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
// Allow display of corrupt frames and frames missing references
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
// Enable slice multi-threading for software decoding
if (!m_HwDecodeCfg) {
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
+1
View File
@@ -20,6 +20,7 @@ public:
int maxFps,
bool enableVsync) override;
virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override;
+6
View File
@@ -25,6 +25,12 @@ SLVideoDecoder::isHardwareAccelerated()
return true;
}
int
SLVideoDecoder::getDecoderCapabilities()
{
return 0;
}
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
+1
View File
@@ -17,6 +17,7 @@ public:
int frameRate,
bool enableVsync);
virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du);
// Unused since rendering is done directly from the decode thread
@@ -12,6 +12,9 @@ TEMPLATE = lib
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
win32 {
INCLUDEPATH += $$PWD/../libs/windows/include
}
+3
View File
@@ -7,6 +7,9 @@ TEMPLATE = lib
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
QMDNSE_DIR = $$PWD/qmdnsengine/src
DEFINES += \
QT_NO_SIGNALS_SLOTS_KEYWORDS
+6
View File
@@ -33,6 +33,12 @@ pushd $BUILD_FOLDER
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
popd
echo Saving dSYM file
pushd $BUILD_FOLDER
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight.dsym || fail "dSYM creation failed!"
cp -R Moonlight.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
popd
echo Copying dylib dependencies
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
+26 -3
View File
@@ -16,6 +16,7 @@ if /I "%BUILD_CONFIG%"=="debug" (
if /I "%BUILD_CONFIG%"=="signed-release" (
set BUILD_CONFIG=release
set SIGN=1
set MUST_DEPLOY_SYMBOLS=1
) else (
echo Invalid build configuration
exit /b 1
@@ -31,7 +32,6 @@ if /I "%ARCH%" NEQ "x86" (
)
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
set VCREDIST_PATH=%VS_PATH%\VC\Redist\MSVC\14.14.26405
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
@@ -42,15 +42,18 @@ set SOURCE_ROOT=%cd%
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
echo Cleaning output directories
rmdir /s /q %DEPLOY_FOLDER%
rmdir /s /q %BUILD_FOLDER%
rmdir /s /q %INSTALLER_FOLDER%
rmdir /s /q %SYMBOLS_FOLDER%
mkdir %BUILD_ROOT%
mkdir %DEPLOY_FOLDER%
mkdir %BUILD_FOLDER%
mkdir %INSTALLER_FOLDER%
mkdir %SYMBOLS_FOLDER%
echo Configuring the project
pushd %BUILD_FOLDER%
@@ -64,6 +67,25 @@ nmake %BUILD_CONFIG%
if !ERRORLEVEL! NEQ 0 goto Error
popd
echo Saving PDBs
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
copy "%%f" "%SYMBOLS_FOLDER%\"
if !ERRORLEVEL! NEQ 0 goto Error
)
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip %SYMBOLS_FOLDER%\*.pdb
if !ERRORLEVEL! NEQ 0 goto Error
if "%ML_SYMBOL_STORE%" NEQ "" (
echo Publishing PDBs to symbol store: %ML_SYMBOL_STORE%
symstore add /f %SYMBOLS_FOLDER%\*.pdb /s %ML_SYMBOL_STORE% /t Moonlight
if !ERRORLEVEL! NEQ 0 goto Error
) else (
if "%MUST_DEPLOY_SYMBOLS%"=="1" (
echo "A symbol server must be specified in ML_SYMBOL_STORE for signed release builds"
exit /b 1
)
)
echo Copying DLL dependencies
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
@@ -99,7 +121,7 @@ if "%SIGN%"=="1" (
)
echo Building bundle
set VCREDIST_INSTALLER=%VCREDIST_PATH%\vcredist_%ARCH%.exe
set VCREDIST_INSTALLER=%VCToolsRedistDir%vcredist_%ARCH%.exe
rem Bundles are always x86 binaries
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
if !ERRORLEVEL! NEQ 0 goto Error
@@ -119,7 +141,7 @@ if "%SIGN%"=="1" (
echo Building portable package
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
rem and should not be harvested for inclusion in the full installer
copy "%VCREDIST_PATH%\%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
copy "%VCToolsRedistDir%%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
rem This file tells Moonlight that it's a portable installation
echo. > %DEPLOY_FOLDER%\portable.dat
if !ERRORLEVEL! NEQ 0 goto Error
@@ -132,6 +154,7 @@ if /i "%APPVEYOR%"=="true" (
if !ERRORLEVEL! NEQ 0 goto Error
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
if !ERRORLEVEL! NEQ 0 goto Error
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
)
echo Build successful!
+28 -4
View File
@@ -39,7 +39,7 @@
</Directory>
</Directory>
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming</Property>
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming Project</Property>
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
we have to roll our own deletion via custom action -->
@@ -60,16 +60,38 @@
Target="[#MoonlightExe]"
Directory="ApplicationProgramsFolder"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
</Component>
<Component Id="MoonlightDesktopShortcut" Guid="*" Directory="INSTALLFOLDER">
<Shortcut Id="DesktopShortcut"
Name="$(var.ShortcutName)"
Description="$(var.ShortcutDesc)"
Target="[#MoonlightExe]"
Directory="DesktopFolder"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
<RegistryValue Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Name="DesktopShortcutInstalled"
Type="integer"
Value="1"
KeyPath="yes" />
<Condition>ADDDESKTOPSHORTCUT=1</Condition>
</Component>
<!-- Persist desktop shortcut's installed state to let Bundle.wxs know if
the desktop shortcut should installed by default when upgrading the
product -->
<Component Id="MoonlightDesktopShortcutState" Guid="*" Directory="INSTALLFOLDER">
<RegistryValue Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Name="DesktopShortcutInstallState"
Type="integer"
Value="[ADDDESKTOPSHORTCUT]"
KeyPath="yes" />
</Component>
<DirectoryRef Id="INSTALLFOLDER">
@@ -85,6 +107,8 @@
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
<ComponentRef Id="Moonlight" />
<ComponentRef Id="MoonlightShortcuts" />
<ComponentRef Id="MoonlightDesktopShortcutState" />
<ComponentRef Id="MoonlightDesktopShortcut" />
<ComponentGroupRef Id="MoonlightDependencies" />
</Feature>
</Product>
+21 -1
View File
@@ -51,13 +51,32 @@
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
<?endif ?>
<!-- Define "Add desktop shortcut" -checkbox's state by defining a variable
which has same name as the checkbox has. Value 1 means that checkbox
is checked, 0 means that is unchecked-->
<!-- Set checkbox's state as checked by default -->
<Variable Name="AddDesktopShortcutCheckbox" Type="numeric" Value="1" />
<!-- Get checkbox's state from registry if present. The registry value
"DesktopShortcutInstallState" is set in Product.wxs. -->
<util:RegistrySearch Variable="HasDesktopShortcutInstallStateRegKey"
Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Value="DesktopShortcutInstallState"
Result="exists" />
<util:RegistrySearch Variable="AddDesktopShortcutCheckbox"
Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Value="DesktopShortcutInstallState"
Condition="HasDesktopShortcutInstallStateRegKey" />
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<bal:WixStandardBootstrapperApplication
ShowVersion="yes"
LicenseFile="license.rtf"
LogoFile="..\..\app\moonlight_wix.png"
ShowFilesInUse="yes"
LaunchTarget="[InstallFolder]\Moonlight.exe" />
LaunchTarget="[InstallFolder]\Moonlight.exe"
ThemeFile="RtfTheme.xml" />
</BootstrapperApplicationRef>
<Chain>
@@ -75,6 +94,7 @@
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
<MsiProperty Name="ADDDESKTOPSHORTCUT" Value="[AddDesktopShortcutCheckbox]" />
</MsiPackage>
</Chain>
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
<!-- This file has been copied from C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes\RtfTheme.xml and then altered. -->
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
<Window Width="485" Height="300" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
<Image X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/>
<Text X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Text>
<Page Name="Help">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
</Page>
<Page Name="Install">
<Richedit Name="EulaRichedit" X="11" Y="80" Width="-11" Height="-70" TabStop="yes" FontId="0" HexStyle="0x800000" />
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
</Page>
<Page Name="Options">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
<!-- Added a checkbox for user to choose if shortcut is placed on
desktop or not. Checkbox's current state (checked/unchecked) is
read from a variable which has same name as this checkbox. The
variable is defined in Bundle.wxs. -->
<Checkbox Name="AddDesktopShortcutCheckbox" X="11" Y="178" Width="250" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">Add desktop shortcut</Checkbox>
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
</Page>
<Page Name="FilesInUse">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
</Page>
<Page Name="Progress">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
<Text X="11" Y="121" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
<Text Name="OverallProgressPackageText" X="85" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="143" Width="-11" Height="15" />
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
</Page>
<Page Name="Modify">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button>
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
</Page>
<Page Name="Success">
<Text Name="SuccessHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessHeader)</Text>
<Text Name="SuccessInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessInstallHeader)</Text>
<Text Name="SuccessRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRepairHeader)</Text>
<Text Name="SuccessUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessUninstallHeader)</Text>
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
<Text Name="SuccessRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRestartText)</Text>
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
<Button Name="SuccessCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.SuccessCloseButton)</Button>
</Page>
<Page Name="Failure">
<Text Name="FailureHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
<Text Name="FailureInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureInstallHeader)</Text>
<Text Name="FailureUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureUninstallHeader)</Text>
<Text Name="FailureRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRepairHeader)</Text>
<Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
<Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
<Text Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
</Page>
</Theme>